Cloud Credential Management
How Tendril assumes temporary credentials per job for AWS, GCP, and Azure.
Cloud Credential Management
Tendril assumes temporary cloud credentials at the start of every job and clears them when the job completes. No credentials persist between jobs.
Per-Job Credential Lifecycle
Job start
└── Read cloud_identity from job configuration
└── Provider-specific credential assumption
└── Set environment variables
└── Execute Terraform (uses env vars)
└── defer: Clear credentialsThe defer ensures credentials are always cleared, even if the job fails or panics.
AWS: STS AssumeRole
Input from cloud_identity:
role_arn— ARN of the IAM role in the user's AWS accountexternal_id— shared secret for confused deputy prevention
Execution:
func AssumeRole(roleArn, externalId string) error {
stsClient := sts.NewFromConfig(cfg)
result, err := stsClient.AssumeRole(ctx, &sts.AssumeRoleInput{
RoleArn: &roleArn,
ExternalId: &externalId,
RoleSessionName: aws.String("trellis-tendril"),
DurationSeconds: aws.Int32(3600), // 1 hour
})
// Set environment variables
os.Setenv("AWS_ACCESS_KEY_ID", *result.Credentials.AccessKeyId)
os.Setenv("AWS_SECRET_ACCESS_KEY", *result.Credentials.SecretAccessKey)
os.Setenv("AWS_SESSION_TOKEN", *result.Credentials.SessionToken)
return nil
}Cleanup:
defer func() {
os.Unsetenv("AWS_ACCESS_KEY_ID")
os.Unsetenv("AWS_SECRET_ACCESS_KEY")
os.Unsetenv("AWS_SESSION_TOKEN")
}()Terraform reads these environment variables automatically via the AWS provider.
GCP: Workload Identity Federation
Input from cloud_identity:
project_id— GCP project IDwif_config— Workload Identity Federation pool and provider configuration
Execution:
func ActivateGcpWIF(projectId string, wifConfig WIFConfig) (cleanup func(), err error) {
// Write credential configuration file
credFile := filepath.Join(tmpDir, "gcp-wif-credential.json")
writeCredentialConfig(credFile, wifConfig)
// Set environment variable
os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", credFile)
os.Setenv("GOOGLE_PROJECT", projectId)
return func() {
os.Remove(credFile)
os.Unsetenv("GOOGLE_APPLICATION_CREDENTIALS")
os.Unsetenv("GOOGLE_PROJECT")
}, nil
}The credential configuration file tells the GCP SDK to exchange an OIDC token for temporary GCP credentials via the STS endpoint. No service account key file is involved.
Azure: Federated Identity
Input from cloud_identity:
tenant_id— Azure Entra ID tenantclient_id— App Registration client IDsubscription_id— Target Azure subscription
Execution:
func ActivateAzureFederated(tenantId, clientId, subscriptionId string) (cleanup func(), err error) {
// Write OIDC token to file
tokenFile := filepath.Join(tmpDir, "azure-oidc-token")
writeOIDCToken(tokenFile)
// Set environment variables
os.Setenv("AZURE_TENANT_ID", tenantId)
os.Setenv("AZURE_CLIENT_ID", clientId)
os.Setenv("AZURE_SUBSCRIPTION_ID", subscriptionId)
os.Setenv("AZURE_FEDERATED_TOKEN_FILE", tokenFile)
return func() {
os.Remove(tokenFile)
os.Unsetenv("AZURE_TENANT_ID")
os.Unsetenv("AZURE_CLIENT_ID")
os.Unsetenv("AZURE_SUBSCRIPTION_ID")
os.Unsetenv("AZURE_FEDERATED_TOKEN_FILE")
}, nil
}The Azure provider (azurerm) reads these environment variables and uses the OIDC token file for federated authentication.
Security Properties
| Property | Guarantee |
|---|---|
| No persistent credentials | Env vars are cleared after every job via defer |
| No credential sharing | Each job gets its own assumed session |
| Time-bounded | AWS sessions expire after 1 hour |
| Scoped | Credentials only have permissions defined in the IAM role / service account / app registration |
| Revocable | User deletes the IAM role / WIF config / App Registration → instant revocation |
| No file leaks | Temporary credential files are deleted in defer cleanup |
CloudIdentity Structure
The cloud_identities table stores provider-specific credentials in a JSONB column:
AWS:
{
"role_arn": "arn:aws:iam::123456789012:role/GrapeProvisionerRole",
"external_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}GCP:
{
"project_id": "my-gcp-project",
"wif_config": {
"pool_id": "trellis-pool",
"provider_id": "trellis-provider",
"service_account_email": "trellis@my-gcp-project.iam.gserviceaccount.com"
}
}Azure:
{
"tenant_id": "12345678-1234-1234-1234-123456789012",
"client_id": "abcdefgh-abcd-abcd-abcd-abcdefghijkl",
"subscription_id": "87654321-4321-4321-4321-210987654321"
}