# Terraform tls_private_key: Best Practices for Secure Agent Authentication

> Secure agent authentication with Terraform tls_private_key. Learn best practices for generating and managing private keys. Mark your Terraform tfstate as sensitive and avoid writing keys to disk.

- Repository: [HashiCorp/terraform](https://github.com/hashicorp/terraform)
- Tags: best-practices
- Published: 2026-02-16

---

**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:

```hcl
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:

```hcl
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`](https://github.com/hashicorp/terraform/blob/main/internal/communicator/ssh/provisioner.go), the communicator extracts the private key from the connection configuration:

```go
case "private_key":
    connInfo.PrivateKey = v.AsString()

```

(source: [provisioner.go lines 104-106](https://github.com/hashicorp/terraform/blob/main/internal/communicator/ssh/provisioner.go#L104-L106))

The connection schema in [`internal/terraform/node_resource_validate.go`](https://github.com/hashicorp/terraform/blob/main/internal/terraform/node_resource_validate.go) defines the `private_key` attribute as an optional string, which Terraform's core engine automatically treats as sensitive:

```go
"private_key": {
    Type:     cty.String,
    Optional: true,
},

```

(source: [node_resource_validate.go lines 200-203](https://github.com/hashicorp/terraform/blob/main/internal/terraform/node_resource_validate.go#L200-L203))

## 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:

```hcl
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`](https://github.com/hashicorp/terraform/blob/main/internal/backend/remote-state/oci/constants.go) establish these attribute names:

```go
PrivateKeyAttrName = "private_key"
PrivateKeyPathAttrName = "private_key_path"

```

(source: [constants.go lines 23-25](https://github.com/hashicorp/terraform/blob/main/internal/backend/remote-state/oci/constants.go#L23-L25))

The backend validation logic in [`internal/backend/remote-state/oci/backend.go`](https://github.com/hashicorp/terraform/blob/main/internal/backend/remote-state/oci/backend.go) prevents configuration conflicts by ensuring only one method is specified:

```go
if cfg.PrivateKey != "" && cfg.PrivateKeyPath != "" {
    return fmt.Errorf("Only one of private_key, private_key_path can be set.")
}

```

(source: [backend.go line 212](https://github.com/hashicorp/terraform/blob/main/internal/backend/remote-state/oci/backend.go#L212))

## 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:

```hcl
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:

```hcl
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_key` resource with strong algorithms (RSA 4096 or ECDSA) rather than hard-coding credentials.
- **Protect sensitive data** by marking outputs as `sensitive = true` and 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 `-replace` flag to minimize downtime during key updates.
- **Validate configuration** by ensuring mutual exclusivity between `private_key` and `private_key_path` attributes 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`](https://github.com/hashicorp/terraform/blob/main/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`](https://github.com/hashicorp/terraform/blob/main/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.