Some checks failed
Adopt the org engineering standard (its-consulting/standards @ v1.0.0).
Adds baseline governance/CI/policy files rendered from the standard's
templates and pins .standards-version=1.0.0. Vendored OPA/Rego policies
under .standards/policies/ so CI runs the gate locally (no cross-repo dep).
Placeholders ({{ORG}}/{{REPO}}/{{OWNER_HANDLE}}/{{MAINTAINER_EMAIL}}) filled in.
Existing files that differ were left untouched by the adopter.
Automated rollout. Files created: 16.
127 lines
4.5 KiB
Rego
127 lines
4.5 KiB
Rego
# Quadlet hardening: lint podctl/mypods `.container` units (systemd --user Quadlet)
|
|
# for safe-by-default container settings.
|
|
#
|
|
# Enforces: docs/adr/0008-container-runtime-podman-quadlet.md
|
|
# docs/adr/0004-policy-as-code-opa-conftest.md
|
|
# SOP: sops/SOP-004-environment-setup.md (provisioning Quadlet units)
|
|
# Input: a parsed Quadlet INI as a nested object. Section headers ([Container],
|
|
# [Service], [Unit]) are top-level keys; each maps to an object of its
|
|
# directives. Directives that may legally repeat (AddDevice, GroupAdd,
|
|
# Volume, PublishPort, Environment) are arrays; single-valued directives
|
|
# are scalars. Example (from podctl preset llama-gpt-oss-120b.container):
|
|
# {
|
|
# "Container": {
|
|
# "ContainerName": "llama-gpt-oss-120b",
|
|
# "Image": "docker.io/kyuz0/amd-strix-halo-toolboxes:vulkan-radv",
|
|
# "Pull": "never",
|
|
# "AddDevice": ["/dev/kfd", "/dev/dri"],
|
|
# "GroupAdd": ["video", "render"]
|
|
# },
|
|
# "Service": { "Restart": "on-failure", "TimeoutStopSec": "30" }
|
|
# }
|
|
#
|
|
# Rules:
|
|
# deny - container is privileged (PrivilegedTrue / extreme cap grants)
|
|
# deny - no Restart= in [Service] (units must be self-healing)
|
|
# warn - User=root (rootless Podman is the target; running as root in-container is a smell)
|
|
# warn - AddDevice present without any GroupAdd (device access usually needs a group, e.g. render/video)
|
|
package standards.quadlet.security
|
|
|
|
import rego.v1
|
|
|
|
container := object.get(input, "Container", {})
|
|
|
|
service := object.get(input, "Service", {})
|
|
|
|
# _is_quadlet gates every rule below so this package only fires on inputs that are
|
|
# actually parsed Quadlet units. A Quadlet INI is recognised by the presence of a
|
|
# [Container] section (every .container unit has one) or a [Unit] section. Without
|
|
# this guard, an unrelated YAML object (e.g. a k8s manifest with no [Service].Restart)
|
|
# would spuriously trip the "missing Restart" deny under conftest --all-namespaces.
|
|
_is_quadlet if {
|
|
_has_key(input, "Container")
|
|
}
|
|
|
|
_is_quadlet if {
|
|
_has_key(input, "Unit")
|
|
}
|
|
|
|
# --- Privileged containers are forbidden ------------------------------------
|
|
|
|
deny contains msg if {
|
|
_is_quadlet
|
|
val := object.get(container, "PrivilegedTrue", "")
|
|
lower(format_int_or_string(val)) == "true"
|
|
msg := "Quadlet runs a privileged container (PrivilegedTrue=true); privileged mode is forbidden (ADR-0008)"
|
|
}
|
|
|
|
# Granting all capabilities is equivalent to privileged.
|
|
deny contains msg if {
|
|
_is_quadlet
|
|
caps := object.get(container, "AddCapability", [])
|
|
some c in _as_array(caps)
|
|
upper(c) == "ALL"
|
|
msg := "Quadlet grants AddCapability=ALL; this is equivalent to privileged and is forbidden (ADR-0008)"
|
|
}
|
|
|
|
# --- A unit must declare a restart policy ------------------------------------
|
|
|
|
deny contains msg if {
|
|
_is_quadlet
|
|
not _has_key(service, "Restart")
|
|
msg := "Quadlet [Service] has no Restart= directive; units must be self-healing (e.g. Restart=on-failure)"
|
|
}
|
|
|
|
# Restart=no defeats the purpose; treat it as a violation.
|
|
deny contains msg if {
|
|
_is_quadlet
|
|
lower(format_int_or_string(object.get(service, "Restart", ""))) == "no"
|
|
msg := "Quadlet [Service] sets Restart=no; declare a real restart policy (e.g. on-failure / always)"
|
|
}
|
|
|
|
# --- Advisory: running as root inside the container --------------------------
|
|
|
|
warn contains msg if {
|
|
_is_quadlet
|
|
lower(format_int_or_string(object.get(container, "User", ""))) == "root"
|
|
msg := "Quadlet sets User=root inside the container; prefer a non-root User= where the image allows it (ADR-0008)"
|
|
}
|
|
|
|
# --- Advisory: device access without a supplementary group -------------------
|
|
|
|
warn contains msg if {
|
|
_is_quadlet
|
|
devices := object.get(container, "AddDevice", [])
|
|
count(_as_array(devices)) > 0
|
|
groups := object.get(container, "GroupAdd", [])
|
|
count(_as_array(groups)) == 0
|
|
msg := "Quadlet uses AddDevice without any GroupAdd; device nodes (e.g. /dev/dri) usually require a supplementary group such as render/video"
|
|
}
|
|
|
|
# --- helpers ----------------------------------------------------------------
|
|
|
|
_has_key(obj, k) if {
|
|
_ := obj[k]
|
|
}
|
|
|
|
# _as_array(x) normalises a scalar-or-array directive into an array, so rules can
|
|
# iterate uniformly whether a directive appeared once or many times.
|
|
_as_array(x) := x if {
|
|
is_array(x)
|
|
}
|
|
|
|
_as_array(x) := [x] if {
|
|
not is_array(x)
|
|
}
|
|
|
|
# format_int_or_string(x) stringifies scalars (Quadlet values may be parsed as
|
|
# numbers, e.g. TimeoutStopSec=30) so `lower`/`upper` never see a non-string.
|
|
format_int_or_string(x) := out if {
|
|
is_string(x)
|
|
out := x
|
|
}
|
|
|
|
format_int_or_string(x) := out if {
|
|
not is_string(x)
|
|
out := sprintf("%v", [x])
|
|
}
|