diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ec703e..945d6a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,23 @@ but container releases use the upstream-derived format documented in - Added a restricted-runtime smoke suite, static landing page, and hardened Compose development service. - Added native PowerShell smoke testing for Podman Desktop development. +- Standardized local runtime testing on the canonical Bash suite with rootless + Podman under native Linux or WSL2, removing the duplicated PowerShell suite. +- Preserved the complete restricted-runtime test matrix while allowing the + Shell harness to enforce `no-new-privileges` using the syntax accepted by + older rootless Podman development engines. +- Added a preview standalone Linux deployment runbook and rootless Podman + Quadlet covering systemd lifecycle, journald logging, updates, rollback, and + exact-host qualification. +- Added a component threat model, security-control ownership model, requirement + analysis method, and cybersecurity evidence checklist. +- Expanded the first-release roadmap with deployment qualification, systemd and + logging tests, OSCAL/control engineering, requirement-source review, FIPS + boundary analysis, go-live evidence, and an assurance-completeness gate. +- Made upgrading the existing Ubuntu WSL2 Podman environment and rerunning the + complete Shell/Quadlet qualification the next roadmap action. +- Added GitHub topics for NGINX, containers, Podman, OpenShift, UBI 9, and + supply-chain security. - Extended CI with native AMD64 and ARM64 builds, restricted-runtime tests, Trivy image scanning, Syft SPDX inventories, blocking Grype analysis, full finding retention, and an aggregate image result. diff --git a/README.md b/README.md index cc350d6..df3f464 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,12 @@ root filesystem, explicit `tmpfs` mounts, dropped capabilities, and and the security boundary of each one. - [Logging](docs/LOGGING.md) documents current stream behavior, use-case fields, sensitive-data rules, and controlled-network responsibilities. +- [Deployment](docs/DEPLOYMENT.md) describes standalone rootless Podman with a + user systemd Quadlet, host logging, lifecycle operations, and qualification. +- [Threat model](docs/THREAT-MODEL.md) identifies assets, trust boundaries, + abuse cases, treatments, owners, and residual risks. +- [Security controls](docs/SECURITY-CONTROLS.md) defines shared control + ownership and the component evidence supplied for cybersecurity review. - [NGINX RPM provenance](docs/RPM-PROVENANCE.md) records the exact Red Hat UBI package source, build path, trust checks, and local verification commands. - [NGINX package-source decision](docs/PACKAGE-SOURCE.md) compares the current @@ -98,9 +104,9 @@ root filesystem, explicit `tmpfs` mounts, dropped capabilities, and pre-build download and verification process and hermetic image assembly contract. -Operational, TLS, configuration, architecture, threat-model, control-matrix, -SCAP, vulnerability-management, support, and disconnected-network guides will -be added as their associated implementations and evidence are developed. +TLS, configuration, architecture, control-matrix/OSCAL, SCAP, +vulnerability-management, support, and disconnected-network guides will be +added as their associated implementations and evidence are developed. ## Images and releases @@ -134,8 +140,8 @@ python -m pip install --require-hashes --only-binary=:all: \ pre-commit run --all-files --show-diff-on-failure ``` -Build and exercise the current AMD64 development image with Podman on Linux, -WSL, or Git Bash: +Build and exercise the current AMD64 development image with rootless Podman on +native Linux or WSL2: ```console podman build --format docker --file Containerfile \ @@ -144,14 +150,6 @@ CONTAINER_RUNTIME=podman IMAGE=localhost/nginx-ubi9:development \ bash tests/smoke.sh ``` -From PowerShell with Podman Desktop: - -```powershell -podman build --format docker --file Containerfile ` - --tag localhost/nginx-ubi9:development . -.\tests\smoke.ps1 -``` - Or start the hardened default service with Compose: ```console diff --git a/docs/CI.md b/docs/CI.md index f9effa1..69dd3da 100644 --- a/docs/CI.md +++ b/docs/CI.md @@ -39,8 +39,8 @@ retroactively validate a local commit message that was never pushed. ## Local Podman development -Podman is the primary local container workflow. Before relying on a result, -record both client and engine details: +Rootless Podman under native Linux or WSL2 is the primary local container +workflow. Before relying on a result, record both client and engine details: ```console podman version @@ -52,10 +52,16 @@ non-root UID while its Linux VM engine itself operates rootfully. That proves the image's non-root process behavior but does not qualify rootless-host user namespace behavior. Release evidence will distinguish these cases. -The README contains the exercised Podman build and smoke commands. The local -PowerShell suite is suitable for Podman Desktop on Windows; the Bash suite is -used on Linux CI. Native AMD64 and ARM64 CI remains required before an image -receives supported multi-architecture status. +Use Podman inside WSL2 or on native Linux for local rootless-host evidence. +The Bash harness retains compatibility with older development engines where +safe, but compatibility does not make an engine part of the production support +boundary. Quadlet deployment requires Podman 4.6 or newer, and first-release +qualification will record a newer exact vendor-supported baseline. + +The README contains the exercised Podman build and smoke commands. Bash is the +single canonical smoke implementation for local Podman and Linux CI, avoiding +behavioral drift between platform-specific suites. Native AMD64 and ARM64 CI +remains required before an image receives supported multi-architecture status. ## Planned external acquisition diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..b450e89 --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,250 @@ +# Deployment guide + +Production readiness is a property of an exact tested deployment, not an image +label. Pin the image digest and record the host, runtime, configuration, trust +material, network policy, and evidence used to approve it. + +The standalone-host procedure below is a **preview profile** until it passes on +the release-candidate image and the exact Linux host baseline recorded in the +qualification ledger. The image-level restrictions are exercised by the smoke +suite; user systemd, SELinux, journald, boot, and host-restart behavior require +target-host evidence. + +## Choose the lifecycle owner + +| Environment | Lifecycle owner | Use systemd for this container? | +| --- | --- | --- | +| Interactive development | Podman CLI or Compose | Optional. | +| Standalone Linux host | User systemd with Podman Quadlet | Recommended. | +| Kubernetes or OpenShift | The orchestrator | No per-container systemd unit. | + +Systemd does not run inside this image. On a standalone host it supervises the +rootless Podman process from outside the container. Do not use a root system +unit merely to make Podman rootless: a rootless Quadlet must be loaded by the +dedicated account's user systemd manager. + +Podman 4.6 and later provide Quadlet. The first-release host qualification will +select an exact supported RHEL 9 and Podman combination; a newer Quadlet key +must not be added to the example without testing that baseline. + +## Prepare a standalone Linux host + +An administrator performs the host-owned steps once: + +1. Patch the supported RHEL 9 host, install its vendor-supported Podman, retain + enforcing SELinux, and confirm cgroup v2. +2. Create a dedicated, non-login service account with subordinate UID and GID + ranges. Do not share the account with unrelated containers. +3. Enable lingering for that account so its user manager can start at boot and + continue after interactive sessions end: + + ```console + sudo loginctl enable-linger nginx-service + ``` + +4. Restrict who can become or administer the service account. Root or another + authorized host administrator can still control and inspect its processes; + rootless is privilege reduction, not a boundary from host root. +5. Configure firewall, time synchronization, journal persistence and limits, + log forwarding, monitoring, registry trust, and approved egress. + +As the service account, verify the runtime rather than assuming it is rootless: + +```console +podman info --format 'rootless={{.Host.Security.Rootless}} cgroup={{.Host.CgroupsVersion}} log={{.Host.LogDriver}}' +podman version +``` + +The expected result includes `rootless=true` and `cgroup=2`. + +## Stage deployment inputs + +Create service-account-owned directories. Configuration and content should be +readable by the container identity but not writable by it. Private keys require +a separately reviewed ownership and rotation procedure; do not make them +world-readable to solve a user-namespace problem. + +```console +install -d -m 0750 "$HOME/nginx-ubi9" \ + "$HOME/nginx-ubi9/conf.d" "$HOME/nginx-ubi9/html" +install -m 0444 nginx.conf "$HOME/nginx-ubi9/nginx.conf" +cp -a conf.d/. "$HOME/nginx-ubi9/conf.d/" +cp -a html/. "$HOME/nginx-ubi9/html/" +chmod -R a-w "$HOME/nginx-ubi9/conf.d" "$HOME/nginx-ubi9/html" +``` + +On SELinux hosts, the example uses private `:Z` relabeling on these dedicated +trees. Do not apply `:Z` to a shared or system directory. If several containers +must share a tree, design and test an appropriate shared label instead of +disabling SELinux separation. + +Before promotion, obtain the released image through the approved transfer or +registry process, verify its digest-bound signature and attestations, and load +or pull the exact digest. The service definition deliberately uses `Pull=never` +so startup cannot silently replace the approved artifact. + +```console +IMAGE='ghcr.io/datopsis/nginx-ubi9@sha256:' +podman pull "$IMAGE" +podman image inspect "$IMAGE" +``` + +## Install the rootless Quadlet + +Copy [the reviewed example](examples/systemd/nginx-ubi9.container) into the +service account's rootless Quadlet search path and replace every placeholder: + +```console +install -d -m 0700 "$HOME/.config/containers/systemd" +install -m 0600 docs/examples/systemd/nginx-ubi9.container \ + "$HOME/.config/containers/systemd/nginx-ubi9.container" +systemctl --user daemon-reload +QUADLET_UNIT_DIRS="$HOME/.config/containers/systemd" \ + /usr/lib/systemd/system-generators/podman-system-generator --user --dryrun +systemctl --user start nginx-ubi9.service +systemctl --user status nginx-ubi9.service +curl --fail http://127.0.0.1:8080/healthz +``` + +Quadlet-generated services are enabled through the source file's `[Install]` +section during generation; do not rely on `systemctl --user enable` against the +generated transient service. `WantedBy=default.target` plus lingering provides +boot start for the user manager. + +The example enforces: + +- an immutable image digest and no startup pull; +- container UID/GID `999:0` under a rootless Podman user namespace; +- a read-only root with only a bounded, `noexec,nosuid,nodev` `/tmp` tmpfs; +- all capabilities dropped, no new privileges, and bounded processes, memory, + shared memory, and file descriptors; +- loopback-only HTTP publication until an exposure decision is approved; +- read-only configuration and content mounts with SELinux relabeling; +- journald collection, graceful `SIGQUIT` stop, and on-failure restart; and +- configuration health checks and an explicit reload operation. + +To make the service reachable beyond the host, change the publish address only +after the firewall, upstream load balancer, TLS boundary, source-IP behavior, +and client network are approved. Rootless port forwarding can change the +address visible to NGINX; qualify the selected Podman network backend before +using an address as an identity, allow-list key, or rate-limit key. + +## Operate, reload, update, and roll back + +Validate configuration before reload, then use the systemd operation: + +```console +podman exec nginx-ubi9 nginx -t +systemctl --user reload nginx-ubi9.service +systemctl --user is-active nginx-ubi9.service +curl --fail http://127.0.0.1:8080/healthz +``` + +For an update: + +1. Verify and preload the new digest. +2. Validate its configuration in a disposable container with the same mounts + and restrictions. +3. Change only `Image=` in the Quadlet and retain the previous file and digest. +4. Run `systemctl --user daemon-reload` and restart the service. +5. Verify image digest, health, logs, routes, TLS, upstream behavior, resource + use, and alerts. +6. If acceptance fails, restore the prior Quadlet and digest, reload, restart, + and record the rollback. + +Stopping the unit sends the image's `SIGQUIT` stop signal and waits for the +configured timeout: + +```console +systemctl --user stop nginx-ubi9.service +``` + +Measure termination during qualification. Increasing the timeout is safe when +real connections need longer drainage; forcing termination without evidence +can truncate responses. + +## Logging with systemd + +NGINX writes access events to stdout and error/lifecycle events to stderr. +`LogDriver=journald` makes Podman submit those streams to the host journal; +systemd also records the generated unit's start, stop, and failure messages. +These are host-side records. No logging daemon or rotating file runs in the +container. + +Use both service and container views during diagnosis: + +```console +journalctl --user -u nginx-ubi9.service --since today +journalctl --user -u nginx-ubi9.service --follow +podman logs --since 10m nginx-ubi9 +podman inspect --format '{{.HostConfig.LogConfig.Type}}' nginx-ubi9 +``` + +Do not assume `journalctl --user` is durable merely because it displays current +events. The host owner must configure and verify journal storage, maximum use, +free-space reserve, rate limiting, retention, reader authorization, forwarding, +and alerting for dropped or stalled logs. Persistent journal storage is also +required for complete `--user` journal views. Central forwarding should be +authenticated and encrypted and must preserve the container name, image digest, +host, timestamp, boot/restart identity, stream, and configured NGINX fields. + +See [Logging](LOGGING.md) for the application field contract and sensitive-data +rules. Journald protects storage and access according to the host policy; it +does not by itself establish non-repudiation or an off-host immutable archive. + +## Deployment cybersecurity package + +Cybersecurity review should receive evidence for the exact deployment, not a +generic statement that the image is hardened: + +- authorization boundary, data flows, ports, protocols, upstreams, DNS, trust + anchors, administrators, and external services; +- image digest plus signature, provenance, SBOM, vulnerability, license, and + tailored SCAP results; +- host/RHEL, kernel, Podman, OCI runtime, cgroup, SELinux, systemd, and journald + versions and effective configuration; +- Quadlet and mounted-configuration digests, runtime inspection, process UID, + capabilities, `NoNewPrivs`, mount flags, resource limits, and network rules; +- TLS and secret inventory, ownership, issuance, rotation, expiry monitoring, + revocation limitations, and negative-test results; +- log schema, destination, time source, access rules, retention, capacity, + forwarding, alerting, failure tests, and disposal; +- availability objectives, capacity and denial-of-service tests, health and + shutdown evidence, monitoring, incident response, patching, update, rollback, + controlled transfer, and decommissioning procedures; and +- the applicable control matrix with inherited, image-owned, deployment-owned, + host-owned, organization-owned, not-applicable, and residual-risk decisions. + +The [threat model](THREAT-MODEL.md) identifies threats and trust boundaries; +the [security-control guide](SECURITY-CONTROLS.md) defines ownership and the +component evidence supplied to a system-level authorization package. + +## Qualification checklist + +Before this profile becomes supported, test on every claimed architecture and +host/runtime combination: + +- boot without an interactive login, logout persistence, orderly host reboot, + unexpected process exit, restart throttling, and no restart after an + intentional stop; +- declared and arbitrary non-root identities, read-only mounts, SELinux + enforcement, capability and `NoNewPrivs` inspection, resource exhaustion, + disk pressure, and temporary-directory failure; +- configuration validation, reload under traffic, graceful stop with active + connections, update, failed update, and rollback; +- HTTP and TLS positive and negative cases, approved ingress/egress, DNS + failure, upstream failure, source-address behavior, and firewall rules; and +- access/error/lifecycle/health events, persistent journal behavior, restart + correlation, forwarding interruption, rate-limit loss, retention, access + control, and recovery without secret disclosure. + +Record exact commands, results, limitations, configuration hashes, timestamps, +and evidence locations in the release-candidate qualification record. + +## Authoritative references + +- [Podman Quadlet and rootless search paths](https://docs.podman.io/en/v5.3.2/markdown/podman-systemd.unit.5.html) +- [Current Podman container-unit options](https://docs.podman.io/en/latest/markdown/podman-container.unit.5.html) +- [RHEL 9 container management and lingering](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/htmlsingle/building_running_and_managing_containers/) +- [systemd journal querying](https://www.freedesktop.org/software/systemd/man/255/journalctl.html) +- [systemd journal storage and limits](https://www.freedesktop.org/software/systemd/man/252/journald.conf.html) diff --git a/docs/LOGGING.md b/docs/LOGGING.md index 8b1c492..dad40f2 100644 --- a/docs/LOGGING.md +++ b/docs/LOGGING.md @@ -102,6 +102,24 @@ paths, so record `podman info` with qualification evidence. Use `podman logs` or the host journal instead of attempting to tail the stream symlinks inside the container. +On a standalone Linux host, the preview deployment profile explicitly selects +Podman's `journald` log driver. The rootless Quadlet is managed by the service +account's user systemd instance: + +```console +journalctl --user -u nginx-ubi9.service --since today +journalctl --user -u nginx-ubi9.service --follow +podman logs --since 10m nginx-ubi9 +``` + +The journal contains NGINX stdout/stderr plus Podman and systemd lifecycle +context. Persistence, size limits, free-space reserve, rate limiting, retention, +reader access, forwarding, integrity protection, and alerting remain host-owned. +Do not infer durable collection from a successful `journalctl` query; validate +boot persistence, restart correlation, collector interruption, rate-limit loss, +and disk-pressure behavior on the exact host. See [Deployment](DEPLOYMENT.md) +for the Quadlet and qualification procedure. + Docker uses the same stream contract: ```console diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 81d0e8b..665971f 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -9,6 +9,37 @@ release asset, or qualification record. Automated success is not sufficient where an item requires human analysis, an external environment, or a support decision. +## Next action: upgrade the existing WSL2 Podman environment + +This is the first work item for the next development session. Keep the current +Ubuntu WSL2 distribution; do not replace it solely to obtain Podman. + +- [ ] Export or otherwise back up the WSL2 development distribution and record + its current Ubuntu, kernel, systemd, Podman, OCI runtime, cgroup, storage, and + rootless configuration. +- [ ] Select a maintained, trustworthy upgrade path for a current Podman and + its compatible Buildah, conmon, crun, networking, and containers-common + dependencies. Do not use an unreviewed or bleeding-edge package source. +- [ ] Fully update the existing Ubuntu installation from its maintained + repositories, review the release-upgrade plan and available disk space, and + preserve the WSL export before package removal or distribution changes. +- [ ] Remove the currently observed Ubuntu 22.04.5 `podman 3.4.4` package + before installing the newer Podman package. Preserve or deliberately retire + rootless container storage; do not use package removal as an implicit data- + deletion operation. +- [ ] Complete the approved Ubuntu maintenance/release upgrade, fully update + the resulting system, and install the selected newer Podman plus its matched + dependencies from the maintained source. +- [ ] Verify rootless operation, cgroup v2 delegation, systemd user services, + subordinate IDs, networking, SELinux/AppArmor status, and the Quadlet + generator. Quadlet requires Podman 4.6 or newer, but the selected version + must also meet the project's complete runtime and support criteria. +- [ ] Load or build the development image and rerun the complete Bash smoke + suite without skipped assertions; retain the engine and result evidence. +- [ ] Dry-run and exercise the preview Quadlet, including health, reload, + journald, restart, stop, boot/login behavior, update, and rollback. WSL2 + remains development evidence and does not replace target RHEL qualification. + ## Evidence lifecycle Evidence has three levels: @@ -63,11 +94,14 @@ Work proceeds in this dependency order: profiles needed for the first supported image; keep additional profiles explicitly preview until their tests close. 6. Complete the repository policy files, support boundary, threat model, - control ownership, vulnerability policy, and tailored SCAP evidence needed - for cyber review. -7. Rehearse the multi-architecture publish, provenance, SBOM, signing, and + requirement analysis, control ownership, vulnerability policy, tailored + SCAP evidence, and deployment cyber package needed for review. +7. Qualify standalone rootless Podman/Quadlet deployment, systemd lifecycle, + journald collection, controlled-network operation, and rollback on an exact + supported Linux host. +8. Rehearse the multi-architecture publish, provenance, SBOM, signing, and verification workflow from an untagged release candidate. -8. Freeze inputs, regenerate release-candidate evidence, approve findings, +9. Freeze inputs, regenerate release-candidate evidence, approve findings, create the immutable tag, publish by digest, and verify the release. Steps 1 through 4 are the immediate engineering critical path. Steps 5 and 6 @@ -137,7 +171,21 @@ local evaluation but is not yet a supported release. - [ ] Prove that release assembly cannot pull images, reach package networks, recalculate dependencies, or expose acquisition credentials. -## Package 5: SCAP and cyber-review package +## Package 5: security engineering and cyber-review package + +- [ ] Establish the authoritative requirement-source register with publisher, + title, release, date, retrieval date, URL, SHA-256, status, and license. +- [ ] Compare applicable NIST SP 800-53/53A, DISA Container Platform and + web/application-server guidance, RHEL 9 STIG content, and product behavior; + require independent review of applicability and mappings. +- [ ] Classify each requirement as image-owned, deployment-supported, + inherited, not applicable, unsupported, or research required, with rationale + and residual risk. +- [ ] Publish a schema-validated NIST OSCAL Component Definition and generate + deterministic CSV and human-readable control views from the same source. +- [ ] Give every supported control an examine/test/interview assessment method, + owner, defaults, configuration and restart behavior, dependencies, impact, + loss-of-function statement, limitations, and evidence pointer. - [ ] Perform discovery with pinned OpenSCAP and ComplianceAsCode content against a root-owner-preserving export of each architecture image. @@ -153,16 +201,42 @@ local evaluation but is not yet a supported release. trust, logs, denial of service, upstreams, DNS, writable storage, evidence integrity, and updates. - [ ] Publish a control matrix mapping requirements, implementation, - configuration, validation, evidence, owner, limitations, and residual risk. + configuration, validation, evidence, owner, limitations, and residual risk. +- [ ] Define the cryptographic boundary and document why TLS configuration and + a UBI base do not independently establish FIPS validation. - [ ] Document vulnerability triage, patch SLAs, exceptions with expiry, incident response, backup/restore responsibilities, logging integration, monitoring, resource limits, network policy, disconnected deployment, and decommissioning. - [ ] Maintain a qualification ledger keyed by commit, image digest, architecture, inputs, runtime/platform versions, configuration, scanner - versions/databases, result, limitations, evidence level, and artifact. + versions/databases, result, limitations, evidence level, and artifact. -## Package 6: signed first release +## Package 6: deployment and platform qualification + +- [ ] Publish supported, compatible, preview/unqualified, and unsupported + definitions plus an exact matrix for architecture, host, Podman/OCI runtime, + Docker compatibility, OpenShift, configuration profiles, TLS modes, + controlled-network operation, SCAP, and FIPS claims. +- [ ] Qualify the rootless standalone-host Quadlet on an exact supported RHEL 9 + baseline, including cgroup v2, SELinux enforcing, subordinate IDs, lingering, + boot, logout, restart throttling, health, reload, graceful stop, update, and + rollback. +- [ ] Qualify journald persistence, rate and capacity limits, access control, + restart correlation, authenticated forwarding, forwarding interruption, + storage pressure, retention, and disposal without sensitive-data leakage. +- [ ] Publish deployment guidance for identity, configuration/content/secrets, + TLS, ingress/egress/DNS, resource limits, probes, monitoring, incident + response, update, rollback, controlled transfer, and decommissioning. +- [ ] Test every supported configuration with positive, negative, restricted- + runtime, load/failure, and logging cases on each claimed platform. +- [ ] Define go-live evidence for the exact image/configuration digest, + platform, controls, capacity, alerting, contacts, exceptions, and procedures. +- [ ] Decide the OpenShift first-release support boundary from exact restricted- + SCC qualification; retain preview status if the required cluster evidence is + unavailable. + +## Package 7: signed first release - [ ] Implement and test the approved `v-r.` tag contract, UTC date and @@ -174,7 +248,8 @@ local evaluation but is not yet a supported release. vendor advisories; document every time-bounded acceptance. - [ ] Complete license and third-party notice review. - [ ] Regenerate native architecture, rootless Podman, TLS, controlled-network, - and tailored SCAP release-candidate evidence from the exact candidate. + standalone Quadlet/systemd/journald, and tailored SCAP release-candidate + evidence from the exact candidate. - [ ] Rehearse tag validation and the entire release workflow without granting broader permissions than production needs. - [ ] Publish an AMD64/ARM64 manifest to GHCR with BuildKit provenance and @@ -183,6 +258,21 @@ local evaluation but is not yet a supported release. - [ ] Verify published digests, platforms, labels, signatures, attestations, SBOMs, scan artifacts, documentation links, and rollback instructions. +## Assurance completeness gate + +Before release, review this repository against the complete assurance model +below and record any intentionally omitted item with an NGINX-specific +rationale. The review must cover evidence lifecycle, +support semantics, qualification ledger, architecture and trust-boundary +diagrams, rootless runtime and platform qualification, use-case profiles, +TLS and FIPS boundaries, authoritative requirement analysis, control ownership +and OSCAL export, tailored SCAP, vulnerability and exception management, +licensing and notices, supply-chain evidence, controlled-network procedures, +production go-live evidence, release rehearsal, failed-candidate handling, +rollback, incident response, and evidence retention. Database storage, +replication, and backup requirements are included only when an NGINX profile +introduces equivalent durable state. + ## After the first release - [ ] Define support and qualification requirements for stream TCP/UDP proxying. diff --git a/docs/RPM-PROVENANCE.md b/docs/RPM-PROVENANCE.md index 0ddf35e..1a1873a 100644 --- a/docs/RPM-PROVENANCE.md +++ b/docs/RPM-PROVENANCE.md @@ -77,10 +77,8 @@ podman run --rm --entrypoint dnf \ localhost/nginx-ubi9-builder:development repoinfo ubi-9-appstream-rpms ``` -On PowerShell, place each command on one line or replace each `\` continuation -with a backtick. CI generates a Syft SPDX inventory for each native -architecture and scans that inventory with Grype in addition to Trivy's image -scan. +CI generates a Syft SPDX inventory for each native architecture and scans that +inventory with Grype in addition to Trivy's image scan. The runtime image deliberately has no `rpm`, `dnf`, `microdnf`, or `yum` command. Absence of those commands reduces runtime tooling but does not erase diff --git a/docs/SECURITY-CONTROLS.md b/docs/SECURITY-CONTROLS.md new file mode 100644 index 0000000..7544796 --- /dev/null +++ b/docs/SECURITY-CONTROLS.md @@ -0,0 +1,127 @@ +# Security controls and cyber-review evidence + +This repository supplies component-level implementation descriptions and +evidence. It cannot produce a completed system Security Control Traceability +Matrix (SCTM), System Security Plan, risk acceptance, or authorization because +those depend on the deployed boundary, baseline, overlays, organization-defined +parameters, inherited services, data impact, and assessor decisions. + +The planned machine-readable deliverable is a NIST OSCAL Component Definition +with deterministic CSV and human-readable views. It will describe how the image +can support a control objective and how to examine and test it; it will not claim +that a deployed system is authorized. + +## Ownership model + +| Owner | Examples of responsibility | +| --- | --- | +| Image project | Verified inputs, minimal package-manager-free filesystem, non-root default, safe baseline configuration, documented writable paths and log streams, SBOM/provenance/signature, native tests, vulnerability response, and image-owned SCAP rules. | +| Deployment profile | Digest pinning, mounted configuration/content/trust/secrets, TLS mode, published ports, upstream and DNS policy, capabilities, resource limits, probes, reload/rollback, and workload-specific tests. | +| Host or orchestrator | Kernel/runtime patching, namespaces, cgroups, seccomp, SELinux, firewall/NetworkPolicy, node identity, time, journal/collector, secret service, admission policy, availability, and node monitoring. | +| Organization/system | Control baseline and parameters, data classification, identities and separation of duties, central PKI/logging/SIEM, incident response, continuous monitoring, exceptions, evidence retention, physical controls, and authorization. | + +A control can be shared by more than one owner. Every matrix row must state the +part this component implements, what must be configured, what is inherited, and +what remains for the system owner. + +## Initial control families + +| Control objective | Image contribution | Deployment/host contribution | Verification evidence | +| --- | --- | --- | --- | +| Software integrity and provenance | Pinned inputs, external verification contract, package inventory, SBOM, provenance, digest signature. | Verify at acquisition and admission; protect registry/transfer; retain approved digest mapping. | Locks, verification logs, attestations, signature result, image digest. | +| Least privilege and isolation | UID `999:0`, high ports, no privileged entrypoint, no package manager in final image. | Rootless runtime, dedicated account, drop all capabilities, no new privileges, seccomp, SELinux, read-only root, narrow mounts. | Image config, `/proc` status, mount/capability inspection, SELinux/runtime record, negative writes. | +| Secure configuration and change control | Reviewed baseline and tested examples; configuration test and reload interface. | Approve and hash mounted files; protect writers; stage, validate, reload, monitor, and roll back. | Configuration digest/review, `nginx -t`, functional/negative tests, change ticket and rollback result. | +| Identification, authentication, and authorization | Does not invent application identity; can enforce mTLS in a qualified profile. | Application identity, trusted proxy chain, PKI, secret store, administrator RBAC, and break-glass controls. | Architecture/data-flow review, identity configuration, certificate tests, access review. | +| Communications protection | Planned TLS 1.2/1.3 ingress and verified upstream examples. | Certificates/keys/trust, renewal, revocation decision, firewall and egress policy, approved termination boundary. | TLS scans and negative tests, key permissions, expiry/rotation evidence, network rules. | +| Audit and accountability | Access/error streams, reviewed structured fields, health-log suppression, sensitive-data rules. | Journald/collector/SIEM, time sync, access, forwarding, capacity, retention, alerting, integrity protection, disposal. | Sample events, schema/config digest, journal/collector settings, failure and access tests. | +| Vulnerability and flaw remediation | Scheduled independent scans, full inventory, advisory triage, rebuild and exception policy. | Host/platform scanning, deployment exposure analysis, promotion cadence, patch window, incident process. | Scanner/tool/database metadata, vendor advisory analysis, owner/expiry, rebuilt digest. | +| Resource protection and availability | Small image, bounded writable-path contract, health metadata, graceful stop signal. | CPU/memory/PID/file/tmpfs/connection limits, external rate and DDoS controls, redundancy, capacity and failure tests. | Runtime limits, load/soak results, restart/health/shutdown tests, alerts. | +| System and information integrity | Immutable image and configuration validation; server-version suppression. | Admission by digest/signature, file/change monitoring, network detection, incident containment and recovery. | Admission result, runtime drift/config review, alert and incident exercises. | +| Media, transfer, and controlled networks | Source-independent verified artifact bundle and offline-capable assembly design. | Approved transfer, malware inspection, hash verification at both boundaries, internal registry/trust, offline data refresh. | Transfer record, hashes/digests, internal mapping, disconnected rehearsal. | +| Assessment and continuous monitoring | Evidence lifecycle, native architecture jobs, qualification ledger, tailored SCAP plan. | Select baseline/depth, assess inherited controls, review changes and evidence, approve residual risks. | Component matrix/OSCAL, qualification record, assessment procedure and findings. | + +## Requirement analysis method + +The system's selected NIST SP 800-53 Rev. 5 baseline, overlays, and parameters +define the actual requirements. NIST SP 800-53A supplies assessment structure. +Where the deployment is subject to DISA guidance, review the current Container +Platform SRG and RHEL 9 STIG for objectives genuinely owned by the image or +host. A web-server or application-server SRG may inform NGINX behavior only +after applicability is analyzed. Product-specific fixes must never be copied +as if they automatically apply. + +For every authoritative source, record publisher, title, release/version, +release date, retrieval date, URL, SHA-256, status, and license. Compare every +in-scope requirement and assign one disposition: + +- adopted as an image implementation; +- supported through deployment configuration; +- inherited from host/platform/organization; +- not applicable with rationale; +- unsupported with documented consequence; or +- research required before a claim is made. + +Every adopted or supported row needs stable source identifiers and wording, +implementation detail, defaults, enable/disable and restart behavior, +dependencies, operational impact, loss of function, owner, limitations, +residual risk, and an assessment procedure with examine, test, and—where +needed—interview steps. Source interpretation and mapping require independent +review before first release. + +## Evidence quality and lifecycle + +Evidence progresses from development to integration to release candidate. The +release-candidate record binds the exact commit and image digest to architecture, +base and RPM inputs, configuration profile/digest, host/runtime/platform, +scanner/tool/database versions, result, limitations, reviewer, artifact +location, and retention. Image-affecting or assessment-method changes invalidate +the affected candidate evidence but never erase historical records. + +Automated tests are strongest for deterministic component behavior. They do not +replace examination of architecture and procedures or interviews about actual +operations. A passing control matrix row must not be inferred from a green job +whose scope does not match the requirement. + +## Required cyber-review deliverables + +Before first release, publish or retain as appropriate: + +- system-context, runtime-data-flow, trust-boundary, TLS-flow, assurance-pipeline, + controlled-network, and control-ownership diagrams; +- the [threat model](THREAT-MODEL.md) and reviewed residual-risk register; +- OSCAL Component Definition, generated component matrix, source-comparison + register, schema validation, and traceability/change checks; +- support matrix and qualification ledger for exact architectures, hosts, + runtimes, platforms, configuration profiles, TLS modes, and evidence levels; +- image digest, SBOM, provenance, signature, licensing/notices, module and RPM + inventory, tailored SCAP, vulnerability and exception evidence; +- deployment runbook and exact runtime configuration, logging, monitoring, + incident, update, rollback, controlled-network, and decommission procedures; + and +- a limitation statement distinguishing component evidence from system-level + compliance, accreditation, certification, and authorization. + +See [Deployment](DEPLOYMENT.md) for the standalone-host evidence checklist and +[Roadmap](ROADMAP.md) for the forward-looking implementation gates. + +## SCAP and FIPS boundaries + +An unmodified RHEL host profile is not an image release gate. Kernel, boot, +partition, systemd, audit-daemon, host-network, SELinux-enforcement, and host +crypto-policy checks are normally host owned. The project will select only +reviewed image-filesystem rules, preserve numeric ownership during offline +inspection, separate operational scanner errors from findings, and publish all +selection/exclusion rationales. Passing those rules is not STIG certification. + +TLS protocol configuration is not a FIPS claim. FIPS status depends on the +host mode, exact cryptographic modules and validations, NGINX linkage and code +paths, certificate algorithms, and complete deployed boundary. The first +release will state that boundary and evidence gaps rather than infer validation +from UBI 9 or TLS 1.2/1.3. + +## Authoritative references + +- [NIST OSCAL Component Definition model](https://pages.nist.gov/OSCAL/learn/concepts/layer/implementation/component-definition/) +- [NIST SP 800-53 Rev. 5](https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final) +- [NIST SP 800-53A Rev. 5](https://csrc.nist.gov/pubs/sp/800/53/a/r5/final) +- [DISA STIG document library](https://public.cyber.mil/stigs/downloads/) diff --git a/docs/THREAT-MODEL.md b/docs/THREAT-MODEL.md new file mode 100644 index 0000000..0b0ab8f --- /dev/null +++ b/docs/THREAT-MODEL.md @@ -0,0 +1,88 @@ +# Threat model + +This component threat model covers the `nginx-ubi9` source, build and release +pipeline, OCI image, configuration interface, and representative deployment. +Each system owner must extend it for the actual application, data, network, +identity, availability objectives, platform, and authorization boundary. + +## Security objectives and assets + +The objectives are to preserve the integrity and provenance of the image; +serve or proxy only approved traffic; protect TLS keys, credentials, content, +configuration, upstream trust, and logs; limit the effect of compromise; keep +the service available within its stated capacity; and produce trustworthy, +reviewable evidence. + +Assets include source and workflows, artifact locks and signing identities, +base images and RPMs, the published image and attestations, NGINX configuration, +site content, TLS private keys and trust stores, request and upstream data, +DNS and routing decisions, logs, vulnerability data, and qualification records. + +## Actors and trust boundaries + +Relevant actors are maintainers and reviewers, CI and registry identities, +artifact publishers, deployment administrators, clients, upstream services, +log operators, host/platform administrators, and an attacker controlling a +client, network path, upstream, dependency source, contributor account, or +compromised workload. + +Trust boundaries exist between: + +1. upstream publishers and the acquisition/verification job; +2. repository changes, protected CI, and release identities; +3. the registry or transfer process and the deployment host; +4. host kernel/runtime controls and the container user namespace; +5. client ingress and NGINX; +6. NGINX, DNS, and upstream services; +7. mounted configuration/content/secrets and the immutable image; +8. stdout/stderr, the runtime log driver, journald or cluster collection, and + the central security platform; and +9. image evidence and the larger system authorization decision. + +Rootless execution reduces host privilege available to a compromised NGINX +process. It does not protect against malicious configuration, authorized host +root, kernel/runtime flaws, data disclosed by proxy policy, denial of service +within allowed resources, or a trusted upstream returning hostile content. + +## Threat and treatment register + +| ID | Threat and consequence | Primary treatment | Owner and residual risk | +| --- | --- | --- | --- | +| TM-01 | A substituted base image, RPM, key, or dependency enters the build. | Protected lock updates; authoritative source; digest/checksum, RPM signature, fingerprint, NEVRA, architecture, and closure verification before a network-disabled build. | Project/CI. Publisher or signing-key compromise remains; use review, key-rotation procedure, provenance, and incident response. | +| TM-02 | A malicious or compromised workflow exfiltrates credentials or publishes an unreviewed image. | Pull-request review, immutable action references, least-privilege tokens, isolated jobs, protected environments/tags, OIDC, and digest-bound signing. | Repository administrators. Platform and maintainer-account compromise remain. | +| TM-03 | A deployment uses the wrong, mutable, unsigned, or failed candidate. | Promote and deploy by digest; verify signature, provenance, SBOM, platforms, and release status; quarantine failed candidates; prohibit first-release mutable tags. | Release and deployment owners. Verification-policy misconfiguration remains. | +| TM-04 | NGINX gains host privilege after request-processing compromise. | Dedicated rootless account, non-root container UID, all capabilities dropped, `no-new-privileges`, default seccomp, enforcing SELinux, read-only root, narrow mounts, no engine socket or host namespaces. | Image, host, and deployment. Host kernel/runtime vulnerabilities and host administrator access remain. | +| TM-05 | Configuration or mounted content is altered to expose files, redirect traffic, weaken TLS, or execute an unintended module. | Read-only dedicated mounts; root-owned immutable image programs; configuration review, digesting, `nginx -t`, change control, module inventory, reload tests, and rollback. | Deployment and project examples. An approved but unsafe NGINX directive remains possible. | +| TM-06 | Credentials, SQL, tokens, personal data, or internal details leak through logs. | Structured allow-listed fields, query-string exclusion, no bodies or credential headers, JSON escaping, restricted readers, retention policy, and adversarial logging tests. | Image configuration and logging operators. NGINX errors and deployment-added fields require separate review. | +| TM-07 | Spoofed forwarding or correlation headers create false identity or poisoned logs. | Define trusted proxy hops; overwrite or validate identifiers; bound length/characters; escape fields; never treat a client header as authenticated identity without a trust rule. | Deployment. Rootless network forwarding may change source addresses and must be qualified. | +| TM-08 | Ingress TLS is downgraded or keys are disclosed. | TLS 1.2/1.3 profile, reviewed ciphers, read-only operator-managed secrets, least-readable permissions, renewal/rotation rehearsal, expiry alerts, and negative protocol/certificate tests. | Deployment. FIPS is a separate boundary; revocation availability and CA compromise remain. | +| TM-09 | Upstream TLS accepts an attacker or DNS routes traffic to an unapproved service. | Certificate-chain and hostname verification, explicit trust store, stable approved names, resolver/timeouts, egress allow-list, DNS protection, and negative trust/name tests. | Deployment/network. Approved DNS, CA, or upstream compromise remains. | +| TM-10 | Proxy behavior enables request smuggling, header confusion, cache poisoning, open proxying, or unintended methods/routes. | Minimal explicit routes and headers, no forward-proxy default, safe timeout/body/header limits, server-token suppression, configuration-specific positive and negative tests, and upstream application validation. | Project examples and application owner. Protocol parser differences require continuing advisory review. | +| TM-11 | Request, connection, WebSocket, upstream, log, or temporary-storage exhaustion denies service or host capacity. | Cgroup memory/PID/CPU limits, bounded tmpfs, file-descriptor and NGINX connection limits, rate limits based on trusted identity, timeouts, body limits, log capacity/rate controls, load and failure testing, and upstream protection. | Deployment/host. A single instance has no inherent high availability and volumetric attacks require external capacity controls. | +| TM-12 | Writable paths or special files enable persistence or execution. | Package-manager-free final image, read-only root, only documented bounded tmpfs, `noexec,nosuid,nodev`, no unnecessary devices, and runtime mount/invariant tests. | Image/deployment. Memory-resident compromise persists until process replacement. | +| TM-13 | Logs or evidence are changed, dropped, flooded, expired, or incorrectly associated with a digest. | Off-host authenticated forwarding, time synchronization, access separation, capacity/retention monitoring, immutable or protected storage where required, failure tests, and evidence records bound to commit/digest/config/tool databases. | Host, logging, and assurance owners. Local journald alone is not non-repudiation. | +| TM-14 | A vulnerability remains unpatched or an update breaks security/availability. | Scheduled scans and advisory review, time-bounded exceptions, update SLAs, SBOM delta review, native tests, canary/staging validation, immutable previous digest, and rehearsed rollback. | Project and deployment owners. Unfixed or unknown vulnerabilities remain subject to risk acceptance. | +| TM-15 | Controlled-network transfer or stale offline data invalidates trust claims. | Hash/signature verification at both boundaries, recorded digest mapping, approved media/transfer station, mirrored trust and scanner data with age recorded, and a fully disconnected rehearsal. | Environment owner. Offline evidence is point-in-time and cannot claim current public intelligence. | +| TM-16 | Health checks show configuration validity while the proxied application is unavailable. | Distinguish startup, liveness, readiness, and external transaction monitoring; keep probes low-cost and non-sensitive; test upstream failure. | Deployment/application. The image health check intentionally does not prove end-to-end service health. | + +## Abuse cases that must be tested + +Configuration profiles should test oversized and malformed request lines and +headers, encoded paths, unsupported methods, untrusted forwarding headers, +control characters in logged values, sensitive query strings, slow clients, +connection floods, WebSocket idle behavior, upstream retry behavior, DNS and +TLS failures, full tmpfs, failed log forwarding, reload under traffic, and +termination with active connections. + +Build and release tests should reject tampered, unsigned, wrong-version, +wrong-architecture, missing, and unexpected artifacts; mutable or malformed +release tags; absent attestations; scanner operational errors; and mismatched +native architecture evidence. + +## Review and maintenance + +Review this model whenever NGINX or UBI inputs, compiled modules, acquisition, +workflows, registry, runtime, network mode, supported configuration, log schema, +TLS boundary, or scanner content changes. Each accepted risk needs an owner, +compensating control, expiry or review date, and evidence. A clean scanner +result does not close threats that require design review or behavioral tests. diff --git a/docs/examples/systemd/nginx-ubi9.container b/docs/examples/systemd/nginx-ubi9.container new file mode 100644 index 0000000..ac9b6a1 --- /dev/null +++ b/docs/examples/systemd/nginx-ubi9.container @@ -0,0 +1,39 @@ +# Preview rootless Podman Quadlet for a standalone Linux host. +# Replace the digest before installation. Paths use the service account's home. + +[Unit] +Description=Datopsis rootless NGINX container +Documentation=https://github.com/datopsis/nginx-ubi9 + +[Container] +Image=ghcr.io/datopsis/nginx-ubi9@sha256: +Pull=never +ContainerName=nginx-ubi9 +User=999:0 +PublishPort=127.0.0.1:8080:8080 +ReadOnly=true +ReadOnlyTmpfs=false +Tmpfs=/tmp:rw,noexec,nosuid,nodev,size=64m,mode=1777 +DropCapability=all +NoNewPrivileges=true +PidsLimit=128 +Memory=512m +ShmSize=16m +Ulimit=nofile=1024:4096 +Volume=%h/nginx-ubi9/nginx.conf:/etc/nginx/nginx.conf:ro,Z +Volume=%h/nginx-ubi9/conf.d:/etc/nginx/conf.d:ro,Z +Volume=%h/nginx-ubi9/html:/usr/share/nginx/html:ro,Z +LogDriver=journald +HealthOnFailure=kill +StopSignal=SIGQUIT +StopTimeout=30 + +[Service] +Restart=on-failure +RestartSec=5s +TimeoutStartSec=90 +TimeoutStopSec=45 +ExecReload=/usr/bin/podman kill --signal HUP nginx-ubi9 + +[Install] +WantedBy=default.target diff --git a/tests/smoke.ps1 b/tests/smoke.ps1 deleted file mode 100644 index 81e8075..0000000 --- a/tests/smoke.ps1 +++ /dev/null @@ -1,257 +0,0 @@ -param( - [string]$ContainerRuntime = "podman", - [string]$Image = "localhost/nginx-ubi9:development" -) - -$ErrorActionPreference = "Stop" -$prefix = "nginx-ubi9-smoke-$PID" -$fixedName = "$prefix-fixed" -$arbitraryName = "$prefix-arbitrary" -$missingTmpName = "$prefix-missing-tmp" -$invalidConfigName = "$prefix-invalid-config" - -function Invoke-ContainerRuntime { - param([Parameter(ValueFromRemainingArguments)] [string[]]$Arguments) - - $previousPreference = $ErrorActionPreference - $ErrorActionPreference = "Continue" - try { - & $ContainerRuntime @Arguments - $runtimeExitCode = $LASTEXITCODE - } - finally { - $ErrorActionPreference = $previousPreference - } - if ($runtimeExitCode -ne 0) { - throw "$ContainerRuntime command failed: $($Arguments -join ' ')" - } -} - -function Get-ContainerLogs { - param([string]$Name) - - $previousPreference = $ErrorActionPreference - $ErrorActionPreference = "Continue" - try { - $records = & $ContainerRuntime logs $Name 2>&1 - $runtimeExitCode = $LASTEXITCODE - } - finally { - $ErrorActionPreference = $previousPreference - } - if ($runtimeExitCode -ne 0) { - throw "Unable to read container logs for $Name" - } - return @($records | ForEach-Object { $_.ToString() }) -} - -function Wait-Nginx { - param([string]$Name) - - foreach ($attempt in 1..30) { - & $ContainerRuntime exec $Name nginx -t -q 2>$null - if ($LASTEXITCODE -eq 0) { - return - } - Start-Sleep -Seconds 1 - } - - & $ContainerRuntime logs $Name - throw "NGINX did not become ready in $Name" -} - -function Assert-ProcessSecurity { - param([string]$Name) - - $script = 'for status in /proc/[0-9]*/status; do uid=; cap_eff=; no_new_privs=; while IFS=: read -r key value; do case ${key} in Uid) set -- ${value}; uid=$1 ;; CapEff) set -- ${value}; cap_eff=$1 ;; NoNewPrivs) set -- ${value}; no_new_privs=$1 ;; esac; done < ${status}; test -n ${uid}; test ${uid} -ne 0; test ${cap_eff} = 0000000000000000; test ${no_new_privs} = 1; done' - Invoke-ContainerRuntime exec $Name sh -eu -c $script -} - -function Assert-TmpfsSecurity { - param([string]$Name) - - $script = 'found=; while read -r device mount_point filesystem options remainder; do if test ${mount_point} = /tmp; then found=1; case ,${options}, in *,rw,*) : ;; *) exit 1 ;; esac; case ,${options}, in *,noexec,*) : ;; *) exit 1 ;; esac; case ,${options}, in *,nosuid,*) : ;; *) exit 1 ;; esac; case ,${options}, in *,nodev,*) : ;; *) exit 1 ;; esac; fi; done < /proc/mounts; test ${found} = 1; cp /bin/true /tmp/noexec-probe; chmod 0700 /tmp/noexec-probe; ! /tmp/noexec-probe >/dev/null 2>&1; rm -f /tmp/noexec-probe' - Invoke-ContainerRuntime exec $Name sh -eu -c $script -} - -function Assert-FailedContainer { - param([string]$Name) - - foreach ($attempt in 1..15) { - $state = & $ContainerRuntime inspect --format "{{.State.Status}}" $Name - if ($LASTEXITCODE -ne 0) { - throw "Unable to inspect failed container $Name" - } - if ($state -ne "running") { - $containerExitCode = & $ContainerRuntime inspect ` - --format "{{.State.ExitCode}}" $Name - if ($LASTEXITCODE -ne 0 -or [int]$containerExitCode -eq 0) { - throw "Expected container $Name to exit with a failure" - } - return - } - Start-Sleep -Seconds 1 - } - throw "Expected container $Name to exit within 15 seconds" -} - -function Assert-CleanStop { - param([string]$Name) - - Invoke-ContainerRuntime stop --time 10 $Name | Out-Null - $containerExitCode = & $ContainerRuntime inspect ` - --format "{{.State.ExitCode}}" $Name - if ($LASTEXITCODE -ne 0 -or [int]$containerExitCode -ne 0) { - throw "Expected a clean exit from $Name; received $containerExitCode" - } -} - -try { - $runtimeVersion = & $ContainerRuntime --version - if ($LASTEXITCODE -ne 0) { - throw "Unable to determine the container runtime version" - } - $missingTmpRuntimeArguments = @() - if (($runtimeVersion -join "`n") -match "podman") { - # Podman otherwise creates writable tmpfs mounts for read-only containers. - $missingTmpRuntimeArguments += "--read-only-tmpfs=false" - } - - $configuredUser = & $ContainerRuntime image inspect --format "{{.Config.User}}" $Image - if ($LASTEXITCODE -ne 0 -or $configuredUser -ne "999:0") { - throw "Expected image user 999:0; received $configuredUser" - } - - Invoke-ContainerRuntime run --detach --name $fixedName ` - --read-only ` - --tmpfs "/tmp:rw,noexec,nosuid,nodev,size=64m,mode=1777" ` - --cap-drop ALL ` - --security-opt "no-new-privileges:true" ` - --publish "127.0.0.1::8080" ` - $Image | Out-Null - Wait-Nginx $fixedName - - $fixedUid = & $ContainerRuntime exec $fixedName id -u - $fixedGid = & $ContainerRuntime exec $fixedName id -g - if ($fixedUid -ne "999" -or $fixedGid -ne "0") { - throw "Expected fixed identity 999:0; received ${fixedUid}:${fixedGid}" - } - - Assert-ProcessSecurity $fixedName - Assert-TmpfsSecurity $fixedName - Invoke-ContainerRuntime exec $fixedName nginx -t -q - Invoke-ContainerRuntime exec $fixedName sh -c "test ! -w /etc/nginx/nginx.conf" - Invoke-ContainerRuntime exec $fixedName sh -c ` - "! command -v dnf && ! command -v microdnf && ! command -v rpm && ! command -v yum" - Invoke-ContainerRuntime exec $fixedName sh -c ` - '! (printf probe > /root-filesystem-probe) >/dev/null 2>&1' - Invoke-ContainerRuntime exec $fixedName sh -c ` - 'read -r pid < /tmp/nginx.pid; test "${pid}" = "1"' - - $binding = & $ContainerRuntime port $fixedName "8080/tcp" - if ($LASTEXITCODE -ne 0) { - throw "Unable to determine the published health port" - } - $hostPort = ($binding -split ":")[-1] - $health = & curl.exe --fail --silent --show-error ` - "http://127.0.0.1:$hostPort/healthz" - if ($LASTEXITCODE -ne 0 -or $health -ne "ok") { - throw "Unexpected health response: $health" - } - $index = & curl.exe --fail --silent --show-error ` - "http://127.0.0.1:$hostPort/" - $indexText = $index -join "`n" - if ($LASTEXITCODE -ne 0 -or $indexText -notmatch "NGINX on UBI 9") { - throw "The static landing page was not served" - } - - $missingStatus = & curl.exe --silent --show-error --output NUL ` - --write-out "%{http_code}" ` - "http://127.0.0.1:$hostPort/missing?smoke-probe=value" - if ($LASTEXITCODE -ne 0 -or $missingStatus -ne "404") { - throw "Expected a 404 response; received $missingStatus" - } - - $headers = & curl.exe --fail --silent --show-error --dump-header - ` - --output NUL "http://127.0.0.1:$hostPort/healthz" - if ($LASTEXITCODE -ne 0 -or ` - -not ($headers | Where-Object { $_.Trim() -ceq "Server: nginx" })) { - throw "The Server header was missing or disclosed the NGINX version" - } - - $fixedLogs = Get-ContainerLogs $fixedName - if (($fixedLogs -join "`n") -notmatch "/missing\?smoke-probe=value") { - throw "Expected access event was not written to container logs" - } - if (($fixedLogs -join "`n") -match "GET /healthz") { - throw "The health endpoint unexpectedly wrote an access event" - } - - Invoke-ContainerRuntime exec $fixedName nginx -s reload - $healthAfterReload = & curl.exe --fail --silent --show-error ` - "http://127.0.0.1:$hostPort/healthz" - if ($LASTEXITCODE -ne 0 -or $healthAfterReload -ne "ok") { - throw "Health request failed after graceful reload" - } - $reloadLogs = Get-ContainerLogs $fixedName - if (($reloadLogs -join "`n") -notmatch "reconfiguring") { - throw "NGINX did not log the graceful reload" - } - - Invoke-ContainerRuntime run --detach --name $arbitraryName ` - --read-only ` - --tmpfs "/tmp:rw,noexec,nosuid,nodev,size=64m,mode=1777" ` - --cap-drop ALL ` - --security-opt "no-new-privileges:true" ` - --user "10001:0" ` - $Image | Out-Null - Wait-Nginx $arbitraryName - - $arbitraryUid = & $ContainerRuntime exec $arbitraryName id -u - $arbitraryGid = & $ContainerRuntime exec $arbitraryName id -g - if ($arbitraryUid -ne "10001" -or $arbitraryGid -ne "0") { - throw "Expected arbitrary identity 10001:0; received ${arbitraryUid}:${arbitraryGid}" - } - - Assert-ProcessSecurity $arbitraryName - Assert-TmpfsSecurity $arbitraryName - Invoke-ContainerRuntime exec $arbitraryName nginx -t -q - - Invoke-ContainerRuntime run --detach --name $missingTmpName ` - --read-only ` - @missingTmpRuntimeArguments ` - --cap-drop ALL ` - --security-opt "no-new-privileges:true" ` - $Image | Out-Null - Assert-FailedContainer $missingTmpName - $missingTmpLogs = Get-ContainerLogs $missingTmpName - if (($missingTmpLogs -join "`n") -notmatch ` - "read-only file system|/tmp/nginx.pid") { - throw "Missing writable /tmp did not produce an actionable diagnostic" - } - - Invoke-ContainerRuntime run --detach --name $invalidConfigName ` - --read-only ` - --tmpfs "/tmp:rw,noexec,nosuid,nodev,size=64m,mode=1777" ` - --cap-drop ALL ` - --security-opt "no-new-privileges:true" ` - --entrypoint sh ` - $Image -eu -c ` - 'echo "invalid_directive;" > /tmp/invalid.conf; exec nginx -t -c /tmp/invalid.conf' ` - | Out-Null - Assert-FailedContainer $invalidConfigName - $invalidLogs = Get-ContainerLogs $invalidConfigName - if (($invalidLogs -join "`n") -notmatch ` - "unknown directive.*invalid_directive|emerg") { - throw "Invalid configuration did not produce an actionable diagnostic" - } - - Assert-CleanStop $arbitraryName - Assert-CleanStop $fixedName - - Write-Output "Rootless restricted-runtime scenario tests passed for $Image" -} -finally { - & $ContainerRuntime rm --force $fixedName $arbitraryName ` - $missingTmpName $invalidConfigName 2>$null | Out-Null -} diff --git a/tests/smoke.sh b/tests/smoke.sh index 1e3490f..4235823 100644 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -9,10 +9,14 @@ arbitrary="${prefix}-arbitrary" missing_tmp="${prefix}-missing-tmp" invalid_config="${prefix}-invalid-config" missing_tmp_runtime_args=() +no_new_privileges="no-new-privileges:true" if "${runtime}" --version 2>&1 | grep -qi podman; then # Podman otherwise creates writable tmpfs mounts for read-only containers. missing_tmp_runtime_args+=(--read-only-tmpfs=false) + # Older supported-for-development Podman releases reject Docker's :true + # spelling but enforce the same security option with the bare name. + no_new_privileges="no-new-privileges" fi cleanup() { @@ -29,7 +33,7 @@ run_restricted() { --read-only \ --tmpfs /tmp:rw,noexec,nosuid,nodev,size=64m,mode=1777 \ --cap-drop ALL \ - --security-opt no-new-privileges:true \ + --security-opt "${no_new_privileges}" \ "$@" \ "${image}" >/dev/null } @@ -115,6 +119,23 @@ wait_for_exit() { return 1 } +wait_for_log() { + local name="$1" + local expected="$2" + local logs + local _ + for _ in {1..15}; do + logs="$("${runtime}" logs "${name}" 2>&1)" + if grep -Fq "${expected}" <<< "${logs}"; then + return + fi + sleep 1 + done + "${runtime}" logs "${name}" >&2 + echo "Timed out waiting for ${name} to log: ${expected}" >&2 + return 1 +} + wait_for_nginx() { local name="$1" local _ @@ -163,7 +184,7 @@ fi "${runtime}" exec "${primary}" nginx -s reload test "$(curl --fail --silent --show-error "http://127.0.0.1:${host_port}/healthz")" = "ok" -"${runtime}" logs "${primary}" 2>&1 | grep -Fq 'reconfiguring' +wait_for_log "${primary}" 'reconfiguring' run_restricted "${arbitrary}" --user 10001:0 wait_for_nginx "${arbitrary}" @@ -177,7 +198,7 @@ assert_tmpfs_security "${arbitrary}" --read-only \ "${missing_tmp_runtime_args[@]}" \ --cap-drop ALL \ - --security-opt no-new-privileges:true \ + --security-opt "${no_new_privileges}" \ "${image}" >/dev/null wait_for_exit "${missing_tmp}" "${runtime}" logs "${missing_tmp}" 2>&1 | grep -Eiq \ @@ -187,7 +208,7 @@ wait_for_exit "${missing_tmp}" --read-only \ --tmpfs /tmp:rw,noexec,nosuid,nodev,size=64m,mode=1777 \ --cap-drop ALL \ - --security-opt no-new-privileges:true \ + --security-opt "${no_new_privileges}" \ --entrypoint sh \ "${image}" -eu -c \ 'printf "invalid_directive;\n" > /tmp/invalid.conf; exec nginx -t -c /tmp/invalid.conf' \