Skip to content

feat(providers): AWS STS AssumeRole refresh strategy and aws-s3 profile#1782

Open
russellb wants to merge 2 commits into
NVIDIA:mainfrom
russellb:feat/1576-aws-sts-with-sigv4
Open

feat(providers): AWS STS AssumeRole refresh strategy and aws-s3 profile#1782
russellb wants to merge 2 commits into
NVIDIA:mainfrom
russellb:feat/1576-aws-sts-with-sigv4

Conversation

@russellb

@russellb russellb commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Add gateway-owned AWS STS AssumeRole as a credential refresh strategy, ship aws and aws-s3 provider profiles, and extend policy validation to support single-label * wildcards in middle DNS labels.

Combined with the SigV4 proxy re-signing from #1638 (now merged), sandboxes can access S3 and other AWS services without ever seeing real credentials — the gateway mints short-lived STS credentials and the proxy re-signs requests on the fly.

Related Issue

Refs #1576

Changes

STS credential refresh (openshell-server)

  • New AwsStsAssumeRole variant in ProviderCredentialRefreshStrategy (proto field AWS_STS_ASSUME_ROLE = 6).
  • provider_refresh.rs implements the refresh loop: calls sts:AssumeRole using the gateway's ambient AWS credentials (or explicit long-lived keys from refresh material), stores the resulting AccessKeyId, SecretAccessKey, and SessionToken on the provider, and schedules re-rotation before expiry.
  • ConfigureProviderRefresh and RotateProviderCredential RPCs gated behind the providers_v2_enabled setting.
  • Validates that configuring STS refresh for one provider won't collide with credential keys already held by another provider attached to the same sandbox.

Provider profiles (openshell-providers)

AWS provider profiles follow the same pattern as the existing aws-bedrock profile: a generic base plus service-specific variants.

  • providers/aws.yaml — base AWS profile with STS refresh material (role_arn, session_name, external_id, aws_region, optional long-lived keys) but no endpoints or binaries allowlist. Intended for AWS services that don't yet have a dedicated profile — the user attaches their own policy to supply endpoints. Same role google-cloud plays relative to google-vertex-ai.
  • providers/aws-s3.yaml — S3-specific profile that adds pre-configured endpoints covering regional (*.s3.*.amazonaws.com), global (*.s3.amazonaws.com with signing_region: us-east-1), and dualstack variants. All endpoints use credential_signing: sigv4 and signing_service: s3. Includes a binaries allowlist for Python, curl, and the AWS CLI.

Policy: single-label * wildcard in middle DNS labels (openshell-policy)

  • validate_host_wildcard now permits * as a complete middle label (e.g. *.s3.*.amazonaws.com) in addition to leading-label and intra-label positions. Matches exactly one DNS label via Rego's glob.match with . separator, consistent with TLS wildcard semantics.
  • OPA endpoint matching and L7 config lookup updated: host_matches_wildcard_middle rule and endpoint_config_for_middle_wildcard in Rego.
  • Validation rejects ** in any position and partial wildcards in middle labels.

TUI (openshell-tui)

  • Added AwsStsAssumeRole match arm to the refresh strategy label in the provider settings view.

Documentation

  • docs/sandboxes/manage-providers.mdx: STS refresh setup instructions
  • examples/aws-s3-sts.md: end-to-end manual test guide (create IAM role, configure provider, test S3 PUT/GET/LIST from sandbox)

Testing

  • cargo test -p openshell-providers — 65 passed (STS serde roundtrip, aws/aws-s3 profile parsing, endpoint validation)
  • cargo test -p openshell-policy — 91 passed (wildcard DNS validation, middle-label wildcards, credential_signing validation)
  • cargo test -p openshell-server — all passed (STS configure v2 gate, success path, credential key collision)
  • cargo test -p openshell-supervisor-network — all passed (wildcard host matching, OPA endpoint config)
  • cargo test -p openshell-tui — 24 passed
  • End-to-end manual test: Podman sandbox → boto3 → S3 PUT/GET/LIST with STS-minted credentials and proxy-side SigV4 re-signing against real AWS (all passed)

Checklist

  • Follows Conventional Commits
  • Commits are signed off (DCO)

@copy-pr-bot

copy-pr-bot Bot commented Jun 5, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

