From f703f1eaba19fee53e71986a82e0f76325c23071 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 25 Apr 2026 23:29:29 +0000 Subject: [PATCH] cleanup --- .gitea/workflows/build-publish.yml | 193 +++++++++++++++++++++++++---- .gitignore | 1 + static/Jammy/nginx.service | 3 +- 3 files changed, 174 insertions(+), 23 deletions(-) create mode 100644 .gitignore diff --git a/.gitea/workflows/build-publish.yml b/.gitea/workflows/build-publish.yml index 6cc890a..b941ab0 100644 --- a/.gitea/workflows/build-publish.yml +++ b/.gitea/workflows/build-publish.yml @@ -1,3 +1,17 @@ +# ============================================================================= +# build-and-publish +# +# Compiles a custom nginx (with ModSecurity, naxsi, lua, brotli, geoip2, etc.), +# packages the result as a Debian .deb named `twiy`, and uploads it to a +# Sonatype Nexus apt-hosted repository so users can install via `apt`. +# +# Triggers: +# * Every push to master. +# * Manual run from the Actions UI (workflow_dispatch). +# +# Required repository secrets (see the "Publish to Nexus" step for details): +# NEXUS_USER, NEXUS_PASS, NEXUS_URL, NEXUS_REPO +# ============================================================================= name: build-and-publish on: @@ -7,6 +21,9 @@ on: jobs: build: + # Pinned to ubuntu-22.04 because the build script targets the toolchain + # versions that ship with that release. Bumping this needs validation + # against the modules pinned in /version. runs-on: ubuntu-22.04 steps: - name: Checkout source @@ -15,45 +32,85 @@ jobs: - name: Install build dependencies run: | set -euo pipefail + # Minimal toolchain to: build nginx (build-essential), package the + # output (dpkg-dev, fakeroot), and fetch sources (git, curl, wget). + # gnupg is kept in case a future step needs to verify upstream sigs. 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 + build-essential gnupg - name: Compile nginx and modules run: | set -euo pipefail + # Touch /.dockerenv so build/run.sh's container-detection branch is + # taken: it skips `systemctl start nginx` (the runner has no systemd). + # The .deb's own postinst handles service start on the user's host. sudo touch /.dockerenv - sudo bash build/run.sh new - sudo bash build/run.sh build - sudo bash build/run.sh postfix + sudo bash build/run.sh new # download sources for nginx + modules + sudo bash build/run.sh build # configure, compile, install + sudo bash build/run.sh postfix # drop default configs into /nginx + # ───────────────────────────────────────────────────────────────────────── + # Assemble the .deb by hand (we don't use debhelper because the build + # script already places everything at its final paths under the runner's + # root; we just need to mirror those paths into PKG_DIR and add control + # metadata). + # ───────────────────────────────────────────────────────────────────────── - name: Assemble .deb package id: pkg run: | set -euo pipefail PKG_NAME="twiy" - VERSION="$(nginx -v 2>&1 | awk -F'/' '{print $2}')" + NGINX_VER="$(nginx -v 2>&1 | awk -F'/' '{print $2}')" + # Append the CI run number as the Debian revision so each rebuild + # produces a strictly-greater version (e.g. 1.26.0-3 > 1.26.0-2 > + # 1.26.0). Without this, `apt upgrade twiy` would be a no-op when + # upstream nginx hasn't moved, so packaging fixes wouldn't reach + # users who already have the package installed. + VERSION="${NGINX_VER}-${GITHUB_RUN_NUMBER:-1}" ARCH="amd64" PKG_DIR="/opt/${PKG_NAME}_${VERSION}_${ARCH}" DEB_DIR="${PKG_DIR}/DEBIAN" + # The `*_temp` dirs under /usr/local/nginx are nginx's compiled-in + # defaults for client_body / proxy / fastcgi / uwsgi / scgi temp + # storage (no --http-*-temp-path was passed to ./configure). They + # must exist before `nginx -t` runs, so we ship them empty in the + # .deb and the postinst chowns them to the nginx user. sudo mkdir -p "${PKG_DIR}/usr/sbin" "${PKG_DIR}/nginx" \ "${PKG_DIR}/etc/systemd/system" "${PKG_DIR}/var/log/nginx" \ "${PKG_DIR}/usr/lib" "${PKG_DIR}/usr/local/lib" \ "${PKG_DIR}/hostdata/default/public_html" \ - "${PKG_DIR}/usr/nginx_lua" + "${PKG_DIR}/usr/nginx_lua" \ + "${PKG_DIR}/usr/local/nginx/client_body_temp" \ + "${PKG_DIR}/usr/local/nginx/proxy_temp" \ + "${PKG_DIR}/usr/local/nginx/fastcgi_temp" \ + "${PKG_DIR}/usr/local/nginx/uwsgi_temp" \ + "${PKG_DIR}/usr/local/nginx/scgi_temp" + # Pull every artifact the build produced into the package tree. + # `|| true` on the recursive copies tolerates a missing source dir + # (e.g. when rebuilding without re-running postfix locally). sudo cp /usr/sbin/nginx "${PKG_DIR}/usr/sbin/" sudo cp -R /nginx/* "${PKG_DIR}/nginx/" || true sudo cp /etc/systemd/system/nginx.service "${PKG_DIR}/etc/systemd/system/" sudo cp -R /hostdata/default "${PKG_DIR}/hostdata/" || true sudo cp -R /usr/nginx_lua "${PKG_DIR}/usr/" || true + # Bundle every shared library nginx links against. This makes the + # package self-contained: users don't need our exact build-host + # versions of libssl, libluajit, libmodsecurity, etc. The grep + # filters out the vDSO and the dynamic linker (which never appear + # as `=> /...`). for lib in $(ldd /usr/sbin/nginx | grep '=> /' | awk '{print $3}'); do sudo cp "$lib" "${PKG_DIR}/usr/lib/" || true done + # ---- DEBIAN/control -------------------------------------------------- + # Minimum metadata dpkg requires. We don't declare runtime Depends: + # the .deb bundles every shared library nginx needs (see the ldd + # loop above), so the only thing the host must provide is glibc. sudo mkdir -p "${DEB_DIR}" sudo tee "${DEB_DIR}/control" >/dev/null </dev/null <<'EOF' #!/bin/bash - useradd -r -d /usr/local/nginx -s /bin/false nginx || true - systemctl daemon-reload || true + # Idempotent: safe on first install, upgrade, and reinstall. + + # System user nginx workers run as. -r = system account (no aging, + # UID below SYS_UID_MAX), no shell, home set to nginx's prefix. + useradd -r -d /usr/local/nginx -s /bin/false nginx 2>/dev/null || true + + # nginx was compiled without --http-*-temp-path, so it defaults to + # / (/usr/local/nginx/client_body_temp etc.). The dirs + # already ship in the .deb, but `install -d` is the cleanest way to + # set owner/group/mode in one shot and is a no-op when the dir + # already exists with the right attributes. + install -d -o nginx -g nginx -m 0755 \ + /usr/local/nginx \ + /usr/local/nginx/client_body_temp \ + /usr/local/nginx/proxy_temp \ + /usr/local/nginx/fastcgi_temp \ + /usr/local/nginx/uwsgi_temp \ + /usr/local/nginx/scgi_temp \ + /var/log/nginx + + # Recursive chown picks up any user-supplied configs already under + # /nginx (vhosts, certs) so reloads don't trip on permissions. + chown -R nginx:nginx /var/log/nginx /nginx /usr/local/nginx 2>/dev/null || true + + # Refresh systemd's view of unit files we just dropped, then bring + # the service up. `restart` (rather than `start`) handles the case + # where a previous broken install left the unit failed. + systemctl daemon-reload 2>/dev/null || true + systemctl enable nginx.service 2>/dev/null || true + systemctl restart nginx.service 2>/dev/null || true + exit 0 EOF sudo chmod 755 "${DEB_DIR}/postinst" + # Build the .deb and hand ownership back to the runner user so the + # next step can read it without sudo. sudo dpkg-deb --build "${PKG_DIR}" DEB_FILE="${PKG_DIR}.deb" sudo chown "$(id -u):$(id -g)" "${DEB_FILE}" @@ -85,6 +182,21 @@ jobs: ls -la "${DEB_FILE}" sha256sum "${DEB_FILE}" + # ───────────────────────────────────────────────────────────────────────── + # Publish the built .deb to a Sonatype Nexus apt-hosted repository. + # + # Threat model for this step (the workflow file is public): + # * Credentials come exclusively from repository secrets, never source. + # * Credentials must never appear in argv (visible via /proc//cmdline + # to any local user) or in the runner's persistent filesystem. + # * If the job is cancelled or killed, secrets must still be wiped. + # + # To run this in your own fork, set four repository secrets: + # NEXUS_USER — Nexus account with write access to the apt repo + # NEXUS_PASS — its password (or token) + # NEXUS_URL — base URL, e.g. https://apt.example.com + # NEXUS_REPO — the apt-hosted repository name in Nexus + # ───────────────────────────────────────────────────────────────────────── - name: Publish to Nexus env: NEXUS_USER: ${{ secrets.NEXUS_USER }} @@ -95,21 +207,49 @@ jobs: PKG_NAME: ${{ steps.pkg.outputs.pkg_name }} run: | set -euo pipefail - umask 077 + umask 077 # any file we create is rw for us only - # All secret material lives in tmpfs (RAM); shredded on exit either way. - SECDIR="$(mktemp -d -p /dev/shm raweb-XXXXXXXX 2>/dev/null \ - || mktemp -d -t raweb-XXXXXXXX)" + # ---- Secret-handling scratch dir ------------------------------------ + # /dev/shm is tmpfs (RAM-backed). Even if the runner's disk is later + # imaged or recovered, secrets written here never touch persistent + # storage. Fall back to /tmp on minimal images that lack /dev/shm. + SECDIR="$(mktemp -d -p /dev/shm twiy-XXXXXXXX 2>/dev/null \ + || mktemp -d -t twiy-XXXXXXXX)" chmod 700 "$SECDIR" - trap 'find "$SECDIR" -type f -exec shred -uz {} + 2>/dev/null || true; rm -rf "$SECDIR"' EXIT - # Auth via netrc file — never via -u user:pass on the command line, - # which would be visible to anything that can read /proc. - printf 'machine apt.julio.al login %s password %s\n' \ - "$NEXUS_USER" "$NEXUS_PASS" > "$SECDIR/netrc" + # Trap covers normal exit, errors (set -e), and the common cancellation + # signals Gitea / GitHub send when a job is cancelled or times out. + # `shred -uz` overwrites then unlinks; on tmpfs the overwrite is mostly + # symbolic, but it's free defence-in-depth in case /dev/shm wasn't + # available and we fell back to a disk-backed /tmp. + cleanup() { + find "$SECDIR" -type f -exec shred -uz {} + 2>/dev/null || true + rm -rf "$SECDIR" + } + trap cleanup EXIT INT TERM HUP + + # ---- Build the netrc ------------------------------------------------- + # Why netrc and not `curl -u user:pass`: + # - `-u` puts the password in argv; any local user can read it from + # /proc//cmdline while the curl is in flight. + # - netrc is a 0600 file curl reads itself; the password never + # appears on a command line. + # Why `printf` (a bash builtin): builtins don't fork an external + # process, so the password is never an argv to any executable. + # The host string in netrc must match the URL host exactly, so we + # derive it from $NEXUS_URL rather than hardcoding it — this lets + # forks reuse the workflow without editing it. + NEXUS_HOST="$(printf '%s' "$NEXUS_URL" | awk -F/ '{print $3}')" + printf 'machine %s login %s password %s\n' \ + "$NEXUS_HOST" "$NEXUS_USER" "$NEXUS_PASS" > "$SECDIR/netrc" + # Drop the in-memory copies now that the file is the source of truth. unset NEXUS_USER NEXUS_PASS - # Replace prior component of the same name, if any (best-effort). + # ---- Replace any prior version of this package ----------------------- + # Nexus's apt-hosted format keeps every uploaded .deb forever unless we + # explicitly delete the old component. Without this, the repo grows + # unboundedly and `apt` may pick a stale version. Best-effort: a + # missing prior component is not an error. OLD_ID="$(curl -fsS --netrc-file "$SECDIR/netrc" \ "$NEXUS_URL/service/rest/v1/components?repository=$NEXUS_REPO" \ | PKG_NAME="$PKG_NAME" python3 -c ' @@ -123,6 +263,10 @@ jobs: "$NEXUS_URL/service/rest/v1/components/$OLD_ID" -o /dev/null fi + # ---- Upload the new .deb -------------------------------------------- + # Body goes to a file inside SECDIR so the trap shreds it too — Nexus + # error responses sometimes echo request metadata we'd rather not + # leave on disk. HTTP="$(curl -sS --netrc-file "$SECDIR/netrc" \ -o "$SECDIR/upload.body" -w '%{http_code}' \ -X POST -F "apt.asset=@$DEB_FILE" \ @@ -132,7 +276,12 @@ jobs: *) echo "Upload failed (HTTP $HTTP)"; head -c 400 "$SECDIR/upload.body"; exit 1 ;; esac - # Note: per-.deb signing intentionally not performed here. apt's trust - # chain is Release.gpg → Packages SHA256 → .deb SHA256, and Nexus signs - # the Release file on every upload using the key bound at repo creation. - # The private key never leaves the Nexus host. + # ---- Why we don't sign each .deb ourselves --------------------------- + # apt's trust chain on the client is: + # Release.gpg → Packages (verified by SHA256 in Release) + # → the .deb (verified by SHA256 in Packages) + # Signing the Release file is enough; per-.deb signatures are not + # consulted by apt during install. Nexus signs Release on every + # upload using a key bound at repo-creation time, and that private + # key never leaves the Nexus host — so we deliberately keep all + # signing material off the CI runner. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c5f206 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.claude/ diff --git a/static/Jammy/nginx.service b/static/Jammy/nginx.service index 1c8a5fd..ae3b11e 100644 --- a/static/Jammy/nginx.service +++ b/static/Jammy/nginx.service @@ -5,7 +5,8 @@ Wants=network-online.target [Service] Type=forking -PIDFile=/var/run/nginx.pid +PIDFile=/run/nginx.pid +ExecStartPre=/usr/bin/install -d -o nginx -g nginx -m 0755 /usr/local/nginx /usr/local/nginx/client_body_temp /usr/local/nginx/proxy_temp /usr/local/nginx/fastcgi_temp /usr/local/nginx/uwsgi_temp /usr/local/nginx/scgi_temp /var/log/nginx ExecStartPre=/usr/sbin/nginx -t ExecStart=/usr/sbin/nginx ExecReload=/usr/sbin/nginx -s reload