diff --git a/.github/workflows/bootstrap.yml b/.github/workflows/bootstrap.yml index e57f438..4002f52 100644 --- a/.github/workflows/bootstrap.yml +++ b/.github/workflows/bootstrap.yml @@ -6,15 +6,8 @@ on: jobs: bootstrap: runs-on: ubuntu-latest - steps: - uses: hashicorp/setup-terraform@v3 - - uses: actions/checkout@v4 - - - uses: nick-fields/retry@v2 - with: - timeout_minutes: 10 - max_attempts: 3 - shell: bash - command: make bootstrap + - run: make bootstrap + - run: make teardown diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl index 4790096..1b68806 100644 --- a/.terraform.lock.hcl +++ b/.terraform.lock.hcl @@ -32,6 +32,26 @@ provider "registry.terraform.io/gavinbunney/kubectl" { ] } +provider "registry.terraform.io/hashicorp/boundary" { + version = "1.1.13" + constraints = "1.1.13" + hashes = [ + "h1:aAuYsLXB+MiLGHW4krsOD9K0MHoc+1OPGJOEokETBxA=", + "zh:3197dbcf6e78908dab24cce7ed9982d378d91ad1ab003ccc819ba2a41d0f0837", + "zh:4af8fd2efd24d23777324c2ef13ce9a008eb4c616ceb71e7574f486cf098da35", + "zh:5fd9dd22b5b45e6d5961aea698fa6b69951d27f3f4c29fb1906df7be0720c53e", + "zh:63ecc78590ca9825abcccf8cf4cb474a06f94ae8f34ee6d1e9f723aab6577cee", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:84431995b5c765aaf6673407bf1209e3938fc19960eccbb41e7838579433182d", + "zh:bb0e148845c87dde168a7d6873be5ed3472d692bb4c1d07c286f5cb6eb5101cb", + "zh:c7bfee2e0bf02d33ba1fcddae012206ef5eb2bc3507543a9bbbdad0e3a66ec47", + "zh:d27d7e1d5895fe74370ae50e1c88beaa1de5b7e5c4bc9751d9c0c7121a61a3ac", + "zh:d7596b58460f4d90c59de6e47559dad11df64834346cf08f5e2d67f2e8a7a295", + "zh:e11440c92db44f62067a7e556df5bfb783eb361841ecef5d6fc8c1fbb83f6d44", + "zh:ea118a0d4c9db95e13b430978ca625dae295ea8f64d9a6a042f53633bd8bc6a3", + ] +} + provider "registry.terraform.io/hashicorp/helm" { version = "2.12.1" constraints = "2.12.1" diff --git a/Makefile b/Makefile index de705d7..fce60a0 100644 --- a/Makefile +++ b/Makefile @@ -12,8 +12,9 @@ fmt: ## fmt .PHONY: bootstrap bootstrap: deps ## boostrap cluster - source .envrc + source .envrc terraform init + terraform apply -target=module.boundary -auto-approve terraform apply -auto-approve .PHONY: teardown @@ -36,8 +37,9 @@ cleanup: ## cleanup docker rm $(shell docker ps -aq) || true docker network rm vault || true - rm terraform.tfstate || true - rm terraform.tfstate.backup || true + minikube delete || true + + rm terraform.tfstate terraform.tfstate.backup || true .PHONY: new-lab new-lab: ## creates a new lab directory diff --git a/README.md b/README.md index 9371865..6ce93c0 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,14 @@ Please refer to the [documentation](https://falcosuessgott.github.io/hashicorp-v * [x] [Vault Agent Injector](https://falcosuessgott.github.io/hashicorp-vault-playground/vai/) * [x] [CSI Driver](https://falcosuessgott.github.io/hashicorp-vault-playground/csi/) * [x] [Certmanager](https://falcosuessgott.github.io/hashicorp-vault-playground/cm/) +* [x] [Kubernetes Secret Method](https://falcosuessgott.github.io/hashicorp-vault-playground/boundary/) ### MySQL Dynamic DB Credentials * [x] [MySQL dynamic DB Credentials](https://falcosuessgott.github.io/hashicorp-vault-playground/databases/) +### Boundary +* [x] [Kubernetes Control Plane Access](https://falcosuessgott.github.io/hashicorp-vault-playground/boundary/) +* [ ] SSH Access + ### ToDos * [ ] Prometheus & Grafana + Vault Metrics -* [ ] Boundary & (kubectl acccess, SSH) diff --git a/boundary-config/files/vault-boundary-k8s.hcl b/boundary-config/files/vault-boundary-k8s.hcl new file mode 100644 index 0000000..7d63eb1 --- /dev/null +++ b/boundary-config/files/vault-boundary-k8s.hcl @@ -0,0 +1,27 @@ +path "auth/token/lookup-self" { + capabilities = ["read"] +} + +path "auth/token/renew-self" { + capabilities = ["update"] +} + +path "auth/token/revoke-self" { + capabilities = ["update"] +} + +path "sys/leases/renew" { + capabilities = ["update"] +} + +path "sys/leases/revoke" { + capabilities = ["update"] +} + +path "sys/capabilities-self" { + capabilities = ["update"] +} + +path "minikube/creds/minikube" { + capabilities = ["update"] +} diff --git a/boundary-config/terraform/main.tf b/boundary-config/terraform/main.tf new file mode 100644 index 0000000..4a14052 --- /dev/null +++ b/boundary-config/terraform/main.tf @@ -0,0 +1,174 @@ +# The global scope is the outermost scope. There is always a single global scope and it cannot be deleted. The global scope can directly contain: users, groups, auth methods, and organizations. +resource "boundary_scope" "org" { + scope_id = "global" + name = "playground" + description = "Vault Playground" + + auto_create_admin_role = false + auto_create_default_role = false +} + +# A project is a type of scope used to organize resources such as targets and host catalogs. +resource "boundary_scope" "project" { + name = "minikube" + description = "Local Minikube Cluster" + scope_id = boundary_scope.org.id + auto_create_admin_role = false + auto_create_default_role = false +} + +# Auth methods allow users to authenticate within a scope. +resource "boundary_auth_method" "password" { + name = "basic" + description = "Password auth method" + type = "password" + scope_id = boundary_scope.org.id +} + +resource "boundary_account_password" "admin" { + name = "admin" + description = "Local Admininistrator Account" + login_name = "admin" + password = "password" + auth_method_id = boundary_auth_method.password.id +} + +# Users are entities authorized to access Boundary. Users may be assigned to roles as principals, thus receiving role grants. +resource "boundary_user" "admin" { + name = boundary_account_password.admin.name + account_ids = [boundary_account_password.admin.id] + scope_id = boundary_scope.org.id +} + +# Roles are collections of capability grants and the principals (users and groups) assigned to them. +resource "boundary_role" "global_anon_listing" { + name = "Global Anon Listing" + scope_id = boundary_scope.org.id + grant_strings = [ + "ids=*;type=auth-method;actions=list,authenticate", + "ids=*;type=scope;actions=list,no-op", + "ids={{.Account.Id}};actions=read,change-password" + ] + principal_ids = ["u_anon"] +} + +# Roles are collections of capability grants and the principals (users and groups) assigned to them. +resource "boundary_role" "org_anon_listing" { + name = "Org Anon Listing" + scope_id = boundary_scope.org.id + grant_strings = [ + "ids=*;type=auth-method;actions=list,authenticate", + "type=scope;actions=list", + "ids={{.Account.Id}};actions=read,change-password" + ] + principal_ids = ["u_anon"] +} + +# Roles are collections of capability grants and the principals (users and groups) assigned to them. +resource "boundary_role" "org_admin" { + name = "Org Admin" + scope_id = "global" + grant_scope_id = boundary_scope.org.id + grant_strings = [ + "ids=*;type=*;actions=*" + ] + principal_ids = [boundary_user.admin.id] +} + +# Roles are collections of capability grants and the principals (users and groups) assigned to them. +resource "boundary_role" "project_admin" { + name = "Project Admin" + scope_id = boundary_scope.org.id + grant_scope_id = boundary_scope.project.id + grant_strings = [ + "ids=*;type=*;actions=*" + ] + principal_ids = [boundary_user.admin.id] +} + +resource "vault_policy" "boundary" { + name = "boundary-minikube" + + policy = file("${path.module}/../files/vault-boundary-k8s.hcl") +} + +resource "vault_token" "boundary" { + policies = [vault_policy.boundary.name] + + renewable = true + no_parent = true + period = "24h" +} + +# A credential store is a collection of credentials and credential libraries. +resource "boundary_credential_store_vault" "this" { + name = "Vault" + description = "Local HashiCorp Vault Cluster" + address = "https://host.docker.internal:443" + token = vault_token.boundary.client_token + scope_id = boundary_scope.project.id + + ca_cert = try(file("${path.root}/vault-tls/output/ca.crt"), null) +} + +# A credential library is a resource that provides credentials. +resource "boundary_credential_library_vault" "this" { + name = "minikube" + description = "Credentials for Minikube Cluster" + credential_store_id = boundary_credential_store_vault.this.id + path = "minikube/creds/minikube" + + http_method = "POST" + http_request_body = jsonencode({ + kubernetes_namespace = "default" + }) +} + +# A host catalog is a collection of hosts and host sets. +resource "boundary_host_catalog_static" "this" { + name = "Minikube" + description = "Minikube Cluster Controlplane" + scope_id = boundary_scope.project.id +} + +# A host is a resource that may be accessed by a Boundary target. +resource "boundary_host_static" "minikube" { + name = "minikube" + description = "Minikube API" + address = var.minikube_ip + + host_catalog_id = boundary_host_catalog_static.this.id +} + +# A host set is a collection of hosts within a host catalog. +resource "boundary_host_set_static" "this" { + host_catalog_id = boundary_host_catalog_static.this.id + host_ids = [boundary_host_static.minikube.id] + +} +# A target is a logical collection of host sets which may be used to initiate sessions. +resource "boundary_target" "this" { + name = "minikube" + description = "Minikube Target" + type = "tcp" + default_port = "443" + + scope_id = boundary_scope.project.id + + host_source_ids = [boundary_host_set_static.this.id] + brokered_credential_source_ids = [boundary_credential_library_vault.this.id] +} + +resource "boundary_role" "minikube" { + name = "minikube" + description = "Minikube Role" + scope_id = boundary_scope.org.id + + grant_scope_id = boundary_scope.project.id + grant_strings = [ + "ids=*;type=target;actions=list,no-op", + "ids=${boundary_target.this.id};actions=authorize-session" + ] + + principal_ids = [boundary_user.admin.id] +} diff --git a/boundary-config/terraform/variables.tf b/boundary-config/terraform/variables.tf new file mode 100644 index 0000000..e5bcf4d --- /dev/null +++ b/boundary-config/terraform/variables.tf @@ -0,0 +1,3 @@ +variable "minikube_ip" { + type = string +} diff --git a/boundary-config/terraform/version.tf b/boundary-config/terraform/version.tf new file mode 100644 index 0000000..30dc18b --- /dev/null +++ b/boundary-config/terraform/version.tf @@ -0,0 +1,22 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + vault = { + source = "hashicorp/vault" + version = "3.24.0" + } + docker = { + source = "kreuzwerker/docker" + version = "3.0.2" + } + boundary = { + source = "hashicorp/boundary" + version = "1.1.13" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "2.25.2" + } + } +} diff --git a/boundary/files/config.hcl b/boundary/files/config.hcl new file mode 100644 index 0000000..a91f76f --- /dev/null +++ b/boundary/files/config.hcl @@ -0,0 +1,69 @@ +# Disable memory lock: https://www.man7.org/linux/man-pages/man2/mlock.2.html +disable_mlock = true + +# Controller configuration block +controller { + name = "default" + description = "Boundary Default Controller" + + database { + url = "postgresql://postgres:postgres@postgres:5432/postgres?sslmode=disable" + max_open_connections = 5 + } +} + +worker { + name = "localhost worker" + description = "boundary localhost worker" + public_addr = "127.0.0.1" +} + +listener "tcp" { + # Should be the address of the NIC that the controller server will be reached on + address = "0.0.0.0" + purpose = "api" + tls_disable = true +} + +listener "tcp" { + # Should be the IP of the NIC that the worker will connect on + address = "boundary" + purpose = "cluster" + tls_disable = true +} + +listener "tcp" { + address = "boundary" + purpose = "proxy" + tls_disable = true +} + +# Root KMS configuration block: this is the root key for Boundary +kms "transit" { + purpose = "root" + address = "https://host.docker.internal:443" + disable_renewal = "false" + key_name = "boundary_root" + mount_path = "boundary/" + tls_ca_cert = "/opt/tls/ca.crt" +} + +# Recovery KMS block: configures the recovery key for Boundary +kms "transit" { + purpose = "recovery" + address = "https://host.docker.internal:443" + disable_renewal = "false" + key_name = "boundary_recovery" + mount_path = "boundary/" + tls_ca_cert = "/opt/tls/ca.crt" +} + +# Worker authorization KMS +kms "transit" { + purpose = "worker-auth" + address = "https://host.docker.internal:443" + disable_renewal = "false" + key_name = "boundary_worker" + mount_path = "boundary/" + tls_ca_cert = "/opt/tls/ca.crt" +} diff --git a/boundary/files/vault-policy.hcl b/boundary/files/vault-policy.hcl new file mode 100644 index 0000000..b55f0a1 --- /dev/null +++ b/boundary/files/vault-policy.hcl @@ -0,0 +1,7 @@ +path "boundary/encrypt/boundary_*" { + capabilities = [ "create", "update" ] +} + +path "boundary/decrypt/boundary_*" { + capabilities = [ "create", "update" ] +} diff --git a/boundary/terraform/boundary.tf b/boundary/terraform/boundary.tf new file mode 100644 index 0000000..348321c --- /dev/null +++ b/boundary/terraform/boundary.tf @@ -0,0 +1,61 @@ +resource "docker_container" "boundary" { + name = "boundary" + image = "hashicorp/boundary:0.15" + + env = [ + "VAULT_TOKEN=${vault_token.this.client_token}" + ] + + capabilities { + add = [ + "IPC_LOCK", + ] + } + + ports { + internal = 9200 + external = 9200 + ip = "0.0.0.0" + } + + ports { + internal = 9201 + external = 9201 + ip = "0.0.0.0" + } + + ports { + internal = 9202 + external = 9202 + ip = "0.0.0.0" + } + + volumes { + host_path = abspath("${path.root}/vault-tls/output") + container_path = "/opt/tls/" + read_only = true + } + + volumes { + host_path = abspath("${path.module}/../files/config.hcl") + container_path = "/boundary/config.hcl" + read_only = true + } + + command = ["server", "-config", "/boundary/config.hcl"] + + host { + host = "host.docker.internal" + ip = "host-gateway" + } + + networks_advanced { + name = data.docker_network.vault.name + } + + lifecycle { + ignore_changes = all + } + + depends_on = [terraform_data.wait_for] +} diff --git a/boundary/terraform/postgres.tf b/boundary/terraform/postgres.tf new file mode 100644 index 0000000..db10070 --- /dev/null +++ b/boundary/terraform/postgres.tf @@ -0,0 +1,87 @@ +data "docker_network" "vault" { + name = "vault" +} + +resource "docker_container" "postgres" { + name = "postgres" + image = "postgres:12.17" + + env = [ + "POSTGRES_PASSWORD=postgres", + "POSTGRES_USER=postgres", + ] + + volumes { + host_path = abspath("${path.root}/vault-tls/output") + container_path = "/opt/tls/" + read_only = true + } + + networks_advanced { + name = data.docker_network.vault.name + } + + lifecycle { + ignore_changes = all + } +} + +# improve this, so that the container is not created every apply +resource "docker_container" "db_init" { + name = "boundary_db_init" + image = "hashicorp/boundary:0.15" + + env = [ + "BOUNDARY_POSTGRES_URL=postgresql://postgres:postgres@${docker_container.postgres.name}:5432/postgres?sslmode=disable", + "VAULT_TOKEN=${vault_token.this.client_token}" + ] + + capabilities { + add = [ + "IPC_LOCK", + ] + } + + volumes { + host_path = abspath("${path.root}/vault-tls/output") + container_path = "/opt/tls/" + read_only = true + } + + volumes { + host_path = abspath("${path.module}/../files/config.hcl") + container_path = "/boundary/config.hcl" + read_only = true + } + + command = ["database", "init", + "-skip-auth-method-creation", "-skip-host-resources-creation", + "-skip-scopes-creation", "-skip-target-creation", + "-config", "/boundary/config.hcl" + ] + + # allow vault access localhost + host { + host = "host.docker.internal" + ip = "host-gateway" + } + + networks_advanced { + name = data.docker_network.vault.name + } + + lifecycle { + ignore_changes = all + } +} + +resource "terraform_data" "wait_for" { + provisioner "local-exec" { + command = < https://www.hashicorp.com/blog/how-to-connect-to-kubernetes-clusters-using-boundary + +## Description + +* In thise use case a user requests kubernetes cluster credentials (token) for the local running minikube cluster +* Boundary then issues credentials utilizing Vaults Kubernetes Secret Engine +* the Token is only allowed to list pods in namespace default, as configured in the Vault Kubernetes Secret Engine +* the user receives a service account token and can then use `kubectl` + +## Requirements +For this lab youre going to need `kubectl` and the [Boundary Desktop Application](https://developer.hashicorp.com/boundary/tutorials/oss-getting-started/oss-getting-started-desktop-app) on your system. + +Also in your `terraform.tfvars`: + +```yaml +# terraform.tfvars +boundary = { + enabled = true +} + +kubernetes = { + enabled = true +} +``` + +You then can bootstrap the cluster using + +```bash +make bootstrap +``` + +## Overview + +* Vault is used as Boundarys KMS Server using Vaults Transit Engine ([https://localhost/ui/vault/secrets/transit/list](https://localhost/ui/vault/secrets/transit/list)) +* Vaults Secret Engine is configured for minikube for creating SAs that are allowed to LIST pods in the default namespace: + +```json +# https://localhost/ui/vault/secrets/minikube/kubernetes/roles/minikube/details +{"rules":[{"apiGroups":[""],"resources":["pods"],"verbs":["list"]}]} +``` + +* a global organization: `playground` +* a projet: `minikube` +* an basic auth method `basic` with an admin account: `admin:password` +* an Admin Role, so the admin account can edit/view everything globally and project wide +* Vault is added as a Credential Store, and has received a proper token + policy +* Vault is used a a Credentials Library for Kubernetes SA +* A Host Catalog `minikube` has been created, containing the Minikubes API Server as a Host +* A Target `minikube` has been created, specifiyin the connection (tcp, port 443, ...) +* A Role for that target has been created and added to the admin user + +## Walkthrough +* Start Boundary Desktop Application +* Connect to the local running boundary +* Choose the `minikube` Target and Click `connect` + +```bash +# create any pod in default namespace +$> kubectl run nginx --image nginx + +# create a new context +$> kubectl config set-context empty && kubectl config use-context empty + +# connect with the received SA token +$> kubectl get pod --insecure-skip-tls-verify --server=https://127.0.0.1:8443 --token= +``` diff --git a/docs/home.md b/docs/home.md index 83a46ad..b9f3456 100644 --- a/docs/home.md +++ b/docs/home.md @@ -21,10 +21,15 @@ Bootstrap a local Vault HA Cluster with many useful learning labs in under a min * [x] [Vault Agent Injector](https://falcosuessgott.github.io/hashicorp-vault-playground/vai/) * [x] [CSI Driver](https://falcosuessgott.github.io/hashicorp-vault-playground/csi/) * [x] [Certmanager](https://falcosuessgott.github.io/hashicorp-vault-playground/cm/) +* [x] [Kubernetes Secret Method](https://falcosuessgott.github.io/hashicorp-vault-playground/boundary/) ### MySQL Dynamic DB Credentials * [x] [MySQL dynamic DB Credentials](https://falcosuessgott.github.io/hashicorp-vault-playground/databases/) +### Boundary +* [x] [Kubernetes Control Plane Access](https://falcosuessgott.github.io/hashicorp-vault-playground/boundary/) +* [ ] SSH Access + ### ToDos * [ ] Prometheus & Grafana + Vault Metrics * [ ] Boundary & (kubectl acccess, SSH) diff --git a/docs/kms.md b/docs/kms.md index 4b2a7d7..0e357d3 100644 --- a/docs/kms.md +++ b/docs/kms.md @@ -49,15 +49,15 @@ $> kubectl -n kube-system exec etcd-vault-playground -- sh -c "ETCDCTL_API=3 etc 00000116 ``` -### Deploy Trousseau +### Deploy Trousseau ```bash # troussea has been deployed as a daemon set in kube-system namespace -$> kubectl get ds -n kube-system +$> kubectl get ds -n kube-system NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE kube-proxy 1 1 1 1 1 kubernetes.io/os=linux 116s trousseau-kms-provider 1 1 1 1 1 46s -# a config map has been mounted into the daemon set, specifying the vault server and the vault token +# a config map has been mounted into the daemon set, specifying the vault server and the vault token $> kubectl describe cm trousseau-config -n kube-system Name: trousseau-config Namespace: kube-system @@ -75,7 +75,7 @@ vault: address: https://host.minikube.internal token: hvs.CAESIJGPZdckGe6vN3-bMUzBmT3XywsQ8eNMWZljladJKsszGh4KHGh2cy5Tb3dpQjNjOEJuWHM2cVk2anhNcWtFSEQ # periodic & orphan token -# Troussea creates a unix socket on the minikube host +# Troussea creates a unix socket on the minikube host $> minikube ssh "ls -la /opt/trousseau-kms" vaultkms.socket ``` @@ -104,7 +104,7 @@ storage-provisioner 1/1 Running 3 (36s ago) 5m13s trousseau-kms-provider-jrflz 1/1 Running 0 4m28s ``` -### Verify Secrets are now encrypted +### Verify Secrets are now encrypted ```bash # create any secret $> kubectl create secret generic secret-post-deploy -n default --from-literal=key=value @@ -139,4 +139,4 @@ $> kubectl -n kube-system exec etcd-vault-playground -- sh -c "ETCDCTL_API=3 etc ### Encrypt all existing secrets ```bash $> kubectl get secrets --all-namespaces -o json | kubectl replace -f - -``` \ No newline at end of file +``` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..1523736 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,25 @@ +# Troubleshooting + +## Teardown Environment + +```bash +$> make teardown +``` + +should destroy all terraform managed ressources. + +## Clean up +If you wanna clean up your development environment enter: + +!!! warning + Use with Caution, check the Makefile before running! + +```bash +$> make cleanup +``` + +## Remove `minikube` cache + +```bash +$> minikube delete --purge +``` \ No newline at end of file diff --git a/k8s-minikube/terraform/kubernetes.tf b/k8s-minikube/terraform/kubernetes.tf deleted file mode 100644 index 2a32ffb..0000000 --- a/k8s-minikube/terraform/kubernetes.tf +++ /dev/null @@ -1,25 +0,0 @@ -resource "kubernetes_config_map" "this" { - count = var.kms_enabled ? 1 : 0 - - metadata { - name = "trousseau-config" - namespace = "kube-system" - } - - data = { - cfg = templatefile("${path.module}/../templates/trousseau-config.yml.tmpl", { - token = vault_token.this.client_token - }) - } - - depends_on = [minikube_cluster.docker] -} - -resource "kubectl_manifest" "secret_store" { - count = var.kms_enabled ? 1 : 0 - - - yaml_body = file("${path.module}/../files/trousseau.yml") - - depends_on = [kubernetes_config_map.this] -} diff --git a/k8s-minikube/terraform/variables.tf b/k8s-minikube/terraform/variables.tf deleted file mode 100644 index 6d0d045..0000000 --- a/k8s-minikube/terraform/variables.tf +++ /dev/null @@ -1,3 +0,0 @@ -variable "kms_enabled" { - type = bool -} diff --git a/k8s-minikube/terraform/vault.tf b/k8s-minikube/terraform/vault.tf deleted file mode 100644 index b88fef3..0000000 --- a/k8s-minikube/terraform/vault.tf +++ /dev/null @@ -1,26 +0,0 @@ -resource "vault_mount" "transit" { - path = "transit" - type = "transit" -} - -resource "vault_transit_secret_backend_key" "key" { - backend = vault_mount.transit.path - name = "kms" - - deletion_allowed = true -} - -resource "vault_policy" "kms" { - name = "kms" - - policy = file("${path.module}/../files/vault-policy.hcl") -} - -resource "vault_token" "this" { - policies = [vault_policy.kms.name] - - renewable = true - no_parent = true - period = "24h" - ttl = "24h" -} diff --git a/main.tf b/main.tf index 9396e4e..0025255 100644 --- a/main.tf +++ b/main.tf @@ -7,7 +7,7 @@ module "tls" { ip_sans = ["127.0.0.1"] dns_sans = concat( - ["host.minikube.internal"], + ["host.minikube.internal", "host.docker.internal"], [for v in range(0, var.vault.nodes) : format("vault-%02d", v + 1)] ) } @@ -16,7 +16,7 @@ module "tls" { module "vault" { source = "./vault-server/terraform" - vault_nodes = 3 + vault_nodes = var.vault.nodes ip_subnet = var.vault.ip_subnet vault_version = var.vault.version @@ -28,6 +28,7 @@ module "vault" { depends_on = [module.tls] } +# Deploy Mysql and Dynamic DB lab module "database" { count = var.databases.enabled ? 1 : 0 @@ -36,15 +37,12 @@ module "database" { depends_on = [module.vault] } -# Spin up a K8s Cluster -module "kubernetes" { +# Spin up a minikube k8s cluster +module "minikube" { count = var.kubernetes.enabled ? 1 : 0 source = "./k8s-minikube/terraform" - kms_enabled = var.kubernetes.kms - - depends_on = [module.vault] } @@ -54,9 +52,12 @@ module "vault_k8s" { source = "./vault-k8s/terraform" - depends_on = [module.kubernetes] + kms_enabled = var.kubernetes.kms + + depends_on = [module.minikube] } +# Setup Vaults PKI module "vault_pki" { source = "./vault-pki/terraform" @@ -75,6 +76,7 @@ module "esm" { depends_on = [module.vault_k8s] } +# Setup Vault Agent Injector module "vai" { count = var.kubernetes.enabled && var.kubernetes.vault_agent_injector ? 1 : 0 @@ -85,6 +87,7 @@ module "vai" { depends_on = [module.vault_k8s] } +# Setup CSI Secet Driver module "csi" { count = var.kubernetes.enabled && var.kubernetes.csi ? 1 : 0 @@ -95,6 +98,7 @@ module "csi" { depends_on = [module.vault_k8s] } +# Setup Vault Secets Operator module "vso" { count = var.kubernetes.enabled && var.kubernetes.vault_secrets_operator ? 1 : 0 @@ -105,13 +109,34 @@ module "vso" { depends_on = [module.vault_k8s] } +# Setup Cert manager module "cm" { count = var.kubernetes.enabled && var.kubernetes.cert_manager ? 1 : 0 source = "./k8s-cert-manager/terraform" ca_cert = module.tls.ca.cert - minikube_ip = module.kubernetes[0].minikube_ip + minikube_ip = module.minikube[0].minikube_ip depends_on = [module.vault_k8s] } + +# Deploy Boundary +module "boundary" { + count = var.boundary.enabled ? 1 : 0 + + source = "./boundary/terraform" + + depends_on = [module.vault_k8s] +} + +# Configure Boundary +module "boundary_cfg" { + count = var.boundary.enabled ? 1 : 0 + + source = "./boundary-config/terraform" + + minikube_ip = module.minikube[0].minikube_ip + + depends_on = [module.boundary] +} diff --git a/mkdocs.yml b/mkdocs.yml index 9ca06a7..2870242 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -36,7 +36,10 @@ nav: - Cert Manager: cm.md - Dynamic DB Credentials: - MySQL: databases.md - - Troubleshooting: Troubleshooting.md + - Boundary: + - Kubernetes Cluster Access: boundary.md + - Troubleshooting: + - troubleshooting.md markdown_extensions: - pymdownx.superfences: diff --git a/output.tf b/output.tf index a313c73..8c81e2a 100644 --- a/output.tf +++ b/output.tf @@ -3,7 +3,7 @@ output "ca_cert" { } output "minikube_ip" { - value = module.kubernetes[0].minikube_ip + value = try(module.minikube[0].minikube_ip, "") } output "root_token" { diff --git a/terraform.tfvars b/terraform.tfvars index afdef11..018df4c 100644 --- a/terraform.tfvars +++ b/terraform.tfvars @@ -20,7 +20,7 @@ vault = { # Dyanmic DB Credentials databases = { - enabled = false + enabled = true # enable mysql db mysql = true @@ -32,7 +32,7 @@ kubernetes = { enabled = true # enable kms plugin for secret encryption at rest - kms = false + kms = true # enable external secrets manager external_secrets_manager = true @@ -49,3 +49,8 @@ kubernetes = { # enable vault agent injector vault_agent_injector = true } + +# enable Boundary Lab +boundary = { + enabled = true +} diff --git a/tests/e2e.tftest.hcl b/tests/e2e.tftest.hcl index cf37aa4..1e2123b 100644 --- a/tests/e2e.tftest.hcl +++ b/tests/e2e.tftest.hcl @@ -1,3 +1,24 @@ +variables { + databases = { + enabled = true + mysql = true + } + + kubernetes = { + enabled = true + kms = true + external_secrets_manager = true + vault_secrets_operator = true + csi = true + cert_manager = true + vault_agent_injector = true + } + + boundary = { + enabled = true + } +} + # only create vault and tls resources run "setup_vault" { plan_options { @@ -73,7 +94,25 @@ run "mysql_user_is_created" { run "setup_minikube" { plan_options { target = [ - module.kubernetes + module.minikube + ] + } +} + +# set up boundary +run "setup_boundary" { + plan_options { + target = [ + module.boundary + ] + } +} + +# setup boundary config +run "setup_boundary_cfg" { + plan_options { + target = [ + module.boundary_cfg ] } } diff --git a/variables.tf b/variables.tf index e4e8fa4..11168f4 100644 --- a/variables.tf +++ b/variables.tf @@ -1,7 +1,7 @@ variable "vault" { type = object({ ip_subnet = optional(string, "172.16.10.0/24") - version = optional(string, "latest") + version = optional(string, "1.15") base_port = optional(number, 8000) nodes = optional(number, 3) initialization = optional(object({ @@ -14,6 +14,12 @@ variable "vault" { }) } +variable "boundary" { + type = object({ + enabled = optional(bool, false) + }) +} + variable "databases" { type = object({ enabled = optional(bool, true) diff --git a/k8s-minikube/files/encryption_provider_config.yml b/vault-k8s/files/encryption_provider_config.yml similarity index 100% rename from k8s-minikube/files/encryption_provider_config.yml rename to vault-k8s/files/encryption_provider_config.yml diff --git a/k8s-minikube/files/kube-api-server.yml b/vault-k8s/files/kube-api-server.yml similarity index 100% rename from k8s-minikube/files/kube-api-server.yml rename to vault-k8s/files/kube-api-server.yml diff --git a/k8s-minikube/files/trousseau.yml b/vault-k8s/files/trousseau.yml similarity index 100% rename from k8s-minikube/files/trousseau.yml rename to vault-k8s/files/trousseau.yml diff --git a/k8s-minikube/files/vault-policy.hcl b/vault-k8s/files/vault-policy.hcl similarity index 100% rename from k8s-minikube/files/vault-policy.hcl rename to vault-k8s/files/vault-policy.hcl diff --git a/k8s-minikube/templates/trousseau-config.yml.tmpl b/vault-k8s/templates/trousseau-config.yml.tmpl similarity index 100% rename from k8s-minikube/templates/trousseau-config.yml.tmpl rename to vault-k8s/templates/trousseau-config.yml.tmpl diff --git a/vault-k8s/terraform/kms.tf b/vault-k8s/terraform/kms.tf new file mode 100644 index 0000000..dd47d92 --- /dev/null +++ b/vault-k8s/terraform/kms.tf @@ -0,0 +1,61 @@ +resource "kubernetes_config_map" "this" { + count = var.kms_enabled ? 1 : 0 + + metadata { + name = "trousseau-config" + namespace = "kube-system" + } + + data = { + cfg = templatefile("${path.module}/../templates/trousseau-config.yml.tmpl", { + token = vault_token.this[0].client_token + }) + } + +} + +resource "kubectl_manifest" "secret_store" { + count = var.kms_enabled ? 1 : 0 + + + yaml_body = file("${path.module}/../files/trousseau.yml") + + depends_on = [kubernetes_config_map.this] +} + + +resource "vault_mount" "transit" { + count = var.kms_enabled ? 1 : 0 + + path = "transit" + type = "transit" +} + +resource "vault_transit_secret_backend_key" "key" { + count = var.kms_enabled ? 1 : 0 + + backend = vault_mount.transit[0].path + name = "kms" + + deletion_allowed = true +} + + +resource "vault_policy" "kms" { + count = var.kms_enabled ? 1 : 0 + + name = "kms" + + policy = file("${path.module}/../files/vault-policy.hcl") +} + +resource "vault_token" "this" { + count = var.kms_enabled ? 1 : 0 + + policies = [vault_policy.kms[0].name] + + renewable = true + no_parent = true + period = "24h" + ttl = "24h" +} diff --git a/vault-k8s/terraform/main.tf b/vault-k8s/terraform/kubernetes_auth.tf similarity index 92% rename from vault-k8s/terraform/main.tf rename to vault-k8s/terraform/kubernetes_auth.tf index 9b1d2f8..92a3d2b 100644 --- a/vault-k8s/terraform/main.tf +++ b/vault-k8s/terraform/kubernetes_auth.tf @@ -30,7 +30,7 @@ resource "kubernetes_secret" "service_account_secret" { } # https://developer.hashicorp.com/vault/docs/auth/kubernetes#use-the-vault-client-s-jwt-as-the-reviewer-jwt -resource "kubernetes_cluster_role_binding" "role_binding" { +resource "kubernetes_cluster_role_binding" "sa_validator" { metadata { name = "vault-token-reviewer" } @@ -48,6 +48,7 @@ resource "kubernetes_cluster_role_binding" "role_binding" { } } + # enable vault kubernetes auth backend for minikube cluster resource "vault_auth_backend" "minikube" { type = "kubernetes" @@ -55,7 +56,7 @@ resource "vault_auth_backend" "minikube" { } # configure vault kubernetes auth backend -resource "vault_kubernetes_auth_backend_config" "kubernetes_config" { +resource "vault_kubernetes_auth_backend_config" "this" { backend = vault_auth_backend.minikube.path kubernetes_host = "https://host.docker.internal:8443" diff --git a/vault-k8s/terraform/kubernetes_secret.tf b/vault-k8s/terraform/kubernetes_secret.tf new file mode 100644 index 0000000..a0f142e --- /dev/null +++ b/vault-k8s/terraform/kubernetes_secret.tf @@ -0,0 +1,77 @@ +resource "vault_kubernetes_secret_backend" "config" { + path = "minikube" + description = "K8s SAs for Minikube Cluster" + + default_lease_ttl_seconds = 43200 + max_lease_ttl_seconds = 86400 + + kubernetes_host = vault_kubernetes_auth_backend_config.this.kubernetes_host + kubernetes_ca_cert = vault_kubernetes_auth_backend_config.this.kubernetes_ca_cert + service_account_jwt = vault_kubernetes_auth_backend_config.this.token_reviewer_jwt + + disable_local_ca_jwt = false +} + +resource "vault_kubernetes_secret_backend_role" "role" { + backend = vault_kubernetes_secret_backend.config.path + name = "minikube" + + allowed_kubernetes_namespaces = ["*"] + token_max_ttl = 43200 + token_default_ttl = 21600 + + generated_role_rules = <