Terraform tls_private_key: Best Practices for Secure Agent Authentication
Mark the tls_private_key resource as sensitive, pass the private_key_pem attribute directly to SSH connection blocks without writing to disk, and store Terraform state in an encrypted remote backend to prevent credential exposure.
The tls_private_key resource in the HashiCorp Terraform TLS provider generates cryptographically secure key pairs for SSH agent authentication. When provisioning infrastructure that requires secure remote access, understanding how Terraform handles these sensitive materials internally—specifically within the SSH communicator and state management subsystems—is critical for maintaining a secure infrastructure as code workflow.
How tls_private_key Works in Terraform
The TLS provider creates asymmetric key pairs through the tls_private_key resource, supporting both RSA and ECDSA algorithms. By default, Terraform stores the generated private_key_pem in the state file, making proper handling essential to prevent credential leakage.
When configuring the resource, specify the algorithm and key size explicitly:
resource "tls_private_key" "ssh_key" {
algorithm = "RSA"
rsa_bits = 4096
}
Secure Handling of Private Key Material
Marking Resources as Sensitive
Always mark outputs and variables that reference the private key as sensitive to prevent Terraform from displaying the value in plan or apply output. While the tls_private_key resource itself does not have a sensitive argument, you control exposure through output declarations:
output "private_key" {
value = tls_private_key.ssh_key.private_key_pem
sensitive = true
}
In-Memory Processing in the SSH Communicator
Terraform's SSH communicator handles private keys as in-memory strings, never persisting them to disk unless explicitly configured otherwise. In internal/communicator/ssh/provisioner.go, the communicator extracts the private key from the connection configuration:
case "private_key":
connInfo.PrivateKey = v.AsString()
(source: provisioner.go lines 104-106)
The connection schema in internal/terraform/node_resource_validate.go defines the private_key attribute as an optional string, which Terraform's core engine automatically treats as sensitive:
"private_key": {
Type: cty.String,
Optional: true,
},
(source: node_resource_validate.go lines 200-203)
State Management and Encryption
Remote Backend Configuration
Store Terraform state in a remote backend with encryption at rest to protect the private key material. Configure backends like AWS S3 with KMS encryption, Azure Storage with customer-managed keys, or Terraform Cloud with built-in encryption:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "infrastructure/terraform.tfstate"
region = "us-west-2"
encrypt = true
kms_key_id = "arn:aws:kms:us-west-2:123456789:key/abcd-1234"
}
}
Enable state locking to prevent concurrent modifications that could corrupt the state file or expose keys during race conditions.
OCI Backend Private Key Handling
For Oracle Cloud Infrastructure (OCI) remote state backends, Terraform enforces mutual exclusion between inline private keys and file paths. The constants defined in internal/backend/remote-state/oci/constants.go establish these attribute names:
PrivateKeyAttrName = "private_key"
PrivateKeyPathAttrName = "private_key_path"
(source: constants.go lines 23-25)
The backend validation logic in internal/backend/remote-state/oci/backend.go prevents configuration conflicts by ensuring only one method is specified:
if cfg.PrivateKey != "" && cfg.PrivateKeyPath != "" {
return fmt.Errorf("Only one of private_key, private_key_path can be set.")
}
(source: backend.go line 212)
Key Rotation Strategies
Implement automated key rotation using Terraform's lifecycle meta-argument to minimize downtime and maintain security hygiene. The create_before_destroy strategy ensures a new key pair exists before the old one is destroyed:
resource "tls_private_key" "ssh_key" {
algorithm = "RSA"
rsa_bits = 4096
lifecycle {
create_before_destroy = true
}
}
To force immediate rotation, use terraform apply -replace=tls_private_key.ssh_key or mark the resource as tainted with terraform taint tls_private_key.ssh_key. Always update the corresponding public key in authorized_keys files or cloud metadata services before the old resource is destroyed to prevent lockout.
Complete Implementation Example
Combine these practices into a cohesive configuration that generates a key pair, exposes only the public key, and uses the private key for secure provisioning:
resource "tls_private_key" "ssh_key" {
algorithm = "RSA"
rsa_bits = 4096
}
output "ssh_public_key" {
value = tls_private_key.ssh_key.public_key_openssh
sensitive = false
}
resource "null_resource" "provision" {
provisioner "remote-exec" {
connection {
type = "ssh"
host = aws_instance.example.public_ip
user = "ubuntu"
private_key = tls_private_key.ssh_key.private_key_pem
}
inline = ["echo 'Securely authenticated via in-memory key'"]
}
}
Summary
- Generate keys using the
tls_private_keyresource with strong algorithms (RSA 4096 or ECDSA) rather than hard-coding credentials. - Protect sensitive data by marking outputs as
sensitive = trueand relying on Terraform's in-memory handling of private keys in the SSH communicator. - Secure state storage using encrypted remote backends (S3 with KMS, Terraform Cloud) with strict IAM controls and state locking enabled.
- Automate rotation using
lifecycle { create_before_destroy = true }and the-replaceflag to minimize downtime during key updates. - Validate configuration by ensuring mutual exclusivity between
private_keyandprivate_key_pathattributes when using OCI or similar backends.
Frequently Asked Questions
How does Terraform prevent the private key from appearing in logs?
Terraform's core engine automatically treats the private_key attribute as sensitive based on the schema defined in internal/terraform/node_resource_validate.go. When you mark outputs with sensitive = true or use the attribute in connection blocks, Terraform redacts the value in plan, apply, and log output, displaying <sensitive> instead of the actual PEM content.
Can I use a file path instead of inline private key content?
Yes, but never for the tls_private_key resource output itself. For connection blocks, you may use the private_key attribute directly (recommended), or use ${file("~/.ssh/id_rsa")} for existing keys. However, the OCI backend enforces mutual exclusivity between private_key and private_key_path attributes as implemented in internal/backend/remote-state/oci/backend.go, preventing configuration conflicts.
What is the safest way to rotate a compromised tls_private_key?
Use the lifecycle { create_before_destroy = true } meta-argument on the resource to ensure Terraform generates a new key pair before destroying the compromised one. Then run terraform apply -replace=tls_private_key.ssh_key to force regeneration without manual tainting. Update the corresponding public key in your infrastructure's authorized_keys or cloud metadata before the old resource is destroyed to prevent lockout.
Should I commit the public key to version control?
Yes, the public key (public_key_openssh or public_key_pem) is safe to commit to version control or expose in outputs, as it is mathematically infeasible to derive the private key from it. However, never commit the private_key_pem attribute or write it to disk within your repository. Always add *.pem and *.key files to .gitignore if your workflow requires temporary file generation, and prefer in-memory handling via direct attribute references.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →