Skip to content

Instantly share code, notes, and snippets.

@mohashari
Created July 22, 2026 01:02
Show Gist options
  • Select an option

  • Save mohashari/98a790397600a21762f34c56ebfacb22 to your computer and use it in GitHub Desktop.

Select an option

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
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
}
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
}
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
}
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
}
{
"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"
}
}
}
]
}
# 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