Hiding Genesys Cloud OAuth client secrets from Terraform state

Hey everyone, running into a classic security headache with the Genesys Cloud Terraform provider. We’ve got a CI/CD pipeline that provisions genesyscloud_oauth_client resources for various internal tools, but the generated client secrets are showing up in plaintext in our remote state file (S3/TF Cloud). That’s a no-go for our audit team.

I know the provider supports secret fields, but it seems like Terraform still stores the unencrypted value in the state unless you mark it as sensitive, and even then, it’s just masked in the console output, not in the actual state file storage if you’re not careful with the backend config. Wait, actually, I’m confused. Does marking a variable as sensitive = true in the root module actually prevent it from being written to the state file, or does it just hide it from terraform plan output?

I tried this approach:

resource "genesyscloud_oauth_client" "my_app" {
 name = "Internal Tool"
 client_type = "confidential"
 # I want to generate the secret, but not store it
 redirect_uris = ["https://myapp.local/callback"]
}

# Outputting it to a vault secret immediately
resource "vault_generic_secret" "oauth_secret" {
 path = "secret/data/genesys/oauth"
 data_json = jsonencode({
 client_id = genesyscloud_oauth_client.my_app.client_id
 client_secret = genesyscloud_oauth_client.my_app.client_secret
 })
}

The problem is, genesyscloud_oauth_client.my_app.client_secret is still in the state. If someone gets access to the state file, they can extract it. I’ve seen mentions of using external data sources or random_password with keepers, but I can’t figure out how to decouple the secret generation from the state storage effectively.

Is there a pattern where I can provision the OAuth client, read the secret from the API response directly in the Terraform run (maybe via a null_resource with a local-exec calling the /api/v2/oauth/clients endpoint?), and store it in Vault before the state is synced? Or is the provider fundamentally designed to keep it in state? Feels like I’m missing a simple flag. The docs aren’t super clear on the ‘secret’ attribute behavior regarding state persistence.

Terraform doesn’t hide values in state files by default, even if you mark them as sensitive. That flag only stops them from printing in CLI output. If your audit team is flagging the S3 bucket contents, you’re stuck with encryption at rest. Enable server-side encryption on that S3 bucket using AWS KMS. It’s the only way to ensure the state file itself isn’t readable plaintext.

resource "aws_s3_bucket" "tfstate" {
 bucket = "my-tf-state-bucket"

 server_side_encryption_configuration {
 rule {
 apply_server_side_encryption_by_default {
 sse_algorithm = "aws:kms"
 kms_master_key_id = aws_kms_key.tfstate.arn
 }
 bucket_key_enabled = true
 }
 }
}

resource "aws_kms_key" "tfstate" {
 description = "KMS key for Terraform state"
}

This encrypts the file on disk. The Genesys provider has nothing to do with how Terraform stores state locally or remotely. You might also consider using remote state encryption options if you’re on Terraform Cloud, but for S3 backend, KMS is the standard fix. Don’t rely on the sensitive argument for actual security.

Yeah, hit the nail on the head regarding S3 encryption. That’s the baseline for any serious infra setup. But let’s be real, encrypting the state file at rest doesn’t actually solve the problem of developers or CI runners having access to the decrypted state in memory during a plan or apply. If someone gets shell access to a runner, they can still grab those secrets.

If you want to keep those OAuth secrets out of the state entirely, you have to stop telling Terraform to manage them. The genesyscloud_oauth_client resource has a secret argument, but you can use ignore_changes to tell TF to create the resource once, then let Genesys Cloud manage the secret rotation and storage.

Here’s how I handle it in my Node.js integration repos. We create the client, grab the initial secret from the output (which we store in a secrets manager like AWS Secrets Manager, not the state), and then ignore future changes to the secret field.

resource "genesyscloud_oauth_client" "api_client" {
 name = "Internal Node.js Worker"
 description = "Managed by Terraform, secrets handled externally"
 
 # Define the scopes you need
 scopes = [
 "conversation:webchat:read",
 "user:read"
 ]

 lifecycle {
 ignore_changes = [
 secret
 ]
 }
}

# Output the secret only on creation, then pipe it to your secrets manager
# via a null_resource or external script. Don't leave it in the TF output long term.
output "initial_client_secret" {
 value = genesyscloud_oauth_client.api_client.secret
 sensitive = true
 description = "Initial secret. Rotate this in Genesys Cloud UI or API."
}

The key is that ignore_changes block. Terraform will create the client and generate the secret, but it won’t store the new value in the state if you rotate it via the API or UI. You’ll need a small script to fetch that initial secret and push it to your secret store before you delete the output from your TF config. It’s a bit of extra work upfront, but it keeps the state file clean of sensitive data.

Just remember to validate the OAuth flow in your Node.js app using the PureCloudPlatformClientV2 SDK with the stored secret. If the secret rotates in Genesys Cloud, your app needs to handle the 401 and fetch the new secret from your secrets manager. It’s not smooth, but it’s secure.