Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.git
.github
.pre-commit-cache
.venv
build
dist
docs
security-results
tests
*.key
*.pem
*.sarif
*.spdx.json
grype*.json
1 change: 0 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf

*.gif binary
*.ico binary
Expand Down
138 changes: 138 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,141 @@ jobs:
format: table
exit-code: "1"
severity: CRITICAL,HIGH

image:
name: image (${{ matrix.architecture }})
strategy:
fail-fast: false
matrix:
include:
- architecture: amd64
runner: ubuntu-24.04
platform: linux/amd64
machine: x86_64
- architecture: arm64
runner: ubuntu-24.04-arm
platform: linux/arm64
machine: aarch64
runs-on: ${{ matrix.runner }}
timeout-minutes: 45
permissions:
contents: read
security-events: write
env:
TEST_IMAGE: localhost/nginx-ubi9:test-${{ matrix.architecture }}
SBOM_FILE: nginx-ubi9-${{ matrix.architecture }}.spdx.json
GRYPE_SARIF: grype-${{ matrix.architecture }}.sarif
GRYPE_ALL: grype-all-${{ matrix.architecture }}.json
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0

- name: Confirm native runner architecture
env:
EXPECTED_MACHINE: ${{ matrix.machine }}
run: test "$(uname -m)" = "${EXPECTED_MACHINE}"

- name: Build test image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: Containerfile
platforms: ${{ matrix.platform }}
load: true
push: false
tags: ${{ env.TEST_IMAGE }}
cache-from: type=gha,scope=image-${{ matrix.architecture }}
cache-to: type=gha,mode=max,scope=image-${{ matrix.architecture }}

- name: Confirm loaded image architecture
env:
EXPECTED_ARCHITECTURE: ${{ matrix.architecture }}
run: >-
test "$(docker image inspect --format '{{.Architecture}}' "${TEST_IMAGE}")"
= "${EXPECTED_ARCHITECTURE}"

- name: Run restricted-runtime smoke tests
env:
CONTAINER_RUNTIME: docker
IMAGE: ${{ env.TEST_IMAGE }}
run: bash tests/smoke.sh

- name: Scan image with Trivy
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: ${{ env.TEST_IMAGE }}
format: table
exit-code: "1"
ignore-unfixed: true
severity: CRITICAL,HIGH

- name: Generate SPDX SBOM with Syft
uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2
with:
image: ${{ env.TEST_IMAGE }}
format: spdx-json
output-file: ${{ env.SBOM_FILE }}
syft-version: v1.51.1
upload-artifact: false
upload-release-assets: false

- name: Scan Syft SBOM with Grype
uses: anchore/scan-action@27805bf3b4e84b4a5c980df22ed233c00390a439 # v7.4.2
with:
sbom: ${{ env.SBOM_FILE }}
output-format: sarif
output-file: ${{ env.GRYPE_SARIF }}
severity-cutoff: high
only-fixed: true
fail-build: true
cache-db: true
grype-version: v0.118.0

- name: Record all Grype findings
if: ${{ always() && hashFiles(env.SBOM_FILE) != '' }}
uses: anchore/scan-action@27805bf3b4e84b4a5c980df22ed233c00390a439 # v7.4.2
with:
sbom: ${{ env.SBOM_FILE }}
output-format: json
output-file: ${{ env.GRYPE_ALL }}
severity-cutoff: negligible
only-fixed: false
fail-build: false
cache-db: true
grype-version: v0.118.0

- name: Retain image security artifacts
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: image-security-${{ github.sha }}-${{ matrix.architecture }}
path: |
${{ env.SBOM_FILE }}
${{ env.GRYPE_SARIF }}
${{ env.GRYPE_ALL }}
if-no-files-found: warn
retention-days: 14

- name: Publish Grype findings to code scanning
if: ${{ always() && github.event_name != 'pull_request' && hashFiles(env.GRYPE_SARIF) != '' }}
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
with:
sarif_file: ${{ env.GRYPE_SARIF }}
category: grype-image-${{ matrix.architecture }}

image-result:
name: image
if: ${{ always() }}
needs: image
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Require every native image job
env:
MATRIX_RESULT: ${{ needs.image.result }}
run: test "${MATRIX_RESULT}" = success
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,14 @@ but container releases use the upstream-derived format documented in
hash-locked CI Python environment.
- Documented local checks, GitHub automation, planned image assurance, and the
evidence boundary of Podman Desktop or a remote Podman machine.
- Added an initial package-manager-free UBI 9 Micro development image using an
exact Red Hat NGINX RPM build.
- Added rootless NGINX defaults for unprivileged HTTP, read-only-root operation,
container log streams, `/tmp` runtime state, health checking, and graceful
shutdown.
- Added a restricted-runtime smoke suite, static landing page, and hardened
Compose development service.
- Added native PowerShell smoke testing for Podman Desktop development.
- 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.
76 changes: 76 additions & 0 deletions Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# syntax=docker/dockerfile:1.7

ARG UBI_MINIMAL_IMAGE="registry.access.redhat.com/ubi9/ubi-minimal:9.8@sha256:7fbeae18dc9476399f565e68255f602a3374ea8614ba3d14843565131a13ff93"
ARG UBI_MICRO_IMAGE="registry.access.redhat.com/ubi9/ubi-micro:9.8@sha256:f332c99eb8f798a8486821c91937f10ad64ee83d7e739303be2df051040918f6"

