Securing OpenTofu State Files with MinIO, Hashicorp Vault, and KES Integration
Table of contents
MinIO Server-Side Encryption (SSE) protects objects as part of write operations, allowing clients to take advantage of server processing power to secure objects at the storage layer (encryption-at-rest). SSE also provides key functionality to regulatory and compliance requirements around secure locking and erasure.
MinIO SSE uses the MinIO Key Encryption Service (KES) and an external Key Management Service (KMS) for performing secured cryptographic operations at scale. MinIO also supports client-managed key management, where the application takes full responsibility for creating and managing encryption keys for use with MinIO SSE.
This tutorial shows how to setup a KES server that uses Vault’s K/V engine as a persistent and secure key store.
In this demo, we use MinIO to store an OpenTofu state file which contains some sensitive data.
$ cd && mkdir encrytion_demo
$ cd encrytion_demo
Hashicorp Vault
$ vault server -dev -dev-listen-address=0.0.0.0:8200
==> Vault server configuration:
Administrative Namespace:
Api Address: http://0.0.0.0:8200
Cgo: disabled
Cluster Address: https://0.0.0.0:8201
Environment Variables: DBUS_SESSION_BUS_ADDRESS, DISPLAY, HOME, HOSTTYPE, LANG, LESSCLOSE, LESSOPEN, LOGNAME, LS_COLORS, NAME, PATH, PULSE_SERVER, PWD, SHELL, SHLVL, TERM, USER, VIRTUALENVWRAPPER_HOOK_DIR, VIRTUALENVWRAPPER_PROJECT_FILENAME, VIRTUALENVWRAPPER_SCRIPT, VIRTUALENVWRAPPER_WORKON_CD, WAYLAND_DISPLAY, WORKON_HOME, WSL2_GUI_APPS_ENABLED, WSLENV, WSL_DISTRO_NAME, WSL_INTEROP, WT_PROFILE_ID, WT_SESSION, XDG_DATA_DIRS, XDG_RUNTIME_DIR, _
Go Version: go1.21.8
Listener 1: tcp (addr: "0.0.0.0:8200", cluster address: "0.0.0.0:8201", disable_request_limiter: "false", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled")
Log Level:
Mlock: supported: true, enabled: false
Recovery Mode: false
Storage: inmem
Version: Vault v1.16.0, built 2024-03-25T12:01:32Z
Version Sha: c20eae3e84c55bf5180ac890b83ee81c9d7ded8b
==> Vault server started! Log data will stream in below:
[...]
You may need to set the following environment variables:
$ export VAULT_ADDR='http://0.0.0.0:8200'
The unseal key and root token are displayed below in case you want to
seal/unseal the Vault or re-authenticate.
Unseal Key: BhtntB6M+S6W/Hv9aE2hmpjhVxE1CG4B/VntMptjkO4=
Root Token: hvs.ZrZqQsOEPw33iIMZxAiZsTRn
Development mode should NOT be used in production installations!
$ export VAULT_ADDR='http://127.0.0.1:8200'
$ export VAULT_TOKEN=hvs.O6QNQB33ksXtMxtlRKlRZL0R
$ vault secrets enable -version=2 kv
$ cat kes-policy.hcl
path "kv/data/*" {
capabilities = [ "create", "read" ]
}
path "kv/metadata/*" {
capabilities = [ "list", "delete" ]
}
$ vault policy write kes-policy kes-policy.hcl
$ vault auth enable approle
$ vault write auth/approle/role/kes-server \
token_num_uses=0 \
secret_id_num_uses=0 \
period=5m
$ vault write auth/approle/role/kes-server \
policies=kes-policy
$ vault read auth/approle/role/kes-server/role-id
$ vault write -f auth/approle/role/kes-server/secret-id
KES (Key Encryption Service)
$ kes identity new \
--key server.key \
--cert server.cert \
--ip "172.21.43.227" \
--dns localhost
Your API key:
kes:v1:ADbNzTdLYBfmdD+pHkj2n+G9zXLbmn2hRVFUpUm2lvxi
This is the only time it is shown. Keep it secret and secure!
Your Identity:
88857114e853938430afdb7bbce39ad643f502fa449c414ebf2dc9cc08bc1706
The identity is not a secret. It can be shared. Any peer
needs this identity in order to verify your API key.
The generated TLS private key is stored at: server.key
The generated TLS certificate is stored at: server.cert
The identity can be computed again via:
kes identity of kes:v1:ADbNzTdLYBfmdD+pHkj2n+G9zXLbmn2hRVFUpUm2lvxi
kes identity of server.cert
$ cat config.yml
admin:
# Use the identity generated above by 'kes identity new'.
identity: "6ef27f0007a4a3cccf9c41bbfec1ffbf5d9fff0af3092e0868f6779c7efab310" # For example: cf6c535e738c1dd47a1d746366fde7f0309d1e0a8471b9f6e909833906afbbfa
tls:
key: server.key # The KES server TLS private key
cert: server.cert # The KES server TLS certificate
keystore:
vault:
endpoint: http://172.21.43.227:8200
version: v2 # The K/V engine version - either "v1" or "v2".
engine: kv # The engine path of the K/V engine. The default is "kv".
approle:
id: "06634c31-6097-bc8f-a8d1-b4f6f4403daa" # Your AppRole ID
secret: "93c428e8-5f17-10c7-f6e6-2195c94f69e2" # Your AppRole Secret
$ kes server --config config.yml
Version 2024-06-17T15-47-05Z commit=12195cc387d860517221548b6297471c92978f68
Runtime go1.22.4 linux/amd64 compiler=gc
License AGPLv3 https://www.gnu.org/licenses/agpl-3.0.html
Copyright MinIO, Inc. 2015-2024 https://min.io
KMS Hashicorp Vault: http://172.21.43.227:8200
API · https://127.0.0.1:7373
· https://10.255.255.254:7373
· https://172.21.43.227:7373
· https://172.18.0.1:7373
· https://172.17.0.1:7373
Docs https://min.io/docs/kes
Admin 6ef27f0007a4a3cccf9c41bbfec1ffbf5d9fff0af3092e0868f6779c7efab310
Logs error=stderr level=INFO
audit=stdout level=INFO
=> Server is up and running...
# $ export MINIO_KMS_KES_ENDPOINT=https://172.21.43.227:7373
# $ export MINIO_KES_API_KEY=kes:v1:ADbNzTdLYBfmdD+pHkj2n+G9zXLbmn2hRVFUpUm2lvxi
# $ kes key create minio-backend-default-key -k
Minio
$ podman run --rm \
-p 9000:9000 -p 9001:9001 \
-v ~/encrytion_demo:/certs \
-e MINIO_KMS_KES_ENDPOINT=https://172.21.43.227:7373 \
-e MINIO_KMS_KES_CAPATH=/certs/server.cert \
-e MINIO_KMS_KES_KEY_NAME=minio-backend-default-key \
-e MINIO_KMS_KES_API_KEY="kes:v1:ADbNzTdLYBfmdD+pHkj2n+G9zXLbmn2hRVFUpUm2lvxi" \
quay.io/minio/minio \
server /data --console-address ":9001"
# Install minio client
$ curl https://dl.min.io/client/mc/release/linux-amd64/mc \
--create-dirs \
-o $HOME/minio-binaries/mc
$ chmod +x $HOME/minio-binaries/mc
$ export PATH=$PATH:$HOME/minio-binaries/
$ mc alias set myminio http://172.21.43.227:9000/ minioadmin minioadmin
$ mc admin info myminio/
● 172.21.43.227:9000
Uptime: 18 minutes
Version: 2024-07-13T01:46:15Z
Network: 1/1 OK
Drives: 1/1 OK
Pool: 1
┌──────┬───────────────────────┬─────────────────────┬──────────────┐
│ Pool │ Drives Usage │ Erasure stripe size │ Erasure sets │
│ 1st │ 0.8% (total: 956 GiB) │ 1 │ 1 │
└──────┴───────────────────────┴─────────────────────┴──────────────┘
0 B Used, 1 Bucket, 0 Objects
1 drive online, 0 drives offline, EC:0
$ mc mb --with-versioning myminio/opentofu
Bucket created successfully `myminio/opentofu`.
$ mc encrypt set sse-kms "minio-backend-default-key" myminio/opentofu
Auto encryption configuration has been set successfully for myminio/opentofu
OpenTofu
$ cat main.tf
terraform {
backend "s3" {
endpoint = "http://172.21.43.227:9000"
key = "terraform.tfstate"
region = "main"
skip_requesting_account_id = true
skip_credentials_validation = true
skip_get_ec2_platforms = true
skip_metadata_api_check = true
skip_region_validation = true
}
}
resource "null_resource" "test" {
}
terraform {
required_providers {
vault = {
source = "hashicorp/vault"
version = "4.3.0"
}
}
}
provider "vault" {
# Configuration options
address = "http://172.21.43.227:8200"
token = "hvs.ZrZqQsOEPw33iIMZxAiZsTRn"
}
resource "vault_mount" "kvv2" {
path = "kvv2"
type = "kv"
options = { version = "2" }
description = "KV Version 2 secret engine mount"
}
resource "vault_kv_secret_v2" "example" {
mount = vault_mount.kvv2.path
name = "secret"
cas = 1
delete_all_versions = true
data_json = jsonencode(
{
zip = var.password,
foo = "bar"
})
}
$ cat variables.tf
variable "password" {
description = "Database administrator password"
type = string
sensitive = true
}
$ export AWS_ACCESS_KEY_ID=minioadmin
$ export AWS_SECRET_ACCESS_KEY=minioadmin
$ export TF_VAR_password=ultrasecurepassword
$ tofu init
Initializing the backend...
Successfully configured the backend "s3"! OpenTofu will automatically
use this backend unless the backend configuration changes.
Initializing provider plugins...
- Finding latest version of hashicorp/null...
- Finding hashicorp/vault versions matching "4.3.0"...
- Installing hashicorp/null v3.2.2...
- Installed hashicorp/null v3.2.2 (signed, key ID 0C0AF313E5FD9F80)
- Installing hashicorp/vault v4.3.0...
- Installed hashicorp/vault v4.3.0 (signed, key ID 0C0AF313E5FD9F80)
Providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://opentofu.org/docs/cli/plugins/signing/
OpenTofu has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that OpenTofu can guarantee to make the same selections by default when
you run "tofu init" in the future.
OpenTofu has been successfully initialized!
You may now begin working with OpenTofu. Try running "tofu plan" to see
any changes that are required for your infrastructure. All OpenTofu commands
should now work.
If you ever set or change modules or backend configuration for OpenTofu,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
$ tofu plan
OpenTofu used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
OpenTofu will perform the following actions:
# null_resource.test will be created
+ resource "null_resource" "test" {
+ id = (known after apply)
}
# vault_kv_secret_v2.example will be created
+ resource "vault_kv_secret_v2" "example" {
+ cas = 1
+ data = (sensitive value)
+ data_json = (sensitive value)
+ delete_all_versions = true
+ disable_read = false
+ id = (known after apply)
+ metadata = (known after apply)
+ mount = "kvv2"
+ name = "secret"
+ path = (known after apply)
}
# vault_mount.kvv2 will be created
+ resource "vault_mount" "kvv2" {
+ accessor = (known after apply)
+ audit_non_hmac_request_keys = (known after apply)
+ audit_non_hmac_response_keys = (known after apply)
+ default_lease_ttl_seconds = (known after apply)
+ description = "KV Version 2 secret engine mount"
+ external_entropy_access = false
+ id = (known after apply)
+ max_lease_ttl_seconds = (known after apply)
+ options = {
+ "version" = "2"
}
+ path = "kvv2"
+ seal_wrap = (known after apply)
+ type = "kv"
}
Plan: 3 to add, 0 to change, 0 to destroy.
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so OpenTofu can't guarantee to take exactly these actions if you run "tofu apply" now.
$ tofu apply -auto-approve
OpenTofu used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
OpenTofu will perform the following actions:
# null_resource.test will be created
+ resource "null_resource" "test" {
+ id = (known after apply)
}
# vault_kv_secret_v2.example will be created
+ resource "vault_kv_secret_v2" "example" {
+ cas = 1
+ data = (sensitive value)
+ data_json = (sensitive value)
+ delete_all_versions = true
+ disable_read = false
+ id = (known after apply)
+ metadata = (known after apply)
+ mount = "kvv2"
+ name = "secret"
+ path = (known after apply)
}
# vault_mount.kvv2 will be created
+ resource "vault_mount" "kvv2" {
+ accessor = (known after apply)
+ audit_non_hmac_request_keys = (known after apply)
+ audit_non_hmac_response_keys = (known after apply)
+ default_lease_ttl_seconds = (known after apply)
+ description = "KV Version 2 secret engine mount"
+ external_entropy_access = false
+ id = (known after apply)
+ max_lease_ttl_seconds = (known after apply)
+ options = {
+ "version" = "2"
}
+ path = "kvv2"
+ seal_wrap = (known after apply)
+ type = "kv"
}
Plan: 3 to add, 0 to change, 0 to destroy.
vault_mount.kvv2: Creating...
null_resource.test: Creating...
null_resource.test: Creation complete after 0s [id=6786809516705012606]
vault_mount.kvv2: Creation complete after 0s [id=kvv2]
vault_kv_secret_v2.example: Creating...
vault_kv_secret_v2.example: Creation complete after 0s [id=kvv2/data/secret]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
🔗 https://min.io/docs/kes/integrations/hashicorp-vault-keystore/
🔗 https://min.io/docs/minio/linux/reference/minio-mc.html
🔗 https://opentofu.org/docs/language/settings/backends/s3/
Subscribe to my newsletter
Read articles from Bruno directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Bruno
Bruno
Depuis août 2024, j'accompagne divers projets sur l'optimisation des processus DevOps. Ces compétences, acquises par plusieurs années d'expérience dans le domaine de l'IT, me permettent de contribuer de manière significative à la réussite et l'évolution des infrastructures de mes clients. Mon but est d'apporter une expertise technique pour soutenir la mission et les valeurs de mes clients, en garantissant la scalabilité et l'efficacité de leurs services IT.