Skip to content

fix: token cache key, policy simulator creds, s3 rm guardrail, sync-aws-config - #18

Merged
DustyStudy merged 4 commits into
mainfrom
fix/cache-key-simulate-guardrails-sync
Sep 22, 2026
Merged

DustyStudy merged 4 commits into
mainfrom
fix/cache-key-simulate-guardrails-sync

Conversation

@DustyStudy

Copy link
Copy Markdown
Owner

Summary

Fixes four correctness bugs found in a review of the repo. Each fix is its own commit and comes with regression tests.

  1. SSO token cache never hit. The cache key used the builtin hash(), which is randomized per process, so every command re-ran the browser login (and creds-process did on every aws call) and orphaned token entries piled up. The key is now a truncated SHA-256 of the start URL.
  2. check-policy / --check-action used the wrong credentials. simulate() built its IAM client from ambient credentials instead of the role's. It now takes the role credentials explicitly. Also removes a reference to a --role-arn flag that doesn't exist.
  3. Built-in recursive s3 rm guardrail never matched. The pattern required --recursive before s3. It is now aws*s3 rm*--recursive*. The built-in ec2 terminate-instances pattern could not match any real command and is removed (users can still deny it in guardrails.yaml).
  4. sync-aws-config bugs.
    • --prefix collapsed every account into one profile. Profiles are now <prefix>-<alias>[-<role>], and two entries mapping to the same name raise an error.
    • The file was round-tripped through configparser, deleting comments and reflowing everything. It is now edited as text, so only orgctl-managed sections change and everything else is preserved byte-for-byte, including line endings.
    • The single .bak was overwritten on every run, so the original was lost after two syncs. Backups are now timestamped and never overwritten, and a run that changes nothing writes nothing.

README and docs/THREAT_MODEL.md are updated to match.

Behavior changes

  • --prefix with --all-roles now yields <prefix>-<alias>-<role> (previously <prefix>-<role>, which collided across accounts).
  • aws_config_sync.sync() returns a SyncResult (a named tuple that now includes the backup path). Callers unpacking four values need updating; only cli.py and the tests call it.
  • Backup files are ~/.aws/config.bak-<timestamp> instead of a single ~/.aws/config.bak.
  • orgctl still does not delete profiles it wrote earlier, so changing --prefix / --all-roles leaves the old ones behind (documented in the README).

Verification

  • ruff check, ruff format --check, mypy src: clean.
  • pytest: 80 passed (was 49); coverage 43% to 51%.
  • The new tests for 1-3 fail against the old code and pass with the fixes.
  • Ran orgctl sync-aws-config end to end against a scratch ~/.aws/config containing comments: comments survived, --prefix produced one profile per account, an identical re-run made no backup, and each change made a new backup with the original intact.

Not in this PR

Still open from the review: other built-in deny patterns also require aws immediately followed by the service (so aws --profile x s3 rb ... --force slips through), CloudWatch push timestamps, uncaught ClientError on a revoked token, and the CLI test coverage gap.

The key was built from the builtin hash(), which is randomized per process,
so a cached token was never found by the next invocation. Every command
re-ran the browser login and orphaned token entries accumulated. Use a
truncated SHA-256 of the start URL instead.
…ones

simulate() built its IAM client from the default credential chain, so
check-policy and exec --check-action ran as whatever identity the shell
happened to have (potentially a different account) rather than the role
being checked. Pass the role credentials through explicitly. Also drop a
reference to a --role-arn flag that does not exist.
The pattern required --recursive to appear before "s3", which no real
invocation does, so "aws s3 rm s3://b --recursive" was never blocked despite
the docs saying so. Match "aws*s3 rm*--recursive*" instead. Also drop the
built-in ec2 terminate-instances pattern, which could not match any real
command; users can still deny it via guardrails.yaml.
…g ~/.aws/config

- --prefix collapsed every account into a single profile named exactly
  <prefix>, so only the last account survived. Profiles are now named
  <prefix>-<alias>[-<role>], and two account/role pairs that map to the same
  name raise a ConfigError instead of silently dropping one.
- The file was round-tripped through configparser, which deleted comments,
  lowercased keys and reflowed everything, contradicting the docs. It is now
  edited as text: only orgctl-managed sections are replaced or appended and
  everything else, including line endings, is preserved byte-for-byte.
- The single .bak was overwritten on every run, so the user's original file
  was gone after the second sync. Backups are now timestamped and never
  overwritten, and a run that changes nothing writes nothing.

sync() now returns a SyncResult that includes the backup path.
@DustyStudy
DustyStudy merged commit 22eeeed into main Sep 22, 2026
9 checks passed
@DustyStudy
DustyStudy deleted the fix/cache-key-simulate-guardrails-sync branch September 22, 2026 00:50
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