terraform-provider-vault icon indicating copy to clipboard operation
terraform-provider-vault copied to clipboard

The `vault_kv_secret_v2` resource's `cas` field does not work

Open busser opened this issue 2 years ago • 2 comments
trafficstars

Terraform Version

Terraform v1.3.2
on darwin_arm64
+ provider registry.terraform.io/hashicorp/vault v3.12.0

Affected Resource(s)

Terraform Configuration Files

terraform {
  required_providers {
    vault = {
      source  = "hashicorp/vault"
      version = "3.12.0"
    }
  }
}

provider "vault" {
  address = "http://localhost:8200"
}

resource "vault_kv_secret_backend_v2" "cas_required" {
  mount        = "/secret"
  cas_required = true
}

# Creating this secret fails unexepectedly, despite the `cas` field being set.
resource "vault_kv_secret_v2" "example" {
  mount = vault_kv_secret_backend_v2.cas_required.mount

  name      = "my_secret"
  cas       = 0
  data_json = jsonencode({ my_key = "my_value" })
}

Expected Behavior

When creating a secret in an engine with cas_required set to true, I expected a vault_kv_secret_v2 resource with the cas field set to be created successfully.

Actual Behavior

The vault_kv_secret_v2 resource's creation fails with an error saying the check-and-set parameter is missing.

Steps to Reproduce

  1. Deploy a fresh Vault instance:

    docker run \
        --cap-add=IPC_LOCK \
        --detach \
        --publish=8200:8200 \
        --name=vault \
        hashicorp/vault:1.12.2
    
  2. Get the root auth token from startup logs for future use:

    # At startup, Vault logs the following line:
    # Root Token: hvs.0123456789abcdefghijklmno
    vault_token="$(docker logs vault 2>/dev/null | awk '/Root Token:/ { print $3 }')"
    
  3. Write the code above to main.tf.

  4. Run Terraform:

    terraform init
    VAULT_TOKEN="$vault_token" terraform apply
    

    This fails with an error saying that the check-and-set field is unset:

    ╷
    │ Error: error writing secret data to /secret/data/my_secret, err=Error making API request.
    │
    │ URL: PUT http://localhost:8200/v1/secret/data/my_secret
    │ Code: 400. Errors:
    │
    │ * check-and-set parameter required for this call
    │
    │   with vault_kv_secret_v2.example,
    │   on main.tf line 20, in resource "vault_kv_secret_v2" "example":
    │   20: resource "vault_kv_secret_v2" "example" {
    │
    ╵
    

Investigation

Setting TF_LOG=debug and TERRAFORM_VAULT_LOG_REQUEST_BODY=true shows that the provider sends the following payload to Vault:

{
  "cas": 0,
  "data": {
    "my_key": "my_value"
  },
  "options": {}
}

The API docs are ambiguous about the position of the cas field. It turns out that putting the cas field inside the options map works:

{
  "data": {
    "my_key": "my_value"
  },
  "options": {
    "cas": 0
  }
}

This can be accomplished by using the vault_kv_secret_v2 resource's options field instead of the cas field:

resource "vault_kv_secret_v2" "example" {
    mount = vault_kv_secret_backend_v2.cas_required.mount

    name      = "my_secret"
    options   = { cas = 0 }
    data_json = jsonencode({ my_key = "my_value" })
}

With this code, the provider sends the following payload:

{
  "cas": 0,
  "data": {
    "my_key": "my_value"
  },
  "options": {
    "cas": 0
  }
}

This works as expected.

The code responsible

Digging into the provider's implementation, I found where it sets the cas and options fields: right here.

The provider assumes that the cas and options fields are neighbors, and not that the cas field is inside the options map. My hypothesis is that this mistake is due to the documentation's ambiguity, as detailed in this issue.

References

  • https://github.com/hashicorp/vault/issues/18724

busser avatar Jan 16 '23 14:01 busser