FROM ${UBI_MINIMAL_IMAGE} AS builder

ARG NGINX_MODULE_STREAM="1.26"
ARG NGINX_RPM_VERSION="2:1.26.3-9.module+el9.8.0+24599+8fde0ff7.3"

# Install the exact Red Hat NGINX build and its runtime dependency closure into
# a separate root. The UBI Micro final stage receives no package-management
# commands or builder caches.
# hadolint ignore=DL3041
RUN microdnf install -y dnf \
&& mkdir -p /runtime \
&& dnf module enable -y \
--installroot=/runtime \
--releasever=9 \
"nginx:${NGINX_MODULE_STREAM}" \
&& dnf install -y \
--installroot=/runtime \
--releasever=9 \
--setopt=install_weak_deps=0 \
--setopt=keepcache=0 \
"nginx-core-${NGINX_RPM_VERSION}" \
ca-certificates tzdata \
&& dnf clean all \
&& microdnf clean all \
&& rm -rf \
/runtime/run/* \
/runtime/tmp/* \
/runtime/var/cache/dnf \
/runtime/var/cache/nginx \
/runtime/var/log/* \
/runtime/var/tmp/* \
&& mkdir -p /runtime/var/log/nginx \
&& ln -s /dev/stdout /runtime/var/log/nginx/access.log \
&& ln -s /dev/stderr /runtime/var/log/nginx/error.log \
&& chown -R 0:0 /runtime/etc/nginx /runtime/usr/share/nginx \
&& chmod -R g=u /runtime/etc/nginx /runtime/usr/share/nginx

FROM ${UBI_MICRO_IMAGE}

ARG NGINX_VERSION="1.26.3"
ARG NGINX_RPM_VERSION="2:1.26.3-9.module+el9.8.0+24599+8fde0ff7.3"

LABEL org.opencontainers.image.title="NGINX on Red Hat UBI 9" \
org.opencontainers.image.description="A security-oriented, rootless NGINX image built on Red Hat UBI 9 Micro" \
org.opencontainers.image.source="https://github.com/datopsis/nginx-ubi9" \
org.opencontainers.image.documentation="https://github.com/datopsis/nginx-ubi9#readme" \
org.opencontainers.image.licenses="BSD-2-Clause AND Apache-2.0" \
org.opencontainers.image.vendor="Datopsis" \
org.opencontainers.image.version="${NGINX_VERSION}" \
io.datopsis.nginx.rpm-version="${NGINX_RPM_VERSION}"

COPY --from=builder /runtime/ /
COPY --chown=0:0 --chmod=0644 container/nginx.conf /etc/nginx/nginx.conf
COPY --chown=0:0 --chmod=0644 container/conf.d/default.conf /etc/nginx/conf.d/default.conf
COPY --chown=0:0 --chmod=0644 container/html/index.html /usr/share/nginx/html/index.html

ENV LANG="C.UTF-8" \
TZ="UTC"

USER 999:0
WORKDIR /usr/share/nginx/html

EXPOSE 8080 8443

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["nginx", "-t", "-q"]

STOPSIGNAL SIGQUIT

ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,34 @@ 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:

```console
podman build --format docker --file Containerfile \
--tag localhost/nginx-ubi9:development .
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
podman compose up --build
curl --fail http://127.0.0.1:8080/healthz
```

CI supplies the canonical Linux shell execution and native architecture
evidence.

See the [continuous integration guide](docs/CI.md) for hook installation and
Podman evidence boundaries. Until the first signed release is published, this
repository should be treated as development material rather than a supported
Expand Down
16 changes: 16 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
services:
nginx:
image: localhost/nginx-ubi9:development
build:
context: .
dockerfile: Containerfile
ports:
- "127.0.0.1:8080:8080"
read_only: true
tmpfs:
- /tmp:size=64m,mode=1777
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
stop_signal: SIGQUIT
18 changes: 18 additions & 0 deletions container/conf.d/default.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
server {
listen 8080 default_server;
listen [::]:8080 default_server;
server_name _;

root /usr/share/nginx/html;
index index.html;

location = /healthz {
access_log off;
default_type text/plain;
return 200 "ok\n";
}

location / {
try_files $uri $uri/ =404;
}
}
12 changes: 12 additions & 0 deletions container/html/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>NGINX on UBI 9</title>
</head>
<body>
<h1>NGINX on UBI 9</h1>
<p>The rootless development image is running.</p>
</body>
</html>
29 changes: 29 additions & 0 deletions container/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
worker_processes auto;
pid /tmp/nginx.pid;
error_log /dev/stderr notice;

events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /dev/stdout main;

client_body_temp_path /tmp/nginx-client-body;
proxy_temp_path /tmp/nginx-proxy;
fastcgi_temp_path /tmp/nginx-fastcgi;
uwsgi_temp_path /tmp/nginx-uwsgi;
scgi_temp_path /tmp/nginx-scgi;

sendfile on;
keepalive_timeout 65;
server_tokens off;

include /etc/nginx/conf.d/*.conf;
}
Loading
Loading