Skip to content

Commit ff19e0a

Browse files
authored
ci: add manual Namecheap secret sync workflow
## Summary - add a manual-only, GitHub-hosted Namecheap secret sync workflow - scope OIDC and its inline session policy to the one allowlisted secret and required KMS keys - document operator use and external least-privilege AWS prerequisites ## Validation - inspected the final diff: two added files only, with no decrypted or plaintext secret material - workflow dispatch was intentionally not run The workflow creates a per-run `automation/secret-sync-namecheap-api-*` branch and PR; it never pushes directly to `main`.
1 parent ccf7e67 commit ff19e0a

2 files changed

Lines changed: 185 additions & 0 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
name: namecheap-secret-sync
2+
3+
on:
4+
workflow_dispatch:
5+
6+
permissions:
7+
contents: write
8+
id-token: write
9+
pull-requests: write
10+
11+
concurrency:
12+
group: namecheap-secret-sync
13+
cancel-in-progress: false
14+
15+
jobs:
16+
sync:
17+
# The OIDC trust policy is restricted to the default branch; fail closed if
18+
# someone dispatches this workflow from another ref.
19+
if: github.ref == 'refs/heads/main'
20+
runs-on: ubuntu-24.04
21+
timeout-minutes: 10
22+
env:
23+
AWS_REGION: us-west-2
24+
SECRET_ID: xnoto-namecheap-api
25+
SOPS_VERSION: v3.13.3
26+
SOPS_SHA256: e5bec3346a873ae91d871550f3e698c1aad962aff462a080e40f25fde17fef6b
27+
AWS_ROLE_ARN: ${{ vars.NAMECHEAP_SECRET_SYNC_AWS_ROLE_ARN }}
28+
SECRETS_KMS_KEY_ARN: ${{ vars.NAMECHEAP_SECRET_SYNC_SECRETS_KMS_KEY_ARN }}
29+
steps:
30+
- name: Check required configuration
31+
shell: bash
32+
run: |
33+
set -euo pipefail
34+
set +x
35+
test -n "$AWS_ROLE_ARN"
36+
test -n "$SECRETS_KMS_KEY_ARN"
37+
38+
- name: Check out main
39+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
40+
with:
41+
ref: main
42+
persist-credentials: true
43+
44+
- name: Configure AWS credentials
45+
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
46+
with:
47+
aws-region: ${{ env.AWS_REGION }}
48+
role-to-assume: ${{ env.AWS_ROLE_ARN }}
49+
role-session-name: namecheap-secret-sync-${{ github.run_id }}
50+
role-duration-seconds: 900
51+
allowed-account-ids: 332355796717
52+
mask-aws-account-id: true
53+
unset-current-credentials: true
54+
retry-max-attempts: 3
55+
action-timeout-s: 30
56+
inline-session-policy: >-
57+
{"Version":"2012-10-17","Statement":[{"Sid":"ReadOnlyNamecheapSecret","Effect":"Allow","Action":"secretsmanager:GetSecretValue","Resource":"arn:aws:secretsmanager:us-west-2:332355796717:secret:xnoto-namecheap-api-*"},{"Sid":"DecryptOnlyRequiredKeys","Effect":"Allow","Action":"kms:Decrypt","Resource":["arn:aws:kms:us-west-2:332355796717:key/0a45c0f6-71dc-4d54-ab33-9df4de1a9e91","${{ env.SECRETS_KMS_KEY_ARN }}"]}]}
58+
59+
- name: Install verified SOPS
60+
id: sops
61+
shell: bash
62+
run: |
63+
set -euo pipefail
64+
set +x
65+
sops_bin="$RUNNER_TEMP/sops-${SOPS_VERSION}"
66+
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \
67+
--output "$sops_bin" \
68+
"https://github.com/getsops/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.linux.amd64"
69+
printf '%s %s\n' "$SOPS_SHA256" "$sops_bin" | sha256sum --check --status
70+
chmod 0755 "$sops_bin"
71+
printf 'path=%s\n' "$sops_bin" >> "$GITHUB_OUTPUT"
72+
73+
- name: Update the allowlisted encrypted key
74+
env:
75+
SOPS_BIN: ${{ steps.sops.outputs.path }}
76+
shell: bash
77+
run: |
78+
set -euo pipefail
79+
set +x
80+
umask 077
81+
82+
# Fail rather than create a new key; no decrypted value is emitted.
83+
"$SOPS_BIN" --decrypt --extract '["namecheap_api_key"]' secrets/secrets.yaml >/dev/null
84+
85+
namecheap_api_key="$(aws secretsmanager get-secret-value \
86+
--secret-id "$SECRET_ID" \
87+
--query SecretString \
88+
--output text)"
89+
test -n "$namecheap_api_key"
90+
91+
# --set edits the existing encrypted document in place, limiting the
92+
# plaintext lifetime to this shell and avoiding decrypted files.
93+
json_value="$(printf '%s' "$namecheap_api_key" | jq -Rsc '.')"
94+
"$SOPS_BIN" --set '["namecheap_api_key"] '"$json_value" secrets/secrets.yaml
95+
unset json_value namecheap_api_key
96+
97+
- name: Create a scoped pull request
98+
env:
99+
GH_TOKEN: ${{ github.token }}
100+
shell: bash
101+
run: |
102+
set -euo pipefail
103+
set +x
104+
branch="automation/secret-sync-namecheap-api-${GITHUB_RUN_ID}"
105+
106+
git add -- secrets/secrets.yaml
107+
git diff --cached --check
108+
if git diff --cached --quiet; then
109+
printf '%s\n' 'The encrypted value is already current; no pull request was created.'
110+
exit 0
111+
fi
112+
113+
changed_files="$(git diff --cached --name-only)"
114+
test "$changed_files" = "secrets/secrets.yaml"
115+
git switch --create "$branch"
116+
git config user.name 'github-actions[bot]'
117+
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
118+
git commit -m 'chore: sync Namecheap API key'
119+
git push --set-upstream origin "$branch"
120+
121+
pr_url="$(gh pr create \
122+
--base main \
123+
--head "$branch" \
124+
--title 'chore: sync Namecheap API key' \
125+
--body 'Automated manual sync of the encrypted `namecheap_api_key` value. Review the encrypted-file diff before merge.')"
126+
{
127+
printf 'Created branch: `%s`\n\n' "$branch"
128+
printf 'Created pull request: %s\n' "$pr_url"
129+
} >> "$GITHUB_STEP_SUMMARY"

