Skip to content

feat: add strapi template + hardening + trust reconcile (v1.1.0)#56

Merged
chiro-hiro merged 5 commits into
orochi-network:mainfrom
chiro-hiro:fix/audit-findings-hardening
Jun 5, 2026
Merged

feat: add strapi template + hardening + trust reconcile (v1.1.0)#56
chiro-hiro merged 5 commits into
orochi-network:mainfrom
chiro-hiro:fix/audit-findings-hardening

Conversation

@chiro-hiro

@chiro-hiro chiro-hiro commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

A multi-agent audit of the repo surfaced a set of correctness/security issues across the trust-verification scripts, the Dockerfile generator, and repo config. This PR fixes them, and additionally adds a first-class strapi template plus a trust-roster reconciliation. Each change was verified locally (shellcheck, bash -n, the CI jobs, checksum freshness, and dockerfile.sh --dry-run).

Intended release: this PR is intended to become v1.1.0 (someone else cuts the tag after merge). Consumers should then pin base_revision: v1.1.0.


New in this revision (Phase 2)

1. First-class strapi template (main deliverable — unblocks website-strapi)

Adds a strapi template to dockerfile.sh (modeled on the node template) plus scripts/build-prod-strapi.sh:

  • Builder: orochinetwork/ubuntu:node, with corepack enable (Strapi uses Yarn 4 berry; the base image only ships Yarn 1).
  • Runner DEFAULT: node:22-trixie-slim — glibc is required, because 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).
  • ENV 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 its outDir to locate dist/.
  • scripts/build-prod-strapi.sh runs yarn install --immutable && yarn build (= strapi build: admin panel + server → dist). It intentionally does not write src/version.ts (Strapi owns its src/ tree and type generation).
  • Registered in SUPPORTED_TEMPLATES; covered by checksum.sha256; documented in DOCKERFILE.md/README.md/CHANGELOG.md; and added to the CI dry-run smoke matrix with an assertion that the template keeps its glibc-runner / corepack / NODE_ENV=production / EXPOSE 1337 / CMD contract (and that -r still overrides the default).

2. Removed a stale, leaky root Dockerfile

An untracked root Dockerfile lingered from before the secret-mount hardening — it wrote .npmrc/.yarnrc.yml across multiple RUNs (credentials persisted in builder layers) and referenced a non-existent build-prod-.sh. It was never git-tracked; removed the local artifact so it can't confuse anyone.

3. Trust-roster reconciliation (security-sensitive — requires human review)

generate-ssh-allowed-signers.sh's GITHUB_USERS had 4 entries while the GPG allowlist (gpg-list.asc / gpg/*.asc) holds 13 keys. Expanded the SSH side to mirror the GPG side for active contributors. No one was removed.

Each username added below already holds a GPG key in the trust anchor and is an active orochi-network/dev-off contributor:

Added username Why
alothanhh GPG key in trust anchor + dev-off contributor
BaoNinh2808 GPG key in trust anchor + dev-off contributor (no SSH key published yet — generator skips with a warning)
brianw3b GPG key in trust anchor + dev-off contributor (no SSH key published yet — generator skips with a warning)
CaoHoaiTan GPG key in trust anchor + dev-off contributor
harris1111 GPG key in trust anchor + dev-off contributor
hungnguyen18 GPG key in trust anchor + dev-off contributor
ngotrongphuc GPG key in trust anchor + dev-off contributor
nguyendinhthang3101 GPG key in trust anchor + dev-off contributor
SangTran-127 GPG key in trust anchor + dev-off contributor
ThanhNguyen03 GPG key in trust anchor + dev-off contributor
wonrax GPG key in trust anchor + dev-off contributor

