AlmaLinux / AlmaLinux/build-system
albs-sign-file, albs-sign-node: support fetching GPG key passphrases from Google Secret Manager
- Lenguaje dominante
- Sin datos de lenguaje
- Estrellas
- 32
- Forks
- 11
- Métricas de merge de PR
- Sin PR fusionados en 30 d
Descripción
### Background
[#280](https://github.com/AlmaLinux/build-system/issues/280) ("Bitwarden (and
other vaults) integration in sign service") added the ability to fetch GPG key
passphrases from a Bitwarden vault, and was closed once Bitwarden shipped.
[#548](https://github.com/AlmaLinux/build-system/issues/548) covers the
HashiCorp Vault half. This issue covers Google Secret Manager, for deployments
that already run on GCP and manage every other secret there.
Bitwarden is the only non-interactive option today, and it is the weakest of
the three for a service running several workers: the `bw` CLI must be
installed on the host, and it keeps local session state that several worker
processes unlock concurrently at startup. Google Secret Manager needs no CLI
and keeps no local state — each worker simply makes its own API call.
### Goal
Allow the sign services to pull per-key GPG passphrases from Google Secret
Manager at startup, as an alternative to Bitwarden.
### Why this is small
The Bitwarden work already introduced the right seam. Passphrases are resolved
once at startup and handed to `PGPPasswordDB` as `preloaded_passwords`:
- `albs-sign-file`: `sign/pgp/bitwarden.py::fetch_passphrases()`, consumed by
`sign/pgp/pgp_password_db.py`, whose documented precedence is already
`preloaded passwords > development password > interactive prompt`.
- `albs-sign-node`: same shape —
`sign_node/utils/bitwarden.py::fetch_passphrases()`, called from
`almalinux_sign_node.py` and passed into
`PGPPasswordDB(..., preloaded_passwords=...)`.
So GSM support is a second provider returning the same
`Dict[keyid, passphrase]`, not a rework of the signing flow.
### Proposed scope
**1. Provider selection and mutual exclusion**
Providers all feed the same password DB, so enabling two would make the
effective passphrase source depend on evaluation order. Enabling more than one
must be a configuration error, rejected at startup — not a merge, and not a
silent precedence rule.
**2. albs-sign-file config** (nested pydantic section in `sign/config.py`,
mirroring the `bitwarden:` block in `config.yaml`, with `SF_GSM_*` env
overrides):
```yaml
gsm:
enabled: false
project_id: almalinux-signing
secret_prefix: '' # optional, prepended to the keyid
secret_version: latest # optional
credentials_file: # optional; ADC is used when unset
```
**3. albs-sign-node config** (flat keys, cerberus schema in
`sign_node/config.py`, alongside the existing `bitwarden_*` keys):
```yaml
gsm_enabled: false
gsm_project_id: almalinux-signing
gsm_secret_prefix:
gsm_secret_version: latest
gsm_credentials_file:
```
**4. Secret layout**
One secret per key, whose **ID is the keyid** (optionally prefixed) and whose
payload is the passphrase — keeping the Bitwarden convention of one item per
keyid:
```
gcloud secrets create 7C3955C2A345DA89 --replication-policy=automatic
printf '%s' '' | \
gcloud secrets versions add 7C3955C2A345DA89 --data-file=-
```
`printf` rather than `echo` so no trailing newline is stored. A single
trailing newline should be stripped on read regardless, since it is an easy
mistake to make from a shell; other whitespace must be preserved, because it
can be part of the passphrase.
**5. Authentication**
Application Default Credentials by default — workload identity, the GCE
metadata server, or `GOOGLE_APPLICATION_CREDENTIALS` — with an explicit
service account key file as the escape hatch for non-GCP hosts. ADC is
preferred and should be documented as such: nothing secret lands on disk.
The service account needs `roles/secretmanager.secretAccessor` on the
secrets. A permission error is a deployment mistake, not a missing key, and
should be reported as such rather than folded into "missing passphrases".
Token renewal is out of scope: passphrases are read once at startup, so no
long-lived session is maintained in-process.
**6. Dependency**
Use `google-cloud-secret-manager`. In `albs-sign-file` add it as a `[gsm]`
extra next to the existing `[bitwarden]` and `[kms]` extras in `setup.py`,
imported lazily so the dependency is only required when the provider is
enabled (same pattern as `sign/pgp/bitwarden.py`). Mirror the lazy import in
`albs-sign-node`.
The client object must be constructed inside the fetch call rather than at
import time: a gRPC channel created before a `fork()` (e.g. under
`gunicorn --preload`) does not survive into the worker.
### Multi-worker note
`albs-sign-file` runs uvicorn with `workers=4` (`start.py`), and the backend
is initialized per worker from the lifespan handler, before the server accepts
requests. Each worker therefore fetches the passphrases independently: N API
calls, no shared state, no locking, and the blocking call costs startup
latency only. This is also what makes multi-worker deployments viable at all —
the interactive `getpass` fallback is unusable with more than one worker, as
every worker prompts on the same terminal.
### Acceptance criteria
- [ ] With `gsm.enabled: true` and all keyids present in Secret Manager, the
service starts with no interactive prompt and signs successfully.
- [ ] A missing secret, an empty payload, or a denied `secretAccessor`
permission fails at startup with a clear error naming the offending
keyid or condition — no silent fallback to a prompt or to a dev
passphrase.
- [ ] Enabling more than one passphrase provider is rejected at config
validation time.
- [ ] `google-cloud-secret-manager` is not required when the provider is
disabled.
- [ ] Unit tests with a mocked Secret Manager client, mirroring
`tests/pgp/bitwarden_test.py` and the sign-node equivalent.
- [ ] README / `config.yaml` / `node-config/sign_node.yml` document the new
keys and the expected secret layout.
### Out of scope
- Storing the GPG *private keys* themselves in Secret Manager (only
passphrases here).
- Google Cloud KMS as a signing backend — the AWS KMS backend already exists
in `albs-sign-file`, and a GCP equivalent deserves its own issue.
- Ansible/deployment wiring in `albs-deploy` — follow-up once the config
surface is settled.
### Related
- [#280](https://github.com/AlmaLinux/build-system/issues/280) — Bitwarden (and other vaults) integration in sign service (closed)
- [#548](https://github.com/AlmaLinux/build-system/issues/548) — the same feature for HashiCorp Vault
- [#542](https://github.com/AlmaLinux/build-system/issues/542) — intermittent RPM sign failures, gpg-agent passphrase cache expiring
Guía de contribución
Evaluación
Este issue todavía no se ha evaluado.