diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml deleted file mode 100644 index 5a4c0ea..0000000 --- a/.github/workflows/dependabot.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Dependabot auto-merge - -on: pull_request - -permissions: - contents: write - pull-requests: write - -jobs: - dependabot: - runs-on: ubuntu-latest - if: github.actor == 'dependabot[bot]' - steps: - - name: Dependabot metadata - id: metadata - uses: dependabot/fetch-metadata@v1 - with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - - name: Enable auto-merge for Dependabot PRs - if: contains(steps.metadata.outputs.dependency-names, 'my-dependency') && steps.metadata.outputs.update-type == 'version-update:semver-patch' - run: gh pr merge --auto --merge "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/README.md b/README.md index a3cfceb..6aaeb6b 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Please refer to the [documentation](https://falcosuessgott.github.io/hashicorp-v * [x] [External Secrets Manager](https://falcosuessgott.github.io/hashicorp-vault-playground/esm/) * [x] [Vault Secrets operator](https://falcosuessgott.github.io/hashicorp-vault-playground/vso/) * [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/) ### MySQL Dynamic DB Credentials @@ -28,3 +29,4 @@ Please refer to the [documentation](https://falcosuessgott.github.io/hashicorp-v ### ToDos * [ ] Prometheus & Grafana + Vault Metrics +* [ ] Boundary & (kubectl acccess, SSH) diff --git a/docs/assets/csi.png b/docs/assets/csi.png new file mode 100644 index 0000000..a9ac3df Binary files /dev/null and b/docs/assets/csi.png differ diff --git a/docs/csi.md b/docs/csi.md new file mode 100644 index 0000000..0bfdce7 --- /dev/null +++ b/docs/csi.md @@ -0,0 +1,200 @@ +# Secret Store CSI Driver + +![img](assets/csi.png) +> https://developer.hashicorp.com/vault/docs/platform/k8s/injector-csi + +## Requirements +For this lab youre going to need `kubectl`, `helm` and `jq` installed. + +Also in your `terraform.tfvars`: + +```yaml +# terraform.tfvars +kubernetes = { + enabled = true + csi = true +} +``` + +You then can bootstrap the cluster using `make bootstrap` + + +## Overview +The following resources will be created: + +1. The Vault Agent Injector Helm Chart is going to be installed in the `csi` Namespace. +2. The CSI Driver is installed using the official Helm Chart +3. A Kubernetes Auth Role `csi` bound to the `csi` Namespace & Service Account +4. KVv2 Secrets under `csi/secrets` containing 2 Example Secrets +5. A policy (`csi`) that allows reading `/csi/secrets` Secrets +6. A Secret Provider Class is created, describing which secret to read and to which secret to write it +7. A Demo App `kuard` is deployed wiht annotations that will get the secret created by the CSI driver attached and mounted and export that secret as an environment variable. + +## Walkthrough +The CSI Driver and the Vault CSI Driver implementation (csi) is going to be installed in the `csi` namespace using the [Helm Chart](https://github.com/hashicorp/vault-helm). + +```bash +$> helm list -n csi +helm list -n csi +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +csi csi 1 2024-01-12 14:13:14.639161204 +0100 CET deployed secrets-store-csi-driver-1.4.0 1.4.0 +vault csi 2 2024-01-12 14:15:14.327992163 +0100 CET deployed vault-0.27.0 1.15.2 +``` + +Additionally, a Vault Kubernetes Auth Role bounded to the Namespace and the vai Service Account has been created: + +```bash +# https://localhost/ui/vault/access/minikube-cluster/item/role/csi +$> vault read auth/minikube-cluster/role/csi +Key Value +--- ----- +alias_name_source serviceaccount_uid +bound_service_account_names [default] +bound_service_account_namespaces [csi] +token_bound_cidrs [] +token_explicit_max_ttl 0s +token_max_ttl 0s +token_no_default_policy false +token_num_uses 0 +token_period 0s +token_policies [csi] +token_ttl 1h +token_type default +``` + +Also KVv2 Secrets under `csi/secrets/` have been created: + +```bash +# https://localhost/ui/vault/secrets/csi/kv/secrets/details?version=1 +$> vault kv get csi/secrets +== Secret Path == +csi/data/secrets + +======= Metadata ======= +Key Value +--- ----- +created_time 2024-01-12T13:13:03.077481563Z +custom_metadata +deletion_time n/a +destroyed false +version 1 + +====== Data ====== +Key Value +--- ----- +password P@ssw0rd +username Admin +``` + +A corresponding policy `csi` that allows reading the vai secrets has also been crated: + +```bash +# https://localhost/ui/vault/policy/acl/csi +$> vault policy read csi +path "csi/" { + capabilities = ["read", "list"] +} + +path "csi/*" { + capabilities = ["read", "list"] +} +``` + +A Demo App with annotations mounting `csi-secret` and exporting the username field as an env var: + +```bash +$> cat k8s-vault-csi/files/kuard.yml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kuard + namespace: csi +spec: + selector: + matchLabels: + app: kuard + replicas: 1 + template: + metadata: + labels: + app: kuard + spec: + containers: + - image: gcr.io/kuar-demo/kuard-amd64:1 + name: kuard + # example for env var + env: + # export an attribute from the Secret as Env Var + - name: USERNAME + valueFrom: + secretKeyRef: + key: username + name: csi-secret + ports: + - containerPort: 8080 + volumeMounts: + # mount the secret + - name: csi + mountPath: /opt/secrets + readOnly: true + volumes: + # attach the csi secret + - name: csi + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: vault-csi +``` + +a secretProviderClass CRD has been deployed: + +```bash +$> cat k8s-vault-csi/files/secret_provider_class.yml +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: vault-csi + namespace: csi +spec: + provider: vault + parameters: + vaultAddress: https://host.minikube.internal + vaultKubernetesMountPath: minikube-cluster + vaultCACertPath: /opt/ca.crt + roleName: csi + objects: | + - objectName: "password" + secretPath: "csi/data/secrets" + secretKey: "password" + - objectName: "username" + secretPath: "csi/data/secrets" + secretKey: "username" + secretObjects: + - data: + - key: username + objectName: username + secretName: csi-secret + type: Opaque +``` + +That Provider Class applied, creates a k8s secret: + +```bash +$> kubectl get secret -n csi csi-secret -o json | jq '.data | map_values(@base64d)' +{ + "username": "Admin" +} +``` + +When deploying `kuard.yml`, the Secret containing the KVv2 Secrets from `csi/secrets/` is available as an environment variable: + +```bash +$> kubectl exec -n csi -it $(kubectl get pods -l=app=kuard -n csi --no-headers -o custom-columns=":metadata.name") -- env | grep USERNAME +USERNAME=Admin +``` + + +# Resources +* [https://developer.hashicorp.com/vault/docs/platform/k8s/csi](https://developer.hashicorp.com/vault/docs/platform/k8s/csi) +* [https://secrets-store-csi-driver.sigs.k8s.io/introduction](https://secrets-store-csi-driver.sigs.k8s.io/introduction) diff --git a/docs/home.md b/docs/home.md index a7e6570..56a04a2 100644 --- a/docs/home.md +++ b/docs/home.md @@ -18,6 +18,7 @@ Bootstrap a local Vault HA Cluster with many useful learning labs in under a min * [x] [External Secrets Manager](https://falcosuessgott.github.io/hashicorp-vault-playground/esm/) * [x] [Vault Secrets operator](https://falcosuessgott.github.io/hashicorp-vault-playground/vso/) * [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/) ### MySQL Dynamic DB Credentials @@ -25,3 +26,4 @@ Bootstrap a local Vault HA Cluster with many useful learning labs in under a min ### ToDos * [ ] Prometheus & Grafana + Vault Metrics +* [ ] Boundary & (kubectl acccess, SSH) diff --git a/k8s-vault-csi/files/kuard.yml b/k8s-vault-csi/files/kuard.yml new file mode 100644 index 0000000..17a9c4d --- /dev/null +++ b/k8s-vault-csi/files/kuard.yml @@ -0,0 +1,38 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kuard + namespace: csi +spec: + selector: + matchLabels: + app: kuard + replicas: 1 + template: + metadata: + labels: + app: kuard + spec: + containers: + - image: gcr.io/kuar-demo/kuard-amd64:1 + name: kuard + # example for env var + env: + - name: USERNAME + valueFrom: + secretKeyRef: + key: username + name: csi-secret + ports: + - containerPort: 8080 + volumeMounts: + - name: csi + mountPath: /opt/secrets + readOnly: true + volumes: + - name: csi + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: vault-csi diff --git a/k8s-vault-csi/files/secret_provider_class.yml b/k8s-vault-csi/files/secret_provider_class.yml new file mode 100644 index 0000000..ffc0cc4 --- /dev/null +++ b/k8s-vault-csi/files/secret_provider_class.yml @@ -0,0 +1,25 @@ +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: vault-csi + namespace: csi +spec: + provider: vault + parameters: + vaultAddress: https://host.minikube.internal + vaultKubernetesMountPath: minikube-cluster + vaultCACertPath: /opt/ca.crt + roleName: csi + objects: | + - objectName: "password" + secretPath: "csi/data/secrets" + secretKey: "password" + - objectName: "username" + secretPath: "csi/data/secrets" + secretKey: "username" + secretObjects: + - data: + - key: username + objectName: username + secretName: csi-secret + type: Opaque diff --git a/k8s-vault-csi/files/values.yml b/k8s-vault-csi/files/values.yml new file mode 100644 index 0000000..9aecb41 --- /dev/null +++ b/k8s-vault-csi/files/values.yml @@ -0,0 +1,17 @@ +# https://github.com/hashicorp/vault-helm/blob/main/values.yaml +server: + enabled: false + +injector: + enabled: false + +csi: + enabled: true + volumes: + - name: ca-cert + secret: + secretName: ca-cert + volumeMounts: + - name: ca-cert + mountPath: /opt + readOnly: true diff --git a/k8s-vault-csi/files/vault-policy.hcl b/k8s-vault-csi/files/vault-policy.hcl new file mode 100644 index 0000000..1302461 --- /dev/null +++ b/k8s-vault-csi/files/vault-policy.hcl @@ -0,0 +1,7 @@ +path "csi/" { + capabilities = ["read", "list"] +} + +path "csi/*" { + capabilities = ["read", "list"] +} diff --git a/k8s-vault-csi/terraform/csi.tf b/k8s-vault-csi/terraform/csi.tf new file mode 100644 index 0000000..32164fb --- /dev/null +++ b/k8s-vault-csi/terraform/csi.tf @@ -0,0 +1,19 @@ +resource "helm_release" "csi" { + name = "csi" + repository = "https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts" + chart = "secrets-store-csi-driver" + namespace = "csi" + + set { + name = "syncSecret.enabled" + value = true + } + + depends_on = [helm_release.vault] +} + +resource "kubectl_manifest" "secret_provider_class" { + yaml_body = file("${path.module}/../files/secret_provider_class.yml") + + depends_on = [helm_release.csi] +} diff --git a/k8s-vault-csi/terraform/kuard.tf b/k8s-vault-csi/terraform/kuard.tf new file mode 100644 index 0000000..d78eb3f --- /dev/null +++ b/k8s-vault-csi/terraform/kuard.tf @@ -0,0 +1,5 @@ +resource "kubectl_manifest" "kuard" { + yaml_body = file("${path.module}/../files/kuard.yml") + + depends_on = [vault_kubernetes_auth_backend_role.csi] +} diff --git a/k8s-vault-csi/terraform/variables.tf b/k8s-vault-csi/terraform/variables.tf new file mode 100644 index 0000000..0fa2c00 --- /dev/null +++ b/k8s-vault-csi/terraform/variables.tf @@ -0,0 +1,11 @@ +variable "secrets" { + type = map(string) + default = { + username = "Admin" + password = "P@ssw0rd" + } +} + +variable "ca_cert" { + type = string +} diff --git a/k8s-vault-csi/terraform/vault.tf b/k8s-vault-csi/terraform/vault.tf new file mode 100644 index 0000000..c93e084 --- /dev/null +++ b/k8s-vault-csi/terraform/vault.tf @@ -0,0 +1,28 @@ +resource "vault_mount" "csi" { + path = "csi" + type = "kv" + options = { version = "2" } + description = "Secrets read by Vault CSI Driver" +} + +resource "vault_kv_secret_v2" "csi" { + mount = vault_mount.csi.path + name = "secrets" + delete_all_versions = true + data_json = jsonencode(var.secrets) +} + +resource "vault_policy" "csi" { + name = "csi" + + policy = file("${path.module}/../files/vault-policy.hcl") +} + +resource "vault_kubernetes_auth_backend_role" "csi" { + backend = "minikube-cluster" + role_name = helm_release.csi.name + bound_service_account_names = ["default"] + bound_service_account_namespaces = [helm_release.csi.namespace] + token_ttl = 3600 + token_policies = [vault_policy.csi.name] +} diff --git a/k8s-vault-csi/terraform/vault_csi.tf b/k8s-vault-csi/terraform/vault_csi.tf new file mode 100644 index 0000000..8cf2977 --- /dev/null +++ b/k8s-vault-csi/terraform/vault_csi.tf @@ -0,0 +1,34 @@ +resource "helm_release" "vault" { + name = "vault" + repository = "https://helm.releases.hashicorp.com" + chart = "vault" + namespace = "csi" + create_namespace = true + + values = [file("${path.module}/../files/values.yml")] +} + +resource "kubernetes_secret" "ca_cert" { + metadata { + name = "ca-cert" + namespace = helm_release.vault.namespace + } + + data = { + "ca.crt" = var.ca_cert + } +} + +# Create a SA Secret for default SA +resource "kubernetes_secret" "sa_secret" { + metadata { + annotations = { + "kubernetes.io/service-account.name" = "default" + } + namespace = helm_release.vault.namespace + generate_name = "${helm_release.vault.name}-${helm_release.vault.chart}-token-" + } + + type = "kubernetes.io/service-account-token" + wait_for_service_account_token = true +} diff --git a/k8s-vault-csi/terraform/version.tf b/k8s-vault-csi/terraform/version.tf new file mode 100644 index 0000000..3ecefdf --- /dev/null +++ b/k8s-vault-csi/terraform/version.tf @@ -0,0 +1,26 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + vault = { + source = "hashicorp/vault" + version = "3.23.0" + } + local = { + source = "hashicorp/local" + version = "2.4.1" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "2.23.0" + } + helm = { + source = "hashicorp/helm" + version = "2.12.1" + } + kubectl = { + source = "gavinbunney/kubectl" + version = "1.14.0" + } + } +} diff --git a/main.tf b/main.tf index 926169f..1af30dd 100644 --- a/main.tf +++ b/main.tf @@ -82,6 +82,16 @@ module "vai" { depends_on = [module.vault_k8s] } +module "csi" { + count = var.kubernetes.enabled && var.kubernetes.csi ? 1 : 0 + + source = "./k8s-vault-csi/terraform" + + ca_cert = module.tls.ca.cert + + depends_on = [module.vault_k8s] +} + module "vso" { count = var.kubernetes.enabled && var.kubernetes.vault_secrets_operator ? 1 : 0 diff --git a/mkdocs.yml b/mkdocs.yml index 9f0867d..22d52bf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -31,6 +31,7 @@ nav: - External Secrets Manager: esm.md - Vault Secrets Operator: vso.md - Vault Agent Injector: vai.md + - Secret Store CSI Driver: csi.md - Cert Manager: cm.md - Dynamic DB Credentials: - MySQL: databases.md diff --git a/terraform.tfvars b/terraform.tfvars index 5f4efe8..9e42602 100644 --- a/terraform.tfvars +++ b/terraform.tfvars @@ -37,6 +37,9 @@ kubernetes = { # enable vault secrets operator vault_secrets_operator = true + # enable secrets using the CSI driver + csi = true + # enable cert manager cert_manager = true diff --git a/tests/e2e.tftest.hcl b/tests/e2e.tftest.hcl index 7848e03..cf37aa4 100644 --- a/tests/e2e.tftest.hcl +++ b/tests/e2e.tftest.hcl @@ -96,6 +96,36 @@ run "kubeapi_is_available" { } } +# run csi +run "setup_csi" { + plan_options { + target = [ + module.csi + ] + } +} + +# check if csi Secret has been created +run "csi_secret_has_been_written_to_pod" { + command = plan + + module { + source = "./tests/external_cmd" + } + + variables { + # external_cmd can only return json + command = <