Notes:

  • gpg/Github.asc is the GitHub web-flow signing bot and is intentionally not an SSH signer.
  • brng1151, bao-ninh-orochi, chirojr were already SSH signers without a matching gpg/*.asc file; left untouched (the drift report flags them as a NOTE only).
  • ⚠️ Trust-list additions are a security boundary and require human review before merge.

Existing hardening (carried from the original PR)

Security / correctness

  • BASE_REVISION validation (check-gpg.sh, check-ssh.sh, dockerfile.sh): the value is interpolated into curl URLs (and the scripts run via curl | bash). It is now validated before any fetch URL is built or any destructive setup runs — only a commit SHA, tag, or branch name is accepted; shell metacharacters and .. are rejected. (curl normalizes ../, so an unvalidated value could repoint fetches at an arbitrary repo/path.) Previously check-gpg.sh even wiped/recreated ~/.gnupg before looking at the value.
  • Dockerfile directive injection (dockerfile.sh): user values for -c/-f/-b/-r/--run are rejected if they contain a newline/CR. Without this, -f $'build\nRUN …' injected an extra RUN directive into the generated Dockerfile.
  • Ambiguous -f parse (dockerfile.sh): a -f value with more than one ; is rejected — the credential guard read dst as the last field while the COPY generator read it as the second, so they disagreed on what was being copied.
  • check-ssh.sh fails closed: if any ssh-allowed-signers line cannot be parsed into a fingerprint, the script now errors instead of silently building a narrower allowlist.
  • Build-time npm token no longer persists in builder layers: credentials are created, used, and deleted inside a single --mount=type=secret build RUN; enforced by the builder-secret-no-leak CI job.
  • generate-yarn-npm.sh: now carries an explicit warning that it writes a plaintext token for ephemeral runners only and must not be used inside a docker build layer — image builds use dockerfile.sh's BuildKit secret mount.
  • CI: ludeeus/action-shellcheck pinned to a commit SHA (00cae50, tag 2.0.0) instead of @master; added regression tests for the injection / ambiguous-path / BASE_REVISION rejections above.

Bug fix

  • nginx template build aborted (Dockerfile.template): chown -R nginx /home/nginx failed because nginx:stable-alpine ships no /home/nginx. The runner stage now pre-creates /home/<runner_user> before chowning.

Cleanup

  • 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"].
  • Refreshed checksum.sha256 for the changed scripts; updated CHANGELOG.md / SECURITY.md.

A note on the nginx port

The audit flagged the nginx runner as "would crash binding :80 because it runs non-root." That is environment-dependent, not a blanket Docker rule: it depends on the host's net.ipv4.ip_unprivileged_port_start. On Docker Desktop (threshold 0) a non-root container binds :80 fine; on a stock-kernel Linux host (threshold 1024) it fails. Per maintainer decision we keep :80 and document the sysctl assumption inline; switching to 8080 is a one-line change if a stock-kernel host is ever a target.


Verification

  • bash -n clean on dockerfile.sh, scripts/build-prod-strapi.sh, generate-ssh-allowed-signers.sh, generate-checksums.sh.
  • shellcheck -e SC1091 clean on the changed scripts.
  • checksum.sha256 regenerated and fresh (CI-equivalent diff is empty); sha256sum -c --strict passes for all 12 covered files (now including scripts/build-prod-strapi.sh).
  • ./dockerfile.sh -t strapi --dry-run produces the expected builder/runner/corepack/NODE_ENV/EXPOSE 1337/CMD/copy-set output; --dry-run writes nothing to disk.
  • Both new commits are SSH-signed.

- 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/<runner_user> 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.
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.
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.
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.
@chiro-hiro chiro-hiro changed the title fix: harden trust scripts and Dockerfile generation (audit findings) feat: add strapi template + hardening + trust reconcile (v1.1.0) Jun 5, 2026
Comment thread scripts/build-prod-strapi.sh Outdated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All nodejs build will be the same? why do we need this file?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — a Strapi build is just the node build, so the separate script is gone. Removed build-prod-strapi.sh; the strapi template now reuses the shared build-prod-node.sh via a BUILD_SCRIPT_TEMPLATE indirection in dockerfile.sh (defaults to the template name, set to node for the strapi case). node/next/nginx are untouched.

Why none of the differences justified a separate file:

  • corepack enable was redundant — the strapi template already emits RUN corepack enable as its own builder layer, so corepack is active before the build script runs.
  • --immutable vs --frozen-lockfile--frozen-lockfile works on Yarn 4 (it is a documented backward-compat alias for --immutable; emits a YN0050 deprecation notice and proceeds). Same effect.
  • Skipping src/version.ts was actually an inconsistency — every other node-family consumer gets it, and the write is already guarded by [ -d src/ ]. Strapi now gets it too.

The genuinely Strapi-specific bits stay in dockerfile.sh: glibc node:22-trixie-slim runner, the corepack builder layer, the default copy set, and NODE_ENV=production / EXPOSE 1337 / CMD ["npm","run","start"]. Dry-run confirms the generated Dockerfile now fetches build-prod-node.sh with all of those intact. Fixed in 046bdb1.

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.
@chiro-hiro chiro-hiro merged commit 38e9d6d into orochi-network:main Jun 5, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant