AWS Bedrock with .NET: Advanced Integration & Production Setup
The Problem: Credentials Buried in the Cloud
Picture this: You've built a Bedrock chatbot locally using aws configure. It works perfectly on your machine. You deploy it to an EC2 instance in production, and boom—UnauthorizedException. The code is identical. The code didn't change. So what's broken?
Your local machine had credentials in ~/.aws/credentials. The EC2 instance doesn't. And hardcoding AWS keys into your source code? That's a security audit nightmare waiting to happen. You need a different approach—one that works for development, staging, and production without storing secrets in git.
This is where advanced authentication patterns come in. They let you securely manage credentials across different deployment environments without changing your code.
What: The SDK's Credential Chain
The AWS SDK doesn't require you to explicitly pass credentials. Instead, it follows a credential resolution chain—a priority-ordered search for credentials from different sources.
Think of it like this: Imagine you're looking for your API key. You check your desk (environment variables), then your backpack (.aws/credentials file), then your jacket pockets (EC2 metadata). Once you find it, you stop searching. The SDK does the same thing, in a predictable order.
Here's the chain (in priority order):
- Explicit credentials: If you pass credentials directly in code (not recommended for production)
- Environment variables:
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN - Credentials file:
~/.aws/credentials(local dev) - IMDS (Instance Metadata Service): EC2 instance role (automatic)
- ECS task role environment variables: Automatic in ECS/Fargate
- IAM role for Lambda: Automatic in Lambda execution
The beauty of this chain is: your code never changes. You write one invocation, and it works locally (using credentials file), in CI/CD (using environment variables), and in production (using EC2 / Lambda roles). The SDK figures out which source to use automatically.
When: Choosing Your Authentication Pattern
Different deployment environments call for different patterns. Here's a decision matrix to help you pick the right approach:
| Scenario | Pattern | Pros | Cons |
|---|---|---|---|
| Local development (your laptop) | ~/.aws/credentials file |
Zero code changes, works offline | Only works on your machine |
| Quick local testing (STS tokens) | SessionAWSCredentials in code (temp tokens) |
Temporary access, portable | Credentials expire; need to refresh |
| Production on EC2 | EC2 Instance Profile Role (IMDS) | Recommended: Auto-refresh, no code changes, no secrets in git | EC2-specific; requires IAM role setup |
| Production on Lambda or ECS | Execution Role (auto via env vars) | Built-in; no setup needed; highly secure | Service-specific setup required |
| CI/CD pipelines (GitHub Actions, etc.) | Environment variables or OIDC federation | No long-lived secrets; portable | Need to configure OIDC trust policy |
How: Implementing Each Pattern
Pattern 1: Session Credentials (Quick Local Testing)
Use this for testing with temporary credentials from AWS STS. Useful when you can't install aws-cli or want isolated, time-limited access.
SessionAWSCredentials GetCredentials()
{
// ⚠️ NEVER hardcode credentials in production!
// Use environment variables or CloudFormation outputs instead
string accessKey = Environment.GetEnvironmentVariable("AWS_ACCESS_KEY_ID")
?? throw new InvalidOperationException("AWS_ACCESS_KEY_ID not set");
string secretKey = Environment.GetEnvironmentVariable("AWS_SECRET_ACCESS_KEY")
?? throw new InvalidOperationException("AWS_SECRET_ACCESS_KEY not set");
string sessionToken = Environment.GetEnvironmentVariable("AWS_SESSION_TOKEN")
?? throw new InvalidOperationException("AWS_SESSION_TOKEN not set");
return new SessionAWSCredentials(accessKey, secretKey, sessionToken);
}
var credentials = GetCredentials();
var client = new AmazonBedrockRuntimeClient(credentials, RegionEndpoint.USEast1);
Code Sample #1 : Creating temporary session credentials programmaticallyPattern 2: Environment Variables (Containers & CI/CD)
Store credentials in environment variables. This is ideal for containerized workloads and CI/CD pipelines where your infrastructure orchestrator (Kubernetes, Docker, GitHub Actions) injects secrets at runtime.
// The SDK automatically looks for AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
// No manual credential handling needed
var client = new AmazonBedrockRuntimeClient(RegionEndpoint.USEast1);
// This works because the SDK's credential chain finds the env vars
var response = await client.InvokeModelAsync(request);
Code Sample #2 : Using environment variables (SDK resolves automatically)In Docker: Pass secrets as environment variables at container startup:
docker run \
-e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \
-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \
-e AWS_REGION=us-east-1 \
my-bedrock-app:latest
Code Sample #3 : Docker example passing AWS credentials as env varsPattern 3: EC2 Instance Profile Role (Production on EC2)
This is the recommended approach for production on EC2. The SDK automatically retrieves temporary credentials from the EC2 Instance Metadata Service (IMDS) — no hardcoded keys, no manual credential rotation.
How it works: Every EC2 instance has access to its metadata service at http://169.254.169.254/. When you create an instance with an IAM role attached, the role's credentials are available at that endpoint. The SDK automatically fetches them, uses them for requests, and refreshes them before expiry.
// No credentials code needed! SDK discovers the EC2 instance role automatically.
var client = new AmazonBedrockRuntimeClient(RegionEndpoint.USEast1);
// This just works because the SDK's credential chain finds IMDS
var response = await client.ConverseAsync(request);
Code Sample #4 : Using EC2 instance role (no credentials needed in code)Creating an EC2 Instance Profile Role (Step-by-Step)
Step 1: Create an IAM Role
- AWS Console → IAM → Roles → Create Role
- Trusted entity type: AWS service
- Service: EC2
- Click "Next" → "Create role"
Step 2: Add Bedrock Permissions (Inline Policy)
Attach this least-privilege policy to the role:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream"
],
"Resource": "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-*"
}
]
}
Code Sample #5 : IAM policy for Bedrock invocation on EC2Step 3: Attach Role to EC2 Instance
- AWS Console → EC2 → Instances → Select your instance
- Actions → Security → Modify IAM role
- Select the role you created
- Click "Update IAM role"
Step 4: Verify (Test the Connection)
# SSH into your EC2 instance
ssh -i your-key.pem ec2-user@your-instance-ip
# Test if IMDS is reachable and the role is attached
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Should output your role name. Then test Bedrock access:
dotnet run # Run your app; it should authenticate automatically
Code Sample #6 : SSH into EC2 and test Bedrock accessPattern 4: Lambda & ECS (Serverless Production)
Both Lambda and ECS automatically provide credentials via execution roles. No manual setup beyond attaching the role to your function/task.
[LambdaFunction]
public async Task<APIGatewayProxyResponse> Handler(APIGatewayProxyRequest request)
{
// SDK automatically discovers Lambda execution role
var client = new AmazonBedrockRuntimeClient(RegionEndpoint.USEast1);
var response = await client.ConverseAsync(new ConverseRequest
{
ModelId = "anthropic.claude-3-haiku-20240307-v1:0",
Messages = new() { new Message { Role = ConversationRole.User, Content = new()
{
new ContentBlock { Text = request.QueryStringParameters?["prompt"] ?? "Hello" }
} } }
});
return new APIGatewayProxyResponse
{
StatusCode = 200,
Body = response.Output.Message.Content[0].Text
};
}
Code Sample #7 : Lambda handler using automatic execution role credentialsTroubleshooting: When Things Break
Common Authentication Errors & Fixes
If auth fails, the cause is usually one of these:
| Error | Likely Cause | Fix |
|---|---|---|
UnauthorizedException |
Credentials missing or not found | Run aws sts get-caller-identity to test. Check ~/.aws/credentials or env vars. |
AccessDeniedException |
Credentials valid but lack Bedrock permissions | Check IAM policy attached to role/user. Must include bedrock:InvokeModel. |
ResourceNotFoundException |
Model not available in region | Check model availability. Claude 3 Haiku is us-east-1 and us-west-2 only. |
| EC2 instance timeout on first invoke | IMDS not reachable (wrong VPC config) | Instance must have internet access (or NAT gateway for private subnets). Test IMDS: curl http://169.254.169.254/ |
Key Takeaways & Best Practices
- Local dev: Use
~/.aws/credentials— runaws configureonce and forget about it. - Production on EC2: Attach an instance profile role with Bedrock permissions. Let IMDS handle credentials automatically.
- Production on Lambda/ECS: Attach the execution role with Bedrock permissions. No code changes needed.
- Never hardcode credentials in source code or config files. Always use the credential chain (env vars, roles, IMDS).
- Test with AWS CLI first. If credentials work with the CLI, they'll work with your .NET app.
For reference, the code repository being discussed is available at github: https://github.com/ajaysskumar/ai-playground
Thanks for reading through. Please share feedback, if any, in comments or on my email ajay.a338@gmail.com
