Use OCI Terraform Providers to Create Secrets in a Vault

In the past few posts I looked at using the Oracle Cloud Infrastructure (OCI) services related to creating database connections in the cloud. Here I want to show how you can build on that by creating secrets in a vault using the OCI provider.

💡
This post is not prescriptive. It is just one way to accomplish the goal of getting a password and some wallet data into a vault in OCI. Bootstrapping a database in the cloud can mean many different things.

Before we dive in I would like to set some expectations about pre-requisites. The post assumes the following already exist:

  • An OCI tenancy with requisite IAM policies to allow management resources in a compartment
  • An OCI vault already with a master key
  • Terraform or a compatible frontend, such as OpenTofu, is installed

Although you should technically be able to follow along using a remote backend or "Terraform as a services" offerings, keep in mind that some concepts described below may not be applicable if you don't have access to use local_file or local-exec. In those cases you will need to find a workaround such as using Object Storage and Functions as a Service.

Here is what we will create below:

  • A random password (for generating a wallet)
  • A random password for the ADMIN account of a new Autonomous Database
  • A secret with the contents of cwallet.sso extracted from the wallet ZIP file
  • A new Database Tools connection using the above resources

Create a Random Password

First we will create a random password using the random provider from HashiCorp and save it as a secret in a vault. First you will need to add the required providers.

terraform {
  ...
  required_providers {
    oci = {
      source  = "oracle/oci"
      version = ">= 6.26.0"
    }
    random = {
      source  = "hashicorp/random"
      version = ">= 3.6.0"
    }
  }
}

After you add new providers you need to re-initialize your project to download the providers. You can learn more about that here, as needed:

terraform init command reference | Terraform | HashiCorp Developer
The `terraform init` command initializes a working directory containing configuration files and installs plugins for required providers.

If you are using OpenTofu you can find those details here.

💡
Providers that connect to cloud services, such as OCI, generally need to be configured before they will work. For this example I use a provider block that tells Terraform how to authenticate my requests to OCI. You can learn more about configuring the OCI provider here.

Now that the random and oci providers are installed and configured (in the case of OCI) we can generate a secret in an existing vault in the tenancy.

resource "random_password" "db_wallet_password" {
  length      = 16
  special     = true
  min_numeric = 1
  min_lower   = 1
  min_upper   = 1
  min_special = 1
}

resource "oci_vault_secret" "wallet_password_secret" {
  compartment_id = <your.target.compartment.id>
  secret_name    = "<your secret name>"
  vault_id       = <your.oci_kms_vault.vault.id>
  key_id         = <your.oci_kms_key.vault_key.id>
  secret_content {
    content_type = "BASE64"
    content = sensitive(base64encode(random_password.db_wallet_password.result))
  }
}

Using sensitive(...) above in this context is redundant since we are using a random_password resource. This shows how you can mark a value to be obscured from your plan and apply logs, or if you happen to use random_string instead to achieve a similar result.

Note: sensitive(...) does not encrypt the password, it obscures it.

💡
Whether using data blocks to query the values -or- creating the vault and key in the same configuration, you need to replace values in the above example such as <your...> with relevant details.

And that's it for this part. After you plan and apply this configuration you will have a new secret in your vault with a randomly generated password.

I don't personally recommend using this approach for anything truly sensitive (like the administrator password for a database) without some additional access controls. The reason is because these values will be cached in your state.

For a random wallet password this approach works fine.

Create a Random Password for ADMIN

For the next step we will use the Secret in Vault service to generate a random password for us that will be saved directly in the vault. This approach has the advantage that only the opaque identifier of the secret is cached in state. There are other benefits to using the vault resource but I'll keep this simple.

resource "oci_vault_secret" "adb_admin_password_secret" {
  compartment_id = <your.target.compartment.id>
  secret_name    = "<your secret name>"
  vault_id       = <your.oci_kms_vault.vault.id>
  key_id         = <your.oci_kms_key.vault_key.id>

  enable_auto_generation = true

  secret_generation_context {
    generation_type     = "PASSPHRASE"
    generation_template = "DBAAS_DEFAULT_PASSWORD"
    passphrase_length   = 14
  }
}

As before, you should replace the <your...> placeholders with relevant details. Once you plan and apply this configuration you will have a new secret in your vault with a randomly generated password, the value of which is not cached in your state.

💡
The feature of oci_vault_secret that allows the enable_auto_generation property to be set is not brand new but it is relatively new. If you get errors from terraform about unexpected properties check that your OCI provider version is up to date. In this post I am using >= 6.26.0

That's all there is to creating the random ADMIN password. For this example, we will use the vault secret to set the ADMIN password for a newly created Autonomous Database. The the ADB service will read our secret from the vault when the ADB instance is created instead of using a hardcoded password.

resource "oci_database_autonomous_database" "adb_database" {
  compartment_id = <your.target.compartment.id>
  display_name   = "<your display name>"
  db_name        = "<your db name>"
  secret_id      = oci_vault_secret.adb_admin_password_secret.id

  db_workload = "OLTP"
  db_version  = "23ai"

  is_free_tier = true
}

Notice this is just a sample ADB configuration. Your ADB workload should include whatever parameters you require. The important part of the above example is the use of secret_id to supply the initial database password. Also notice that we are referencing the vault secret resource we created just above.

It is worth noting there are also pre-built functions (PBFs) that can help with ADB secret rotation.

At this point you have seen how to create secrets in a vault using Terraform. For extra credit we will set up Database Tools connections using the above secrets combined with one more secret that is required, the SSO wallet secret.

Create a Wallet Secret

At the time of this writing the OCI provider for ADB resources does not support reading the cwallet.sso file directly. Instead we download the entire regional or instance wallet ZIP file and extract the bytes we need.