docs/namecheap-secret-sync.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Namecheap API Key Secret Sync
2+
3+
`.github/workflows/namecheap-secret-sync.yml` is a manual recovery/rotation workflow. It is intentionally available only through `workflow_dispatch`, runs on GitHub-hosted `ubuntu-24.04`, and creates a new scoped pull request instead of updating `main`.
4+
5+
## Operator Procedure
6+
7+
1. Confirm that the AWS secret named `xnoto-namecheap-api` has its complete Namecheap API key in `SecretString`; it must not be JSON-wrapped.
8+
2. In this repository, select **Actions**, **namecheap-secret-sync**, and **Run workflow** from `main`.
9+
3. Review the resulting `automation/secret-sync-namecheap-api-<run-id>` pull request. The encrypted file should be the only changed file. Merge it only after normal branch-protection checks pass.
10+
4. If the key is already current, the run exits without creating a branch or pull request.
11+
12+
Do not add inputs to this workflow for secret identifiers or values. The workflow has a fixed allowlist for `xnoto-namecheap-api` and updates only the pre-existing `namecheap_api_key` path in `secrets/secrets.yaml`. It does not print or write decrypted secret material to a file.
13+
14+
## AWS Prerequisites
15+
16+
These resources are intentionally not managed by this OpenTofu root. Before dispatching the workflow, an AWS administrator must configure the following least-privilege prerequisites.
17+
18+
Set these non-secret repository variables:
19+
20+
- `NAMECHEAP_SECRET_SYNC_AWS_ROLE_ARN`: ARN of the dedicated OIDC role.
21+
- `NAMECHEAP_SECRET_SYNC_SECRETS_KMS_KEY_ARN`: CMK ARN that encrypts the `xnoto-namecheap-api` Secrets Manager secret.
22+
23+
The role trust policy must allow only GitHub's OIDC provider, audience `sts.amazonaws.com`, and this repository's `main` ref. For this existing repository, use this subject condition (replace only the account ID in the provider ARN):
24+
25+
```json
26+
{
27+
"Version": "2012-10-17",
28+
"Statement": [{
29+
"Effect": "Allow",
30+
"Principal": {
31+
"Federated": "arn:aws:iam::332355796717:oidc-provider/token.actions.githubusercontent.com"
32+
},
33+
"Action": "sts:AssumeRoleWithWebIdentity",
34+
"Condition": {
35+
"StringEquals": {
36+
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
37+
"token.actions.githubusercontent.com:sub": "repo:makeitworkcloud/tfroot-github:ref:refs/heads/main"
38+
}
39+
}
40+
}]
41+
}
42+
```
43+
44+
If this repository has opted into GitHub's immutable OIDC subject claims, use its GitHub-provided immutable `sub` value instead of the legacy value above. Do not broaden the condition to an organization or branch wildcard.
45+
46+
Attach a role policy limited to these resources:
47+
48+
- `secretsmanager:GetSecretValue` for `arn:aws:secretsmanager:us-west-2:332355796717:secret:xnoto-namecheap-api-*`.
49+
- `kms:Decrypt` for `arn:aws:kms:us-west-2:332355796717:key/0a45c0f6-71dc-4d54-ab33-9df4de1a9e91`, which is the SOPS key in `.sops.yaml`.
50+
- `kms:Decrypt` for the CMK in `NAMECHEAP_SECRET_SYNC_SECRETS_KMS_KEY_ARN`.
51+
52+
The workflow applies the same resource limits as an inline session policy, so the role must not rely on broader permissions. The Secrets Manager CMK key policy must also permit this role to decrypt that one secret. No other Secrets Manager secret, KMS key, or AWS action is required.
53+
54+
## Supply Chain
55+
56+
The workflow downloads the `linux.amd64` binary for SOPS `v3.13.3` directly from the official `getsops/sops` GitHub release and verifies SHA-256 `e5bec3346a873ae91d871550f3e698c1aad962aff462a080e40f25fde17fef6b` before execution. GitHub actions are commit-SHA pinned.

0 commit comments

Comments
 (0)