secrets: ship encrypted SA key, switch install to git-clone + decrypt

- secrets/bookkeeping-sa.json.enc: team service-account key, encrypted with
  AES-256-CBC + PBKDF2(100k iter) using a 48-char random passphrase. Safe to
  commit to a public repo; the passphrase lives in the team password manager.
- scripts/decrypt-key.sh: one-liner that decrypts to ~/.config/gcp/ (mode 600)
  and prints the service-account email so users know which address to share
  their Sheet with.
- secrets/README.md: explains the crypto, decrypt flow, and rotation
  procedures (passphrase rotation vs underlying GCP key rotation).
- README + DEPLOY.md + setup.md: install flow updated. Users no longer wait
  for the admin to send a JSON; they git clone, run decrypt-key.sh with the
  passphrase from the team password manager, and continue. Cuts one
  out-of-band file transfer from the user experience.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 11:24:48 +08:00
parent d9707aeba7
commit 56003b7f69
7 changed files with 254 additions and 95 deletions

View File

@@ -23,39 +23,65 @@ The shared-key model: you create **one** Google Cloud service account and distri
5. **Copy the service account's email** (e.g. `autoacct@<project>.iam.gserviceaccount.com`).
6. Rename the downloaded file to `autoacct-sa.json` (recommended — DEPLOY.md assumes this name).
### A.2 — Distribute the key + email to users
### A.2 — Encrypt the JSON and commit it to the repo
The JSON is a private key. Use a secure channel:
Generate a strong random passphrase (48 chars; alphanumeric + `-_`):
| Channel | Verdict |
|---|---|
| **Password manager shared vault** (1Password, Bitwarden, Vaultwarden) | **Recommended.** Easy to revoke, no copies floating in inboxes. |
| Encrypted email / Signal / private DM | OK for small teams. |
| Cloud drive with strict per-user ACLs | OK if your org already uses one. |
| **Plain email** | Do not. |
| **Git repo (even private)** | Do not — `.gitignore` already excludes `*-sa.json`. |
```bash
python3 -c "import secrets, string; print(''.join(secrets.choice(string.ascii_letters + string.digits + '_-') for _ in range(48)))"
```
Encrypt with openssl (AES-256-CBC + PBKDF2, 100k iterations):
```bash
PASSPHRASE='<paste-passphrase-here>' openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -salt \
-pass env:PASSPHRASE \
-in ~/Downloads/<downloaded-key>.json \
-out secrets/bookkeeping-sa.json.enc
git add secrets/bookkeeping-sa.json.enc
git commit -m "secrets: add encrypted SA key"
git push
```
Then **store the passphrase in your team password manager** (1Password / Bitwarden shared vault). The passphrase is the only out-of-band thing your users need.
Move the plaintext key out of the repo dir and protect it on your own machine:
```bash
mv ~/Downloads/<downloaded-key>.json ~/.config/gcp/bookkeeping-sa.json
chmod 600 ~/.config/gcp/bookkeeping-sa.json
```
Tell each user:
- The JSON file (attach / share)
- The service-account email (so they know who to share their sheet with)
- Pointer to [`DEPLOY.md`](../DEPLOY.md) if they want hand-holding, or [`README.md`](../README.md) if they're comfortable with the terminal
- A pointer to the repo (`git clone https://github.com/CharlesZhang2023/AutoACCT.git`)
- The passphrase (via 1Password share — **never via plain email / chat**)
- Link to [`DEPLOY.md`](../DEPLOY.md) for hand-holding or [`README.md`](../README.md) if they're comfortable in the terminal
### A.3 — Verify your own install first
Before sending the JSON to anyone, run through the user-side install yourself (Steps 111 of `DEPLOY.md`) to confirm everything works end-to-end. It's also a chance to catch any GCP-side misconfiguration before users hit it.
Before announcing it to anyone, run through the user-side install yourself (`DEPLOY.md` Parts 14) on a clean directory to confirm `git clone` + `decrypt-key.sh` + sheet creation + smoke test all work end-to-end. Catches any GCP-side misconfiguration before users hit it.
### A.4 — Key rotation (when you need to)
### A.4 — Rotation
Rotate the JSON key when:
- A user leaves the team (so you stop trusting their copy of the key)
- You suspect a leak
- Every 612 months as routine hygiene
**Passphrase rotation** (when a user leaves, or every 612 months):
1. Generate a new passphrase as in A.2.
2. Decrypt with the old passphrase, re-encrypt with the new one:
```bash
openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -d \
-in secrets/bookkeeping-sa.json.enc -out /tmp/sa.json
PASSPHRASE='<new>' openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -salt \
-pass env:PASSPHRASE -in /tmp/sa.json -out secrets/bookkeeping-sa.json.enc
shred -u /tmp/sa.json # rm -P on macOS
```
3. Commit + push the new `.enc`. Update the passphrase entry in the team password manager. Users `git pull` + re-run `decrypt-key.sh`.
To rotate:
1. In GCP Console → Service Accounts → keys → **Add Key** (creates a new one) → download.
2. **Delete the old key** in the same panel. (After deletion, any existing copies stop working.)
3. Re-distribute the new JSON to all current users via your secure channel.
4. Users replace their `~/.config/gcp/autoacct-sa.json` with the new file. No other changes needed — `config.json`, sheet sharing, etc. all stay intact.
**Underlying GCP key rotation** (when the passphrase leaks, or a user with a decrypted copy leaves):
- Passphrase rotation alone is **not enough** if someone already has the decrypted JSON on their machine — they retain a working credential.
- GCP Console → Service Accounts → Keys → **Add Key** (download new) → **Delete old**. The deleted key stops working immediately, globally.
- Re-encrypt the new JSON (A.2 flow), commit, push. Users pull + decrypt.
See [`secrets/README.md`](../secrets/README.md) for the same procedures with copy-pasteable commands.
---
@@ -70,13 +96,16 @@ The `worksheet` value in `config.json` doesn't match the actual tab name. Most c
### `HTTP 404` / `Requested entity was not found`
`sheet_id` in `config.json` is wrong. Tell user to re-copy the long string from `/d/.../edit` in their browser's URL bar.
### `FileNotFoundError ... autoacct-sa.json`
The JSON file isn't where `config.json` expects. Common causes:
- User saved with a different filename (e.g. `autoacct-project-12345.json`) and never renamed it.
- User skipped the `mv` step and the file is still in Downloads.
### `FileNotFoundError ... bookkeeping-sa.json`
User skipped `bash scripts/decrypt-key.sh`, or decryption failed and they didn't notice. Have them re-run it and confirm the success line `Decrypted to ~/.config/gcp/bookkeeping-sa.json`.
Run `ls -la ~/.config/gcp/` to check.
### `bad decrypt` from openssl
Wrong passphrase. Most common causes:
- They pasted the wrong entry from the password manager.
- The passphrase has been rotated since last time. Have them check the password manager for the latest version.
### `ImportError: No module named 'googleapiclient'`
Python deps not installed. Run `pip install google-api-python-client google-auth`. If `pip` is missing, try `pip3` or `python3 -m pip install ...`.