On-the-Wire Credential Injection: Secretless AWS Bedrock Access example

Written by
Toader Sebastian
Published on
September 1, 2025

On-the-Wire Credential Injection: Secretless AWS Bedrock Access example

We’ve previously delved deep into how Riptides reimagines workload identity by anchoring it directly in the Linux kernel, removing reliance on sidecars, proxies, or application-level identity logic. Our approach is rooted in SPIFFE standards and designed for seamless federation, enabling per-process cryptographic identity issuance right at the OS layer.

In earlier posts, we’ve explored how SPIFFE Verifiable Identity Documents (SVIDs) are issued and enforced in the kernel, providing each workload with a strong, runtime-scoped identity that’s both portable and secure, while federation lets external systems trust those identities without embedding secrets.

In this post, we focus on another critical capability: credential injection. With Riptides, AWS credentials are injected dynamically into outgoing requests — secretless and just-in-time — allowing workloads to call services like Amazon Bedrock without ever touching long-lived keys or tokens.

In our earlier post, Why Cloud-Native Federation Isn’t Enough for Non-Human Identities in AWS, GCP, and Azure, we outlined the limitations of relying only on cloud-native federation for non-human identities. One of the biggest challenges is how to provide credentials securely when workloads need to access cloud services.

This post takes that discussion further by showing how on-the-wire credential injection with Riptides eliminates the need for stored secrets. To make the idea concrete, we’ll walk through a real-world example using the Amazon Product Agent Review sample application.

The Product Agent Review app, built on Amazon Bedrock, uses LLMs to let clients query product reviews. It consists of two parts:

  • Server-side: an Amazon Bedrock agent running inside AWS.
    (Deployed via the provided main.ipynb notebook.)
  • Client-side: a Python application designed to run outside AWS using Streamlit.
    (Logic available in main.py.)

This client–server separation makes it a perfect case study. With Riptides, credentials are never embedded in the client environment. Instead, Riptides operates at the kernel level: as each HTTP request leaves the client, it transparently injects short-lived AWS credentials and re-signs the request with libsigv4. The client never handles secrets — yet the request still arrives at AWS fully authenticated.

Note: Riptides’ libsigv4 library, which handles AWS SigV4 signing at the kernel level, is fully open-sourced. For a deep dive, see our post: Introducing libsigv4: AWS SigV4 Signatures in Portable C with Kernel Compatibility.

The standard way: Authenticating and authorizing the client application

Normally, for a client application to invoke an Amazon Bedrock agent, it needs to be wired into AWS IAM. This typically involves:

  • An IAM role with permissions to invoke the deployed Product Review Agent.
  • A trust relationship, which defines who is allowed to assume this role. That trust can be established in two common ways:
    • An IAM user that assumes the role, using long-lived AWS credentials (Access Key ID / Secret Access Key).
    • A federated OIDC identity that assumes the role, using short-lived credentials obtained after authenticating with an external identity provider (IDP) and retrieving an ID token.

Creating and assigning a role for the client application

To let a client application call the Bedrock agent, you need an IAM role with the right permissions and trust relationships.

Role permissions (allowing the client to invoke a specific Bedrock agent):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "bedrock:InvokeAgent",
      "Resource": [
        "arn:aws:bedrock:eu-central-1:<AWS_ACCOUNT_ID>:agent/<AGENT_ID>",
        "arn:aws:bedrock:eu-central-1:<AWS_ACCOUNT_ID>:agent-alias/<AGENT_ID>/<AGENT_ALIAS_ID>"
      ]
    }
  ]
}

Role trust relationships: The trust policy defines who can assume this role. Two common patterns are:

  • IAM user (using long-lived credentials):
{
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::<AWS_ACCOUNT_ID>:user/<CLIENT_APP_IAM_USER>"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
{
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::<AWS_ACCOUNT_ID>:oidc-provider/<EXTERNAL_IDP_OIDC_URL>"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "<EXTERNAL_IDP_OIDC_URL>:sub": "<ID_TOKEN_SUBJECT_CLAIM>",
                    "<EXTERNAL_IDP_OIDC_URL>oidc:aud": "sts.amazonaws.com"
                }
            }
        }
    ]
}