Comment thread providers/aws-s3.yaml Outdated
Comment thread providers/aws-s3.yaml
@github-actions

Copy link
Copy Markdown

This pull request has had no activity for 14 days and is now marked stale. It may be closed in 7 days if there is no further activity.

@github-actions github-actions Bot added the state:stale Inactive item at risk of automatic closure. label Jun 23, 2026
@russellb

Copy link
Copy Markdown
Contributor Author

This is still waiting on #1638 to go in first.

@github-actions github-actions Bot removed the state:stale Inactive item at risk of automatic closure. label Jun 24, 2026
@russellb russellb force-pushed the feat/1576-aws-sts-with-sigv4 branch from ab56381 to c49d8c6 Compare June 30, 2026 15:28
…rofile

Add server-side AWS STS AssumeRole credential refresh so gateways can
mint short-lived AWS credentials for sandboxes. Combined with the SigV4
proxy re-signing from NVIDIA#1638, sandboxes can access S3 (and other AWS
services) without ever seeing real credentials.

## STS credential refresh (server)

- New `AwsStsAssumeRole` variant in `ProviderCredentialRefreshStrategy`.
- `provider_refresh.rs` implements the refresh loop: calls
  `sts:AssumeRole` using the gateway's ambient AWS credentials (or
  explicit long-lived keys from material), stores the resulting
  `AccessKeyId`, `SecretAccessKey`, and `SessionToken` on the provider,
  and schedules re-rotation before expiry.
- `ConfigureProviderRefresh` and `RotateProviderCredential` RPCs gated
  behind the `providers_v2_enabled` setting. Validates that configuring
  STS refresh for one provider won't collide with credential keys already
  held by another provider attached to the same sandbox.
- Proto: `AWS_STS_ASSUME_ROLE = 3` on `ProviderCredentialRefreshStrategy`.

## Provider profiles: `aws` and `aws-s3`

AWS provider profiles follow the same pattern as the existing
`aws-bedrock` profile: a generic base plus service-specific variants.

- `providers/aws.yaml`: base AWS profile with STS refresh material
  (role_arn, session_name, external_id, aws_region, optional long-lived
  keys) but no endpoints or binaries allowlist. Intended for AWS
  services that don't yet have a dedicated profile — the user attaches
  their own policy to supply endpoints. This is the same role
  `google-cloud` plays relative to `google-vertex-ai`.
- `providers/aws-s3.yaml`: S3-specific profile that adds pre-configured
  endpoints covering regional (`*.s3.*.amazonaws.com`), global
  (`*.s3.amazonaws.com` with `signing_region: us-east-1`), and dualstack
  variants. All endpoints use `credential_signing: sigv4` and
  `signing_service: s3` so the proxy re-signs requests automatically.
  Includes a binaries allowlist for Python, curl, and the AWS CLI.
- End-to-end walkthrough added in `examples/aws-s3-sts.md`.
- Provider docs updated in `docs/sandboxes/manage-providers.mdx`.

## Policy: single-label `*` wildcard in middle DNS labels

- `validate_host_wildcard` in `openshell-policy` now permits `*` as a
  complete middle label (e.g. `*.s3.*.amazonaws.com`) in addition to the
  existing leading-label and intra-label positions. A bare `*` label
  matches exactly one DNS label via Rego's `glob.match` with `.`
  separator, consistent with TLS wildcard semantics.
- OPA endpoint matching and L7 config lookup updated to handle
  middle-label wildcards: `host_matches_wildcard_middle` rule and
  `endpoint_config_for_middle_wildcard` in Rego.
- Validation rejects `**` in any position and partial wildcards in
  middle labels (e.g. `s*.example.com` in a non-leading label).

## SigV4 body validation fix

- Allow bodyless requests (GET/HEAD/DELETE) through the SignBody path.
  boto3 sets `x-amz-content-sha256` to the empty-body hash on all
  requests, routing them through SignBody via `detect_payload_mode`.
  Previously `BodyLength::None` was rejected; now it signs with the
  empty-body hash, matching AWS SDK behavior.

## TUI

- Added `AwsStsAssumeRole` match arm to the refresh strategy label in
  the provider settings view.

Validated end-to-end: gateway mints STS credentials, sandbox runs boto3
PutObject/ListObjects/GetObject against real S3 through the SigV4
re-signing proxy. The sandbox sees only placeholder credentials.

Signed-off-by: Russell Bryant <rbryant@redhat.com>
@russellb russellb force-pushed the feat/1576-aws-sts-with-sigv4 branch from 1b1cbd3 to cb10f83 Compare June 30, 2026 18:22
@russellb russellb marked this pull request as ready for review June 30, 2026 18:22
@russellb

russellb commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

@pimlock willing to take a look at this one since you reviewed #1638 ?

@pimlock

pimlock commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

/ok to test cb10f83

@pimlock pimlock left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @russellb, I did a first pass, but I run out of time to get through this fully. I needed to check this out locally and run this to understand how it worked (wasn't as familiar with the cred refresh).

I added a few comments.
The biggest item I had was around how the implicit extra env vars that are managed by the refresh, but not defined as such in the provider profile.
I did investigation around this and options. I would need to spend more time figuring out what makes sense, but I will need to come back to it on Thursday. In the meantime, I can share raw notes from agent, but I haven't checked these very deeply yet: https://gist.github.com/pimlock/6a99b3d9c4e19c6efc3b0d2da59b6b63

Comment thread docs/sandboxes/manage-providers.mdx Outdated
```shell
openshell settings set --global --key providers_v2_enabled --value true --yes

openshell provider create --name my-aws --type aws-s3

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call will fail without setting a dummy value for --credential AWS_ACCESS_KEY_ID=dummy (interestingly, it doesn't even need to be a declared credential, something like --credential FOO=bar will do).

I think ideally we could use the --runtime-credentials flag, but using that fails, because of the STS refresh not being in here:

  • pub fn is_gateway_mintable(&self) -> bool {
    matches!(
    self.strategy,
    ProviderCredentialRefreshStrategy::Oauth2RefreshToken
    | ProviderCredentialRefreshStrategy::Oauth2ClientCredentials
    | ProviderCredentialRefreshStrategy::GoogleServiceAccountJwt
    )
    }
    }

However that won't fully work either, since the refresh is only configured on the access_key_id and not on the secret key/session token.

That last thing is one thing that I wonder how we can solve -> the only cred that is marked as being refreshed is the access_key_id, but in fact the secret key and session token are as well.

And right now we are missing a way to represent this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both parts addressed in cbe9b43.

Runtime credentials: root cause was is_gateway_mintable in openshell-providers omitting AwsStsAssumeRole — it had diverged from the server-side check. Consolidated into one shared function that includes STS, so the profile's credentials are runtime-resolvable and openshell provider create --type aws-s3 --runtime-credentials works with no placeholder --credential. Docs and examples/aws-s3-sts.md updated.

Representing multi-output refresh: went declarative. A refresh now declares the extra credentials it co-mints via additional_outputs, mapping each strategy-produced output to a sibling credential:

refresh:
  strategy: aws_sts_assume_role
  additional_outputs:
    - output: secret_access_key
      credential: secret_access_key
    - output: session_token
      credential: session_token

The resolved output→env-key mapping is pinned into the refresh state at configure time (so later profile edits can't silently redirect where minted values are written), and minting, collision reservation, and active_provider_environment_keys all read it instead of the previously hardcoded AWS list.

Concretely this closes the gap you described: configure now reserves all three keys up front, so "configure STS, don't rotate, another provider claims AWS_SECRET_ACCESS_KEY, attach both" is rejected at attach time instead of failing at mint. The SigV4 signer stays on the standard AWS names, now enforced by profile validation (outputs must resolve to AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN and the primary to AWS_ACCESS_KEY_ID).

Comment on lines 288 to 296
| ProviderCredentialRefreshStrategy::GoogleServiceAccountJwt
| ProviderCredentialRefreshStrategy::AwsStsAssumeRole
)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed that this check is duplicated in

pub fn is_gateway_mintable(&self) -> bool {
matches!(
self.strategy,
ProviderCredentialRefreshStrategy::Oauth2RefreshToken
| ProviderCredentialRefreshStrategy::Oauth2ClientCredentials
| ProviderCredentialRefreshStrategy::GoogleServiceAccountJwt
)
}
}
.

Can we replace this one with the one in openshell-providers?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consolidated into a single is_gateway_mintable_strategy in openshell-providers; both server call sites (provider_refresh.rs and grpc/provider.rs) now delegate to it. Good catch that these had diverged — the providers-side copy was missing AwsStsAssumeRole, which is exactly what broke --runtime-credentials (your docs comment). Fixing the duplication fixes both. cbe9b43

Comment thread providers/aws-s3.yaml
Comment on lines +9 to +49
- name: access_key_id
description: AWS access key ID (gateway-minted via STS)
env_vars: [AWS_ACCESS_KEY_ID]
required: true
refresh:
strategy: aws_sts_assume_role
refresh_before_seconds: 300
max_lifetime_seconds: 3600
material:
- name: role_arn
description: ARN of the IAM role to assume
required: true
secret: false
- name: session_name
description: Session name for CloudTrail attribution
required: false
secret: false
- name: external_id
description: External ID for cross-account role assumption
required: false
secret: false
- name: aws_region
description: AWS region for STS endpoint
required: false
secret: false
- name: aws_access_key_id
description: Long-lived IAM access key (only needed if gateway lacks ambient AWS credentials)
required: false
secret: false
- name: aws_secret_access_key
description: Long-lived IAM secret key (only needed if gateway lacks ambient AWS credentials)
required: false
secret: true
- name: secret_access_key
description: AWS secret access key (co-managed with access_key_id)
env_vars: [AWS_SECRET_ACCESS_KEY]
required: true
- name: session_token
description: AWS session token (co-managed with access_key_id)
env_vars: [AWS_SESSION_TOKEN]
required: true

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering if there is a way to reuse credentials and only define endpoints in the service-specific profile.

I think the closest we could do today is something like:

  • aws.yaml has credentials (what it already has)
  • aws-service.yaml has endpoints
  • the user needs to attach both providers to the sandbox for this to work

The benefit here would be they configure the aws profile instance once and can reuse it with multiple services.

The problem is that the relationship is implicit and running a sandbox with aws-service, but not aws will fail in runtime during signing. Perhaps there is a way to make that relationship explicit and be able to validate it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @johntmyers I wonder if something like this came up before:

  • there is one credential type for given suite of services
  • endpoints for each service are defined separately

In this example:

  • provider_1: I want to bring in AWS creds
  • provider_2: I want to access S3
  • provider_3: I want to access Lambda

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice direction and I think it's the natural next step. The blocker is exactly what you named — the credentials↔endpoints relationship would be implicit and only fail at signing time. Making it explicit and validated is worth its own design pass (it overlaps the additional_outputs model added in this PR), so rather than expand this PR I'd like to track it as a follow-up. Flagging it here as a follow-up for now — happy to open a tracking issue for you and @johntmyers to weigh in on.

@pimlock pimlock self-assigned this Jul 7, 2026
@pimlock pimlock added area:gateway Gateway server and control-plane work area:providers labels Jul 7, 2026
@russellb

russellb commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

@pimlock thank you very much for the review! I know it can be very time-consuming. I will dig into the comments tomorrow.

AWS STS AssumeRole mints three credentials from one refresh, but the model
could only represent one; the extra AWS keys were hardcoded across the server.
Add `additional_outputs` to the refresh profile so a refresh declares the
sibling credentials it co-mints, resolve that mapping to concrete env keys, pin
it in the refresh state, and drive minting, collision reservation, and env-key
surfacing from it.

- Consolidate the gateway-mintable strategy check into one shared function that
  includes AwsStsAssumeRole, fixing runtime resolvability so aws/aws-s3
  providers can be created with --runtime-credentials.
- Reserve all co-minted keys at configure time (resolved from the profile)
  instead of a hardcoded AWS list, so a configured-but-not-yet-minted refresh
  cannot collide at mint time.
- Validate additional_outputs: known/required outputs per strategy, unique ids,
  existing single-env-var siblings, no self-refresh, standard AWS env keys, and
  pinned primary key.
- Update aws/aws-s3 profiles and provider docs.

Refs NVIDIA#1576

Signed-off-by: Russell Bryant <rbryant@redhat.com>
@pimlock

pimlock commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

/ok to test cbe9b43

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:gateway Gateway server and control-plane work area:providers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants