Linux Package Repositories — Operations
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.
Infrastructure inventory
Cloudflare R2 buckets (Eastern Europe region, Standard storage class):
mcpproxy-apt— bound to custom domainapt.mcpproxy.appmcpproxy-rpm— bound to custom domainrpm.mcpproxy.app
Cloudflare 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 secret
GitHub Actions variable:
PACKAGES_GPG_KEY_ID— full fingerprint of the current signing key
Local-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).
GPG key rotation procedure
Rotate the signing key annually (calendar reminder), or immediately on any hint of compromise. Budget: 30 minutes end-to-end.
1. Generate the new key
# 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: mcpproxy-packages@mcpproxy.app
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}"
2. Refresh local backup
# 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
3. Update the public-key file in the repo
gpg --armor --export "${NEW_FPR}" \
> ~/repos/mcpproxy-go/contrib/signing/mcpproxy-packages.asc
4. Update GitHub Actions secrets and variable
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}"
5. Commit the new public key and tag a release
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.
6. Announce rotation
- Add a note to the release notes (in the release PR or GitHub release body).
- Update the fingerprint shown in
docs/features/linux-package-repos.mdandREADME.md. - Update
docs/getting-started/installation.md. - Tell users they may need to re-import the key after the rotation (
curl -fsSL .../mcpproxy.gpg | sudo tee /etc/apt/keyrings/mcpproxy.gpg > /dev/nulland equivalent for dnf).
7. Revoke the old key (optional)
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.
Manual republish of a single release
If the publish-linux-repos job failed for a specific tag and you need to re-run it without re-tagging:
- Go to the Actions tab in the repository
- Pick the failed workflow run for the tag
- Click "Re-run failed jobs" — only the
publish-linux-reposjob re-runs - Scripts are stateless; a retry converges to the correct final state
If 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/
Purge a bad release
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.
Cost model (Cloudflare R2)
The current usage fits the free tier:
- Storage: ~200 MB at 10 releases × 4 artifacts × ~16 MB (limit 10 GB)
- Class A (writes): ~30–50/run × 1 run/release × ~10 releases/month ≈ 300–500/month (limit 1M/month)
- Class B (reads): dominated by
apt update/dnf makecache; capacity for ~300k Linux users/day before hitting the 10M/month limit
Egress is free on R2. No billing expected under normal usage.
Related
- User-facing: Linux Package Repositories
- Feature spec:
specs/043-linux-package-repos/(internal reference)