Sensitive Attributes

7 minute read

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: true to 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.

Next: Nested Schemas →


Previous: ← Validation and Constraints

Last updated: June 30, 2026

Updated: