When you need the tls provider
Real cases:
- SSH keys for EC2. You generate a pair, put the public key into
aws_key_pair, and use the private key to connect or hand it to the user. - Test fixtures for PKI. Lessons and tests that need a real TLS connection without a real CA.
- Self-signed certs for internal services. A service mesh with an internal CA, or ingress in a dev cluster.
- A CSR for an external CA to sign. You generate the request, send it to Let's Encrypt, Hashicorp Vault, or a private CA, and get back a signed cert.
The provider is not meant for production secrets management. Keys land in state in plain text. That is fine for a lab and for SSH keys in one-off projects. It is not fine for signing payments.
Installation
terraform { required_providers { tls = {source = "hashicorp/tls"
version = "~> 4.0"
}
}
}
The provider has no configuration. It never calls an API. Every operation runs locally.
tls_private_key
resource "tls_private_key" "ssh" {algorithm = "ED25519"
}
output "private_pem" {value = tls_private_key.ssh.private_key_pem
sensitive = true
}
output "public_openssh" {value = tls_private_key.ssh.public_key_openssh
}
Algorithms:
| Algorithm | Size | Use |
|---|---|---|
RSA | 2048 / 3072 / 4096 | Compatible with everything. Default is 2048, which is old for production. |
ECDSA | P256 / P384 / P521 | Smaller and faster, but not supported everywhere. |
ED25519 | 256 | The best choice for SSH keys and modern systems. |
Resource attributes:
private_key_pemis the PEM private key. sensitive.private_key_opensshis the OpenSSH-format private key (forssh -i).public_key_pem,public_key_openssh,public_key_fingerprint_md5,public_key_fingerprint_sha256.
Tying it to aws_key_pair
resource "tls_private_key" "demo" {algorithm = "ED25519"
}
resource "aws_key_pair" "demo" {key_name = "demo-key"
public_key = tls_private_key.demo.public_key_openssh
}
resource "local_sensitive_file" "key" { filename = "${path.module}/demo.pem"content = tls_private_key.demo.private_key_pem
file_permission = "0600"
}
After apply, the pair is ready and the file sits on disk with 0600 permissions for whoever ran it. In CI it works the same way, but the state file still holds the key, so protecting that state is critical.
For production, keys usually live in Secrets Manager or AWS Systems Manager Parameter Store, not in state.
tls_self_signed_cert
resource "tls_private_key" "ca" {algorithm = "RSA"
rsa_bits = 4096
}
resource "tls_self_signed_cert" "ca" {private_key_pem = tls_private_key.ca.private_key_pem
subject {common_name = "linuxlab.local"
organization = "LinuxLab Test CA"
}
validity_period_hours = 8760 # 1 year
is_ca_certificate = true
allowed_uses = [
"cert_signing",
"crl_signing",
]
}
output "ca_cert" {value = tls_self_signed_cert.ca.cert_pem
}
This is an internal CA. From here you can sign CSRs with it.
tls_cert_request → tls_locally_signed_cert
A full PKI flow inside Terraform:
# key for the service
resource "tls_private_key" "service" {algorithm = "ECDSA"
ecdsa_curve = "P256"
}
# CSR
resource "tls_cert_request" "service" {private_key_pem = tls_private_key.service.private_key_pem
subject {common_name = "api.linuxlab.local"
organization = "LinuxLab"
}
dns_names = [
"api.linuxlab.local",
"api-internal.linuxlab.local",
]
}
# signed by your own CA
resource "tls_locally_signed_cert" "service" {cert_request_pem = tls_cert_request.service.cert_request_pem
ca_private_key_pem = tls_private_key.ca.private_key_pem
ca_cert_pem = tls_self_signed_cert.ca.cert_pem
validity_period_hours = 720 # 30 days
allowed_uses = [
"key_encipherment",
"digital_signature",
"server_auth",
]
}
After apply, you have a CA, a service key, and a signed certificate. You can load it into an ALB, a k8s ingress, or any system that accepts PEM.
tls data sources
Reading existing certificates:
# Fetch the cert chain of a remote server
data "tls_certificate" "example" {url = "https://example.com"
}
output "issuer" {value = data.tls_certificate.example.certificates[0].issuer
}
Useful for discovery: "which CA signs our ingress."
Pitfalls
-
Private keys in state. This is the fourth time it comes up, but it is the main point: state is a file full of secrets. Without an encrypted backend (S3 + KMS) it is a security incident.
-
The RSA 2048 default is outdated. In 2026 it is the minimum for compatibility, but not for security. Use RSA 4096 or ED25519.
-
Certificates do not rotate on their own.
tls_self_signed_certholds a valid period, but Terraform will not reissue it once the period expires. You needtime_rotatingpluslifecycle.replace_triggered_by:hclresource "time_rotating" "year" {rotation_days = 350
}
resource "tls_self_signed_cert" "ca" {# ...
lifecycle {replace_triggered_by = [time_rotating.year]
}
}
-
subjectdoes not accept every DN type. If you need unusual fields (emailAddress,userID), they are not supported. Only common_name, organization, country, locality, province, street_address, postal_code, organizational_unit, serial_number. -
dns_namesworks for wildcards.["*.linuxlab.local"]gets signed. Browsers accept it (if the root CA is trusted). -
You cannot drop in a CSR signed by another CA here. This is for local signing. To get a cert from Let's Encrypt, use the
acmeprovider or an external script. -
Use a real cert for production, not one from the tls provider. Use AWS Certificate Manager, Let's Encrypt, or an internal Vault PKI. The tls provider is for labs, tests, and SSH keys.
See also in LinuxLab
- tls-certificates covers what an
X.509 certificate is, who signs whom, and how to read
openssl x509. Without that grounding the tls provider looks like magic. - tls-handshake covers exactly what a self-signed cert does during the TLS handshake, why the browser turns red, and what CN/SAN/SNI mean.
- ssh-hardening:
tls_private_keyis often used as an SSH key (openssh_pem). Theed25519vsrsaparameters are covered there in detail.