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
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
!.env.example

# Runtime artifacts produced by the check scripts on CI runners
.allowed-keyids.txt
.allowed-ssh-fingerprints.txt
gpg-list.asc.1
*.bak

# Generated Dockerfiles (templates live in Dockerfile.template)
Expand Down
46 changes: 26 additions & 20 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,46 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).

## [Unreleased]

### Removed
- **GPG commit verification — commit signing is now SSH-only.** Deleted
`check-gpg.sh`, `security.sh`, `gpg-list.asc`, and the `gpg/` key directory;
dropped them from `checksum.sha256` coverage and CODEOWNERS, and removed the
GPG-vs-SSH drift report from `generate-ssh-allowed-signers.sh`. Consumers must
use `check-ssh.sh`; signer onboarding is documented in SECURITY.md.
- **Trusted-signer roster reduction (security boundary):** `GITHUB_USERS` /
`ssh-allowed-signers` now cover only `brng1151`, `chiro-hiro`,
`bao-ninh-orochi`, and `chirojr`. Removed `alothanhh`, `BaoNinh2808`,
`brianw3b`, `CaoHoaiTan`, `harris1111`, `hungnguyen18`, `ngotrongphuc`,
`nguyendinhthang3101`, `SangTran-127`, `ThanhNguyen03`, and `wonrax`
(`BaoNinh2808`/`brianw3b` published no SSH keys, so only the other nine had
live entries). Their commits will no longer pass `check-ssh.sh`; re-add via
the SECURITY.md onboarding flow if they resume committing.

