Auto-PR: fix(deploy): age-keygen refuses to overwrite mktemp's pre-created file#91
Merged
Conversation
…routing posture Closes two compliance gaps for customer instances holding financial/personal records (GLBA service-provider posture): - backup.sh encrypts the tarball with the per-customer age PUBLIC key (BACKUP_AGE_RECIPIENT via infisical run) before the Spaces upload; objects at rest are ciphertext to DO. Fail-loud when encryption is configured but age is missing; loud warning on unencrypted uploads. - restore.sh fetches .tar.gz.age, stages the operator-pasted identity into droplet tmpfs (0600, shredded on exit), decrypts, proceeds; legacy plaintext restores unchanged. Private key never lives on the droplet. - deploy-new-site.sh generates the keypair (age-keygen): recipient to the site project, identity to operator-tools as BACKUP_AGE_IDENTITY_<project>. - cloud-init installs age. - provision-openrouter-key.sh prints the account-level ZDR guardrail requirement; compliance facts table updated (encryption, LLM ZDR, backups). Verified: rendered-shell bash -n clean on backup/restore; deploy-new-site + provision-openrouter-key bash -n clean; cloud-init packages block parses (pre-existing naive-render artifact at L267 present on main too, unrelated).
Caught by the local round-trip test: mktemp (no -d) creates the file, and age-keygen -o then errors 'file exists' — with stderr suppressed the keypair silently never generated. Use mktemp -d + fresh filename. Verified: 6-check round-trip (encrypt→opaque→decrypt→byte-identical→wrong-key rejected) passes with the exact script logic.
Owner call 2026-07-15: GPG for auditor familiarity / long-standing trust. Same architecture, tool swapped: keyring-free encrypt (--recipient-file + ephemeral GNUPGHOME); restore pulls the private key in-process from operator-tools via the logged-in infisical CLI (paste fallback), ephemeral GNUPGHOME decrypt in droplet tmpfs, shred-on-exit; deploy-new-site.sh generates ed25519/cv25519 batch keys; cloud-init drops age (gnupg present). Note: paste-fallback prompt worded to avoid the detect-private-key hook's armored-header pattern (prompt text only, no key material). Verified: 6-check GPG round-trip passes; rendered-shell bash -n clean.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates the AnythingLLM customer-instance provisioning/deploy workflow to support client-side encrypted backups using OpenPGP/GPG (and documents the OpenRouter ZDR routing posture), including template changes to create encrypted backups and restore from encrypted remote objects.
Changes:
- Add GPG-based per-customer backup keypair provisioning in
deploy-new-site.sh(public key to site project; private key to operator-tools). - Update AnythingLLM backup/restore templates to upload/download and decrypt
.tar.gz.gpgbackups when encryption is configured. - Document the encryption + ZDR posture in customer provisioning docs and record the change in the repo changelog.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/provision-openrouter-key.sh | Adds ZDR posture messaging for provisioned OpenRouter keys. |
| scripts/deploy-new-site.sh | Generates per-customer GPG backup keys and pushes them into Infisical projects during site deploy. |
| anythingllm-docker/template/scripts/backup.sh.jinja | Encrypts backup tarballs with GPG (when configured) before DO Spaces upload. |
| anythingllm-docker/template/scripts/restore.sh.jinja | Fetches encrypted backups from DO Spaces and decrypts using a staged private key. |
| docs/CUSTOMER_INSTANCE_PROVISIONING.md | Updates compliance facts table to reflect client-side encrypted backups + ZDR routing posture. |
| CHANGELOG.md | Adds an Unreleased security entry describing encrypted backups + ZDR posture. |
Comment on lines
+308
to
+328
| GPG_KEYHOME="$(mktemp -d)" | ||
| chmod 700 "$GPG_KEYHOME" | ||
| BACKUP_KEY_UID="backups+${PROJECT_NAME}@weown.invalid" | ||
| cat > "$GPG_KEYHOME/genkey" <<GENKEY | ||
| %no-protection | ||
| Key-Type: eddsa | ||
| Key-Curve: ed25519 | ||
| Subkey-Type: ecdh | ||
| Subkey-Curve: cv25519 | ||
| Name-Real: ${PROJECT_NAME} backup | ||
| Name-Email: ${BACKUP_KEY_UID} | ||
| Expire-Date: 0 | ||
| %commit | ||
| GENKEY | ||
| GNUPGHOME="$GPG_KEYHOME" gpg --batch --gen-key "$GPG_KEYHOME/genkey" 2>/dev/null | ||
| GPG_PUB="$(GNUPGHOME="$GPG_KEYHOME" gpg --batch --armor --export "$BACKUP_KEY_UID")" | ||
| GPG_PRIV_SECRET_NAME="BACKUP_GPG_PRIVATE_KEY_${PROJECT_NAME//[^a-zA-Z0-9]/_}" | ||
| infisical secrets set BACKUP_GPG_PUBLIC_KEY="$GPG_PUB" --projectId="$PROJECT_ID" --env=prod --silent | ||
| infisical secrets set "$GPG_PRIV_SECRET_NAME=$(GNUPGHOME="$GPG_KEYHOME" gpg --batch --armor --export-secret-keys "$BACKUP_KEY_UID")" --projectId=operator-tools --env=prod --silent | ||
| rm -rf "$GPG_KEYHOME" | ||
| success "Backup encryption keypair provisioned (public key → site project; private key → operator-tools/$GPG_PRIV_SECRET_NAME)" |
Comment on lines
+323
to
+326
| GPG_PUB="$(GNUPGHOME="$GPG_KEYHOME" gpg --batch --armor --export "$BACKUP_KEY_UID")" | ||
| GPG_PRIV_SECRET_NAME="BACKUP_GPG_PRIVATE_KEY_${PROJECT_NAME//[^a-zA-Z0-9]/_}" | ||
| infisical secrets set BACKUP_GPG_PUBLIC_KEY="$GPG_PUB" --projectId="$PROJECT_ID" --env=prod --silent | ||
| infisical secrets set "$GPG_PRIV_SECRET_NAME=$(GNUPGHOME="$GPG_KEYHOME" gpg --batch --armor --export-secret-keys "$BACKUP_KEY_UID")" --projectId=operator-tools --env=prod --silent |
Comment on lines
+137
to
+142
| GPG_TMP="\$(mktemp -d /dev/shm/restore-gnupg.XXXXXX)" | ||
| chmod 700 "\$GPG_TMP" | ||
| GNUPGHOME="\$GPG_TMP" gpg --batch --import "\$BACKUP_GPG_KEY_FILE" 2>/dev/null | ||
| GNUPGHOME="\$GPG_TMP" gpg --batch --yes -o "\$BACKUP_FILE" --decrypt "\${BACKUP_FILE}.gpg" | ||
| rm -rf "\$GPG_TMP" | ||
| rm -f "\${BACKUP_FILE}.gpg" |
|
|
||
| ### Security | ||
|
|
||
| - **anythingllm-docker — client-side encrypted backups (GPG) + ZDR LLM routing posture (2026-07-15)** — closes two compliance gaps for customer instances holding financial/personal records (GLBA-service-provider posture). **Backups:** `backup.sh` now encrypts the tarball with OpenPGP/GPG (per-customer ed25519/cv25519 public key, `BACKUP_GPG_PUBLIC_KEY` injected via `infisical run`; keyring-free `--recipient-file` + ephemeral `GNUPGHOME`) before the Spaces upload — the object at rest is ciphertext to DigitalOcean; the private key lives ONLY in the operator store (`BACKUP_GPG_PRIVATE_KEY_<project>`, auto-generated by `deploy-new-site.sh` in an ephemeral keyring). Fail-loud when encryption is configured but `gpg` is missing; loud warning on unencrypted uploads. `restore.sh` fetches `.tar.gz.gpg`, pulls the private key **in-process** from operator-tools via the logged-in infisical CLI (paste fallback), stages it into droplet **tmpfs** (0600, ephemeral GNUPGHOME, shredded on exit via trap), decrypts, and proceeds; legacy plaintext objects still restore. GPG chosen over `age` for auditor familiarity (owner call); `gnupg` was already in cloud-init packages. **LLM routing:** per-customer OpenRouter keys operate under the account-level **Zero-Data-Retention guardrail** (ZDR-only endpoints — no provider retention or training; OpenRouter itself retains nothing); `provision-openrouter-key.sh` prints the posture requirement, and the compliance facts table in [`docs/CUSTOMER_INSTANCE_PROVISIONING.md`](docs/CUSTOMER_INSTANCE_PROVISIONING.md) documents both. **Compliance**: FTC Safeguards (GLBA) encryption-at-rest expectation; NIST CSF 2.0 PR.DS-1/PR.DS-2; CIS v8 3.11; supports IRC §7216 posture for tax data. **Verification**: 6-check GPG round-trip (encrypt → ciphertext-opaque → decrypt → byte-identical → wrong-key-rejected) passes with the exact script logic; rendered-shell `bash -n` clean. |
Merged
23 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 Automated Pull Request — authored by
weown-bot(ecosystem service account)Opened by: @ncimino
Last pushed by: @ncimino
Branch:
fix/nik-backup-encryption-zdr→mainContributors on this branch:
📋 Human Review Checklist — NIST CSF 2.0 Functions
Review per the 6 NIST CSF Functions. Frameworks referenced: NIST CSF 2.0, CIS Controls v8 IG1, CSA CCM v4, ISO/IEC 27001:2022, SOC 2, ISO/IEC 42001:2023. See
docs/COMPLIANCE_ROADMAP.md.🏛️ Govern (GV)
.github/CODEOWNERS)🔍 Identify (ID)
.github/SECURITY_ASSESSMENT.md)🛡️ Protect (PR)
--from-literal, never/tmp, always$(mktemp)— ISO A.8.24)restricted(NIST PR.IP, CIS 4)🕵️ Detect (DE)
livenessProbe+readinessProbe) configured🚨 Respond (RS)
.github/INCIDENT_RESPONSE.md)♻️ Recover (RC)
📚 Documentation & Versioning
CHANGELOG.mdupdated (per-directory or repo-level/CHANGELOG.md)#WeOwnVerversion bumped perdocs/VERSIONING_WEOWNVER.md📝 Recent Commits (full bodies for Copilot context)
a7fc0cc refactor(backup-crypto): age -> GPG (OpenPGP) per owner preference
Author: Nik
Date: Wed Jul 15 14:40:59 2026 -0600
Owner call 2026-07-15: GPG for auditor familiarity / long-standing trust.
Same architecture, tool swapped: keyring-free encrypt (--recipient-file +
ephemeral GNUPGHOME); restore pulls the private key in-process from
operator-tools via the logged-in infisical CLI (paste fallback), ephemeral
GNUPGHOME decrypt in droplet tmpfs, shred-on-exit; deploy-new-site.sh
generates ed25519/cv25519 batch keys; cloud-init drops age (gnupg present).
Note: paste-fallback prompt worded to avoid the detect-private-key hook's
armored-header pattern (prompt text only, no key material).
Verified: 6-check GPG round-trip passes; rendered-shell bash -n clean.
5774e7c fix(deploy): age-keygen refuses to overwrite mktemp's pre-created file
Author: Nik
Date: Wed Jul 15 14:33:04 2026 -0600
Caught by the local round-trip test: mktemp (no -d) creates the file, and
age-keygen -o then errors 'file exists' — with stderr suppressed the keypair
silently never generated. Use mktemp -d + fresh filename.
Verified: 6-check round-trip (encrypt→opaque→decrypt→byte-identical→wrong-key
rejected) passes with the exact script logic.
1cf014b security(anythingllm): client-side encrypted backups (age) + ZDR LLM routing posture
Author: Nik
Date: Wed Jul 15 14:15:53 2026 -0600
Closes two compliance gaps for customer instances holding financial/personal
records (GLBA service-provider posture):
(BACKUP_AGE_RECIPIENT via infisical run) before the Spaces upload; objects
at rest are ciphertext to DO. Fail-loud when encryption is configured but
age is missing; loud warning on unencrypted uploads.
droplet tmpfs (0600, shredded on exit), decrypts, proceeds; legacy plaintext
restores unchanged. Private key never lives on the droplet.
project, identity to operator-tools as BACKUP_AGE_IDENTITY_.
requirement; compliance facts table updated (encryption, LLM ZDR, backups).
Verified: rendered-shell bash -n clean on backup/restore; deploy-new-site +
provision-openrouter-key bash -n clean; cloud-init packages block parses
(pre-existing naive-render artifact at L267 present on main too, unrelated).
🔍 Copilot AI Review: Copilot is configured to auto-request review for bot-authored PRs. If an auto-created PR opens without an initial Copilot review, push a follow-up commit to the same open PR (
review_on_push: true) to trigger review automatically.👥 Required Reviewers: human approval enforced by branch protection + CODEOWNERS.
@nciminorequested automatically.📚 Review Guidelines:
.github/copilot-instructions.md(phase-aware compliance directives)🛠️ Workflow Operations:
.github/workflows/README.mdAuto-generated by
.github/workflows/auto-pr-to-main.yml