docs/operations/linux-package-repos-infrastructure.md
Maintainer runbook for the apt and yum repositories at apt.mcpproxy.app / rpm.mcpproxy.app.
User-facing details live in Linux Package Repositories. This document covers the operator perspective only.
Cloudflare R2 buckets (Eastern Europe region, Standard storage class):
mcpproxy-apt — bound to custom domain apt.mcpproxy.appmcpproxy-rpm — bound to custom domain rpm.mcpproxy.appCloudflare R2 API token (dashboard → R2 → API tokens): MCPProxy Packages CI, Object Read & Write, scoped to the two buckets, no expiry.
GitHub Actions secrets in smart-mcp-proxy/mcpproxy-go:
PACKAGES_GPG_PRIVATE_KEY — ASCII-armored GPG private keyPACKAGES_GPG_PASSPHRASE — passphrase for that keyR2_ACCOUNT_ID — Cloudflare account ID (appears in the R2 S3 endpoint URL)R2_ACCESS_KEY_ID — R2 API token access keyR2_SECRET_ACCESS_KEY — R2 API token secretGitHub Actions variable:
PACKAGES_GPG_KEY_ID — full fingerprint of the current signing keyLocal-only backup on maintainer workstation:
~/repos/PACKAGES_GPG_PRIVATE_KEY.txt (mode 0600, outside any git repo) — ASCII-armored private key + usage instructions.CI job: publish-linux-repos in .github/workflows/release.yml, triggered on every v* tag that does not contain a - (stable channel only).
Helper scripts: contrib/linux-repos/*.sh — orchestrator (publish.sh), per-format publishers (apt-publish.sh, rpm-publish.sh), smoke tests (smoke-test-debian.sh, smoke-test-fedora.sh), key import (import-key.sh), expiry check (check-key-expiry.sh).
Rotate the signing key annually (calendar reminder), or immediately on any hint of compromise. Budget: 30 minutes end-to-end.
# On maintainer workstation — same recipe as initial setup
cat > /tmp/gpg-batch.txt <<'EOF'
%echo Generating MCPProxy package signing key
Key-Type: RSA
Key-Length: 4096
Key-Usage: sign
Subkey-Type: RSA
Subkey-Length: 4096
Subkey-Usage: encrypt
Name-Real: MCPProxy Packages
Name-Email: [email protected]
Name-Comment: Linux repository signing key
Expire-Date: 5y
Passphrase: <generate fresh, capture into 1Password>
%commit
%echo done
EOF
gpg --batch --generate-key /tmp/gpg-batch.txt
shred -u /tmp/gpg-batch.txt
NEW_FPR=$(gpg --list-keys --with-colons 'MCPProxy Packages' \
| awk -F: '/^fpr:/ {print $10}' | tail -1)
echo "New fingerprint: ${NEW_FPR}"
# Export the new private key (and metadata) into the backup file
gpg --armor --export-secret-keys "${NEW_FPR}" > ~/repos/PACKAGES_GPG_PRIVATE_KEY.txt
# Edit the header lines (Key ID / Created / Expires / Passphrase) by hand
chmod 600 ~/repos/PACKAGES_GPG_PRIVATE_KEY.txt
gpg --armor --export "${NEW_FPR}" \
> ~/repos/mcpproxy-go/contrib/signing/mcpproxy-packages.asc
gh secret set PACKAGES_GPG_PRIVATE_KEY \
--repo smart-mcp-proxy/mcpproxy-go < ~/repos/PACKAGES_GPG_PRIVATE_KEY.txt
echo -n "<new passphrase>" | gh secret set PACKAGES_GPG_PASSPHRASE \
--repo smart-mcp-proxy/mcpproxy-go
gh variable set PACKAGES_GPG_KEY_ID \
--repo smart-mcp-proxy/mcpproxy-go --body "${NEW_FPR}"
cd ~/repos/mcpproxy-go
git add contrib/signing/mcpproxy-packages.asc
git commit -m "chore(signing): rotate linux package signing key to <short-id>"
git push
git tag -a vX.Y.Z -m "Release vX.Y.Z"
git push origin vX.Y.Z
The next workflow run regenerates all signed metadata using the new key and republishes the public key to both buckets.
docs/features/linux-package-repos.md and README.md.docs/getting-started/installation.md.curl -fsSL .../mcpproxy.gpg | sudo tee /etc/apt/keyrings/mcpproxy.gpg > /dev/null and equivalent for dnf).Only if you suspect compromise. Generate a revocation certificate and publish it to the same URL as the public key. Most users don't check revocations for package-signing keys; the stronger lever is rolling the GitHub Actions secret (done in step 4), which invalidates any attacker-held copy.
If the publish-linux-repos job failed for a specific tag and you need to re-run it without re-tagging:
publish-linux-repos job re-runsIf the workflow_dispatch trigger is enabled, you can also dispatch manually with the tag as input.
For a completely offline republish from the maintainer workstation (last resort):
export PACKAGES_GPG_PRIVATE_KEY=$(cat ~/repos/PACKAGES_GPG_PRIVATE_KEY.txt)
export PACKAGES_GPG_PASSPHRASE='<passphrase>'
export GPG_KEY_ID='3B6FA1AD5D5359DA51F18DDCE1B59B9BA1CB8A3B'
export APT_BUCKET=mcpproxy-apt
export RPM_BUCKET=mcpproxy-rpm
export APT_BASE_URL=https://apt.mcpproxy.app
export RPM_BASE_URL=https://rpm.mcpproxy.app
export AWS_ACCESS_KEY_ID=<from secrets>
export AWS_SECRET_ACCESS_KEY=<from secrets>
export AWS_ENDPOINT_URL=https://<account>.r2.cloudflarestorage.com
export AWS_DEFAULT_REGION=auto
# Download the .deb/.rpm from the GitHub release into ./release-artifacts/
./contrib/linux-repos/publish.sh ./release-artifacts/
If a published version needs to be pulled (security regression, broken binary, etc.):
# From the workstation, with R2 creds loaded:
export BAD_VERSION=0.24.7
export AWS_ENDPOINT_URL=https://<account>.r2.cloudflarestorage.com
export AWS_DEFAULT_REGION=auto
export AWS_ACCESS_KEY_ID=<from secrets>
export AWS_SECRET_ACCESS_KEY=<from secrets>
# Delete pool artifacts from apt bucket
aws s3 rm "s3://mcpproxy-apt/pool/main/m/mcpproxy/mcpproxy_${BAD_VERSION}_amd64.deb"
aws s3 rm "s3://mcpproxy-apt/pool/main/m/mcpproxy/mcpproxy_${BAD_VERSION}_arm64.deb"
# Delete rpms from rpm bucket
aws s3 rm "s3://mcpproxy-rpm/x86_64/mcpproxy-${BAD_VERSION}-1.x86_64.rpm"
aws s3 rm "s3://mcpproxy-rpm/aarch64/mcpproxy-${BAD_VERSION}-1.aarch64.rpm"
# Re-run the publish job (which will regenerate metadata without the bad files).
# Simplest way: tag a new release, which naturally pushes the bad version out anyway.
If you cannot cut a new release immediately, you can force a metadata-only refresh by re-running the most recent successful publish-linux-repos workflow.
The current usage fits the free tier:
apt update / dnf makecache; capacity for ~300k Linux users/day before hitting the 10M/month limitEgress is free on R2. No billing expected under normal usage.
specs/043-linux-package-repos/ (internal reference)