Skip to content

infra: hardened Dockerfile, modern k8s manifest, .gitignore & .dockerignore - #27

Merged
SebastienPittet merged 4 commits into
masterfrom
infra/kubernetes-docker-gitignore
Oct 26, 2026
Merged

SebastienPittet merged 4 commits into
masterfrom
infra/kubernetes-docker-gitignore

Conversation

@SebastienPittet

@SebastienPittet SebastienPittet commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

Dockerfile

  • Base python:3.12.2-alpine3.19 (Feb 2024, nearly EOL) → python:3.13.5-alpine3.22 (verified tag).
  • Non-root app user (uid 10001, matching the k8s runAsUser) + USER 10001.
  • sh -c wrapper removed → ENTRYPOINT ["python","-m","bot"]: python is PID 1, gets SIGTERM → graceful shutdown (the new code handles it).
  • PYTHONDONTWRITEBYTECODE/PYTHONUNBUFFERED, deps layer cached before COPY bot/.
  • /app/state heartbeat dir for the liveness probe; new .dockerignore keeps secrets, tests and .git out of the build context.

Kubernetes manifest (current-cluster best practices, validated with kubeconform strict)

  • Secrets: BOT_TOKEN/CHANNEL_ID were missing/hardcodable → Secret (stringData placeholder, rotate via SOPS/sealed-secrets) + ConfigMap for non-secret settings via envFrom.
  • Security: pod+container securityContext — runAsNonRoot: 10001, readOnlyRootFilesystem, capabilities.drop: [ALL], allowPrivilegeEscalation: false, seccompProfile: RuntimeDefault, automountServiceAccountToken: false.
  • Image: :latest → pinned v2.0.0 + imagePullPolicy: IfNotPresent + commented imagePullSecrets for the private registry.
  • Resources: cpu 0.1 / mem 20M (under-provisioned, invalid-ish unit style) → requests 50m/64Mi + limits 250m/192Mi.
  • Probes: ls /app liveness (always true) → real heartbeat freshness probe (age < 3× cycle) + readiness probe; state on a size-limited emptyDir.
  • Rollout: Recreate instead of 25%/25% surge — prevents two pods posting the same news during rollouts; revisionHistoryLimit: 3.
  • Fixed the RSS_URLS value that embedded literal " quote characters into the env var; added standard app.kubernetes.io/* labels.

.gitignore

Added: .env.* (+ .env.example negation), *.pem/*.key/*.p12, secrets.yaml (example negated), configuration.*.toml, .ruff_cache/, IDE & OS noise, sbom.json/coverage.xml artifacts, docker-compose.override.yml.

Together with #1 (heartbeat-aware app code) and #2 (kubeconform gate).


Merged master (conflict resolution)

master (PR #26 + #28) is now merged into this branch. Only Dockerfile conflicted: both sides
rewrote the tail of the image. The hardened version from this PR is kept — python:3.13.5-alpine3.22,
non-root uid 10001, writable /app/state, HEARTBEAT_FILE=/app/state/heartbeat and the exec-form
ENTRYPOINT ["python", "-m", "bot"]. It is a superset of master's CMD ["python", "-m", "bot"]
(same entry point, plus signal handling as PID 1), so nothing from master is lost.

Verified on the merged tree: ruff check + ruff format --check clean, mypy clean,
82 tests pass (81 unit + 1 integration).

Also in this PR: the lint job was unfixable as written

Lint & type-check failed here for a reason unrelated to this PR's content. The step ran
docker run ghcr.io/rhysd/actionlint:latest, and ghcr.io refuses anonymous pulls of that image —
its token endpoint answers 403 DENIED — so the job exits 125 on every branch; master is red
for exactly this reason. It now downloads the pinned v1.7.12 release archive from github.com and
verifies the published SHA-256 before executing it: same check, no container-registry dependency,
no mutable :latest, no third-party action code. Both pinned digests were checked against upstream
actionlint_1.7.12_checksums.txt, and actionlint 1.7.12 passes against all three workflows locally.

It is one isolated commit (ci(actionlint): …) — happy to split it into its own PR if you prefer
to keep this one purely infra-as-authored.

Also in this PR: the Kubernetes manifests job never ran kubeconform

uses: helm/kubeconform-action@v1.6.0 referenced a repository that does not exist, so the job died
in Set up job ("Unable to resolve action") without ever validating anything — a green-looking
check that checked nothing, and here a red one. It now downloads the pinned kubeconform v0.8.0
release binary, verifies the published SHA-256, and runs -strict -summary against kubernetes/
with a pinned schema version (the same pattern as the actionlint step above).

kubernetes/kube-manifest.yaml is in fact valid: kubeconform 0.8.0 -strict reports
4 resources found — Valid: 4, Invalid: 0 against both the 1.33 and 1.34 schemas.

Why both CI commits belong here

Both broken steps came from ci.yml on master, which this branch now carries, so PR #27 could not
go green without them. Each is a separate, self-contained commit (ci(actionlint): …,
ci(kubeconform): …) and can be cherry-picked out if you would rather keep this PR purely infra.

…st (securityContext, probes, configmap/secret), richer .gitignore, .dockerignore
…cr image

ghcr.io refuses anonymous pulls of rhysd/actionlint: even the token endpoint answers 403 DENIED, so 'docker run ghcr.io/rhysd/actionlint' exits 125 on every runner and the lint job could never pass.

Instead download the pinned v1.7.12 release archive from github.com for the runner architecture and verify it against the published SHA-256 before executing it: same check, no container-registry dependency, no :latest, no third-party action code.
…action

'uses: helm/kubeconform-action@v1.6.0' cannot resolve -- that repository does not exist, so the job died in 'Set up job' before any manifest was ever validated (it was, in effect, a check that always passed nothing).

Fetch the pinned v0.8.0 release binary from github.com, verify the published SHA-256, and run it in strict mode against kubernetes/ with a pinned schema version. The manifest itself is valid: kubeconform 0.8.0 reports 4/4 resources valid in strict mode.
@SebastienPittet
SebastienPittet merged commit ea9ba8e into master Oct 26, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant