Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions docs/docs/designs/aws-lab-account.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,8 @@ An AWS-resident bootstrap instance must be able to, starting from only its
IAM role:

1. Reach the tailnet.
2. Clone the private `secrets/` repo from GitHub.
3. Decrypt SOPS-encrypted files in that repo.
2. Fetch encrypted files from the private `secrets/` repo on GitHub.
3. Decrypt those SOPS-encrypted files locally.

At no point may the instance hold a durable, plaintext credential for any of
the three systems (Tailscale, GitHub, SOPS). All identity traces back to the
Expand All @@ -297,22 +297,25 @@ automation paths live in a separate secrets design doc. This document only
establishes that the KMS key lives in the `lab` account and is the anchor
for machine decryption.

### GitHub App for repo access
### GitHub App token broker

Cloning private repos from an AWS-resident bootstrap instance uses a
Fetching private repo contents from an AWS-resident bootstrap instance uses a
**GitHub App** owned by the `GilmanLab` organization and installed on the
`secrets` repo (plus any other private repos bootstrap needs to reach).
`secrets` repo.

- The App's **private signing key** is stored in an SSM Parameter Store
`SecureString` in the `lab` account.
- The instance's IAM role grants `ssm:GetParameter` + `kms:Decrypt` on that
specific parameter path only.
- On bootstrap, the instance fetches the key, generates a JWT, exchanges it
for a short-lived installation token (1-hour TTL), and clones.
- The `github-token-broker` Lambda execution role grants `ssm:GetParameter` on
the GitHub App parameter path.
- Bootstrap principals get `lambda:InvokeFunction` on the broker, not direct
access to the App private key.
- On bootstrap, the caller invokes the broker, receives a short-lived
installation token for `GilmanLab/secrets` with `contents:read`, and then
fetches encrypted files with `git` or the GitHub Contents API.

The only durable non-AWS secret anywhere in the chain is the App's private
signing key itself, and that key is at rest in AWS, gated by IAM. Installation
tokens are never stored on disk.
signing key itself, and that key is at rest in AWS behind the broker execution
role. Installation tokens are never stored on disk.

### The single-anchor property

Expand All @@ -321,9 +324,10 @@ Taken together, the chain on a single EC2 bootstrap instance is:
| Step | Identity used |
|------|----------------------------------------------------------------|
| Join tailnet | IAM role (via workload identity federation) |
| Read GitHub App key | IAM role (via instance profile → SSM + KMS) |
| Mint installation token | App private key (short-lived, in memory) |
| Clone `secrets/` repo | Installation token (short-lived, in memory) |
| Invoke GitHub token broker | IAM role (via instance profile → Lambda) |
| Read GitHub App key | Lambda execution role (via SSM, optionally KMS) |
| Mint installation token | App private key (short-lived, in broker memory) |
| Fetch encrypted `secrets/` files | Installation token (short-lived, in caller memory) |
| Decrypt SOPS files | IAM role (via instance profile → KMS) |

Every persistent identity is the IAM role. Lose AWS, lose bootstrap. Gain AWS,
Expand Down
16 changes: 9 additions & 7 deletions docs/docs/designs/secrets-and-pki.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,6 @@ arn:aws:kms:us-west-2:186067932323:key/2aba1d94-6eaf-4d80-8d26-2077f32fd7c5
The existing PGP and age recipients are removed from SOPS metadata. After the
cutover, AWS is the only routine decrypt control surface.

This is an intentional break from the current state. Today, `secrets/.sops.yaml`
contains only age and PGP recipients. The KMS key exists, but the SOPS
recipient rollout has not been performed.

### Scoped decryption

SOPS files are encrypted with AWS KMS encryption context so IAM can grant
Expand Down Expand Up @@ -154,9 +150,10 @@ Private repository access uses a GitHub App owned by `GilmanLab` and installed
on `GilmanLab/secrets`.

The App private signing key is stored in SSM Parameter Store as a SecureString
in the `lab` account. A bootstrap workload uses its AWS identity to read that
specific SSM parameter, generates a GitHub App JWT, exchanges it for a
short-lived installation token, and clones the repository over HTTPS.
in the `lab` account. Bootstrap workloads do not read that key directly. They
invoke the `github-token-broker` Lambda with their AWS identity. The broker is
the only normal principal that can read the App private key, generate a GitHub
App JWT, and exchange it for a short-lived installation token.

Installation tokens are requested with the narrowest useful shape:

Expand All @@ -170,6 +167,11 @@ The GitHub token grants access to encrypted files. AWS KMS grants access to
plaintext. A workload may be able to clone the whole private repository while
still being unable to decrypt files outside its KMS context scope.

Callers may use either `git` or the GitHub Contents API with `curl` to fetch
encrypted files. The no-`git` path still uses the GitHub token only for
encrypted repository access; the Lambda does not return SOPS files and does
not decrypt them.

### Historical exposure

Removing PGP and age recipients from current SOPS files does not remove their
Expand Down