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
129 changes: 129 additions & 0 deletions .github/workflows/namecheap-secret-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
name: namecheap-secret-sync

on:
workflow_dispatch:

permissions:
contents: write
id-token: write
pull-requests: write

concurrency:
group: namecheap-secret-sync
cancel-in-progress: false

jobs:
sync:
# The OIDC trust policy is restricted to the default branch; fail closed if
# someone dispatches this workflow from another ref.
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
timeout-minutes: 10
env:
AWS_REGION: us-west-2
SECRET_ID: xnoto-namecheap-api
SOPS_VERSION: v3.13.3
SOPS_SHA256: e5bec3346a873ae91d871550f3e698c1aad962aff462a080e40f25fde17fef6b
AWS_ROLE_ARN: ${{ vars.NAMECHEAP_SECRET_SYNC_AWS_ROLE_ARN }}
SECRETS_KMS_KEY_ARN: ${{ vars.NAMECHEAP_SECRET_SYNC_SECRETS_KMS_KEY_ARN }}
steps:
- name: Check required configuration
shell: bash
run: |
set -euo pipefail
set +x
test -n "$AWS_ROLE_ARN"
test -n "$SECRETS_KMS_KEY_ARN"

- name: Check out main
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: main
persist-credentials: true

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
with:
aws-region: ${{ env.AWS_REGION }}
role-to-assume: ${{ env.AWS_ROLE_ARN }}
role-session-name: namecheap-secret-sync-${{ github.run_id }}
role-duration-seconds: 900
allowed-account-ids: 332355796717
mask-aws-account-id: true
unset-current-credentials: true
retry-max-attempts: 3
action-timeout-s: 30
inline-session-policy: >-
{"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 }}"]}]}

- name: Install verified SOPS
id: sops
shell: bash
run: |
set -euo pipefail
set +x
sops_bin="$RUNNER_TEMP/sops-${SOPS_VERSION}"
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \
--output "$sops_bin" \
"https://github.com/getsops/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.linux.amd64"
printf '%s %s\n' "$SOPS_SHA256" "$sops_bin" | sha256sum --check --status
chmod 0755 "$sops_bin"
printf 'path=%s\n' "$sops_bin" >> "$GITHUB_OUTPUT"

- name: Update the allowlisted encrypted key
env:
SOPS_BIN: ${{ steps.sops.outputs.path }}
shell: bash
run: |
set -euo pipefail
set +x
umask 077

# Fail rather than create a new key; no decrypted value is emitted.
"$SOPS_BIN" --decrypt --extract '["namecheap_api_key"]' secrets/secrets.yaml >/dev/null

namecheap_api_key="$(aws secretsmanager get-secret-value \
--secret-id "$SECRET_ID" \
--query SecretString \
--output text)"
test -n "$namecheap_api_key"

# --set edits the existing encrypted document in place, limiting the
# plaintext lifetime to this shell and avoiding decrypted files.
json_value="$(printf '%s' "$namecheap_api_key" | jq -Rsc '.')"
"$SOPS_BIN" --set '["namecheap_api_key"] '"$json_value" secrets/secrets.yaml
unset json_value namecheap_api_key

- name: Create a scoped pull request
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
set +x
branch="automation/secret-sync-namecheap-api-${GITHUB_RUN_ID}"

git add -- secrets/secrets.yaml
git diff --cached --check
if git diff --cached --quiet; then
printf '%s\n' 'The encrypted value is already current; no pull request was created.'
exit 0
fi

changed_files="$(git diff --cached --name-only)"
test "$changed_files" = "secrets/secrets.yaml"
git switch --create "$branch"
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git commit -m 'chore: sync Namecheap API key'
git push --set-upstream origin "$branch"

pr_url="$(gh pr create \
--base main \
--head "$branch" \
--title 'chore: sync Namecheap API key' \
--body 'Automated manual sync of the encrypted `namecheap_api_key` value. Review the encrypted-file diff before merge.')"
{
printf 'Created branch: `%s`\n\n' "$branch"
printf 'Created pull request: %s\n' "$pr_url"
} >> "$GITHUB_STEP_SUMMARY"
56 changes: 56 additions & 0 deletions docs/namecheap-secret-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Namecheap API Key Secret Sync

`.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`.

## Operator Procedure

1. Confirm that the AWS secret named `xnoto-namecheap-api` has its complete Namecheap API key in `SecretString`; it must not be JSON-wrapped.
2. In this repository, select **Actions**, **namecheap-secret-sync**, and **Run workflow** from `main`.
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.
4. If the key is already current, the run exits without creating a branch or pull request.

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.

## AWS Prerequisites

These resources are intentionally not managed by this OpenTofu root. Before dispatching the workflow, an AWS administrator must configure the following least-privilege prerequisites.

Set these non-secret repository variables:

- `NAMECHEAP_SECRET_SYNC_AWS_ROLE_ARN`: ARN of the dedicated OIDC role.
- `NAMECHEAP_SECRET_SYNC_SECRETS_KMS_KEY_ARN`: CMK ARN that encrypts the `xnoto-namecheap-api` Secrets Manager secret.

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):

```json
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::332355796717:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:makeitworkcloud/tfroot-github:ref:refs/heads/main"
}
}
}]
}
```

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.

Attach a role policy limited to these resources:

- `secretsmanager:GetSecretValue` for `arn:aws:secretsmanager:us-west-2:332355796717:secret:xnoto-namecheap-api-*`.
- `kms:Decrypt` for `arn:aws:kms:us-west-2:332355796717:key/0a45c0f6-71dc-4d54-ab33-9df4de1a9e91`, which is the SOPS key in `.sops.yaml`.
- `kms:Decrypt` for the CMK in `NAMECHEAP_SECRET_SYNC_SECRETS_KMS_KEY_ARN`.

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.

## Supply Chain

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.
Loading