From e4d458b18557a2a81d316f71f92de026f6378697 Mon Sep 17 00:00:00 2001 From: claude Date: Sat, 25 Apr 2026 21:12:55 +0000 Subject: [PATCH] =?UTF-8?q?ci:=20harden=20secret=20handling=20=E2=80=94=20?= =?UTF-8?q?tmpfs=20in=20/dev/shm,=20file-based=20passphrase,=20netrc=20aut?= =?UTF-8?q?h,=20EXIT=20trap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/build-publish.yml | 142 +++++++++++++++++------------ 1 file changed, 82 insertions(+), 60 deletions(-) diff --git a/.gitea/workflows/build-publish.yml b/.gitea/workflows/build-publish.yml index 9be5f53..24af465 100644 --- a/.gitea/workflows/build-publish.yml +++ b/.gitea/workflows/build-publish.yml @@ -9,29 +9,31 @@ jobs: build: runs-on: ubuntu-22.04 steps: - - name: Checkout + - name: Checkout source uses: actions/checkout@v4 - - name: Install build deps + - name: Install build dependencies run: | + set -euo pipefail sudo apt-get update -y sudo apt-get install -y --no-install-recommends \ git curl wget ca-certificates dpkg-dev fakeroot \ build-essential gnupg dpkg-sig - - name: Build NGINX (run.sh new + build + postfix) + - name: Compile nginx and modules run: | + set -euo pipefail sudo touch /.dockerenv sudo bash build/run.sh new sudo bash build/run.sh build sudo bash build/run.sh postfix - - name: Package .deb + - name: Assemble .deb package id: pkg run: | - set -e + set -euo pipefail PKG_NAME="twiy" - VERSION=$(nginx -v 2>&1 | awk -F'/' '{print $2}') + VERSION="$(nginx -v 2>&1 | awk -F'/' '{print $2}')" ARCH="amd64" PKG_DIR="/opt/${PKG_NAME}_${VERSION}_${ARCH}" DEB_DIR="${PKG_DIR}/DEBIAN" @@ -60,7 +62,7 @@ jobs: Priority: optional Architecture: ${ARCH} Maintainer: Julio - Description: Nginx L7 DDoS Protection (The-World-Is-Yours) built by RAWeb CI. + Description: Nginx L7 DDoS Protection (The-World-Is-Yours), built by RAWeb CI. EOF sudo tee "${DEB_DIR}/postinst" >/dev/null <<'EOF' @@ -72,68 +74,88 @@ jobs: sudo dpkg-deb --build "${PKG_DIR}" DEB_FILE="${PKG_DIR}.deb" - echo "deb_file=${DEB_FILE}" >> "$GITHUB_OUTPUT" - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "pkg_name=${PKG_NAME}" >> "$GITHUB_OUTPUT" + sudo chown "$(id -u):$(id -g)" "${DEB_FILE}" + + { + echo "deb_file=${DEB_FILE}" + echo "version=${VERSION}" + echo "pkg_name=${PKG_NAME}" + } >> "$GITHUB_OUTPUT" + ls -la "${DEB_FILE}" sha256sum "${DEB_FILE}" - - name: Import GPG key + sign .deb + - name: Sign and publish to Nexus env: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} + NEXUS_USER: ${{ secrets.NEXUS_USER }} + NEXUS_PASS: ${{ secrets.NEXUS_PASS }} + NEXUS_URL: ${{ secrets.NEXUS_URL }} + NEXUS_REPO: ${{ secrets.NEXUS_REPO }} DEB_FILE: ${{ steps.pkg.outputs.deb_file }} + PKG_NAME: ${{ steps.pkg.outputs.pkg_name }} run: | - set -e - export GNUPGHOME="$(mktemp -d)" - chmod 700 "$GNUPGHOME" - echo "$GPG_PRIVATE_KEY" | gpg --batch --import - # Pre-cache passphrase via gpg-agent so dpkg-sig can sign non-interactively - echo "allow-loopback-pinentry" >> "$GNUPGHOME/gpg-agent.conf" - echo "pinentry-mode loopback" >> "$GNUPGHOME/gpg.conf" - gpg-connect-agent reloadagent /bye >/dev/null - # Sign with dpkg-sig (preferred) — falls back to debsigs if dpkg-sig missing - if command -v dpkg-sig >/dev/null; then - sudo --preserve-env=GNUPGHOME,GPG_PASSPHRASE dpkg-sig \ - -k "$GPG_KEY_ID" \ - -g "--batch --pinentry-mode loopback --passphrase $GPG_PASSPHRASE" \ - --sign builder "$DEB_FILE" - fi - # Verify the signature is present (informational) - dpkg-sig --verify "$DEB_FILE" || true + set -euo pipefail + umask 077 - - name: Publish to apt.julio.al/${{ secrets.NEXUS_REPO }} - env: - NEXUS_URL: ${{ secrets.NEXUS_URL }} - NEXUS_REPO: ${{ secrets.NEXUS_REPO }} - NEXUS_USER: ${{ secrets.NEXUS_USER }} - NEXUS_PASS: ${{ secrets.NEXUS_PASS }} - DEB_FILE: ${{ steps.pkg.outputs.deb_file }} - PKG_NAME: ${{ steps.pkg.outputs.pkg_name }} - run: | - set -e - # Best-effort: delete an existing same-named component so re-publishes overwrite cleanly - COMPONENT_ID=$(curl -s -u "${NEXUS_USER}:${NEXUS_PASS}" \ - "${NEXUS_URL}/service/rest/v1/components?repository=${NEXUS_REPO}" \ - | python3 -c " - import sys, json - d=json.load(sys.stdin) - for c in d.get('items', []): - if c.get('name') == '${PKG_NAME}': - print(c.get('id')); break - " || true) - if [ -n "$COMPONENT_ID" ]; then - echo "Removing previous ${PKG_NAME} component ${COMPONENT_ID}" - curl -s -u "${NEXUS_USER}:${NEXUS_PASS}" -X DELETE \ - "${NEXUS_URL}/service/rest/v1/components/${COMPONENT_ID}" + # Tmpfs (RAM-only) scratch for every byte of secret material. + # Falls back to /tmp if /dev/shm is absent. + SECDIR="$(mktemp -d -p /dev/shm raweb-XXXXXXXX 2>/dev/null \ + || mktemp -d -t raweb-XXXXXXXX)" + chmod 700 "$SECDIR" + export GNUPGHOME="$SECDIR/gnupg" + + cleanup() { + (gpg --batch --yes --quiet \ + --delete-secret-and-public-key "$GPG_KEY_ID" 2>/dev/null) || true + (gpgconf --kill all 2>/dev/null) || true + find "$SECDIR" -type f -exec shred -uz {} + 2>/dev/null || true + rm -rf "$SECDIR" + } + trap cleanup EXIT + + # Materialise secrets to in-memory files. After this, drop them from env + # so any subprocess (or accidental `env`) cannot read them. + printf '%s' "$GPG_PRIVATE_KEY" > "$SECDIR/key.asc" + printf '%s' "$GPG_PASSPHRASE" > "$SECDIR/pp" + printf 'machine apt.julio.al login %s password %s\n' \ + "$NEXUS_USER" "$NEXUS_PASS" > "$SECDIR/netrc" + unset GPG_PRIVATE_KEY GPG_PASSPHRASE NEXUS_PASS NEXUS_USER + + # Ephemeral keyring on tmpfs. + mkdir -p "$GNUPGHOME"; chmod 700 "$GNUPGHOME" + printf 'allow-loopback-pinentry\n' > "$GNUPGHOME/gpg-agent.conf" + printf 'pinentry-mode loopback\n' > "$GNUPGHOME/gpg.conf" + gpg --batch --import "$SECDIR/key.asc" + + # Sign the .deb. Passphrase passed via FILE — never argv, never env. + dpkg-sig -k "$GPG_KEY_ID" \ + -g "--batch --pinentry-mode loopback --passphrase-file $SECDIR/pp" \ + --sign builder "$DEB_FILE" + dpkg-sig --verify "$DEB_FILE" + + # Replace any prior component of the same name (best-effort). + OLD_ID="$(curl -fsS --netrc-file "$SECDIR/netrc" \ + "$NEXUS_URL/service/rest/v1/components?repository=$NEXUS_REPO" \ + | PKG_NAME="$PKG_NAME" python3 -c ' + import sys, json, os + for c in json.load(sys.stdin).get("items", []): + if c.get("name") == os.environ["PKG_NAME"]: + print(c["id"]); break + ' || true)" + if [ -n "$OLD_ID" ]; then + curl -fsS -X DELETE --netrc-file "$SECDIR/netrc" \ + "$NEXUS_URL/service/rest/v1/components/$OLD_ID" -o /dev/null fi - HTTP=$(curl -s -o /tmp/upload.out -w '%{http_code}' \ - -u "${NEXUS_USER}:${NEXUS_PASS}" \ - -X POST \ - -F "apt.asset=@${DEB_FILE}" \ - "${NEXUS_URL}/service/rest/v1/components?repository=${NEXUS_REPO}") - echo "Upload HTTP: $HTTP" - [ "$HTTP" = "204" ] || [ "$HTTP" = "201" ] || { cat /tmp/upload.out; exit 1; } - echo "Published $(basename "$DEB_FILE") to ${NEXUS_URL}/repository/${NEXUS_REPO}/" + # Upload — auth via netrc (not -u, not query string). + HTTP="$(curl -sS --netrc-file "$SECDIR/netrc" \ + -o "$SECDIR/upload.body" -w '%{http_code}' \ + -X POST -F "apt.asset=@$DEB_FILE" \ + "$NEXUS_URL/service/rest/v1/components?repository=$NEXUS_REPO")" + case "$HTTP" in + 201|204) echo "Uploaded $(basename "$DEB_FILE") to $NEXUS_URL/repository/$NEXUS_REPO/" ;; + *) echo "Upload failed (HTTP $HTTP)"; head -c 400 "$SECDIR/upload.body"; exit 1 ;; + esac