ci: harden secret handling — tmpfs in /dev/shm, file-based passphrase, netrc auth, EXIT trap
build-and-publish / build (push) Failing after 2m46s
build-and-publish / build (push) Failing after 2m46s
This commit is contained in:
@@ -9,29 +9,31 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout source
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install build deps
|
- name: Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
sudo apt-get install -y --no-install-recommends \
|
sudo apt-get install -y --no-install-recommends \
|
||||||
git curl wget ca-certificates dpkg-dev fakeroot \
|
git curl wget ca-certificates dpkg-dev fakeroot \
|
||||||
build-essential gnupg dpkg-sig
|
build-essential gnupg dpkg-sig
|
||||||
|
|
||||||
- name: Build NGINX (run.sh new + build + postfix)
|
- name: Compile nginx and modules
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
sudo touch /.dockerenv
|
sudo touch /.dockerenv
|
||||||
sudo bash build/run.sh new
|
sudo bash build/run.sh new
|
||||||
sudo bash build/run.sh build
|
sudo bash build/run.sh build
|
||||||
sudo bash build/run.sh postfix
|
sudo bash build/run.sh postfix
|
||||||
|
|
||||||
- name: Package .deb
|
- name: Assemble .deb package
|
||||||
id: pkg
|
id: pkg
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -euo pipefail
|
||||||
PKG_NAME="twiy"
|
PKG_NAME="twiy"
|
||||||
VERSION=$(nginx -v 2>&1 | awk -F'/' '{print $2}')
|
VERSION="$(nginx -v 2>&1 | awk -F'/' '{print $2}')"
|
||||||
ARCH="amd64"
|
ARCH="amd64"
|
||||||
PKG_DIR="/opt/${PKG_NAME}_${VERSION}_${ARCH}"
|
PKG_DIR="/opt/${PKG_NAME}_${VERSION}_${ARCH}"
|
||||||
DEB_DIR="${PKG_DIR}/DEBIAN"
|
DEB_DIR="${PKG_DIR}/DEBIAN"
|
||||||
@@ -60,7 +62,7 @@ jobs:
|
|||||||
Priority: optional
|
Priority: optional
|
||||||
Architecture: ${ARCH}
|
Architecture: ${ARCH}
|
||||||
Maintainer: Julio <me@julio.al>
|
Maintainer: Julio <me@julio.al>
|
||||||
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
|
EOF
|
||||||
|
|
||||||
sudo tee "${DEB_DIR}/postinst" >/dev/null <<'EOF'
|
sudo tee "${DEB_DIR}/postinst" >/dev/null <<'EOF'
|
||||||
@@ -72,68 +74,88 @@ jobs:
|
|||||||
|
|
||||||
sudo dpkg-deb --build "${PKG_DIR}"
|
sudo dpkg-deb --build "${PKG_DIR}"
|
||||||
DEB_FILE="${PKG_DIR}.deb"
|
DEB_FILE="${PKG_DIR}.deb"
|
||||||
echo "deb_file=${DEB_FILE}" >> "$GITHUB_OUTPUT"
|
sudo chown "$(id -u):$(id -g)" "${DEB_FILE}"
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "pkg_name=${PKG_NAME}" >> "$GITHUB_OUTPUT"
|
{
|
||||||
|
echo "deb_file=${DEB_FILE}"
|
||||||
|
echo "version=${VERSION}"
|
||||||
|
echo "pkg_name=${PKG_NAME}"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
ls -la "${DEB_FILE}"
|
ls -la "${DEB_FILE}"
|
||||||
sha256sum "${DEB_FILE}"
|
sha256sum "${DEB_FILE}"
|
||||||
|
|
||||||
- name: Import GPG key + sign .deb
|
- name: Sign and publish to Nexus
|
||||||
env:
|
env:
|
||||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }}
|
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 }}
|
DEB_FILE: ${{ steps.pkg.outputs.deb_file }}
|
||||||
|
PKG_NAME: ${{ steps.pkg.outputs.pkg_name }}
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -euo pipefail
|
||||||
export GNUPGHOME="$(mktemp -d)"
|
umask 077
|
||||||
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
|
|
||||||
|
|
||||||
- name: Publish to apt.julio.al/${{ secrets.NEXUS_REPO }}
|
# Tmpfs (RAM-only) scratch for every byte of secret material.
|
||||||
env:
|
# Falls back to /tmp if /dev/shm is absent.
|
||||||
NEXUS_URL: ${{ secrets.NEXUS_URL }}
|
SECDIR="$(mktemp -d -p /dev/shm raweb-XXXXXXXX 2>/dev/null \
|
||||||
NEXUS_REPO: ${{ secrets.NEXUS_REPO }}
|
|| mktemp -d -t raweb-XXXXXXXX)"
|
||||||
NEXUS_USER: ${{ secrets.NEXUS_USER }}
|
chmod 700 "$SECDIR"
|
||||||
NEXUS_PASS: ${{ secrets.NEXUS_PASS }}
|
export GNUPGHOME="$SECDIR/gnupg"
|
||||||
DEB_FILE: ${{ steps.pkg.outputs.deb_file }}
|
|
||||||
PKG_NAME: ${{ steps.pkg.outputs.pkg_name }}
|
cleanup() {
|
||||||
run: |
|
(gpg --batch --yes --quiet \
|
||||||
set -e
|
--delete-secret-and-public-key "$GPG_KEY_ID" 2>/dev/null) || true
|
||||||
# Best-effort: delete an existing same-named component so re-publishes overwrite cleanly
|
(gpgconf --kill all 2>/dev/null) || true
|
||||||
COMPONENT_ID=$(curl -s -u "${NEXUS_USER}:${NEXUS_PASS}" \
|
find "$SECDIR" -type f -exec shred -uz {} + 2>/dev/null || true
|
||||||
"${NEXUS_URL}/service/rest/v1/components?repository=${NEXUS_REPO}" \
|
rm -rf "$SECDIR"
|
||||||
| python3 -c "
|
}
|
||||||
import sys, json
|
trap cleanup EXIT
|
||||||
d=json.load(sys.stdin)
|
|
||||||
for c in d.get('items', []):
|
# Materialise secrets to in-memory files. After this, drop them from env
|
||||||
if c.get('name') == '${PKG_NAME}':
|
# so any subprocess (or accidental `env`) cannot read them.
|
||||||
print(c.get('id')); break
|
printf '%s' "$GPG_PRIVATE_KEY" > "$SECDIR/key.asc"
|
||||||
" || true)
|
printf '%s' "$GPG_PASSPHRASE" > "$SECDIR/pp"
|
||||||
if [ -n "$COMPONENT_ID" ]; then
|
printf 'machine apt.julio.al login %s password %s\n' \
|
||||||
echo "Removing previous ${PKG_NAME} component ${COMPONENT_ID}"
|
"$NEXUS_USER" "$NEXUS_PASS" > "$SECDIR/netrc"
|
||||||
curl -s -u "${NEXUS_USER}:${NEXUS_PASS}" -X DELETE \
|
unset GPG_PRIVATE_KEY GPG_PASSPHRASE NEXUS_PASS NEXUS_USER
|
||||||
"${NEXUS_URL}/service/rest/v1/components/${COMPONENT_ID}"
|
|
||||||
|
# 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
|
fi
|
||||||
|
|
||||||
HTTP=$(curl -s -o /tmp/upload.out -w '%{http_code}' \
|
# Upload — auth via netrc (not -u, not query string).
|
||||||
-u "${NEXUS_USER}:${NEXUS_PASS}" \
|
HTTP="$(curl -sS --netrc-file "$SECDIR/netrc" \
|
||||||
-X POST \
|
-o "$SECDIR/upload.body" -w '%{http_code}' \
|
||||||
-F "apt.asset=@${DEB_FILE}" \
|
-X POST -F "apt.asset=@$DEB_FILE" \
|
||||||
"${NEXUS_URL}/service/rest/v1/components?repository=${NEXUS_REPO}")
|
"$NEXUS_URL/service/rest/v1/components?repository=$NEXUS_REPO")"
|
||||||
echo "Upload HTTP: $HTTP"
|
case "$HTTP" in
|
||||||
[ "$HTTP" = "204" ] || [ "$HTTP" = "201" ] || { cat /tmp/upload.out; exit 1; }
|
201|204) echo "Uploaded $(basename "$DEB_FILE") to $NEXUS_URL/repository/$NEXUS_REPO/" ;;
|
||||||
echo "Published $(basename "$DEB_FILE") to ${NEXUS_URL}/repository/${NEXUS_REPO}/"
|
*) echo "Upload failed (HTTP $HTTP)"; head -c 400 "$SECDIR/upload.body"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|||||||
Reference in New Issue
Block a user