First we will update our configuration to add the local provider from HashiCorp.

terraform {
  ...
  required_providers {
    ...
    local = {
      source  = "hashicorp/local"
      version = ">= 2.5.0"
    }
  }
}

Remember to re-initialize your project to download this provider as needed!

If your use case doesn't supportlocal_file or the local-exec provisioner then you will need to find some other workaround for extracting a file from a ZIP archive and storing it in a secret. OCI Functions as a Service may help.

First, we add the ADB wallet resource to our configuration to generate the wallet from our ADB database, created just above.

resource "oci_database_autonomous_database_wallet" "wallet" {
  autonomous_database_id = oci_database_autonomous_database.adb_database.id
  password               = random_password.db_wallet_password.result
  base64_encode_content  = "true"
}

Notice that we are referencing the ADB database and the random_password we generated before. Adjust your configuration as needed if not following along. Next we will extract the ADB wallet ZIP file to the local file system.

locals {
  wallet_name = "wallet_${oci_database_autonomous_database.adb_database.db_name}.zip"
  target_path = "${path.module}/.wallet"
  wallet_path = "${local.target_path}/${local.wallet_name}"
}

resource "local_sensitive_file" "db_wallet_zip" {
  content_base64 = oci_database_autonomous_database_wallet.wallet.content
  filename       = local.wallet_path
}

Once you re-run plan and apply you should now have a ZIP file locally that contains the ADB wallet. For Database Tools connections we normally use the cwallet.sso file but you can also use the Java key stores.

💡
Notice in the locals block above that a file name and paths are declared. In this example the wallet file will be downloaded and extracted inside of a folder called .wallet/temp under the project where I am running terraform apply. If you want the file saved elsewhere or to have a different name, adjust your config as needed.

Next we need to extract the contents of the ZIP file. I tested this against Windows 10 as well as Ubuntu 24.04 but your mileage may vary. Update your command parameter of local-exec as needed if you run into problems or if you need to use a different utility to extract the contents of a ZIP file.

resource "terraform_data" "extract_wallet" {
  provisioner "local-exec" {
    command = "unzip -o -u ${local.wallet_path} -d ${local.target_path}/temp"
  }
  depends_on = [
    local_sensitive_file.db_wallet_zip,
  ]
}

data "local_sensitive_file" "cwallet_sso" {
  filename = "${local.target_path}/temp/cwallet.sso"
  depends_on = [
    terraform_data.extract_wallet,
  ]
}

I should mention at this point that this example of using local files in your Terraform configuration can make things difficult to deal with, especially if you are working in a collaborative environment. Be aware that if you use this approach for anything other than a simple set up you will have more design challenges to figure out regarding how you manage these local file resources.

n.b. If I figure out a better way in the future, I will update this post.

Hopefully someday we will be able to use a data provider to get the SSO wallet directly from ADB. Now that we have extracted the ADB wallet ZIP files we can upload the cwallet.sso file to a new secret in the vault.

resource "oci_vault_secret" "sso_wallet_secret" {
  compartment_id = <your.target.compartment.id>
  secret_name    = "<your secret name>"
  vault_id       = <your.oci_kms_vault.vault.id>
  key_id         = <your.oci_kms_key.vault_key.id>
  secret_content {
    content_type = "BASE64"
    content = sensitive(data.local_sensitive_file.cwallet_sso.content_base64)
  }
}

As before, replace the relevant values for your own vault and set a secret name. Once you rerun plan and apply you should have a new secret in your vault with the SSO wallet contents.

Sometimes you can use sensitive(...) simply as a trick to cleanup the plan and apply logs. I don't personally care to see the noise of Base64 file contents dumped to my logs. It is probably redundant in this case but something useful to keep in mind.

Create a Database Tools Connection

Now that we have automated the creation of all of required secrets in our vault, its time to create a Database Tools connection that references those secrets.

Just for fun I am also showing one way to extract connection strings from an ADB resource to automatically create a connection for each profile. You can of course just create one with a specific connection string if you prefer.

If you followed along from above and you created the ADB instance and secrets as shown then you should be able to do the following:

locals {
  all_profiles = {
    for p in oci_database_autonomous_database.adb_database.connection_strings[0].profiles :
    lower(p.consumer_group) => p.value
  }
  connection_strings = {
    low = local.all_profiles.low
    medium = local.all_profiles.medium
    high = local.all_profiles.high
  }
}

resource "oci_database_tools_database_tools_connection" "adb_connection" {
  for_each = local.connection_strings
  compartment_id = <your.target.compartment.id>
  display_name = "wadb_admin_conn_${each.key}_dev"
  type = "ORACLE_DATABASE"

  related_resource {
    entity_type = "AUTONOMOUSDATABASE"
    identifier = oci_database_autonomous_database.adb_database.id
  }

  user_name = "ADMIN"
  user_password {
    value_type = "SECRETID"
    secret_id = oci_vault_secret.adb_admin_password_secret.id
  }

  connection_string = each.value

  key_stores {
    key_store_type = "SSO"
    key_store_content {
      value_type = "SECRETID"
      secret_id = oci_vault_secret.sso_wallet_secret.id
    }
  }
}

As before, make sure you define relevant values for <your... parameters and update the display_name as you see fit.

Once you plan and apply your configuration you should now have three Database Tools connections in your target compartment. Each will be setup referencing the secrets created above, as well as the connection string for each ADB profile.

Example showing three OCI Database Tools connections created using Terraform
Example using one of the Database Tools connections with SQL Developer Web

I hope you found this useful (or at least informative) and that it helps you on your journey using OCI and Terraform. Thanks for reading and I'll see you next time!

Cheers.