### Security
- **Build-time npm token no longer persists in builder layers:** credentials are
created, used, and deleted inside a single `--mount=type=secret` build `RUN`
(`Dockerfile.template` + `dockerfile.sh`), so the token never lands in any image
layer. Enforced by a new CI job (`builder-secret-no-leak`) that builds the
builder stage with a canary secret and fails if it is found in the image.
- **Closed the checksum coverage gap:** `checksum.sha256` now covers the
executable scripts that are fetched via `curl | bash` (`check-gpg.sh`,
`check-ssh.sh`, `dockerfile.sh`, `generate-yarn-npm.sh`) and
`Dockerfile.template`, not just the allowlist data files.
executable scripts that are fetched via `curl | bash` (`check-ssh.sh`,
`dockerfile.sh`, `generate-yarn-npm.sh`) and `Dockerfile.template`, not just
the allowlist data files.
- **`dockerfile.sh` injection hardening:** template values are now passed to
`perl` via the environment (`$ENV{…}`) so user-controlled content (commands,
image names, `CMD`) can never be interpreted as perl/regex. Added
`set -euo pipefail`, `mktemp` working dir (no predictable `/tmp` names),
template/argument validation, and a guard refusing to copy credential files
(`.npmrc`/`.yarnrc`/`.yarnrc.yml`/`.netrc`) into images.
- **Fail-closed signature checks:** `check-gpg.sh` / `check-ssh.sh` now error on
an empty commit range and warn loudly when `BASE_SHA`/`HEAD_SHA` are unset,
instead of silently passing. Quoted URLs; `curl -fsSL`; revision logging.
- **Fail-closed signature checks:** `check-ssh.sh` now errors on an empty
commit range and warns loudly when `BASE_SHA`/`HEAD_SHA` are unset, instead
of silently passing. Quoted URLs; `curl -fsSL`; revision logging.
(Signature *matching* logic was verified correct and left unchanged.)
- **Secrets hygiene:** `generate-yarn-npm.sh` uses `umask 077`; `.env` removed
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
- **`BASE_REVISION` is validated before use:** `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
Expand All @@ -49,9 +64,9 @@ 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).
- **Empty signer-key guard:** `check-ssh.sh` rejects an empty key 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`
Expand All @@ -69,17 +84,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
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:
`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
`generate-ssh-allowed-signers.sh`.
`generate-ssh-allowed-signers.sh` delegates to it.
- `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`,
Expand Down
4 changes: 0 additions & 4 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,11 @@
* @chiro-hiro

# Trust anchors — signature verification + allowlists
/check-gpg.sh @chiro-hiro
/check-ssh.sh @chiro-hiro
/gpg-list.asc @chiro-hiro
/ssh-allowed-signers @chiro-hiro
/gpg/ @chiro-hiro
/checksum.sha256 @chiro-hiro
/generate-checksums.sh @chiro-hiro
/generate-ssh-allowed-signers.sh @chiro-hiro
/security.sh @chiro-hiro

# Build/distribution surface
/dockerfile.sh @chiro-hiro
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

Easy develove <3 Orochi Network

## Check GPG for all commits in a given PR
## Check SSH signatures for all commits in a given PR

You can set `BASE_REVISION` environment variable to specify a version of the given script

```bash
curl -sL https://raw.githubusercontent.com/orochi-network/dev-off/main/check-gpg.sh | bash
curl -sL https://raw.githubusercontent.com/orochi-network/dev-off/main/check-ssh.sh | bash
```

## Docker template
Expand Down
22 changes: 9 additions & 13 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ repositories (via [`orochi-network/actions`](https://github.com/orochi-network/a
fetch and execute scripts from this repo **at runtime**:

```bash
curl -sL https://raw.githubusercontent.com/orochi-network/dev-off/${BASE_REVISION}/check-gpg.sh | bash
curl -sL https://raw.githubusercontent.com/orochi-network/dev-off/${BASE_REVISION}/check-ssh.sh | bash
```

A compromise of any executable file here runs on **every** downstream CI runner.
Expand All @@ -17,17 +17,17 @@ Treat changes to this repository with the same care as production secrets.

| Asset | Threat | Control |
|-------|--------|---------|
| Executable scripts (`check-*.sh`, `dockerfile.sh`, `generate-yarn-npm.sh`) | Tampering on `main` → RCE on all runners | Signed commits + branch protection; `checksum.sha256` coverage; consumer-side pinning |
| Allowlists (`gpg-list.asc`, `ssh-allowed-signers`) | Unauthorized signer added | `checksum.sha256`; CODEOWNERS review; signed PRs |
| Executable scripts (`check-ssh.sh`, `dockerfile.sh`, `generate-yarn-npm.sh`) | Tampering on `main` → RCE on all runners | Signed commits + branch protection; `checksum.sha256` coverage; consumer-side pinning |
| Allowlist (`ssh-allowed-signers`) | Unauthorized signer added | `checksum.sha256`; CODEOWNERS review; signed PRs |
| `checksum.sha256` | Edited to match malicious files | Signed commits are the real anchor (a hash is not a signature) |
| Build-time npm token | Leaks into image layers | BuildKit `--mount=type=secret`; credential-file copy guard in `dockerfile.sh` |

## Integrity model — how trust actually flows

1. **`checksum.sha256` covers the executable scripts**, not just the data files.
Generate it only via `./generate-checksums.sh` (the single source of truth).
`security.sh` and `generate-ssh-allowed-signers.sh` both delegate to it so the
covered file set can never drift.
`generate-ssh-allowed-signers.sh` delegates to it so the covered file set can
never drift.

2. **A checksum is not a signature.** Anyone who can write to `main` can edit a
file *and* its checksum. The real anchor is therefore **branch protection +
Expand Down Expand Up @@ -81,15 +81,11 @@ a public issue for undisclosed vulnerabilities.
## Adding a trusted signer (security boundary)

Adding a key here grants someone authority to sign commits that pass CI.
Commit signing is **SSH only**: add the person's GitHub username to
`GITHUB_USERS` in `generate-ssh-allowed-signers.sh`, run it, commit
`ssh-allowed-signers` + `checksum.sha256`.

- **GPG:** add their public key as `gpg/<github-username>.asc`, run
`./security.sh`, commit `gpg-list.asc` + `checksum.sha256`.
- **SSH:** add their GitHub username to `GITHUB_USERS` in
`generate-ssh-allowed-signers.sh`, run it, commit `ssh-allowed-signers` +
`checksum.sha256`. The script prints a drift report comparing GPG vs SSH
coverage — investigate any warnings.

Both require a reviewed, signed PR approved by a CODEOWNER.
This requires a reviewed, signed PR approved by a CODEOWNER.

## Follow-ups in the `actions` repo (coordinated changes)

Expand Down
98 changes: 0 additions & 98 deletions check-gpg.sh

This file was deleted.

4 changes: 1 addition & 3 deletions checksum.sha256
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
1e0c7cc14593577dcbd2b343f39a87120bc42482f25df215c52a9373029dbd4a ./ssh-allowed-signers
48f2d49fab1d7c52f6930bfb239874f1e61479210ceb260d0e2dd9a54c2877e6 ./gpg-list.asc
4939e1364e91cfbb589ddf645ae26530a1e4ecbb4e21c607aad8ee64c9414bca ./scripts/build-prod-node.sh
4aa825fa514655414968415bebfbec5b0252803fbba16e994b29ff1fd3004110 ./check-ssh.sh
51569cfd0e70e480d4a3e9d43b203972e8ae52cbc5d028f263b6b81295c74454 ./ssh-allowed-signers
5b7f96cd62b02c59adb90159877bd61230c3d95eca3e19a1c4e5dfb4140a986c ./scripts/build-prod-next.sh
66ae4f55698cd5320bf009314ae59c4e34c07bd24a3a2b30540189e6089565b0 ./Dockerfile.template
aad9d75f80076441f3164f81fb9fca4f4d55ec3c021129cad29633d89b24170d ./scripts/build-prod-nginx.sh
beee0be70f1a3e4a219e142bf7400415a26dea1873ce279f95f401e682fe0fe6 ./check-gpg.sh
f15c4e8e374105e07ab7de3cf94e3ec6aaccd02a76ca9907368782612bc7e50f ./configs/nginx.conf
fb6fe984478967e5b25939954d1296b8effe74c038e469cc60d93b0e4ffae0a9 ./dockerfile.sh
fcf790d01c0ccde52a74a4fa49df33ba9a0f6210868dfa7895048a58bd613710 ./generate-yarn-npm.sh
12 changes: 5 additions & 7 deletions generate-checksums.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,20 @@ set -euo pipefail
# Lists EXACTLY the files that are distributed to (or fetched at runtime by)
# downstream consumers and therefore must be integrity-verifiable. Crucially this
# now includes the executable scripts that are run via `curl | bash`
# (check-gpg.sh, check-ssh.sh, dockerfile.sh, generate-yarn-npm.sh) — previously
# only the allowlist data files were covered, leaving the scripts themselves
# (check-ssh.sh, dockerfile.sh, generate-yarn-npm.sh) — previously only the
# allowlist data files were covered, leaving the scripts themselves
# unverifiable.
#
# Run this after changing any covered file and commit the updated checksum.sha256.
# Both security.sh and generate-ssh-allowed-signers.sh delegate here so the file
# set never drifts between generators.
# generate-ssh-allowed-signers.sh delegates here so the file set never drifts
# between generators.
# ============================================================================

cd "$(dirname "$0")"

# Covered files (relative paths). Add new distributed scripts/templates here.
FILES=(
# Executable scripts fetched via curl|bash by the actions repo
check-gpg.sh
check-ssh.sh
dockerfile.sh
generate-yarn-npm.sh
Expand All @@ -32,8 +31,7 @@ FILES=(
scripts/build-prod-node.sh
scripts/build-prod-next.sh
scripts/build-prod-nginx.sh
# Trust allowlists
gpg-list.asc
# Trust allowlist
ssh-allowed-signers
)

Expand Down
44 changes: 1 addition & 43 deletions generate-ssh-allowed-signers.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash

# Generates ssh-allowed-signers from GitHub SSH public keys.
# Run locally and commit the result. Mirrors the gpg/ folder membership.
# Run locally and commit the result.
# Usage: ./generate-ssh-allowed-signers.sh

set -euo pipefail
Expand All @@ -16,22 +16,6 @@ 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"
Expand Down Expand Up @@ -66,32 +50,6 @@ for USER in "${GITHUB_USERS[@]}"; do
echo " $USER: $KEY_COUNT key(s) added"
done

# --------------------------------------------------------------------------
# Drift visibility: the GPG allowlist (gpg/*.asc) and the SSH signer list are
# two halves of the same trust anchor and should track the same people. We do
# NOT auto-expand SSH trust (that requires a human decision), but we surface any
# gap so maintainers notice when one side is updated without the other.
# --------------------------------------------------------------------------
echo ""
echo "Trust-anchor drift check (GPG keys vs SSH signers):"
gpg_owners=()
for asc in ./gpg/*.asc; do
[[ -f "$asc" ]] || continue
gpg_owners+=("$(basename "$asc" .asc)")
done
echo " gpg/ keys: ${#gpg_owners[@]}"
echo " SSH signers: ${#GITHUB_USERS[@]}"
for owner in "${gpg_owners[@]}"; do
found=false
for u in "${GITHUB_USERS[@]}"; do [[ "$owner" == "$u" ]] && found=true && break; done
[[ "$found" == true ]] || echo " WARNING: gpg/${owner}.asc has no matching SSH signer entry (GITHUB_USERS)"
done
for u in "${GITHUB_USERS[@]}"; do
found=false
for owner in "${gpg_owners[@]}"; do [[ "$owner" == "$u" ]] && found=true && break; done
[[ "$found" == true ]] || echo " NOTE: SSH signer '${u}' has no matching gpg/${u}.asc (filenames may differ from usernames)"
done

# Refresh checksums via the single source of truth.
echo ""
./generate-checksums.sh
Expand Down
Loading
Loading