Supplying credentials to the client application

How the client obtains and uses AWS credentials depends on the SDK it uses. For example, with boto3 (the SDK used by this sample app), credentials can be provided via:

  • Environment variables
  • AWS config and credentials files
  • Legacy Boto2 config file
  • Container credential provider (e.g. on Amazon ECS)
  • Custom logic that passes credentials directly to the client code

The problem with storing credentials

No matter which authentication method you choose, credentials are always a liability.

  • With STS AssumeRole, you need to manage and securely store the AWS Access Key ID and Secret Access Key.
  • With federated authentication, you must securely store not only the credentials used to connect to the external IDP, but also ensure the ID token is refreshed before expiration.

On top of that, there’s no guarantee that every workload running in your environment can safely access these credentials. For non-human identities, handling secrets introduces risk, complexity, and potential points of failure.

Running the client application

For demonstration purposes, we’ll run the streamlit client application using the STS AssumeRole authentication method, with credentials passed via AWS config file:

~/.aws/credentials

[bedrock-pra-user]
aws_access_key_id = <ACCESS_KEY_ID>
aws_secret_access_key = <SECRET_ACCESS_KEY>


~/.aws/config

[profile bedrock-pra-user]
region = eu-central-1
source_profile = bedrock-pra-user
role_arn=<ROLE_ARN>

$ AWS_PROFILE=bedrock-pra-user streamlit run main.py -- --id <AGENT_ID> --alias <AGENT_ALIAS_ID>

By default, the Streamlit web interface is available at http://localhost:8501, where users can interact with the client application:

As shown in the screenshot, the client successfully invoked the Product Review Agent with the input "Give me the last 2 reviews" and received a response from the agent, demonstrating that the setup works with standard AWS credentials.

On-the-wire credential injection with Riptides

Now let’s see how this works when the client application runs in a Riptides managed environment.
Riptides solves the problem of stored secrets by injecting credentials dynamically at runtime, so the client never has to store or manage AWS credentials directly.

Prerequisites

Register Riptides Control Plane as an external IdP in AWS

First, we need to register the Riptides Control Plane as an OIDC identity provider in AWS. This allows AWS to trust identity tokens issued by Riptides and exchange them for temporary credentials via STS:

aws iam create-open-id-connect-provider \
    --url "https://enjoyed-previously-llama.ngrok-free.app/oidc" \
    --client-id-list "sts.amazonaws.com"

Creating and assigning a role for the client application

To allow the client application to call the Bedrock agent, we need an IAM role with the appropriate permissions and a trust relationship.

Role permissions (allowing the client to invoke a specific Bedrock agent):

arn:aws:iam::<AWS_ACOUNT_ID>:role/bedrock-pra-user-role-wif:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "bedrock:InvokeAgent",
      "Resource": [
        "arn:aws:bedrock:eu-central-1:<AWS_ACCOUNT_ID>:agent/<AGENT_ID>",
        "arn:aws:bedrock:eu-central-1:<AWS_ACCOUNT_ID>:agent-alias/<AGENT_ID>/<AGENT_ALIAS_ID>"
      ]
    }
  ]
}

Role trust relationships:

The trust relationship must allow ID tokens from Riptides Control Plane to be exchanged for AWS temporary credentials, which will assume this role:

{
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::<ACCOUNT_ID>:oidc-provider/enjoyed-previously-llama.ngrok-free.app/oidc"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "enjoyed-previously-llama.ngrok-free.app/oidc:aud": "sts.amazonaws.com",
                    "enjoyed-previously-llama.ngrok-free.app/oidc:sub": "spiffe://acme.org/streamlit"
                }
            }
        }
    ]
}

Here, the sub field specifies the identity that Riptides will assign to the client application.

Setting up the client application with Riptides

From Riptides’ perspective, the Product Review Agent is an external service that it doesn’t know about by default. To make Riptides aware of it, you need to register the service with the Riptides Control Plane. This is done by creating a Kubernetes custom resource like the following:

apiVersion: core.riptides.io/v1alpha1
kind: Service
metadata:
  name: bedrock
  namespace: riptides-system
spec:
  addresses:
    - address: bedrock-agent-runtime.eu-central-1.amazonaws.com # AWS Bedrock service endpoint
      port: 443 # Port the client connects to
  labels:
    app: bedrock-pra # Label for matching this service
  external: true # Indicates this is an external service, not managed by Riptides

The address and port must match the AWS Bedrock Agent Runtime service endpoint, since the client application connects to the desired Bedrock agent via this AWS service.

Defining How to Obtain Workload Credentials

We define how the client workload obtains temporary AWS credentials using Riptides.

apiVersion: core.riptides.io/v1alpha1
kind: CredentialSource
metadata:
  name: aws-cred-1
  namespace: riptides-system
spec:
  aws: # Temporary credentials sourced from AWS
    roleArn: arn:aws:iam::<AWS_ACCOUNT_ID>:role/bedrock-pra-user-role-wif # The IAM role to assume for temporary credentials
    audience: [] # Optional, defaults to sts.amazonaws.com. Must match the OIDC provider settings in AWS
    lifetime: "900s" # Optional, requested lifetime for AWS temporary credentials
    idTokenClaims: [] # Optional, additional claims to include in the ID token besides "sub"
    idTokenLifetime: # Optional, lifetime of ID tokens issued by Riptides

---

apiVersion: core.riptides.io/v1alpha1
kind: WorkloadCredential
metadata:
  name: streamlit-aws-cred-1
  namespace: riptides-system
spec:
  credentialSource: aws-cred-1 # Source of temporary credentials
  workloadID: streamlit        # Workload ID to get AWS temporary credentials for

How it works

  1. CredentialSource defines how Riptides will obtain temporary AWS credentials for a workload.
  2. WorkloadCredential links a specific workload ID (here, streamlit) to a credential source.

Riptides Control Plane issues an ID token for the workload. The sub claim in the token follows this format: spiffe://<TRUST_DOMAIN>/<WORKLOAD_ID>. In our demo, the trust domain is acme.org, so the sub claim becomes: spiffe://acme.org/streamlit. This matches the enjoyed-previously-llama.ngrok-free.app/oidc:sub we configured in AWS for the role trust relationship. The ID token is then exchanged via AWS STS for temporary credentials, which assume the specified role (bedrock-pra-user-role-wif) and allow the workload to invoke the Bedrock agent, all without storing long-lived credentials. The Control Plane automatically refreshes these temporary credentials when they expire, ensuring uninterrupted access for the workload.

Assigning Workload IDs to processes

We define which processes can be assigned the streamlit workload ID, and under what conditions:

apiVersion: core.riptides.io/v1alpha1
kind: WorkloadIdentity
metadata:
  name: streamlit-id
  namespace: riptides-system
spec:
  scope:
    agent: 
      id: <AGENT_WORKLOAD_ID> # Scope: node(s) where this workload identity can be assigned
  workloadID: streamlit       # The workload ID assigned to matching processes
  selectors:
    - process:name: [streamlit] # Runtime process attribute that must match to get this identity
  connection:
    tls:
      mode: PERMISSIVE # Allows access to Streamlit's web interface on localhost via HTTP
  egress: # Egress rules for credential injection
    - selectors:
        - app: bedrock-pra # Service endpoint(s) targeted by this rule
      credentialName: streamlit-aws-cred-1 # Credentials to inject, referenced from WorkloadCredential
      connection:
        tls:
          intercept: true # Intercept traffic and inject credentials into HTTP requests

How it works

  1. Riptides Agent runs on nodes and acts as the bridge between the Control Plane and the Linux kernel module.
  2. Workload IDs and credentials issued by the Control Plane are restricted to processes on the node where the agent runs — this is controlled by the scope field in the WorkloadIdentity CR. Multiple agents can also be targeted if needed.
  3. The Linux kernel module monitors running processes and checks their runtime attributes against the spec.selectors values. Only matching processes are assigned the workload ID.
  4. When a process with an assigned workload ID sends a request to a service referenced in the egress rules, the temporary credentials from the specified WorkloadCredential are injected directly into the request.

In this example, any process named streamlit will receive AWS temporary credentials automatically when sending requests to the Amazon Bedrock Agent Runtime service endpoint. Note: A single workload can be assigned multiple credentials if it interacts with multiple services — for example, both AWS and GCP at the same time.

AWS specific details: SigV4 signing

Since this example uses AWS, on-the-wire credential injection requires resigning HTTP requests with the correct AWS SigV4 signature.
Riptides accomplishes this using our libsigv4 library, implemented in portable C with Linux kernel compatibility. This enables credentials to be injected and signed at the kernel level just before the request is sent, ensuring full AWS authentication without exposing keys to the client application.

For a deeper dive, see our previous post: Introducing libsigv4: AWS SigV4 Signatures in Portable C with Kernel Compatibility.

Running the client application

For demonstration purposes, we’ll run the streamlit client application with placeholder AWS credentials:

AWS_DEFAULT_REGION=eu-central-1 AWS_ACCESS_KEY_ID=none AWS_SECRET_ACCESS_KEY=none AWS_CA_BUNDLE=/sys/kernel/riptides/ca-certificates.crt streamlit run main.py -- --id <AGENT_ID> --alias <AGENT_ALIAS_ID>

Here, we intentionally provide an invalid AWS Access Key ID and Secret Access Key. Riptides will automatically inject valid temporary credentials into the requests sent by the client application.

Now let’s ask the Product Review Agent: "What are your capabilities?"

The client never sees real credentials, yet the request is fully authenticated and processed.

Key points

  • No credentials are stored on the client machine or in configuration files.
  • Credentials are provided just-in-time for each request, minimizing the risk of leaks.
  • Requests are automatically signed or authorized according to the target service’s requirements.
  • The workflow of invoking external services remains unchanged from the client’s perspective.

Benefits of on-the-wire credential injection

Using Riptides provides several clear advantages over traditional approaches:

Traditional approach Riptides approach
Store service credentials locally No credentials stored on the client
Rotate and secure secrets manually Credentials injected dynamically
Risk of credential leakage Reduced attack surface
Operational overhead Minimal, automated
  • Improved security posture for non-human clients.
  • Reduced operational complexity.
  • Works transparently with existing applications and cloud or service providers.

Final thoughts

At Riptides, we’re strong believers in SPIFFE-based workload identities, enabling fine-grained, zero-touch authentication with secure, ephemeral credentials and no secrets ever stored. This is the most robust and future-proof way to handle non-human identities, and it works seamlessly across multi-cloud environments.

But where SPIFFE adoption isn’t possible, Riptides provides a fallback: on-the-wire credential injection, delivering the right cloud or service credentials directly into the request stream without ever touching the application.

On-the-wire credential injection with Riptides enables secure, secretless invocation of external services. In this example, we demonstrated the approach with an AWS Bedrock agent, but Riptides provides the same seamless support across all major cloud providers — AWS, GCP, and Azure.

Beyond cloud credentials, Riptides can also inject OAuth2 tokens, whether sourced from Kubernetes secrets or obtained via OAuth2 authorization code flows, directly into client requests.

This pattern can be extended to agents, applications, and services in any environment, offering a unified, secure approach for managing non-human identities at scale.

In upcoming posts, we’ll show how the same pattern applies to GCP and Azure services — making secretless, on-the-wire identity truly multi-cloud.

Share this post
aws
non-human identity
federation
workload-id

Ready to replace secrets
with trusted identities?

Build with trust at the core.