Skip to content

Store secrets in SSM Parameter Store instead of Secrets Manager (breaking) - #154

Merged
sebsto merged 4 commits into
mainfrom
feat/parameter-store
Aug 12, 2026
Merged

Store secrets in SSM Parameter Store instead of Secrets Manager (breaking)#154
sebsto merged 4 commits into
mainfrom
feat/parameter-store

Conversation

@sebsto

@sebsto sebsto commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Summary

Moves the AWS secrets backend from Secrets Manager to SSM Parameter Store. Secrets Manager bills $0.40 per secret per month, so the two secrets this tool keeps cost about $0.80/month per region. Standard-tier Parameter Store parameters are free, and nothing in our usage needed Secrets Manager: no rotation, no resource policies, and only three API calls in total.

This is a breaking change and warrants a major version bump. Existing users must update their IAM policy, re-create their secrets, and rename one flag in their scripts. See Migration below.

Plan and measurements: .claude/plans/parameter-store-migration.md

What changed

  • SotoSecretsManagerSotoSSM.
  • GetSecretValue / PutSecretValue / CreateSecretGetParameter / PutParameter.
  • PutParameter with overwrite: true is an upsert, so createSecret and the executeRequestAndCreateWhenNotExist retry loop are gone. That is most of the deleted lines.
  • Values stored as SecureString, encrypted with the account's default aws/ssm managed key, so no kms:* permission is required.
  • Secret names become a hierarchy: /xcodeinstall/apple-credentials and /xcodeinstall/apple-session-token, so one IAM resource covers both.
  • AuthenticateCommand now catches SSMErrorType.parameterNotFound in place of SecretsManagerErrorType.resourceNotFoundException, keeping the transparent "credentials not stored yet, prompt for them" path working.
  • --secretmanager-region renamed to --secret-region since the backend is no longer Secrets Manager. -s is unchanged.

IAM permissions shrink from three actions to two:

{
    "Sid": "xcodeinstall",
    "Effect": "Allow",
    "Action": [
        "ssm:PutParameter",
        "ssm:GetParameter"
    ],
    "Resource": "arn:aws:ssm:*:000000000000:parameter/xcodeinstall/*"
}

Migration

Three things are required of an existing user:

  1. Update the IAM policy to the two ssm:* actions above. Nothing works until this is done.
  2. Re-create the secrets. Nothing migrates automatically. Re-run storesecrets and authenticate, then delete the old Secrets Manager secrets so they stop being billed.
  3. Rename the flag in CI scripts: --secretmanager-region--secret-region. Scripts using the short -s form need no change.

Also, ~/.xcodeinstall/config.json uses a new key name for the region, so a saved region is dropped on the first run after upgrading and re-saved by the next command that passes -s. The saved profile is unaffected.

The README has all of this in a dedicated migration callout.

Why Intelligent-Tiering

Standard-tier parameters cap the value at 4096 bytes, where Secrets Manager allowed 64 KB. Measured against a real account:

Secret Bytes
apple-credentials 73
apple-session-token right after authenticate 2282
apple-session-token after a later download 2808

The 526-byte jump is the download flow adding ADCDownloadAuth (384) and DSESSIONID (137). Peak observed is 69% of the limit, so standard tier would fit today. Two things make that an unsafe assumption: mergeCookies replaces same-name cookies but never prunes expired ones, so the value only grows; and no measured session contains the aasp cookie that idmsa.apple.com sets on the two-factor path, which would likely land a session around 3.3–3.8 KB.

Intelligent-Tiering keeps the parameter in the free standard tier while it is small and promotes it to advanced ($0.05/parameter/month, 8 KB) only if it ever crosses 4 KB, instead of hard-failing. Worst case is still 8x cheaper than Secrets Manager. Note the promotion is one-way.

Testing

  • swift build clean.
  • swift test: 203 tests in 19 suites pass.
  • Flag rename verified against the built binary: -s, --secret-region on both authenticate and storesecrets, with -p, --profile intact.
  • Package.resolved unchanged.
  • iam/ec2-policy.json validated as JSON.

Not yet verified against a live account, and worth doing before merge:

  1. storesecrets + authenticate through a real MFA prompt, then list / download, to confirm the session round-trips.
  2. Whether Intelligent-Tiering promotes the session parameter on the MFA path (the one size still unmeasured).
  3. That the two-action IAM policy suffices with no kms:*, using a least-privilege role rather than an admin profile.

Notes for the reviewer

ArgumentParser's .short specifier derives the short flag from the property name. Renaming the property to something like parameterStoreRegion would have silently emitted -p and collided with --profile. Keeping the secret prefix (secretRegion) keeps -s correct without an explicit customShort.

Drive-by: the README anchor #using-aws-secrets-manager-1 was already a dead link on main (only one matching heading, so no -1 suffix is generated). Since the line was being edited anyway, it now points at #using-aws-parameter-store.

@sebsto
sebsto requested a lite review from Copilot August 12, 2026 09:56
@sebsto sebsto self-assigned this Aug 12, 2026
@sebsto sebsto changed the title Store secrets in SSM Parameter Store instead of Secrets Manager Store secrets in SSM Parameter Store instead of Secrets Manager (breaking) Aug 12, 2026
sebsto added 2 commits August 12, 2026 12:32
Secrets Manager charges $0.40 per secret per month, so the two secrets
this tool keeps cost ~$0.80/month per region. Standard-tier Parameter
Store parameters are free, and nothing in our usage (no rotation, no
resource policies, three API calls total) needed Secrets Manager.

Swap SotoSecretsManager for SotoSSM and replace GetSecretValue /
PutSecretValue / CreateSecret with GetParameter / PutParameter.
PutParameter with overwrite: true is an upsert, so createSecret and the
executeRequestAndCreateWhenNotExist retry loop are no longer needed.

Values are stored as SecureString, encrypted with the account default
aws/ssm managed key, so no kms:* permission is required.

Parameters use Intelligent-Tiering. Measured session secrets are 2282
bytes right after authenticate and 2808 bytes once the download flow
adds its cookies, against a 4096-byte standard-tier limit. That fits
today, but mergeCookies never prunes expired entries and the MFA path
adds an aasp cookie that has not been measured, so Intelligent-Tiering
keeps the parameter free while it is small and promotes it rather than
failing if it ever crosses 4 KB.

Secrets are renamed to a hierarchy, /xcodeinstall/apple-credentials and
/xcodeinstall/apple-session-token, so a single IAM resource covers both.

The -s/--secretmanager-region flag and the secretManagerRegion config
key keep their names so existing ~/.xcodeinstall/config.json files keep
working. Nothing migrates from Secrets Manager: users re-run
storesecrets and authenticate, as documented in the README.
The backend is no longer Secrets Manager, so the flag name should not
mention it. The long form becomes --secret-region and the short form -s
is unchanged, so existing `-s <region>` invocations keep working.

The saved config key is renamed to match, which means the region in
~/.xcodeinstall/config.json is dropped on first run after upgrading and
re-saved by the next command that passes -s. The profile is unaffected.

Note that the ArgumentParser `.short` specifier derives the short flag
from the property name, so renaming the property to parameterStoreRegion
would have silently produced -p and collided with --profile. Keeping the
"secret" prefix keeps -s correct without an explicit customShort.

The README migration section now lists the three things an existing user
has to do: update the IAM policy, re-create the secrets, and rename the
flag in their scripts.
@sebsto
sebsto force-pushed the feat/parameter-store branch from ee9e6ce to 7ae1818 Compare August 12, 2026 10:34
@sebsto

sebsto commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

Code Review — Migrate from Secrets Manager to SSM Parameter Store

Overall Verdict

Clean, well-scoped migration. The diff removes more code than it adds (−235 / +188 net), which is always a good sign for a simplification. SSM Parameter Store with SecureString is cheaper, simpler (upsert semantics via overwrite: true), and requires fewer IAM permissions.


Simplicity

Good:

  • The old createSecret + executeRequestAndCreateWhenNotExist retry loop is gone. SSM's PutParameter(overwrite: true) is a natural upsert that eliminated ~40 lines of retry logic.
  • The maxRetries constant and recursive retry machinery are removed entirely.
  • Only 2 IAM actions needed now (ssm:PutParameter, ssm:GetParameter) vs. 3 before.

Minor:

  • SecretsStorageAWSSoto.forRegion has two overloads: one public (3 params) that forwards to a second (5 params, with injectable clients). Consider collapsing into a single entry point with defaulted parameters to save ~5 lines.

Duplicated Code

promptForCredentials is defined twice:

  1. StoreSecretsCommand.swiftXCodeInstall.promptForCredentials() (lines 31–56)
  2. AuthenticateCommand.swiftCLIAuthenticationDelegate.promptForCredentials(storingToAWS:) (lines 131–162)

Both prompt for username + password using deps.readLine.readLine, both throw CLIError.invalidInput on nil, and both return an AppleCredentialsSecret. The only difference is the introductory text.

Suggestion: Extract a shared helper that takes the display message as a parameter.


Security

Good:

  • Parameters stored as SecureString, encrypted at rest with the default aws/ssm KMS key.
  • GetParameterRequest(withDecryption: true) used correctly.
  • clearSecrets overwrites with an empty value rather than deleting — minimal permission surface.
  • Credential provider chain (.environment, .ec2, .configFile, .sso, .login) is appropriate for both local dev and EC2.
  • No secrets are logged — only parameter names appear in debug output.

Observation (not a blocker):

  • SecretsStorageAWSError.swift reads ~/.aws/config and ~/.aws/credentials at error time for diagnostic messages. Fine for now, but note it's filesystem I/O inside an error property getter.
  • retrieveSecret uses as! T (acknowledged with swiftlint disable). Correct given the two-variant enum, but fragile if a third secret name is added later.

Other

  • Tests/coverage.html still references old SotoSecretsManager code (generated file, cosmetic).
  • .claude/plans/parameter-store-migration.md references old key names (stale planning doc).
  • Migration callout in README is excellent — clear three-step migration path.

Summary

Aspect Rating
Simplicity ✅ Simpler than before
Duplication ⚠️ promptForCredentials duplicated — consolidation recommended
Security ✅ Solid

Ready to merge. Duplicated prompt will be consolidated in a follow-up commit on this branch.

@sebsto
sebsto merged commit dd63e89 into main Aug 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant