This repository has been archived on 2026-07-11. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
Dienstplan/.standards/policies/containerfile/containerfile_hardening.rego
Kenearos 11747fa7e5
Some checks failed
ci / standards-gate (push) Has been cancelled
chore: adopt engineering standard v1.0.0 (#34)
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: 15.
2026-07-02 01:19:30 +02:00

177 lines
5.3 KiB
Rego

# Containerfile / Dockerfile hardening for the mypods image builds.
#
# Enforces: docs/adr/0004-policy-as-code-opa-conftest.md
# docs/adr/0008-container-runtime-podman-quadlet.md
# SOP: sops/SOP-002-release-process.md (images are built then published)
# Input: a parsed Containerfile as an ordered list of instructions:
# {
# "instructions": [
# {"cmd": "FROM", "value": "archlinux:latest"},
# {"cmd": "ARG", "value": "API_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx"},
# {"cmd": "USER", "value": "app"},
# {"cmd": "RUN", "value": "pacman -Syu"}
# ]
# }
# `cmd` is the instruction; `value` is the remainder of the line.
# (A frontend such as `dockerfile_parse` or a small awk shim produces this shape;
# conftest's built-in Dockerfile parser yields a compatible `Cmd`/`Value` form,
# handled by the accessor helpers below.)
#
# Rules:
# deny - a secret-looking value baked into ARG or ENV
# warn - a FROM with an unpinned/moving base tag (no tag, or :latest)
# warn - the final effective user is root (no non-root USER set)
package standards.containerfile.hardening
import rego.v1
import data.standards.lib
# _is_containerfile gates every rule below so this package only fires on inputs that
# are actually parsed Containerfiles. The expected shape carries an `instructions`
# ARRAY (our {cmd,value} list, or conftest's Dockerfile parser output). Without this
# guard, an unrelated YAML object (e.g. a k8s manifest with no `instructions`) would
# spuriously trip the "never sets a non-root USER" warn under conftest --all-namespaces.
_is_containerfile if {
is_array(input.instructions)
}
# Normalise instructions: accept either our {cmd,value} shape or conftest's
# {Cmd, Value:[...]} Dockerfile-parser shape. Keyed by index to preserve order.
instructions[i] := inst if {
some i
raw := input.instructions[i]
inst := {"cmd": _cmd_of(raw), "value": _value_of(raw)}
}
_cmd_of(raw) := c if {
raw.cmd
c := upper(raw.cmd)
}
_cmd_of(raw) := c if {
not raw.cmd
c := upper(raw.Cmd)
}
_value_of(raw) := v if {
is_string(raw.value)
v := raw.value
}
_value_of(raw) := v if {
not raw.value
is_array(raw.Value)
v := concat(" ", raw.Value)
}
_value_of(raw) := v if {
not raw.value
is_string(raw.Value)
v := raw.Value
}
# --- Secrets baked into the image (ARG / ENV) -------------------------------
deny contains msg if {
_is_containerfile
some i
inst := instructions[i]
{"ARG", "ENV"}[inst.cmd]
parts := split(inst.value, "=")
count(parts) >= 2
val := concat("=", array.slice(parts, 1, count(parts)))
lib.looks_like_secret_value(trim_space(val))
msg := sprintf("%v sets '%v' to a secret-looking value; never bake secrets into an image layer (SOP-005)", [inst.cmd, parts[0]])
}
# Also catch a secret-NAMED ARG/ENV with a non-placeholder literal value.
deny contains msg if {
_is_containerfile
some i
inst := instructions[i]
{"ARG", "ENV"}[inst.cmd]
parts := split(inst.value, "=")
count(parts) >= 2
key := parts[0]
lib.looks_like_secret_name(key)
val := trim_space(concat("=", array.slice(parts, 1, count(parts))))
not lib.is_placeholder(val)
val != ""
msg := sprintf("%v '%v' is secret-named with an inline value; pass secrets at runtime, not in the image (SOP-005)", [inst.cmd, key])
}
# --- Unpinned base image -----------------------------------------------------
warn contains msg if {
_is_containerfile
some i
inst := instructions[i]
inst.cmd == "FROM"
base := _from_image(inst.value)
parts := lib.split_image(base)
lib.is_mutable_tag(parts.tag)
msg := sprintf("FROM '%v' uses an unpinned/moving base tag '%v'; pin to a SemVer or digest for reproducible builds", [base, parts.tag])
}
# --- Running as root ---------------------------------------------------------
# Warn when no non-root USER is ever declared: the build runs as root by default.
warn contains msg if {
_is_containerfile
not has_nonroot_user
msg := "Containerfile never sets a non-root USER; the image runs as root by default — add `USER <non-root>` where the workload allows (ADR-0008)"
}
# Warn when the LAST USER instruction puts the runtime user back to root.
warn contains msg if {
_is_containerfile
last_user := _last_user
last_user != ""
lower(last_user) == "root"
msg := "the final USER in the Containerfile is root; drop privileges before the image's default command runs (ADR-0008)"
}
# --- helpers ----------------------------------------------------------------
# _from_image(value) strips an `AS <stage>` suffix and `--platform=` flags from a FROM line.
_from_image(value) := img if {
toks := split(trim_space(value), " ")
img := _first_non_flag(toks)
}
_first_non_flag(toks) := t if {
some i
t := toks[i]
not startswith(t, "--")
# the image ref is the first non-flag token
count([x | some j; x := toks[j]; j < i; not startswith(x, "--")]) == 0
}
# user_instructions: every USER value, keyed by its instruction index.
user_instructions[idx] := val if {
some idx
inst := instructions[idx]
inst.cmd == "USER"
val := trim_space(inst.value)
}
has_nonroot_user if {
some idx
val := user_instructions[idx]
lower(val) != "root"
val != ""
}
# _last_user returns the value of the last USER instruction, or "" if none.
_last_user := val if {
idxs := [i | some i; user_instructions[i]]
count(idxs) > 0
max_idx := max(idxs)
val := user_instructions[max_idx]
}
_last_user := "" if {
count([i | some i; user_instructions[i]]) == 0
}