Terraform plan prints sensitive information - terraform

when performing terraform plan, if an azurerm_kubernetes_cluster (Azure) resource exists in the state, terraform will print some information from kube_config which seems sensitive
Example printout: (all ... values get printed)
kube_config = [
{
client_certificate = (...)
client_key = (...)
cluster_ca_certificate = (...)
host = (...)
password = (...)
}
I'm not exactly sure WHICH of those values are sensitive, but password probably is...right?
On the other hand, terraform does seem to have some knowledge of which values are sensitive, as it does print the client_secret this way:
service_principal {
client_id = "(...)"
client_secret = (sensitive value)
}
So, my questions would be:
Are those values actually sensitive?
If so, is there a way to instruct terraform to mask those values in the plan?
Versions we are using:
provider "azurerm" {
version = "~>1.37.0"
}
The reason why this is problematic is that we pipe the plan in a Github PR comment.
Thanks

Are those values actually sensitive?
Yes, there are sensitive data. Actually they are the config that you need to use to control the AKS cluster. It's the AKS credential. I think it's necessary to output these data, just make a suppose that you only have Terraform and use it to create an AKS cluster, if Terraform does not output the credential, you cannot control your AKS cluster.
If so, is there a way to instruct terraform to mask those values in
the plan?
According to the explanation above, you should not wrong about the sensitive data in the Terraform state file. What you need to care about is how to protect the state file. I suggest you store the Terraform state file in Azure storage then you can encrypt it. Follow the steps in Store Terraform state in Azure Storage.

Terraform now offers the ability to set variables as sensitive, and outputs as sensitive.
variable example:
variable "user_information" {
type = object({
name = string
address = string
})
sensitive = true
}
output example:
output "db_password" {
value = aws_db_instance.db.password
description = "The password for logging in to the database."
sensitive = true
}
However, as of July 1, 2021 there is no option to hide plan output for something that isn't derived from a sensitive input.
References:
https://www.hashicorp.com/blog/terraform-0-14-adds-the-ability-to-redact-sensitive-values-in-console-output
https://www.terraform.io/docs/language/values/outputs.html

Related

Terraform json/map conversion in dynamic section

I am trying to configure a specific list of user with terraform in a dynamic section.
First, I have all my users / password as a json in a Vault like this:
{
"user1": "longPassword1",
"user2amq": "longPassword2",
"user3": "longPassword3"
}
then I declare the Vault data with
data "vault_kv_secret_v2" "all_clients" {
provider = my.vault.provider
mount = "credentials/aws/amq"
name = "dev/clients"
}
and in a locals section:
locals {
all_clients = tomap(jsondecode(data.vault_kv_secret_v2.all_clients.data.client_list))
}
in my tf file, I declare the dynamic section like this:
dynamic "user" {
for_each = local.all_clients
content {
username = each.key
password = each.value
console_access = "false"
groups = ["users"]
}
}
When I apply my terraform I got an error:
│ on modules/amq/amq.tf line 66, in resource "aws_mq_broker" "myproject":
│ 66: for_each = local.all_clients
│
│ Cannot use a map of string value in for_each. An iterable collection is
│ required.
I tried many ways to manage such a map but always terminating with an error
(like using = instead : and bypassing the jsondecode, or having a Map or a List of Object with
"username": "user1"
"pasword": "pass1"
etc... (I am open to adjust the Json for making it working)
Nothing was working and I am a bit out of idea how to map such a simple thing into terraform. I already check plenty of questions/answers in SO and none are working for me.
Terraform version 1.3.5
UPDATE:
By just putting an output on the local variable outside my module:
locals {
all_clients = jsondecode(data.vault_kv_secret_v2.all_clients.data.client_list)
}
output all_clients {
value = local.all_clients
}
after I applied the code, the command terraform output -json all_clients will show my json structure properly (and same if I put = instead and just displayed as a map, without the jsondecode).
As the answer says, the issue is more related to sensitiveness while declaring the loop.
On the other side, I had to adjust my username not being emails because not supported by AWS AmazonMQ (ActiveMQ) and password field must be greater than 12 chars (max 250 chars).
I think the problem here is something Terraform doesn't support but isn't explaining well: you can't use a map that is marked as sensitive directly as the for_each expression, because doing so would disclose some information about that sensitive value in a way that Terraform can't hide in the UI. At the very least, it would expose the number of elements.
It seems like in this particular case it's overly conservative to consider the entire map to be sensitive, but neither Vault nor Terraform understand the meaning of your data structure and so are treating the whole thing as sensitive just to make sure nothing gets disclosed accidentally.
Assuming that only the passwords in this result are actually sensitive, I think the best answer would be to specify explicitly what is and is not sensitive using the sensitive and nonsensitive functions to override the very coarse sensitivity the hashicorp/vault provider is generating:
locals {
all_clients = tomap({
for user, password in jsondecode(nonsensitive(data.vault_kv_secret_v2.all_clients.data.client_list)) :
user => sensitive(password)
})
}
Using nonsensitive always requires care because it's overriding Terraform's automatic inference of sensitive values and so if you use it incorrectly you might show sensitive information in the Terraform UI.
In this case I first used nonsensitive on the whole JSON string returned by the vault_kv_secret_v2 data source, which therefore avoids making the jsondecode result wholly sensitive. Then I used the for expression to carefully mark just the passwords as sensitive, so that the final value would -- if assigned to somewhere that causes it to appear in the UI -- appear like this:
tomap({
"user1" = (sensitive value)
"user2amq" = (sensitive value)
"user3" = (sensitive value)
})
Now that the number of elements in the map and the map's keys are no longer sensitive, this value should be compatible with the for_each in a dynamic block just like the one you showed. Because the value of each element is sensitive, Terraform will treat the password argument value in particular as sensitive, while disclosing the email addresses.
If possible I would suggest testing this with fake data that isn't really sensitive first, just to make sure that you don't accidentally expose real sensitive data if anything here isn't quite correct.

How to design a Terraform resource with multiple sensitive attributes

Context: I'm developing a new resource for my TF Provider.
This foo resource has a name and associated config: a list of key value pairs (both sensitive and non-sensitive).
There're 3 options I've identified:
resource "foo" "option1" {
name = "option1"
config = {
"name" = "option1"
"errors.length" = 3
"tasks.type" = "FOO"
}
config_sensitive = {
"jira.key" = "..."
"credentials.json" = "..."
}
}
resource "foo" "option2" {
name = "option2"
config = {
"name" = "option1"
"errors.length" = 3
"tasks.type" = "FOO"
"jira.key" = "..."
"credentials.json" = "..."
}
}
resource "foo" "option3" {
name = "option3"
config = file("config.json")
}
The advantage of option #3 is it looks very readable but requires a user to store an extra json file (with secrets) in the same folder (I'm not sure how acceptable that setup is). Option #2 looks tempting but foo should accept updates and if we mark the whole block as sensitive (since it may contain secret key-value pairs), the update functionality will suffer (user won't see the expected change). So Option #1 is the winner in my eyes since it's the most explicit one and allows us to distinguish between sensitive and non-sensitive attributes (while allowing updates for non-sensitive ones). Reading from file the whole config is probably not ideal since it doesn't really allow an engineer to see how the config looks like without opening another file.
There's also this weird duplicated name attribute but let's ignore it for now.
What configuration is the most acceptable and used by other TF Providers?
Option #3 should be struck immediately for three reasons:
You cannot realiably use the sensitive flag in the schema struct like you can with 1 and 2.
It requires a JSON format value which is cumbersome to work with unless you are forced into it (e.g. security policies).
Someone could inline the JSON and not store it in a file, which would completely workaround your attempt to obscure the secrets.
Options 1 and 2 are honestly no different from a secrets management perspective. You could apply the sensitive flag to either in the nested schema struct on a per-attribute basis, and use e.g. Vault to pass in values on a KV basis for either.
I would opt for 1 over 2 simply because it appears to me from your question that the arguments and values in the two blocks have no relationship with each other. Therefore, it makes more sense to organize your schema into two separate blocks for code cleanliness purposes.
I will also mention that if it is possible to refactor the credentials.json into your provider, and leverage the JIRA provider for the jira.key, then that would be best practices by both code architecture and security. It is also how the major providers handle this situation.
Terraform providers should handle the credential/auth implementation and the resource handles the resource configuration.
e.g.
resource "jira_issue" "some_story" {
title = "My story"
type = "story"
labels = ["someexampleonstackoverflow","jakewashere"]
}
Notice there's no config that doesn't relate to the thing I'm creating inside the Terraform resource.
It's very acceptable to have some documented convention in your provider that reads credentials from somewhere, whether that's an OS variable, file on disk etc.
For example: The Google Cloud provider, will read an environment variable if it's populated, if not it'll attempt to read either a configuration file that sits inside a hidden directory within $HOME or attempts to read a localhost http metadata server for the credentials.

How to create/overwrite a parameter in AWS Parameter Store only if it does not exist?

I am using terraform to create a parameter in the AWS Parameter Store.
resource "aws_ssm_parameter" "username" {
name = "username"
type = "SecureString"
value = "to_be_defined"
overwrite = false
}
provider "aws" {
version = "~> 1.53"
}
When I run terraform apply for the first time, if the parameter does not exist terraform creates the parameter. However, if I run it again (usually with a different value) I get the error
ParameterAlreadyExists: The parameter already exists. To overwrite
this value, set the overwrite option in the request to true
If I understand correctly, this is due to the behaviour of AWS Cli (not specific to the provider).
The current behavior for overwrite = false is
If the parameter does not exist, create it
If the parameter exists, throw exception
What I want to achieve is
If the parameter does not exist, create it
If the parameter exists, do nothing
I did not find a way in AWS CLI documentation to achieve the desired behavior.
I would like to know if there is any way to achieve the desired behaviour using terraform (or directly via AWS CLI)
I agree with #ydaetskcoR that you should maintain the value with terraform state as well.
But if you insist to ignore the value to be updated if the SSM key is exist, you can use lifecycle ignore_changes(https://www.terraform.io/docs/configuration/resources.html#ignore_changes)
So in your case, you can update the code to
resource "aws_ssm_parameter" "username" {
name = "username"
type = "SecureString"
value = "to_be_defined"
overwrite = false
lifecycle {
ignore_changes = [
value,
]
}
}
overwrite - (Optional) Overwrite an existing parameter. If not specified, will default to false if the resource has not been created by terraform to avoid overwrite of existing resource and will default to true otherwise (terraform lifecycle rules should then be used to manage the update behavior).
By the way, it is not good design to manage SecureString SSM key/value with terraform, because its tfstate file is not encrypted.

resource output value from one plan into another plan

I have two plans, in which I am creating two different servers(just for example otherwise it's really complex). In one plan, I am outputing the value of the security group like this:
output "security_group_id" {
value = "${aws_security_group.security_group.id}"
}
I have second plan, in which I want to use that value, how I can achieve it, I have tried couple of things but nothing work for me.
I know how to use the output value return by module but don't know that how I can use the output of one plan to another.
When an output is used in the top-level module of a configuration (the directory where you run terraform plan) its value is recorded in the Terraform state.
In order to use this value from another configuration, the state must be published to a location where it can be read by the other configuration. The usual way to achieve this is to use Remote State.
With remote state enabled for the first configuration, it becomes possible to read the resulting values from the second configuration using the terraform_remote_state data source.
For example, it's possible to keep the state for the first configuration in Amazon S3 by using a backend configuration like the following:
terraform {
backend "s3" {
bucket = "example-s3-bucket"
key = "example-bucket-key"
region = "us-east-1"
}
}
After adding this to the first configuration, Terraform will prompt you to run terraform init to initialize the new backend, which includes migrating the existing state to be stored on S3.
Then in the second configuration this can be retrieved by providing the same configuration to the terraform_remote_state data source:
data "terraform_remote_state" "example" {
backend = "s3"
config {
bucket = "example-s3-bucket"
key = "example-bucket-key"
region = "us-east-1"
}
}
resource "aws_instance" "foo" {
# ...
vpc_security_group_ids = "${data.terraform_remote_state.example.security_group_id}"
}
Note that since the second configuration is reading the state from the first it is necessary to terraform apply the first configuration so that this value will actually be recorded in the state. The second config must be re-applied any time the outputs are changed in the first.
For the local backend the process is same. In first step, we need to declare the following code snippet to publish the state.
terraform {
backend local {
path = "./terraform.tfstate"
}
}
When you execute terraform init and terraform apply command, please observe that in .terraform directory new terraform.tfsate file would be created which contains backend information and tell terraform to use the following tfstate file.
Now in the second configuration we need to use data source to import the outputs by using this code snippet
data "terraform_remote_state" "test" {
backend = "local"
config {
path = "${path.module}/../regionalvpc/terraform.tfstate"
}
}

terraform conditionally create resource based on external data?

As part of a setup, I create TLS certs and store them in S3. Creating the certs is done via external data source that runs the command to generate the certs. I then use those outputs to create S3 bucket object resources.
This works very well the first time I run terraform apply. However, if I change any other (non-cert) variable, resource, etc. and rerun, it reruns the external command, which generates a new key/cert pair, uploads them to S3, and breaks everything that already works.
Is there any way to create the resource conditionally? What pattern could I use to make the certs created only if they don't exist?
I did look at storing the generated keys/certs locally, but this is sensitive key material; I do not want it stored in local disk (and there are keys per environment).
Key/cert generation and storage:
data "external" "ca" {
program = ["sh","-c","jq '.root|fromjson' | cfssl gencert -initca -"]
#
query = {root = "${ data.template_file.etcd-ca-csr.rendered }"}
# the result will be saved in
# data.external.etcd-ca.result.key
# data.external.etcd-ca.result.csr
# data.external.etcd-ca.result.cert
}
resource "aws_s3_bucket_object" "ca_cert" {
bucket = "${aws_s3_bucket.my_bucket.id}"
key = "ca.pem"
content = "${data.external.ca.result.cert}"
}
resource "aws_s3_bucket_object" "ca_key" {
bucket = "${aws_s3_bucket.my_bucket.id}"
key = "ca-key.pem"
content = "${data.external.ca.result.key}"
}
Happy to look at using some form of conditional or entirely different generation pattern.
The reason for this behavior is that external is a data source, and thus Terraform expects that it is is read-only and side-effect-free. It re-runs data sources for every plan.
In order to do this via an external script, it would be necessary to use a resource provisioner to run the script and upload it to S3, since there is currently no external equivalent for resources, which are allowed to have side-effects, and provisioners are side-effect-only (that is, they can't produce results to use elsewhere in config.)
Another approach, though, would be to use Terraform's built-in TLS provider, which allows creation of certificates within Terraform itself. In this case it looks like you're trying to create a new CA cert and key, which could be done with tls_self_signed_cert like this:
resource "tls_private_key" "ca" {
algorithm = "RSA"
rsa_bits = 2048
}
resource "tls_self_signed_cert" "ca" {
key_algorithm = "RSA"
private_key_pem = "${tls_private_key.ca.private_key_pem}"
# ... subject and validity settings, as appropriate
is_ca_certificate = true
allowed_uses = ["cert_signing"]
}
resource "aws_s3_bucket_object" "ca_cert" {
bucket = "${aws_s3_bucket.my_bucket.id}"
key = "ca.pem"
content = "${resource.tls_self_signed_cert.ca.cert_pem}"
}
resource "aws_s3_bucket_object" "ca_key" {
bucket = "${aws_s3_bucket.my_bucket.id}"
key = "ca-key.pem"
content = "${resource.tls_self_signed_cert.ca.private_key_pem}"
}
The generated private key will be included in the state for use on future runs, so it's important to ensure that the state is stored securely. Note that this would also be true using the external data source, since data source results are also stored in state. Thus this approach is equivalent from the standpoint of where the secrets get stored.
I wrote more details about using Terraform for TLS certificate management in an article on my website. Its scope is broader than your requirements here, but may be of some interest.

Resources