Skip to content

[FEATURE] Allow bedrock integration to support AWS environments with refreshable credentials #754

@martinemde

Description

@martinemde

Scope check

  • This is core LLM communication (not application logic)
  • This benefits most users (not just my use case)
  • This can't be solved in application code with current RubyLLM
  • I read the Contributing Guide

Due diligence

  • I searched existing issues
  • I checked the documentation

What problem does this solve?

Problem

RubyLLM’s Bedrock configuration currently centers on static credential values:

  • bedrock_api_key
  • bedrock_secret_key
  • bedrock_session_token

That works for long-lived credentials, but it is fragile for normal AWS production environments that use refreshable credentials, especially EKS/IRSA, ECS task roles, instance profiles, SSO, process credentials, or assume-role chains.

In our EKS/IRSA deployment, Bedrock requests started failing after the STS token lifetime elapsed:

RubyLLM::ForbiddenError: The security token included in the request is expired

The AWS SDK credential provider can refresh these credentials, but RubyLLM’s Bedrock integration snapshots the current values into static config strings. After expiration, RubyLLM continues signing requests with stale credentials.

Desired behavior

Proposed solution

RubyLLM should support Bedrock authentication through refreshable AWS credentials, not only static access-key strings.

A user should be able to configure Bedrock in one of these ways:

RubyLLM.configure do |config|
  config.bedrock_region = "us-west-2"

  # Option A: let RubyLLM resolve AWS credentials through the AWS SDK
  config.bedrock_use_aws_credentials_provider = true
end

or:

RubyLLM.configure do |config|
  config.bedrock_region = "us-west-2"

  # Option B: provide an explicit AWS SDK credentials provider
  config.bedrock_credentials_provider = Aws::CredentialProviderChain.new.resolve
end

Static credentials should remain supported for existing users.

Requirements

  • Bedrock signing must work with refreshable AWS SDK credential providers.
  • RubyLLM should not require users to manually copy access_key_id, secret_access_key, and session_token into static config values when using AWS-managed credentials.
  • A single Bedrock request signature must use one internally consistent credential snapshot: access key, secret key, and session token must come from the same provider read.
  • Existing static credential configuration should remain backward compatible.
  • The feature should work with common AWS SDK providers including IRSA/web identity, ECS credentials, instance profile credentials, SSO credentials, process credentials, and assume-role credentials.
  • The feature should not require app-level timers, reinitializing RubyLLM, or manual credential refresh logic.

Why this matters

AWS SDK credential providers already know how to refresh credentials. For example, Aws::AssumeRoleWebIdentityCredentials includes AWS SDK refresh behavior and refreshes when credentials approach expiration.

RubyLLM currently loses that behavior because the provider is resolved once and its current fields are copied into strings. This makes Bedrock unusable in long-running processes with expiring AWS credentials.

Important correctness concern

A naive implementation might make bedrock_api_key, bedrock_secret_key, and bedrock_session_token dynamic getters that each call credentials_provider.credentials.

That improves freshness, but it can still be incorrect. RubyLLM’s Bedrock signer reads those fields separately while building one SigV4 signature. If the provider refreshes between reads, one signature could mix:

  • session token from credential set A
  • access key from credential set B
  • secret key from credential set B or C

The implementation should instead ensure each signed request uses one credential snapshot.

This could be done by using AWS’s own signing machinery, by snapshotting the provider once per request internally, or by another design that provides the same guarantee.

Possible API shapes

These are suggestions, not requirements.

config.bedrock_use_aws_credentials_provider = true

This would let RubyLLM resolve credentials internally through the AWS SDK default provider chain.

config.bedrock_credentials_provider = Aws::CredentialProviderChain.new.resolve

This would let callers provide an explicit provider, useful for custom chains or tests.

Both could coexist.

Suggested test coverage

  1. Bedrock can be configured with AWS-provider mode enabled and no static access-key strings.

  2. Bedrock can be configured with an explicit object responding to #credentials.

  3. Missing bedrock_region still fails configuration.

  4. Existing static credential configuration continues to work.

  5. One Bedrock signature reads credentials once, or otherwise proves that access key, secret key, and session token come from the same credential snapshot.

  6. A fake refreshable provider returning different credentials on each call does not produce a mixed signature.

  7. Configuration survives RubyLLM.context / config duplication if that is expected to work for provider config.

  8. Signing works for both Bedrock runtime requests and Bedrock model-listing requests.

Research notes

Observed with:

  • ruby_llm 1.14.1
  • aws-sdk-core 3.244.0
  • aws-sigv4 1.12.1

Relevant RubyLLM areas:

  • lib/ruby_llm/providers/bedrock.rb
  • lib/ruby_llm/providers/bedrock/auth.rb

Relevant AWS SDK behavior:

  • Aws::CredentialProviderChain can resolve refreshable providers.
  • Aws::AssumeRoleWebIdentityCredentials supports refreshable IRSA credentials.
  • AWS signing APIs can sign from a credentials provider and avoid manually handling separate credential fields.

Operational caveat

AWS’s provider chain checks some static credential sources before web identity. If an environment injects static AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN, the resolved provider may still be static. That is normal AWS SDK behavior, but RubyLLM should not force static credentials when refreshable AWS provider credentials are available.

Why this belongs in RubyLLM

The only way to correct this problem at present seems to be monkey patching, therefore it must either become a part of RubyLLM or be abstracted in such a way to allow it to happen without monkey patching.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions