Created
July 22, 2026 01:02
-
-
Save mohashari/98a790397600a21762f34c56ebfacb22 to your computer and use it in GitHub Desktop.
Mitigating Replay Attacks in AWS IAM Roles Anywhere with Cryptographic Challenge-Response and SPIFFE — code snippets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package spiffeclient | |
| import ( | |
| "context" | |
| "crypto/x509" | |
| "fmt" | |
| "time" | |
| "github.com/spiffe/go-spiffe/v2/spiffeid" | |
| "github.com/spiffe/go-spiffe/v2/workloadapi" | |
| ) | |
| // SVIDProvider retrieves short-lived X.509 SVIDs from the SPIRE Workload API. | |
| type SVIDProvider struct { | |
| socketPath string | |
| client *workloadapi.Client | |
| } | |
| func NewSVIDProvider(socketPath string) (*SVIDProvider, error) { | |
| ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | |
| defer cancel() | |
| client, err := workloadapi.New(ctx, workloadapi.WithAddress(socketPath)) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to connect to SPIRE workload API: %w", err) | |
| } | |
| return &SVIDProvider{ | |
| socketPath: socketPath, | |
| client: client, | |
| }, nil | |
| } | |
| func (p *SVIDProvider) GetCurrentSVID(ctx context.Context) (*workloadapi.X509SVID, error) { | |
| svid, err := p.client.FetchX509SVID(ctx) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to fetch SVID from Workload API: %w", err) | |
| } | |
| return svid, nil | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package spiffeclient | |
| import ( | |
| "crypto" | |
| "crypto/rand" | |
| "crypto/sha256" | |
| "fmt" | |
| "github.com/spiffe/go-spiffe/v2/workloadapi" | |
| ) | |
| // SignChallenge signs a cryptographic nonce using the private key from the SVID. | |
| func SignChallenge(svid *workloadapi.X509SVID, nonce []byte) ([]byte, error) { | |
| if len(nonce) != 32 { | |
| return nil, fmt.Errorf("invalid nonce length: expected 32 bytes, got %d", len(nonce)) | |
| } | |
| // Hash the nonce to prepare for signing | |
| hash := sha256.Sum256(nonce) | |
| // SVID.PrivateKey implements crypto.Signer (typically ECDSA or RSA) | |
| signer, ok := svid.PrivateKey.(crypto.Signer) | |
| if !ok { | |
| return nil, fmt.Errorf("SVID private key does not implement crypto.Signer") | |
| } | |
| // Sign the hash using the private key | |
| signature, err := signer.Sign(rand.Reader, hash[:], crypto.SHA256) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to sign nonce hash: %w", err) | |
| } | |
| return signature, nil | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package broker | |
| import ( | |
| "context" | |
| "crypto" | |
| "crypto/rsa" | |
| "crypto/sha256" | |
| "fmt" | |
| "github.com/redis/go-redis/v9" | |
| ) | |
| type VerificationEngine struct { | |
| rdb *redis.Client | |
| } | |
| func NewVerificationEngine(redisAddr string) *VerificationEngine { | |
| return &VerificationEngine{ | |
| rdb: redis.NewClient(&redis.Options{Addr: redisAddr}), | |
| } | |
| } | |
| // VerifyChallengeResponse checks the nonce validity in Redis and verifies the signature. | |
| func (v *VerificationEngine) VerifyChallengeResponse(ctx context.Context, nonce []byte, signature []byte, publicKey crypto.PublicKey) error { | |
| nonceHex := fmt.Sprintf("%x", nonce) | |
| // Atomic check-and-delete using Lua script to prevent double-spend of nonces. | |
| luaScript := ` | |
| if redis.call("EXISTS", KEYS[1]) == 1 then | |
| local val = redis.call("GET", KEYS[1]) | |
| redis.call("DEL", KEYS[1]) | |
| return val | |
| else | |
| return nil | |
| end | |
| ` | |
| result, err := v.rdb.Eval(ctx, luaScript, []string{fmt.Sprintf("nonce:%s", nonceHex)}).Result() | |
| if err != nil { | |
| return fmt.Errorf("database failure checking nonce: %w", err) | |
| } | |
| if result == nil { | |
| return fmt.Errorf("challenge invalid or already replayed (nonce not found)") | |
| } | |
| // Hash the nonce to verify the signature | |
| hash := sha256.Sum256(nonce) | |
| switch pub := publicKey.(type) { | |
| case *rsa.PublicKey: | |
| err = rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash[:], signature) | |
| if err != nil { | |
| return fmt.Errorf("invalid signature for RSA public key: %w", err) | |
| } | |
| default: | |
| // In production, support both RSA and ECDSA keys (SPIFFE supports both) | |
| return fmt.Errorf("unsupported public key type: %T", publicKey) | |
| } | |
| return nil | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package broker | |
| import ( | |
| "context" | |
| "crypto/x509" | |
| "fmt" | |
| "github.com/aws/aws-sdk-go-v2/config" | |
| "github.com/aws/aws-sdk-go-v2/service/rolesanywhere" | |
| "github.com/aws/aws-sdk-go-v2/service/rolesanywhere/types" | |
| ) | |
| type RolesAnywhereClient struct { | |
| client *rolesanywhere.Client | |
| } | |
| func NewRolesAnywhereClient(ctx context.Context, region string) (*RolesAnywhereClient, error) { | |
| cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) | |
| if err != nil { | |
| return nil, fmt.Errorf("unable to load AWS config: %w", err) | |
| } | |
| return &RolesAnywhereClient{ | |
| client: rolesanywhere.NewFromConfig(cfg), | |
| }, nil | |
| } | |
| // FetchAWSCredentials calls the AWS IAM Roles Anywhere CreateSession API. | |
| func (c *RolesAnywhereClient) FetchAWSCredentials(ctx context.Context, profileARN, roleARN, trustAnchorARN string, clientSVID *x509.Certificate) (*types.Credentials, error) { | |
| // Extract SPIFFE ID from client cert's SAN URI to pass as the session name | |
| spiffeID := "unknown-spiffe-id" | |
| if len(clientSVID.URIs) > 0 { | |
| spiffeID = clientSVID.URIs[0].String() | |
| } | |
| input := &rolesanywhere.CreateSessionInput{ | |
| ProfileArn: &profileARN, | |
| RoleArn: &roleARN, | |
| TrustAnchorArn: &trustAnchorARN, | |
| SessionName: &spiffeID, // Map SPIFFE ID to CloudTrail session name | |
| } | |
| output, err := c.client.CreateSession(ctx, input) | |
| if err != nil { | |
| return nil, fmt.Errorf("rolesanywhere.CreateSession failed: %w", err) | |
| } | |
| if output.CredentialSet == nil || len(output.CredentialSet.Credentials) == 0 { | |
| return nil, fmt.Errorf("no credentials returned in the session set") | |
| } | |
| return &output.CredentialSet.Credentials[0], nil | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "Version": "2012-10-17", | |
| "Statement": [ | |
| { | |
| "Effect": "Allow", | |
| "Principal": { | |
| "Service": "rolesanywhere.amazonaws.com" | |
| }, | |
| "Action": [ | |
| "sts:AssumeRole", | |
| "sts:TagSession", | |
| "sts:SetSourceIdentity" | |
| ], | |
| "Condition": { | |
| "ArnEquals": { | |
| "aws:PrincipalTrustAnchorArn": "arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d" | |
| }, | |
| "StringLike": { | |
| "aws:PrincipalTag/SessionName": "spiffe://prod.internal/ns/payment/sa/billing-service" | |
| } | |
| } | |
| } | |
| ] | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Terraform Configuration for AWS IAM Roles Anywhere Resources | |
| resource "aws_rolesanywhere_trust_anchor" "broker_anchor" { | |
| name = "spiffe-broker-trust-anchor" | |
| enabled = true | |
| source { | |
| source_type = "CERTIFICATE_BUNDLE" | |
| source_data { | |
| x509_certificate_data = file("${path.module}/certs/broker_ca.pem") | |
| } | |
| } | |
| } | |
| resource "aws_rolesanywhere_profile" "workload_profile" { | |
| name = "spiffe-workload-profile" | |
| enabled = true | |
| role_arns = [aws_iam_role.workload_role.arn] | |
| # Enforce session duration limit to 1 hour to match SPIFFE SVID rotation cycle | |
| duration_seconds = 3600 | |
| # Enforce session policies for additional runtime boundaries | |
| session_policy = jsonencode({ | |
| Version = "2012-10-17" | |
| Statement = [ | |
| { | |
| Effect = "Allow" | |
| Action = "*" | |
| Resource = "*" | |
| } | |
| ] | |
| }) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment