Sensitive Attributes
Understanding Sensitive Data
Sensitive attributes contain data that should not be displayed in logs or terminal output, such as:
- Passwords
- API keys and tokens
- Private keys
- Connection strings with credentials
- Secrets
The Plugin Framework provides the Sensitive flag to handle this data properly.
Why Mark Attributes as Sensitive?
Without Sensitive Flag
resource "mycloud_database" "example" {
name = "mydb"
password = "super-secret-password"
}
Terraform output:
# terraform plan
Terraform will perform the following actions:
# mycloud_database.example will be created
+ resource "mycloud_database" "example" {
+ name = "mydb"
+ password = "super-secret-password" # 😱 Exposed!
}
With Sensitive Flag
resource "mycloud_database" "example" {
name = "mydb"
password = "super-secret-password"
}
Terraform output:
# terraform plan
Terraform will perform the following actions:
# mycloud_database.example will be created
+ resource "mycloud_database" "example" {
+ name = "mydb"
+ password = (sensitive value) # ✅ Protected!
}
Marking Attributes as Sensitive
Schema Declaration
"password": schema.StringAttribute{
Required: true,
Sensitive: true, // ← Mark as sensitive
MarkdownDescription: "Database password",
},
Complete Example
func (r *DatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
},
"name": schema.StringAttribute{
Required: true,
},
"password": schema.StringAttribute{
Required: true,
Sensitive: true,
MarkdownDescription: "Database password (sensitive)",
},
"connection_string": schema.StringAttribute{
Computed: true,
Sensitive: true,
MarkdownDescription: "Connection string with credentials (sensitive)",
},
},
}
}
What Happens When Marked Sensitive?
1. Plan Output
Sensitive values are hidden:
+ password = (sensitive value)
2. Apply Output
Changes shown as sensitive:
~ password = (sensitive value) # forces replacement
3. State File
⚠️ Important: Values are still stored in plaintext in state file:
{
"resources": [{
"instances": [{
"attributes": {
"name": "mydb",
"password": "super-secret-password"
}
}]
}]
}
Terraform does NOT encrypt sensitive values in state!
4. Show Output
Hidden from terraform show:
# mycloud_database.example:
resource "mycloud_database" "example" {
name = "mydb"
password = (sensitive value)
}
5. Output Values
Sensitive propagation:
output "db_password" {
value = mycloud_database.example.password
# Error: Output value must be marked sensitive
}
output "db_password" {
value = mycloud_database.example.password
sensitive = true # ✅ Required for sensitive attributes
}
Common Sensitive Attributes
Passwords
"password": schema.StringAttribute{
Required: true,
Sensitive: true,
},
API Keys and Tokens
"api_key": schema.StringAttribute{
Required: true,
Sensitive: true,
},
"access_token": schema.StringAttribute{
Computed: true,
Sensitive: true,
},
Private Keys
"private_key": schema.StringAttribute{
Required: true,
Sensitive: true,
},
"ssh_private_key": schema.StringAttribute{
Computed: true,
Sensitive: true,
},
Connection Strings
"connection_string": schema.StringAttribute{
Computed: true,
Sensitive: true,
MarkdownDescription: "Connection string with embedded credentials",
},
Certificates
"certificate": schema.StringAttribute{
Computed: true,
Sensitive: true,
},
"certificate_key": schema.StringAttribute{
Computed: true,
Sensitive: true,
},
Logging Sensitive Data
❌ Never Log Sensitive Values
// BAD: Logs sensitive data
func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan DatabaseModel
req.Plan.Get(ctx, &plan)
// ❌ DON'T DO THIS!
tflog.Debug(ctx, fmt.Sprintf("Creating database with password: %s", plan.Password.ValueString()))
}
✅ Log Safely
// GOOD: Doesn't expose sensitive data
func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan DatabaseModel
req.Plan.Get(ctx, &plan)
// ✅ Safe logging
tflog.Debug(ctx, "Creating database", map[string]any{
"name": plan.Name.ValueString(),
// Don't log password!
})
// Create database
db, err := r.client.CreateDatabase(ctx, CreateDatabaseRequest{
Name: plan.Name.ValueString(),
Password: plan.Password.ValueString(), // Use it, don't log it
})
if err != nil {
resp.Diagnostics.AddError("Create Error", err.Error())
return
}
// ✅ Safe success log
tflog.Info(ctx, "Database created successfully", map[string]any{
"id": db.ID,
})
// Set state
plan.ID = types.StringValue(db.ID)
resp.State.Set(ctx, plan)
}
Terraform Plugin SDK Logging
Use structured logging with tflog:
import "github.com/hashicorp/terraform-plugin-log/tflog"
// ✅ Good: Structured logging without sensitive data
tflog.Debug(ctx, "Configuring authentication", map[string]any{
"username": username,
// Omit password/token
})
// ❌ Bad: Might log sensitive data
tflog.Debug(ctx, fmt.Sprintf("Auth: %s:%s", username, password))
State File Security
Understanding State Security
Critical fact: Terraform stores sensitive values in plaintext in state files.
{
"resources": [{
"instances": [{
"attributes": {
"password": "super-secret-password", // Plaintext!
"api_key": "sk-abc123..." // Plaintext!
}
}]
}]
}
State File Protection Methods
1. Remote State with Encryption
Use Terraform Cloud, S3, Azure, etc.:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true # ✅ Encrypt at rest
dynamodb_table = "terraform-locks"
}
}
2. Vault Integration
Store secrets in HashiCorp Vault:
data "vault_generic_secret" "db_password" {
path = "secret/database/password"
}
resource "mycloud_database" "example" {
name = "mydb"
password = data.vault_generic_secret.db_password.data["value"]
}
3. Environment Variables
Sensitive values from environment:
variable "db_password" {
type = string
sensitive = true
}
resource "mycloud_database" "example" {
name = "mydb"
password = var.db_password
}
export TF_VAR_db_password="super-secret-password"
terraform apply
Provider Configuration Patterns
Pattern 1: API Key in Provider Config
// Provider schema
"api_key": schema.StringAttribute{
Optional: true,
Sensitive: true,
MarkdownDescription: "API key for authentication",
},
provider "mycloud" {
api_key = var.api_key # From variable
}
variable "api_key" {
type = string
sensitive = true
}
Pattern 2: Computed Credentials
// Generate credentials during creation
func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// ... create database ...
// API returns credentials
plan.Username = types.StringValue(db.Username)
plan.Password = types.StringValue(db.Password) // Sensitive
plan.ConnectionString = types.StringValue(db.ConnStr) // Sensitive
resp.State.Set(ctx, plan)
}
Schema:
"username": schema.StringAttribute{
Computed: true,
},
"password": schema.StringAttribute{
Computed: true,
Sensitive: true,
},
"connection_string": schema.StringAttribute{
Computed: true,
Sensitive: true,
},
Pattern 3: Rotating Credentials
"rotate_password": schema.BoolAttribute{
Optional: true,
Default: booldefault.StaticBool(false),
},
"password": schema.StringAttribute{
Computed: true,
Sensitive: true,
},
resource "mycloud_database" "example" {
name = "mydb"
rotate_password = true # Trigger rotation
}
Common Patterns
Pattern 1: Optional Password (Computed if not provided)
"password": schema.StringAttribute{
Optional: true,
Computed: true,
Sensitive: true,
MarkdownDescription: "Database password. If not specified, a random password will be generated.",
},
Implementation:
func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan DatabaseModel
req.Plan.Get(ctx, &plan)
var password string
if plan.Password.IsNull() {
// Generate random password
password = generateRandomPassword()
plan.Password = types.StringValue(password)
} else {
password = plan.Password.ValueString()
}
db, err := r.client.CreateDatabase(ctx, CreateDatabaseRequest{
Name: plan.Name.ValueString(),
Password: password,
})
// Set state
plan.ID = types.StringValue(db.ID)
resp.State.Set(ctx, plan)
}
Pattern 2: Separate Password and Password Hash
"password": schema.StringAttribute{
Optional: true,
Sensitive: true,
Validators: []validator.String{
stringvalidator.ConflictsWith(path.Expressions{
path.MatchRoot("password_hash"),
}...),
},
},
"password_hash": schema.StringAttribute{
Optional: true,
Sensitive: true,
Validators: []validator.String{
stringvalidator.ConflictsWith(path.Expressions{
path.MatchRoot("password"),
}...),
},
},
Pattern 3: Credentials Block
"credentials": schema.SingleNestedAttribute{
Optional: true,
Attributes: map[string]schema.Attribute{
"username": schema.StringAttribute{
Required: true,
},
"password": schema.StringAttribute{
Required: true,
Sensitive: true,
},
},
},
resource "mycloud_service" "example" {
name = "myservice"
credentials {
username = "admin"
password = var.admin_password
}
}
Error Handling
❌ Don’t Expose Sensitive Data in Errors
// BAD: Password in error message
if err != nil {
resp.Diagnostics.AddError(
"Authentication Failed",
fmt.Sprintf("Failed to authenticate with password: %s", password), // ❌
)
}
✅ Safe Error Messages
// GOOD: No sensitive data
if err != nil {
resp.Diagnostics.AddError(
"Authentication Failed",
"Failed to authenticate with provided credentials", // ✅
)
}
Testing Sensitive Attributes
Test Configuration
func TestDatabaseResource(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccDatabaseResourceConfig("test-db", "test-password"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("mycloud_database.test", "name", "test-db"),
// ✅ Can check sensitive attributes in tests
resource.TestCheckResourceAttr("mycloud_database.test", "password", "test-password"),
),
},
},
})
}
Best Practices
1. Always Mark Sensitive Data
// ✅ Good: All credential fields marked sensitive
"username": schema.StringAttribute{
Required: true,
},
"password": schema.StringAttribute{
Required: true,
Sensitive: true, // ✅
},
"api_key": schema.StringAttribute{
Computed: true,
Sensitive: true, // ✅
},
2. Don’t Log Sensitive Values
// ✅ Good: No sensitive data in logs
tflog.Debug(ctx, "Creating database", map[string]any{
"name": plan.Name.ValueString(),
// Omit password
})
// ❌ Bad: Logs sensitive data
tflog.Debug(ctx, fmt.Sprintf("Password: %s", plan.Password.ValueString()))
3. Use Remote State with Encryption
# ✅ Good: Encrypted remote state
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true # ✅ Encrypt at rest
}
}
4. Document Sensitive Attributes
"api_key": schema.StringAttribute{
Required: true,
Sensitive: true,
MarkdownDescription: "API key for authentication. This value is sensitive and will not be displayed in logs.",
},
5. Validate Sensitive Input
"password": schema.StringAttribute{
Required: true,
Sensitive: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(8),
// Other validation rules
},
},
Common Mistakes
❌ Mistake 1: Forgetting to Mark Sensitive
// BAD: Password not marked sensitive
"password": schema.StringAttribute{
Required: true, // ❌ Missing Sensitive: true
},
❌ Mistake 2: Logging Sensitive Data
// BAD: Logs password
tflog.Debug(ctx, fmt.Sprintf("Password: %s", password)) // ❌
❌ Mistake 3: Sensitive Data in Error Messages
// BAD: Exposes credentials in error
resp.Diagnostics.AddError(
"Error",
fmt.Sprintf("Failed with key: %s", apiKey), // ❌
)
❌ Mistake 4: Relying on State File Encryption
// State file ALWAYS contains plaintext!
// Marking sensitive only hides from output,
// does NOT encrypt in state
Security Checklist
- ✅ Mark all passwords as
Sensitive: true - ✅ Mark all API keys/tokens as
Sensitive: true - ✅ Mark all private keys as
Sensitive: true - ✅ Mark all connection strings with credentials as
Sensitive: true - ✅ Never log sensitive values
- ✅ Don’t include sensitive data in error messages
- ✅ Use remote state with encryption
- ✅ Consider using Vault or other secret managers
- ✅ Document which attributes are sensitive
- ✅ Test sensitive attribute handling
Key Takeaways
- ✅ Use
Sensitive: trueto hide values from logs and output - ✅ Sensitive values are still stored in plaintext in state files
- ✅ Never log sensitive data
- ✅ Use encrypted remote state backends
- ✅ Consider external secret management (Vault, AWS Secrets Manager)
- ✅ Mark computed credentials as sensitive
- ✅ Validate sensitive input without logging values
- ✅ Document sensitive attributes clearly
What’s Next?
Now let’s learn about nested schemas for complex configurations.
Previous: ← Validation and Constraints
Last updated: June 30, 2026