├── .github ├── dependabot.yml └── workflows │ ├── commitlint.yml │ ├── e2e.yml │ ├── goreleaser.yml │ ├── lint.yml │ ├── mkdocs.yml │ ├── test.yml │ └── trivy.yml ├── .gitignore ├── .golang-ci.yml ├── .goreleaser.yml ├── .pre-commit-config.yaml ├── Dockerfile ├── Dockerfile.goreleaser ├── LICENSE ├── Makefile ├── README.md ├── assets ├── encryption_provider_config_v2.yml ├── kind-config.yaml └── vault-kubernetes-kms.yml ├── cmd ├── plugin.go ├── plugin_test.go └── v2_client │ ├── README.md │ └── main.go ├── codecov.yml ├── commitlint.config.js ├── docs ├── arch.drawio ├── arch.svg ├── concepts.md ├── configuration.md ├── dashboard.png ├── development.md ├── index.md ├── integration.md ├── metrics.md ├── quickstart.md ├── sign.md └── troubleshooting.md ├── go.mod ├── go.sum ├── main.go ├── mkdocs.yml ├── pkg ├── logging │ └── zap.go ├── metrics │ └── metrics.go ├── plugin │ ├── plugin_test.go │ ├── plugin_v1.go │ └── plugin_v2.go ├── probes │ ├── probes.go │ └── probes_test.go ├── socket │ ├── socket.go │ └── socket_test.go ├── testutils │ ├── testutils.go │ └── testutils_test.go ├── utils │ ├── utils.go │ └── utils_test.go └── vault │ ├── client.go │ ├── client_test.go │ ├── const.go │ ├── lease.go │ ├── lease_test.go │ ├── transit.go │ └── transit_test.go ├── requirements.txt ├── scripts ├── encryption_provider_config_v1.yml ├── encryption_provider_config_v2.yml ├── grafana_values.yml ├── kind-config_v1.yaml ├── kind-config_v2.yaml ├── local-registry.sh ├── prometheus_values.yml ├── svc.yml ├── vault-kubernetes-kms.yml └── vault.sh └── tools └── tools.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | time: "08:00" 8 | labels: 9 | - "dependencies" 10 | commit-message: 11 | prefix: "feat" 12 | include: "scope" 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "daily" 17 | time: "08:00" 18 | labels: 19 | - "dependencies" 20 | commit-message: 21 | prefix: "chore" 22 | include: "scope" 23 | - package-ecosystem: "docker" 24 | directory: "/" 25 | schedule: 26 | interval: "daily" 27 | time: "08:00" 28 | labels: 29 | - "dependencies" 30 | commit-message: 31 | prefix: "feat" 32 | include: "scope" 33 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint Commit Messages 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | commitlint: 11 | permissions: read-all 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | - uses: wagoid/commitlint-github-action@v5 18 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: E2E 2 | 3 | on: 4 | pull_request_target: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | E2E: 11 | strategy: 12 | matrix: 13 | vault: [1.16, 1.17, 1.18] 14 | versions: 15 | - k8s_version: v1.28.0 16 | kind_cfg: kind-config_v1.yaml 17 | - k8s_version: v1.29.0 18 | kind_cfg: kind-config_v2.yaml 19 | - k8s_version: v1.30.0 20 | kind_cfg: kind-config_v2.yaml 21 | runs-on: ubuntu-latest 22 | 23 | services: 24 | vault: 25 | image: hashicorp/vault:${{ matrix.vault }} 26 | env: 27 | VAULT_DEV_ROOT_TOKEN_ID: root 28 | credentials: 29 | username: ${{ secrets.DOCKERHUB_USERNAME }} 30 | password: ${{ secrets.DOCKERHUB_TOKEN }} 31 | ports: 32 | - 8200:8200 33 | registry: 34 | image: registry:2 35 | ports: 36 | - 5000:5000 37 | options: >- 38 | --name registry 39 | 40 | steps: 41 | - name: connect registry to kind 42 | run: | 43 | docker network create kind 44 | docker network connect kind registry 45 | 46 | - name: configure vault 47 | run: | 48 | curl -X POST -H "X-Vault-Token: root" -d '{"type":"transit"}' http://localhost:8200/v1/sys/mounts/transit 49 | curl -X PUT -H "X-Vault-Token: root" -d 'null' http://127.0.0.1:8200/v1/transit/keys/kms 50 | 51 | - name: checkout repo 52 | uses: actions/checkout@v4 53 | 54 | - name: setup go 55 | uses: actions/setup-go@v5 56 | with: 57 | go-version-file: go.mod 58 | cache: false 59 | 60 | - name: setup qemu 61 | uses: docker/setup-qemu-action@v3 62 | 63 | - name: setup docker build 64 | uses: docker/setup-buildx-action@v3 65 | with: 66 | driver-opts: network=host 67 | 68 | - name: tag & push docker 69 | uses: docker/build-push-action@v6 70 | with: 71 | context: . 72 | push: true 73 | tags: localhost:5000/vault-kubernetes-kms 74 | 75 | - name: setup kind 76 | uses: helm/kind-action@v1 77 | with: 78 | cluster_name: kms 79 | node_image: "kindest/node:${{ matrix.versions.k8s_version }}" 80 | config: "scripts/${{ matrix.versions.kind_cfg }}" 81 | 82 | - name: create a simple k8s secret 83 | run: | 84 | kubectl create secret generic secret --from-literal="key=value" 85 | kubectl get secret secret -o json | jq '.data | map_values(@base64d)' 86 | kubectl -n kube-system exec etcd-kms-control-plane -- sh -c "ETCDCTL_API=3 etcdctl \ 87 | --endpoints=https://127.0.0.1:2379 \ 88 | --cert /etc/kubernetes/pki/etcd/server.crt \ 89 | --key /etc/kubernetes/pki/etcd/server.key \ 90 | --cacert /etc/kubernetes/pki/etcd/ca.crt \ 91 | get /registry/secrets/default/secret" | hexdump -C 92 | 93 | - name: rotate kms key 94 | run: | 95 | curl -X POST -H "X-Vault-Token: root" http://localhost:8200/v1/transit/keys/kms/rotate 96 | kubectl get secret secret -o json | jq '.data | map_values(@base64d)' 97 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | pull_request_target: 8 | 9 | permissions: 10 | contents: write 11 | packages: write 12 | id-token: write 13 | 14 | jobs: 15 | goreleaser: 16 | runs-on: ubuntu-latest 17 | env: 18 | DOCKER_CLI_EXPERIMENTAL: "enabled" 19 | steps: 20 | - 21 | name: Checkout 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | - 26 | uses: actions/setup-go@v5 27 | with: 28 | go-version-file: go.mod 29 | cache: false 30 | 31 | - uses: sigstore/cosign-installer@v3.8.2 32 | - uses: anchore/sbom-action/download-syft@v0.19.0 33 | - 34 | name: Set up QEMU 35 | uses: docker/setup-qemu-action@v3 36 | - 37 | name: Set up Docker Buildx 38 | uses: docker/setup-buildx-action@v3 39 | 40 | - 41 | name: Login to Docker Hub 42 | uses: docker/login-action@v3 43 | with: 44 | username: ${{ secrets.DOCKERHUB_USERNAME }} 45 | password: ${{ secrets.DOCKERHUB_TOKEN }} 46 | - 47 | name: ghcr-login 48 | uses: docker/login-action@v3 49 | with: 50 | registry: ghcr.io 51 | username: ${{ github.repository_owner }} 52 | password: ${{ secrets.GITHUB_TOKEN }} 53 | 54 | # if tag release 55 | - 56 | name: Run GoReleaser 57 | uses: goreleaser/goreleaser-action@v6 58 | if: startsWith(github.ref, 'refs/tags/v') 59 | with: 60 | version: '~> v2' 61 | args: release --clean 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 | COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} 65 | 66 | - 67 | name: Docker Hub Description 68 | uses: peter-evans/dockerhub-description@v4 69 | if: startsWith(github.ref, 'refs/tags/v') 70 | with: 71 | username: ${{ secrets.DOCKERHUB_USERNAME }} 72 | password: ${{ secrets.DOCKERHUB_TOKEN }} 73 | repository: falcosuessgott/vault-kubernetes-kms 74 | 75 | # if no tag test release build 76 | - 77 | name: Run GoReleaser skip publishing 78 | uses: goreleaser/goreleaser-action@v6 79 | if: "!startsWith(github.ref, 'refs/tags/v')" 80 | with: 81 | version: '~> v2' 82 | args: release --snapshot 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} 86 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**.go' 7 | branches: 8 | - main 9 | pull_request: 10 | 11 | permissions: 12 | contents: read 13 | pull-requests: read 14 | checks: write 15 | 16 | jobs: 17 | golangci: 18 | name: lint 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - uses: actions/setup-go@v5 24 | with: 25 | go-version-file: go.mod 26 | 27 | - name: golangci-lint 28 | uses: golangci/golangci-lint-action@v6 29 | with: 30 | version: v1.63.4 31 | args: -c .golang-ci.yml -v --timeout=5m 32 | env: 33 | GO111MODULES: off 34 | -------------------------------------------------------------------------------- /.github/workflows/mkdocs.yml: -------------------------------------------------------------------------------- 1 | name: mkdocs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | deploy: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Configure Git Credentials 21 | run: | 22 | git config user.name github-actions[bot] 23 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 24 | 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: 3.x 28 | 29 | - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 30 | 31 | - uses: actions/cache@v4 32 | with: 33 | key: mkdocs-material-${{ env.cache_id }} 34 | path: .cache 35 | restore-keys: | 36 | mkdocs-material- 37 | 38 | - run: pip install mkdocs-material 39 | - run: pip install -r requirements.txt 40 | 41 | - run: mkdocs gh-deploy --force 42 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test and coverage 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - uses: actions/setup-go@v5 17 | with: 18 | go-version-file: go.mod 19 | cache: false 20 | 21 | - run: go generate -tags tools tools/tools.go 22 | 23 | - name: go get 24 | run: go get ./... 25 | 26 | - name: Run coverage 27 | run: make test 28 | env: 29 | # https://github.com/testcontainers/testcontainers-go/issues/1782 30 | TESTCONTAINERS_RYUK_DISABLED: true 31 | 32 | - name: Upload coverage reports to Codecov 33 | uses: codecov/codecov-action@v5.4.2 34 | with: 35 | token: ${{ secrets.CODECOV_TOKEN }} 36 | slug: FalcoSuessgott/vault-kubernetes-kms 37 | -------------------------------------------------------------------------------- /.github/workflows/trivy.yml: -------------------------------------------------------------------------------- 1 | name: image_scan 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | schedule: 8 | - cron: '40 21 * * 5' 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | build: 15 | name: Build 16 | permissions: 17 | contents: read 18 | security-events: write 19 | actions: read 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: go.mod 27 | cache: false 28 | 29 | - uses: hadolint/hadolint-action@v3.1.0 30 | with: 31 | dockerfile: Dockerfile 32 | 33 | - name: Build an image from Dockerfile 34 | run: | 35 | docker build -t docker.io/falcosuessgott/vault-kubernetes-kms:${{ github.sha }} . 36 | 37 | - name: Run Trivy vulnerability scanner 38 | uses: aquasecurity/trivy-action@master 39 | with: 40 | image-ref: 'docker.io/falcosuessgott/vault-kubernetes-kms:${{ github.sha }}' 41 | format: 'sarif' 42 | output: 'trivy-results.sarif' 43 | 44 | - name: Upload Trivy scan results to GitHub Security tab 45 | uses: github/codeql-action/upload-sarif@v3 46 | with: 47 | sarif_file: trivy-results.sarif 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vault-kubernetes-kms 2 | vendor/ 3 | coverage.out 4 | /dist 5 | .envrc 6 | manpages/ 7 | dist/ 8 | nohup.out 9 | -------------------------------------------------------------------------------- /.golang-ci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | lll: 3 | line-length: 180 4 | linters: 5 | enable-all: true 6 | disable: 7 | - testpackage 8 | - forbidigo 9 | - paralleltest 10 | - varnamelen 11 | - rowserrcheck 12 | - sqlclosecheck 13 | - wastedassign 14 | - exhaustruct 15 | - nolintlint 16 | - wrapcheck 17 | - depguard 18 | - err113 19 | - intrange 20 | - copyloopvar 21 | - gochecknoglobals 22 | - revive 23 | - exportloopref -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | builds: 4 | - 5 | env: 6 | - CGO_ENABLED=0 7 | binary: vault-kubernetes-kms 8 | flags: 9 | - -trimpath 10 | goos: 11 | - linux 12 | - darwin 13 | goarch: 14 | - amd64 15 | - arm64 16 | goarm: 17 | - "7" 18 | ignore: 19 | - goos: windows 20 | goarch: arm 21 | 22 | archives: 23 | - 24 | builds: 25 | - vault-kubernetes-kms 26 | format_overrides: 27 | - goos: windows 28 | format: zip 29 | name_template: >- 30 | {{- .ProjectName }}_ 31 | {{- title .Os }}_ 32 | {{- if eq .Arch "amd64" }}x86_64 33 | {{- else if eq .Arch "386" }}i386 34 | {{- else }}{{ .Arch }}{{ end }} 35 | {{- if .Arm }}v{{ .Arm }}{{ end -}} 36 | 37 | dockers: 38 | - image_templates: 39 | - "ghcr.io/falcosuessgott/{{.ProjectName}}:{{ .Tag }}-amd64" 40 | - "falcosuessgott/{{.ProjectName}}:{{ .Tag }}-amd64" 41 | dockerfile: Dockerfile.goreleaser 42 | use: buildx 43 | build_flag_templates: 44 | - "--pull" 45 | - "--label=io.artifacthub.package.readme-url=https://raw.githubusercontent.com/FalcoSuessgott/vault-kubernetes-kms/main/README.md" 46 | - "--label=io.artifacthub.package.maintainers=[{\"name\":\"Tom Morelly\",\"email\":\"tommorelly@gmail.com\"}]" 47 | - "--label=io.artifacthub.package.license=MIT" 48 | - "--label=org.opencontainers.image.description=Encrypt Kubernetes Secrets using Hashicorp Vault as the KMS Provider" 49 | - "--label=org.opencontainers.image.created={{.Date}}" 50 | - "--label=org.opencontainers.image.name={{.ProjectName}}" 51 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 52 | - "--label=org.opencontainers.image.version={{.Version}}" 53 | - "--label=org.opencontainers.image.source={{.GitURL}}" 54 | - "--platform=linux/amd64" 55 | extra_files: 56 | - go.mod 57 | - go.sum 58 | - image_templates: 59 | - "ghcr.io/falcosuessgott/{{.ProjectName}}:{{ .Tag }}-arm64" 60 | - "falcosuessgott/{{.ProjectName}}:{{ .Tag }}-arm64" 61 | dockerfile: Dockerfile.goreleaser 62 | use: buildx 63 | build_flag_templates: 64 | - "--pull" 65 | - "--label=io.artifacthub.package.readme-url=https://raw.githubusercontent.com/FalcoSuessgott/vault-kubernetes-kms/main/README.md" 66 | - "--label=io.artifacthub.package.maintainers=[{\"name\":\"Tom Morelly\",\"email\":\"tommorelly@gmail.com\"}]" 67 | - "--label=io.artifacthub.package.license=MIT" 68 | - "--label=org.opencontainers.image.description=Encrypt Kubernetes Secrets using Hashicorp Vault as the KMS Provider" 69 | - "--label=org.opencontainers.image.created={{.Date}}" 70 | - "--label=org.opencontainers.image.name={{.ProjectName}}" 71 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 72 | - "--label=org.opencontainers.image.version={{.Version}}" 73 | - "--label=org.opencontainers.image.source={{.GitURL}}" 74 | - "--platform=linux/arm64" 75 | extra_files: 76 | - go.mod 77 | - go.sum 78 | goarch: arm64 79 | 80 | docker_manifests: 81 | - name_template: "ghcr.io/falcosuessgott/{{.ProjectName}}:{{ .Tag }}" 82 | image_templates: 83 | - "ghcr.io/falcosuessgott/{{.ProjectName}}:{{ .Tag }}-amd64" 84 | - "ghcr.io/falcosuessgott/{{.ProjectName}}:{{ .Tag }}-arm64" 85 | - name_template: "ghcr.io/falcosuessgott/{{.ProjectName}}:latest" 86 | image_templates: 87 | - "ghcr.io/falcosuessgott/{{.ProjectName}}:{{ .Tag }}-amd64" 88 | - "ghcr.io/falcosuessgott/{{.ProjectName}}:{{ .Tag }}-arm64" 89 | - name_template: "falcosuessgott/{{.ProjectName}}:{{ .Tag }}" 90 | image_templates: 91 | - "falcosuessgott/{{.ProjectName}}:{{ .Tag }}-amd64" 92 | - "falcosuessgott/{{.ProjectName}}:{{ .Tag }}-arm64" 93 | - name_template: "falcosuessgott/{{.ProjectName}}:latest" 94 | image_templates: 95 | - "falcosuessgott/{{.ProjectName}}:{{ .Tag }}-amd64" 96 | - "falcosuessgott/{{.ProjectName}}:{{ .Tag }}-arm64" 97 | 98 | gomod: 99 | proxy: true 100 | 101 | source: 102 | enabled: true 103 | 104 | sboms: 105 | - artifacts: archive 106 | - id: source 107 | artifacts: source 108 | 109 | signs: 110 | - cmd: cosign 111 | certificate: "${artifact}.pem" 112 | artifacts: checksum 113 | output: true 114 | args: 115 | - sign-blob 116 | - "--output-certificate=${certificate}" 117 | - "--output-signature=${signature}" 118 | - "${artifact}" 119 | - "--yes" 120 | 121 | docker_signs: 122 | - cmd: cosign 123 | artifacts: manifests 124 | output: true 125 | args: 126 | - "sign" 127 | - "${artifact}@${digest}" 128 | - "--yes" 129 | 130 | checksum: 131 | name_template: "checksums.txt" 132 | 133 | changelog: 134 | sort: asc 135 | use: github 136 | filters: 137 | exclude: 138 | - "^test:" 139 | - "^chore" 140 | - "merge conflict" 141 | - Merge pull request 142 | - Merge remote-tracking branch 143 | - Merge branch 144 | - go mod tidy 145 | groups: 146 | - title: Dependency updates 147 | regexp: '^.*?(feat|fix)\(deps\)!?:.+$' 148 | order: 300 149 | - title: 'New Features' 150 | regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$' 151 | order: 100 152 | - title: 'Bug fixes' 153 | regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$' 154 | order: 200 155 | - title: 'Documentation updates' 156 | regexp: ^.*?doc(\([[:word:]]+\))??!?:.+$ 157 | order: 400 158 | - title: Other work 159 | order: 9999 160 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: check-case-conflict 9 | - id: check-symlinks 10 | - id: check-json 11 | - id: mixed-line-ending 12 | args: ["--fix=lf"] 13 | - id: no-commit-to-branch 14 | args: [--branch, main] 15 | - id: pretty-format-json 16 | args: [--autofix, --no-sort-keys] 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23 AS builder 2 | WORKDIR /usr/src/vault-kubernetes-kms 3 | COPY go.mod go.sum ./ 4 | RUN go mod download && go mod verify 5 | COPY . . 6 | RUN CGO_ENABLED=0 go build -v -o /vault-kubernetes-kms main.go 7 | 8 | # https://github.com/GoogleContainerTools/distroless/issues/1360#issuecomment-1646667145 9 | FROM gcr.io/distroless/static-debian12@sha256:3d0f463de06b7ddff27684ec3bfd0b54a425149d0f8685308b1fdf297b0265e9 10 | WORKDIR / 11 | COPY --from=builder /vault-kubernetes-kms . 12 | 13 | ENTRYPOINT ["/vault-kubernetes-kms"] 14 | -------------------------------------------------------------------------------- /Dockerfile.goreleaser: -------------------------------------------------------------------------------- 1 | # Goreleaer does not support multi staged builds: https://github.com/goreleaser/goreleaser/issues/609#issuecomment-534748384 2 | FROM gcr.io/distroless/static-debian12@sha256:3d0f463de06b7ddff27684ec3bfd0b54a425149d0f8685308b1fdf297b0265e9 3 | WORKDIR / 4 | COPY vault-kubernetes-kms . 5 | 6 | ENTRYPOINT ["/vault-kubernetes-kms"] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 FalcoSuessgott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: help 2 | 3 | .PHONY: help 4 | help: ## list makefile targets 5 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 6 | 7 | PHONY: fmt 8 | fmt: ## format go files 9 | gofumpt -w . 10 | gci write . 11 | pre-commit run -a 12 | 13 | .PHONY: docs 14 | docs: ## render docs locally 15 | mkdocs serve 16 | 17 | PHONY: test 18 | test: ## test 19 | gotestsum -- -v --shuffle=on -race -coverprofile="coverage.out" -covermode=atomic ./... 20 | 21 | PHONY: lint 22 | lint: ## lint go files 23 | golangci-lint run -c .golang-ci.yml 24 | 25 | ..PHONY: setup-vault 26 | setup-vault: ## setup a local vault dev server with transit engine + key 27 | ./scripts/vault.sh 28 | 29 | .PHONY: setup-registry 30 | setup-registry: ## setup a local docker registry for pulling in kind 31 | ./scripts/local-registry.sh 32 | 33 | .PHONY: gen-load 34 | gen-load: ## generate load on KMS plugin 35 | while true; do \ 36 | go run cmd/v2_client/main.go $$(openssl rand -base64 12);\ 37 | done; 38 | 39 | .PHONY: gen-secrets 40 | gen-secrets: ## generate secrets on KMS plugin 41 | while true; do \ 42 | kubectl create secret generic $$(openssl rand -hex 8 | tr '[:upper:]' '[:lower:]')\ 43 | --from-literal=$$(openssl rand -hex 8 | tr '[:upper:]' '[:lower:]')=$$(openssl rand -hex 8 | tr '[:upper:]' '[:lower:]');\ 44 | done; 45 | 46 | .PHONY: setup-kind 47 | setup-kind: ## setup kind cluster with encrpytion provider configured 48 | kind delete cluster --name=kms || true 49 | kind create cluster --name=kms --config scripts/kind-config_v2.yaml 50 | 51 | .PHONY: setup-o11y 52 | setup-o11y: ## install grafana and prometheus via helm 53 | kubectl apply -f scripts/svc.yml 54 | 55 | helm repo add prometheus-community https://prometheus-community.github.io/helm-charts 56 | helm repo add grafana https://grafana.github.io/helm-charts 57 | helm repo update 58 | 59 | helm install prometheus prometheus-community/prometheus --values scripts/prometheus_values.yml 60 | helm install grafana grafana/grafana --values scripts/grafana_values.yml 61 | 62 | kubectl get secret --namespace default grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo 63 | 64 | .PHONY: setup-local 65 | setup-local: setup-vault setup-registry setup-kind ## complete local setup 66 | 67 | .PHONY: destroy 68 | destroy: ## destroy kind cluster 69 | kind delete cluster --name=kms 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vault-kms-plugin 2 | > [!IMPORTANT] 3 | > as of [`v1.0.0`](https://github.com/FalcoSuessgott/vault-kubernetes-kms/releases/tag/v1.0.0) `vault-kubernetes-kms` is considered stable and production-grade 4 | 5 | A Kubernetes KMS Plugin that uses [HashiCorp Vaults](https://developer.hashicorp.com/vault) [Transit Engine](https://developer.hashicorp.com/vault/docs/secrets/transit) for securely encrypting Secrets, Config Maps and other Kubernetes Objects in etcd at Rest (on disk). 6 | 7 | [![E2E](https://github.com/FalcoSuessgott/vault-kubernetes-kms/actions/workflows/e2e.yml/badge.svg)](https://github.com/FalcoSuessgott/vault-kubernetes-kms/actions/workflows/e2e.yml) 8 | drawing drawing drawing 9 | 10 | 11 | ## Why 12 | HashiCorp Vault already offers useful [Kubernetes integrations](https://developer.hashicorp.com/vault/docs/platform/k8s), such as the Vault Secrets Operator for populating Kubernetes secrets from Vault Secrets or the Vault Agent Injector for injecting Vault secrets into a Pod using a sidecar container approach. 13 | 14 | Wouldn't it be nice if you could also use your Vault server to encrypt Kubernetes secrets before they are written into etcd? The `vault-kubernetes-kms` plugin does exactly this! 15 | 16 | Since the key used for encrypting secrets is not stored in Kubernetes, an attacker who intends to get unauthorized access to the plaintext values would need to compromise etcd and the Vault server. 17 | 18 | ## How does it work? 19 | ![img](docs/arch.svg) 20 | 21 | `vault-kubernetes-kms` is supposed to run as a static pod on every control plane node or on that node where the `kube-apiserver` will run. 22 | 23 | `vault-kubernetes-kms` will start a UNIX domain socket and listens for encryption requests from the `kube-apiserver`. The plugin will then use the specified Vault transit encryption key to encrypt the data and send it back to the `kube-apiserver`, who will then store the encrypted response in `etcd`. 24 | 25 | To do so, you will have to enable Data at Rest encryption, by configuring the `kube-apiserver` to use a `EncryptionConfiguration` (See [https://falcosuessgott.github.io/vault-kubernetes-kms/configuration/](https://falcosuessgott.github.io/vault-kubernetes-kms/configuration/) for more details). 26 | 27 | :warning: As a result of that, **the `kube-apiserver` requires the `vault-kubernetes-kms` plugin to be up & running before the `kube-apiserver` starts**. To ensure this, setting a priority class in the plugins manifest (`"priorityClassName: system-node-critical"`) is recommended. :warning: 28 | 29 | Following the scenario that `vault-kubernetes-kms` is deployed as a static pod, then your Vault server has to reside **outside** of your Kubernetes cluster, as it is [recommended](https://developer.hashicorp.com/vault/tutorials/kubernetes/kubernetes-security-concerns). If you decide to deploy the plugin not as a static pod, then theoretically the Vault server can be deployed on the same cluster, you then would have to patch the `kube-apiserver` after startup to find its `EncryptionProviderConfig`. 30 | 31 | **[Check out the official documentation](https://falcosuessgott.github.io/vault-kubernetes-kms/)** 32 | 33 | ## Features 34 | * support [Vault Token](https://developer.hashicorp.com/vault/docs/auth/token), [AppRole](https://developer.hashicorp.com/vault/docs/auth/approle) authentication (Since a static pod cannot reference any other Kubernetes API-Objects, Vaults Kubernetes Authentication is not possible.) 35 | * support Kubernetes [KMS Plugin v1 (deprecated since `v1.28.0`) & v2 (stable in `v1.29.0`)](https://kubernetes.io/docs/tasks/administer-cluster/kms-provider/#before-you-begin) 36 | * [automatic Token Renewal for avoiding Token expiry](https://falcosuessgott.github.io/vault-kubernetes-kms/configuration/#cli-args-environment-variables) 37 | * [Exposes useful Prometheus Metrics](https://falcosuessgott.github.io/vault-kubernetes-kms/metrics/#prometheus-metrics) 38 | 39 | ## Without a KMS Provider 40 | ```bash 41 | # create any secret 42 | $> kubectl create secret generic secret-unencrypted -n default --from-literal=key=value 43 | 44 | # proof that k8s secrets are stored unencrypted on disk in etctd 45 | $> kubectl -n kube-system exec etcd-minikube -- sh -c "ETCDCTL_API=3 etcdctl \ 46 | --endpoints=https://127.0.0.1:2379 \ 47 | --cert /var/lib/minikube/certs/etcd/server.crt \ 48 | --key /var/lib/minikube/certs/etcd/server.key \ 49 | --cacert /var/lib/minikube/certs/etcd/ca.crt \ 50 | get /registry/secrets/default/secret-unencrypted" | hexdump -C 51 | 00000000 2f 72 65 67 69 73 74 72 79 2f 73 65 63 72 65 74 |/registry/secret| 52 | 00000010 73 2f 64 65 66 61 75 6c 74 2f 73 65 63 72 65 74 |s/default/secret| 53 | 00000020 2d 75 6e 65 6e 63 72 79 70 74 65 64 0a 6b 38 73 |-unencrypted.k8s| 54 | 00000030 00 0a 0c 0a 02 76 31 12 06 53 65 63 72 65 74 12 |.....v1..Secret.| 55 | 00000040 d1 01 0a b8 01 0a 12 73 65 63 72 65 74 2d 75 6e |.......secret-un| 56 | 00000050 65 6e 63 72 79 70 74 65 64 12 00 1a 07 64 65 66 |encrypted....def| 57 | 00000060 61 75 6c 74 22 00 2a 24 33 62 31 66 34 34 31 32 |ault".*$3b1f4412| 58 | 00000070 2d 37 61 39 34 2d 34 38 62 35 2d 61 38 36 39 2d |-7a94-48b5-a869-| 59 | 00000080 38 62 66 36 62 35 33 39 63 38 36 34 32 00 38 00 |8bf6b539c8642.8.| 60 | 00000090 42 08 08 fc 92 e9 ad 06 10 00 8a 01 60 0a 0e 6b |B...........`..k| 61 | 000000a0 75 62 65 63 74 6c 2d 63 72 65 61 74 65 12 06 55 |ubectl-create..U| 62 | 000000b0 70 64 61 74 65 1a 02 76 31 22 08 08 fc 92 e9 ad |pdate..v1"......| 63 | 000000c0 06 10 00 32 08 46 69 65 6c 64 73 56 31 3a 2c 0a |...2.FieldsV1:,.| 64 | 000000d0 2a 7b 22 66 3a 64 61 74 61 22 3a 7b 22 2e 22 3a |*{"f:data":{".":| 65 | 000000e0 7b 7d 2c 22 66 3a 6b 65 79 22 3a 7b 7d 7d 2c 22 |{},"f:key":{}},"| # secret keys unencrypted 66 | 000000f0 66 3a 74 79 70 65 22 3a 7b 7d 7d 42 00 12 0c 0a |f:type":{}}B....| 67 | 00000100 03 6b 65 79 12 05 76 61 6c 75 65 1a 06 4f 70 61 |.key..value..Opa| # secret values unencrypted 68 | 00000110 7 69 | ``` 70 | 71 | ## After configuring a KMS Provider 72 | 73 | ```bash 74 | # create any k8s secret 75 | $> kubectl create secret generic secret-encrypted -n default --from-literal=key=value 76 | 77 | # proof that now secrets are stored encrypted on disk in etctd 78 | $> kubectl -n kube-system exec etcd-minikube -- sh -c "ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \ 79 | --cert /var/lib/minikube/certs/etcd/server.crt \ 80 | --key /var/lib/minikube/certs/etcd/server.key \ 81 | --cacert /var/lib/minikube/certs/etcd/ca.crt \ 82 | get /registry/secrets/default/secret-encrypted" | hexdump -C 83 | 00000000 2f 72 65 67 69 73 74 72 79 2f 73 65 63 72 65 74 |/registry/secret| 84 | 00000010 73 2f 64 65 66 61 75 6c 74 2f 73 65 63 72 65 74 |s/default/secret| 85 | 00000020 2d 65 6e 63 72 79 70 74 65 64 0a 6b 38 73 3a 65 |-encrypted.k8s:e| 86 | 00000030 6e 63 3a 6b 6d 73 3a 76 32 3a 76 61 75 6c 74 3a |nc:kms:v2:vault:| 87 | 00000040 0a 84 02 54 b2 35 ef 01 ca 9a 3b 00 00 00 00 9e |...T.5....;.....| 88 | 00000050 c6 26 2a e6 23 f0 c0 b7 22 3a 99 d8 36 81 07 af |.&*.#...":..6...| 89 | 00000060 ae d7 33 62 3f 99 62 ed b1 f0 10 f9 47 05 46 0e |..3b?.b.....G.F.| 90 | 00000070 c4 4f b5 63 aa 7d e5 3b 44 fd 9e 6d e7 42 2f 8f |.O.c.}.;D..m.B/.| 91 | 00000080 44 27 57 f6 ee 62 69 9b 49 6b 00 bb d3 38 d4 85 |D'W..bi.Ik...8..| 92 | 00000090 ce 57 b6 fa 95 4b 88 ea 9c de 1f c9 e0 05 48 a5 |.W...K........H.| 93 | 000000a0 5f 08 01 c4 c9 f2 3d 5d 35 e6 0e e7 0a 89 18 ab |_.....=]5.......| 94 | 000000b0 72 f2 ba 2b 3e cb 20 bf cd 9a 0f 97 78 d4 79 05 |r..+>. .....x.y.| 95 | 000000c0 77 52 1b ba bd 55 2b 9f e0 f1 af dc 04 3a b0 a9 |wR...U+......:..| 96 | 000000d0 70 8e 7a 97 10 8f b4 41 75 4b b8 24 dc 6f 10 04 |p.z....AuK.$.o..| 97 | 000000e0 4b b9 a0 fc a5 cc 02 e9 53 6e 1a be 31 c2 2b 38 |K.......Sn..1.+8| 98 | 000000f0 d3 d9 07 6f ee 40 9d 20 dc d6 68 29 e0 20 3f 8f |...o.@. ..h). ?.| 99 | 00000100 0a 1c a0 03 4f bf 9f 4b 8a 76 9b 8c 06 5b 4f c8 |....O..K.v...[O.| 100 | 00000110 75 b7 a1 a3 d1 4e b2 00 81 53 ed 6a b2 d9 03 88 |u....N...S.j....| 101 | 00000120 cb 3c 3d bb 12 b4 88 d3 e0 c7 a7 e1 31 0c 18 55 |.<=.........1..U| 102 | 00000130 26 fb 38 86 5b fb 5c bc 2b e0 8b f3 56 84 78 b2 |&.8.[.\.+...V.x.| 103 | 00000140 ae fc 11 98 1e a7 b9 12 0a 31 37 31 30 37 34 34 |.........1710744| 104 | 00000150 32 30 33 1a 59 76 61 75 6c 74 3a 76 31 3a 30 4f |203.Yvault:v1:0O| # encrypted secret stored in etcd on disk 105 | 00000160 31 7a 58 5a 31 54 34 33 37 70 53 59 7a 69 58 41 |1zXZ1T437pSYziXA| 106 | 00000170 32 30 62 4a 44 58 48 62 4a 31 55 65 50 75 6b 61 |20bJDXHbJ1UePuka| 107 | 00000180 70 70 47 4f 4f 54 51 65 6b 61 61 7a 6e 32 76 73 |ppGOOTQekaazn2vs| 108 | 00000190 35 4f 41 54 56 66 65 2b 31 63 75 7a 76 6a 64 6a |5OATVfe+1cuzvjdj| 109 | 000001a0 41 43 2b 6f 31 45 61 6a 57 72 32 53 6c 57 0a |AC+o1EajWr2SlW.| 110 | 000001af 111 | ``` 112 | -------------------------------------------------------------------------------- /assets/encryption_provider_config_v2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: EncryptionConfiguration 3 | apiVersion: apiserver.config.k8s.io/v1 4 | resources: 5 | - resources: 6 | - secrets 7 | providers: 8 | - kms: 9 | apiVersion: v2 10 | name: vault-kubernetes-kms 11 | endpoint: unix:///opt/kms/vaultkms.socket 12 | - identity: {} 13 | -------------------------------------------------------------------------------- /assets/kind-config.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | extraMounts: 6 | # mount encryption provider config available on all cp nodes 7 | - containerPath: /etc/kubernetes/encryption_provider_config_v2.yaml 8 | hostPath: assets/encryption_provider_config_v2.yml 9 | readOnly: true 10 | propagation: None 11 | # vault-kubernetes-kms as a static Pod 12 | - containerPath: /etc/kubernetes/manifests/vault-kubernetes-kms.yaml 13 | hostPath: assets/vault-kubernetes-kms.yml 14 | readOnly: true 15 | propagation: None 16 | # patch kube-apiserver 17 | kubeadmConfigPatches: 18 | - | 19 | kind: ClusterConfiguration 20 | apiServer: 21 | extraArgs: 22 | encryption-provider-config: "/etc/kubernetes/encryption_provider_config_v2.yaml" 23 | extraVolumes: 24 | - name: encryption-config 25 | hostPath: "/etc/kubernetes/encryption_provider_config_v2.yaml" 26 | mountPath: "/etc/kubernetes/encryption_provider_config_v2.yaml" 27 | readOnly: true 28 | pathType: File 29 | - name: socket 30 | hostPath: "/opt/kms" 31 | mountPath: "/opt/kms" 32 | -------------------------------------------------------------------------------- /assets/vault-kubernetes-kms.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: vault-kubernetes-kms 5 | namespace: kube-system 6 | spec: 7 | priorityClassName: system-node-critical 8 | hostNetwork: true 9 | containers: 10 | - name: vault-kubernetes-kms 11 | image: falcosuessgott/vault-kubernetes-kms:latest 12 | imagePullPolicy: IfNotPresent 13 | command: 14 | - /vault-kubernetes-kms 15 | - -vault-address=http://host.docker.internal:8200 16 | - -auth-method=token 17 | - -token=root 18 | volumeMounts: 19 | # mount /opt/kms host directory 20 | - name: kms 21 | mountPath: /opt/kms 22 | livenessProbe: 23 | httpGet: 24 | path: /health 25 | port: 8080 26 | readinessProbe: 27 | httpGet: 28 | path: /live 29 | port: 8080 30 | resources: 31 | requests: 32 | cpu: 100m 33 | memory: 128Mi 34 | limits: 35 | cpu: 2 36 | memory: 1Gi 37 | volumes: 38 | # mount /opt/kms host directory 39 | - name: kms 40 | hostPath: 41 | path: /opt/kms 42 | -------------------------------------------------------------------------------- /cmd/plugin.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "slices" 12 | "strings" 13 | "syscall" 14 | "time" 15 | 16 | "github.com/FalcoSuessgott/vault-kubernetes-kms/pkg/logging" 17 | "github.com/FalcoSuessgott/vault-kubernetes-kms/pkg/metrics" 18 | "github.com/FalcoSuessgott/vault-kubernetes-kms/pkg/plugin" 19 | "github.com/FalcoSuessgott/vault-kubernetes-kms/pkg/probes" 20 | "github.com/FalcoSuessgott/vault-kubernetes-kms/pkg/socket" 21 | "github.com/FalcoSuessgott/vault-kubernetes-kms/pkg/utils" 22 | "github.com/FalcoSuessgott/vault-kubernetes-kms/pkg/vault" 23 | "github.com/prometheus/client_golang/prometheus/promhttp" 24 | "go.uber.org/zap" 25 | "go.uber.org/zap/zapcore" 26 | "google.golang.org/grpc" 27 | ) 28 | 29 | type Options struct { 30 | Socket string `env:"SOCKET" envDefault:"unix:///opt/kms/vaultkms.socket"` 31 | ForceSocketOverwrite bool `env:"FORCE_SOCKET_OVERWRITE"` 32 | 33 | Debug bool `env:"DEBUG"` 34 | 35 | // vault server 36 | VaultAddress string `env:"VAULT_ADDR"` 37 | VaultNamespace string `env:"VAULT_NAMESPACE"` 38 | 39 | // auth 40 | AuthMethod string `env:"AUTH_METHOD"` 41 | 42 | // token auth 43 | Token string `env:"TOKEN"` 44 | 45 | // approle auth 46 | AppRoleRoleID string `env:"APPROLE_ROLE_ID"` 47 | AppRoleRoleSecretID string `env:"APPROLE_SECRET_ID"` 48 | AppRoleMount string `env:"APPROLE_MOUNT" envDefault:"approle"` 49 | 50 | // token refresh 51 | TokenRefreshInterval string `env:"TOKEN_REFRESH_INTERVAL" envDefault:"60s"` 52 | TokenRenewalSeconds int `env:"TOKEN_RENEWAL_SECONDS" envDefault:"3600"` 53 | 54 | // transit 55 | TransitKey string `env:"TRANSIT_KEY" envDefault:"kms"` 56 | TransitMount string `env:"TRANSIT_MOUNT" envDefault:"transit"` 57 | 58 | // healthz check 59 | HealthPort string `env:"HEALTH_PORT" envDefault:"8080"` 60 | 61 | // Disable KMSv1 Plugin 62 | DisableV1 bool `env:"DISABLE_V1" envDefault:"false"` 63 | 64 | Version bool 65 | } 66 | 67 | // NewPlugin instantiates the plugin. 68 | // nolint: funlen, cyclop 69 | func NewPlugin(version string) error { 70 | opts := &Options{} 71 | 72 | // first parse any env vars 73 | if err := utils.ParseEnvs("VAULT_KMS_", opts); err != nil { 74 | return fmt.Errorf("error parsing env vars: %w", err) 75 | } 76 | 77 | flag := flag.FlagSet{} 78 | // then flags, since they have precedence over env vars 79 | flag.StringVar(&opts.Socket, "socket", opts.Socket, "Destination path of the socket (required)") 80 | flag.BoolVar(&opts.ForceSocketOverwrite, "force-socket-overwrite", opts.ForceSocketOverwrite, "Force creation of the socket file."+ 81 | "Use with caution deletes whatever exists at -socket!") 82 | 83 | flag.BoolVar(&opts.Debug, "debug", opts.Debug, "Enable debug logs") 84 | 85 | flag.StringVar(&opts.VaultAddress, "vault-address", opts.VaultAddress, "Vault API address (required)") 86 | flag.StringVar(&opts.VaultNamespace, "vault-namespace", opts.VaultNamespace, "Vault Namespace (only when Vault Enterprise)") 87 | 88 | flag.StringVar(&opts.AuthMethod, "auth-method", opts.AuthMethod, "Auth Method. Supported: token, approle, k8s") 89 | 90 | flag.StringVar(&opts.Token, "token", opts.Token, "Vault Token (when Token auth)") 91 | 92 | flag.StringVar(&opts.AppRoleMount, "approle-mount", opts.AppRoleMount, "Vault Approle mount name (when approle auth)") 93 | flag.StringVar(&opts.AppRoleRoleID, "approle-role-id", opts.AppRoleRoleID, "Vault Approle role ID (when approle auth)") 94 | flag.StringVar(&opts.AppRoleRoleSecretID, "approle-secret-id", opts.AppRoleRoleSecretID, "Vault Approle Secret ID (when approle auth)") 95 | 96 | flag.StringVar(&opts.TokenRefreshInterval, "token-refresh-interval", opts.TokenRefreshInterval, "Interval to check for a token renewal") 97 | flag.IntVar(&opts.TokenRenewalSeconds, "token-renewal", opts.TokenRenewalSeconds, "The number of seconds to renew the token") 98 | 99 | flag.StringVar(&opts.TransitMount, "transit-mount", opts.TransitMount, "Vault Transit mount name") 100 | flag.StringVar(&opts.TransitKey, "transit-key", opts.TransitKey, "Vault Transit key name") 101 | 102 | flag.StringVar(&opts.HealthPort, "health-port", opts.HealthPort, "Health Check Port") 103 | 104 | flag.BoolVar(&opts.DisableV1, "disable-v1", opts.DisableV1, "disable the v1 kms plugin") 105 | 106 | flag.BoolVar(&opts.Version, "version", opts.Version, "prints out the plugins version") 107 | 108 | if err := flag.Parse(os.Args[1:]); err != nil { 109 | return fmt.Errorf("error parsing flags: %w", err) 110 | } 111 | 112 | if opts.Version { 113 | fmt.Fprintf(os.Stdout, "vault-kubernetes-kms v%s\n", version) 114 | 115 | return nil 116 | } 117 | 118 | if err := opts.validateFlags(); err != nil { 119 | return fmt.Errorf("error validating args: %w", err) 120 | } 121 | 122 | logLevel := zapcore.InfoLevel 123 | 124 | if opts.Debug { 125 | logLevel = zapcore.DebugLevel 126 | } 127 | 128 | l, err := logging.NewStandardLogger(logLevel) 129 | if err != nil { 130 | return fmt.Errorf("failed to configure logging: %w", err) 131 | } 132 | 133 | zap.ReplaceGlobals(l) 134 | 135 | var ( 136 | authMethod vault.Option 137 | logFields []zapcore.Field 138 | healthChecks = []probes.Prober{} 139 | ctx = shutDownSignal(context.Background()) 140 | ) 141 | 142 | logFields = append(logFields, 143 | zap.String("auth-method", opts.AuthMethod), 144 | zap.String("socket", opts.Socket), 145 | zap.Bool("debug", opts.Debug), 146 | zap.String("vault-address", opts.VaultAddress), 147 | zap.String("vault-namespace", opts.VaultNamespace), 148 | zap.String("transit-engine", opts.TransitMount), 149 | zap.String("transit-key", opts.TransitKey), 150 | zap.String("health-port", opts.HealthPort), 151 | zap.String("token-refresh-interval", opts.TokenRefreshInterval), 152 | zap.Int("token-renewal-seconds", opts.TokenRenewalSeconds), 153 | zap.Bool("disable-v1", opts.DisableV1), 154 | ) 155 | 156 | switch strings.ToLower(opts.AuthMethod) { 157 | case "token": 158 | authMethod = vault.WithTokenAuth(opts.Token) 159 | case "approle": 160 | authMethod = vault.WithAppRoleAuth(opts.AppRoleMount, opts.AppRoleRoleID, opts.AppRoleRoleSecretID) 161 | logFields = append(logFields, 162 | zap.String("approle-mount", opts.AppRoleMount), 163 | zap.String("approle-role-id", opts.AppRoleRoleID)) 164 | default: 165 | return fmt.Errorf("invalid auth method: %s", opts.AuthMethod) 166 | } 167 | 168 | zap.L().Info("starting kms plugin", logFields...) 169 | 170 | vc, err := vault.NewClient( 171 | vault.WithVaultAddress(opts.VaultAddress), 172 | vault.WithVaultNamespace(opts.VaultNamespace), 173 | vault.WithTransit(opts.TransitMount, opts.TransitKey), 174 | vault.WithTokenRenewalSeconds(opts.TokenRenewalSeconds), 175 | authMethod, 176 | ) 177 | if err != nil { 178 | zap.L().Fatal("Failed to create vault client", zap.Error(err)) 179 | } 180 | 181 | zap.L().Info("Successfully authenticated to vault") 182 | 183 | go func() { 184 | zap.L().Info("Starting token refresher", 185 | zap.String("interval", opts.TokenRefreshInterval), 186 | zap.Int("renewal-seconds", opts.TokenRenewalSeconds), 187 | ) 188 | 189 | t, _ := time.ParseDuration(opts.TokenRefreshInterval) 190 | 191 | vc.LeaseRefresher(ctx, t) 192 | }() 193 | 194 | s, err := socket.NewSocket(opts.Socket) 195 | if err != nil { 196 | zap.L().Fatal("Cannot create socket", zap.Error(err)) 197 | } 198 | 199 | zap.L().Info("Successfully created unix socket", zap.String("socket", s.Path)) 200 | 201 | listener, err := s.Listen(opts.ForceSocketOverwrite) 202 | if err != nil { 203 | zap.L().Fatal("failed to listen on socket: Use -force-socket-overwrite (VAULT_KUBERNETES_KMS_FORCE_SOCKET_OVERWRITE)", 204 | zap.String("socket", opts.Socket), 205 | zap.Any("error", err)) 206 | } 207 | 208 | zap.L().Info("Listening for connection") 209 | 210 | grpc := grpc.NewServer() 211 | 212 | if !opts.DisableV1 { 213 | pluginV1 := plugin.NewPluginV1(vc) 214 | pluginV1.Register(grpc) 215 | 216 | healthChecks = append(healthChecks, pluginV1) 217 | 218 | zap.L().Info("Successfully registered kms plugin v1") 219 | } 220 | 221 | pluginV2 := plugin.NewPluginV2(vc) 222 | pluginV2.Register(grpc) 223 | healthChecks = append(healthChecks, pluginV2) 224 | 225 | zap.L().Info("Successfully registered kms plugin v2") 226 | 227 | go func() { 228 | if err := grpc.Serve(listener); err != nil { 229 | zap.L().Fatal("Failed to start kms plugin", zap.Error(err)) 230 | } 231 | }() 232 | 233 | go func() { 234 | mux := &http.ServeMux{} 235 | 236 | mux.HandleFunc("/metrics", promhttp.HandlerFor(metrics.RegisterPrometheusMetrics(), promhttp.HandlerOpts{}).ServeHTTP) 237 | mux.HandleFunc("/health", probes.HealthZ(healthChecks)) 238 | mux.HandleFunc("/live", probes.HealthZ(healthChecks)) 239 | 240 | //nolint: mnd 241 | server := &http.Server{ 242 | Addr: ":" + opts.HealthPort, 243 | Handler: mux, 244 | ReadHeaderTimeout: 3 * time.Second, 245 | } 246 | 247 | if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 248 | zap.L().Fatal("Failed to start health check handlers", zap.Error(err)) 249 | } 250 | 251 | zap.L().Info("Exposing metrics under /metrics", zap.String("port", opts.HealthPort)) 252 | zap.L().Info("Exposing health check under /health", zap.String("port", opts.HealthPort)) 253 | zap.L().Info("Exposing live check under /live", zap.String("port", opts.HealthPort)) 254 | }() 255 | 256 | <-ctx.Done() 257 | 258 | grpc.GracefulStop() 259 | 260 | zap.L().Info("Exiting...") 261 | 262 | return nil 263 | } 264 | 265 | // nolint: cyclop 266 | func (o *Options) validateFlags() error { 267 | switch { 268 | case o.VaultAddress == "": 269 | return errors.New("vault address required") 270 | // check auth method 271 | case !slices.Contains([]string{"token", "approle"}, o.AuthMethod): 272 | return errors.New("invalid auth method. Supported: token, approle") 273 | 274 | // validate token auth 275 | case o.AuthMethod == "token" && o.Token == "": 276 | return errors.New("token required when using token auth") 277 | 278 | // validate approle auth 279 | case o.AuthMethod == "approle" && (o.AppRoleRoleID == "" || o.AppRoleRoleSecretID == ""): 280 | return errors.New("approle role id and secret id required when using approle auth") 281 | } 282 | 283 | if _, err := time.ParseDuration(o.TokenRefreshInterval); err != nil { 284 | return fmt.Errorf("invalid token refresh interval: %w", err) 285 | } 286 | 287 | return nil 288 | } 289 | 290 | func shutDownSignal(ctx context.Context) context.Context { 291 | signalChan := make(chan os.Signal, 1) 292 | signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGINT, os.Interrupt) 293 | 294 | parentCtx, cancel := context.WithCancel(ctx) 295 | 296 | go func() { 297 | signal := <-signalChan 298 | 299 | cancel() 300 | 301 | zap.L().Info("Received signal", zap.Stringer("signal", signal)) 302 | zap.L().Info("Shutting down server") 303 | }() 304 | 305 | return parentCtx 306 | } 307 | -------------------------------------------------------------------------------- /cmd/plugin_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "sync" 8 | "syscall" 9 | "testing" 10 | "time" 11 | 12 | "github.com/FalcoSuessgott/vault-kubernetes-kms/pkg/testutils" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | // nolint: perfsprint, funlen 17 | func TestNewPlugin(t *testing.T) { 18 | testCases := []struct { 19 | name string 20 | envVars map[string]string 21 | args []string 22 | vaultCmd []string 23 | extraArgs func(c *testutils.TestContainer) ([]string, error) 24 | err bool 25 | }{ 26 | { 27 | name: "token auth", 28 | vaultCmd: []string{ 29 | "secrets enable transit", 30 | "write -f transit/keys/kms", 31 | }, 32 | args: []string{ 33 | "vault-kubernetes-kms", 34 | "-auth-method=token", 35 | "-token=root", 36 | "-health-port=8081", 37 | fmt.Sprintf("-socket=unix:///%s/vaultkms.socket", t.TempDir()), 38 | }, 39 | extraArgs: func(c *testutils.TestContainer) ([]string, error) { 40 | return []string{fmt.Sprintf("-vault-address=%s", c.URI)}, nil 41 | }, 42 | }, 43 | { 44 | name: "approle auth", 45 | vaultCmd: []string{ 46 | "secrets enable transit", 47 | "write -f transit/keys/kms", 48 | "auth enable approle", 49 | "write auth/approle/role/kms token_ttl=1h", 50 | }, 51 | args: []string{ 52 | "vault-kubernetes-kms", 53 | "-auth-method=approle", 54 | "-health-port=8082", 55 | fmt.Sprintf("-socket=unix:///%s/vaultkms.socket", t.TempDir()), 56 | }, 57 | extraArgs: func(c *testutils.TestContainer) ([]string, error) { 58 | roleID, secretID, err := c.GetApproleCreds("approle", "kms") 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return []string{ 64 | fmt.Sprintf("-vault-address=%s", c.URI), 65 | fmt.Sprintf("-approle-role-id=%s", roleID), 66 | fmt.Sprintf("-approle-secret-id=%s", secretID), 67 | }, nil 68 | }, 69 | }, 70 | { 71 | name: "mixed with env vars", 72 | envVars: map[string]string{ 73 | "VAULT_KMS_TRANSIT_KEY": "abc", 74 | "VAULT_KMS_TRANSIT_MOUNT": "transit", 75 | "VAULT_KMS_AUTH_METHOD": "approle", 76 | }, 77 | vaultCmd: []string{ 78 | "secrets enable transit", 79 | "write -f transit/keys/abc", 80 | "auth enable -path=approle2 approle", 81 | "write auth/approle2/role/kms token_ttl=1h", 82 | }, 83 | args: []string{ 84 | "vault-kubernetes-kms", 85 | "-approle-mount=approle2", 86 | "-health-port=8083", 87 | fmt.Sprintf("-socket=unix:///%s/vaultkms.socket", t.TempDir()), 88 | }, 89 | extraArgs: func(c *testutils.TestContainer) ([]string, error) { 90 | roleID, secretID, err := c.GetApproleCreds("approle2", "kms") 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | return []string{ 96 | fmt.Sprintf("-vault-address=%s", c.URI), 97 | fmt.Sprintf("-approle-role-id=%s", roleID), 98 | fmt.Sprintf("-approle-secret-id=%s", secretID), 99 | }, nil 100 | }, 101 | }, 102 | } 103 | 104 | for _, tc := range testCases { 105 | t.Run(tc.name, func(t *testing.T) { 106 | vc, err := testutils.StartTestContainer(tc.vaultCmd...) 107 | require.NoError(t, err, "failed to start test container") 108 | 109 | //nolint: errcheck 110 | defer vc.Terminate() 111 | 112 | for k, v := range tc.envVars { 113 | t.Setenv(k, v) 114 | } 115 | 116 | if tc.extraArgs != nil { 117 | extraArgs, err := tc.extraArgs(vc) 118 | require.NoError(t, err, "failed to perform extra args func: %w", err) 119 | 120 | tc.args = append(tc.args, extraArgs...) 121 | } 122 | 123 | os.Args = tc.args 124 | 125 | var wg sync.WaitGroup 126 | 127 | wg.Add(2) 128 | 129 | // invoke NewPlugin() 130 | go func() { 131 | defer wg.Done() 132 | 133 | if err := NewPlugin(""); err != nil { 134 | log.Fatal(err) 135 | } 136 | }() 137 | 138 | // cancel after 5 seconds to avoid test timeout 139 | go func() { 140 | defer wg.Done() 141 | 142 | time.AfterFunc(5*time.Second, func() { 143 | _ = syscall.Kill(syscall.Getpid(), syscall.SIGINT) 144 | }) 145 | }() 146 | 147 | wg.Wait() 148 | }) 149 | } 150 | } 151 | 152 | func TestValidateFlags(t *testing.T) { 153 | testCases := []struct { 154 | name string 155 | opts *Options 156 | err bool 157 | }{ 158 | { 159 | name: "no vault address", 160 | err: true, 161 | opts: &Options{ 162 | Token: "abc", 163 | }, 164 | }, 165 | { 166 | name: "invalid auth method", 167 | err: true, 168 | opts: &Options{ 169 | AuthMethod: "invalid", 170 | }, 171 | }, 172 | { 173 | name: "token auth, but no token", 174 | err: true, 175 | opts: &Options{ 176 | VaultAddress: "e2e", 177 | AuthMethod: "token", 178 | }, 179 | }, 180 | { 181 | name: "approle auth, but no approle creds", 182 | err: true, 183 | opts: &Options{ 184 | VaultAddress: "e2e", 185 | AuthMethod: "approle", 186 | }, 187 | }, 188 | } 189 | 190 | for _, tc := range testCases { 191 | err := tc.opts.validateFlags() 192 | if tc.err { 193 | require.Error(t, err, tc.name) 194 | 195 | continue 196 | } 197 | 198 | require.NoError(t, err, tc.name) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /cmd/v2_client/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | See [https://falcosuessgott.github.io/vault-kubernetes-kms/development/](https://falcosuessgott.github.io/vault-kubernetes-kms/development/) 3 | -------------------------------------------------------------------------------- /cmd/v2_client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | "log" 8 | "os" 9 | "strings" 10 | 11 | "google.golang.org/grpc" 12 | "google.golang.org/grpc/credentials/insecure" 13 | pb "k8s.io/kms/apis/v2" 14 | ) 15 | 16 | var socket = "/tmp/kms.socket" 17 | 18 | //nolint:funlen,gocritic,cyclop 19 | func main() { 20 | if len(os.Args) == 1 { 21 | fmt.Println("Usage: cmd/v2_Client/main.go ") 22 | 23 | os.Exit(0) 24 | } 25 | 26 | conn, err := grpc.NewClient("unix:"+socket, grpc.WithTransportCredentials(insecure.NewCredentials())) 27 | if err != nil { 28 | log.Fatalf("Failed to initialize grpc client: %v", err) 29 | } 30 | 31 | defer conn.Close() 32 | 33 | client := pb.NewKeyManagementServiceClient(conn) 34 | ctx := context.Background() 35 | 36 | _, err = client.Status(ctx, &pb.StatusRequest{}) 37 | if err != nil { 38 | log.Fatalf("Failed to get version: %v", err) 39 | } 40 | 41 | if len(os.Args) > 1 { 42 | input := strings.Join(os.Args[1:], " ") 43 | 44 | encRes, err := client.Encrypt(ctx, &pb.EncryptRequest{Plaintext: []byte(input)}) 45 | if err != nil { 46 | log.Fatalf("Failed to encrypt: %v", err) 47 | } 48 | 49 | resp := base64.StdEncoding.EncodeToString(encRes.GetCiphertext()) 50 | fmt.Printf("\"%s\" -> \"%s\"", input, resp) 51 | 52 | b, err := base64.StdEncoding.DecodeString(resp) 53 | if err != nil { 54 | log.Fatalf("Failed to decode: %v", err) 55 | } 56 | 57 | decResp, err := client.Decrypt(ctx, &pb.DecryptRequest{Ciphertext: b}) 58 | if err != nil { 59 | log.Fatalf("Failed to encrypt: %v", err) 60 | } 61 | 62 | fmt.Printf(" -> \"%s\"\n", string(decResp.GetPlaintext())) 63 | 64 | os.Exit(0) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 5% 7 | patch: 8 | default: 9 | target: 50% 10 | threshold: 5% -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | // commitlint.config.js 2 | module.exports = { 3 | extends: ['@commitlint/config-conventional'], 4 | ignores: [(message) => /^Bumps \[.+]\(.+\) from .+ to .+\.$/m.test(message)], 5 | } 6 | -------------------------------------------------------------------------------- /docs/arch.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /docs/concepts.md: -------------------------------------------------------------------------------- 1 | # Concepts 2 | Read the official [Kubernetes KMS docs](https://kubernetes.io/docs/tasks/administer-cluster/kms-provider/) for more details. 3 | 4 | ## Encryption Request 5 | ```mermaid 6 | %%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#326ce5', 'primaryTextColor': '#fff', 'textColor': '#000'}}}%% 7 | sequenceDiagram 8 | participant etcd 9 | participant kubeapiserver 10 | participant kmsplugin 11 | participant externalkms 12 | kubeapiserver->>kmsplugin: encrypt request 13 | alt using key hierarchy 14 | kmsplugin->>kmsplugin: encrypt DEK with local KEK 15 | kmsplugin->>externalkms: encrypt local KEK with remote KEK 16 | externalkms->>kmsplugin: encrypted local KEK 17 | kmsplugin->>kmsplugin: cache encrypted local KEK 18 | kmsplugin->>kubeapiserver: return encrypt response
{"ciphertext": "", key_id: "",
"annotations": {"kms.kubernetes.io/local-kek": ""}} 19 | else not using key hierarchy 20 | %% current behavior 21 | kmsplugin->>externalkms: encrypt DEK with remote KEK 22 | externalkms->>kmsplugin: encrypted DEK 23 | kmsplugin->>kubeapiserver: return encrypt response
{"ciphertext": "", key_id: "", "annotations": {}} 24 | end 25 | kubeapiserver->>etcd: store encrypt response and encrypted DEK 26 | ``` 27 | 28 | 29 | ## Decryption Request 30 | ```mermaid 31 | %%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#326ce5', 'primaryTextColor': '#fff', 'textColor': '#000'}}}%% 32 | sequenceDiagram 33 | participant kubeapiserver 34 | participant kmsplugin 35 | participant externalkms 36 | %% if local KEK in annotations, then using hierarchy 37 | alt encrypted local KEK is in annotations 38 | kubeapiserver->>kmsplugin: decrypt request
{"ciphertext": "", key_id: "",
"annotations": {"kms.kubernetes.io/local-kek": ""}} 39 | alt encrypted local KEK in cache 40 | kmsplugin->>kmsplugin: decrypt DEK with local KEK 41 | else encrypted local KEK not in cache 42 | kmsplugin->>externalkms: decrypt local KEK with remote KEK 43 | externalkms->>kmsplugin: decrypted local KEK 44 | kmsplugin->>kmsplugin: decrypt DEK with local KEK 45 | kmsplugin->>kmsplugin: cache decrypted local KEK 46 | end 47 | kmsplugin->>kubeapiserver: return decrypt response
{"plaintext": "", key_id: "",
"annotations": {"kms.kubernetes.io/local-kek": ""}} 48 | else encrypted local KEK is not in annotations 49 | kubeapiserver->>kmsplugin: decrypt request
{"ciphertext": "", key_id: "",
"annotations": {}} 50 | kmsplugin->>externalkms: decrypt DEK with remote KEK (same behavior as today) 51 | externalkms->>kmsplugin: decrypted DEK 52 | kmsplugin->>kubeapiserver: return decrypt response
{"plaintext": "", key_id: "",
"annotations": {}} 53 | end 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | Enabling KMS Encryption in your Cluster involves 3 steps: 3 | 4 | 1. Preparing your Vaults Transit Engine & Auth Method 5 | 2. Deploying the `vault-kubernetes-kms` Plugin 6 | 3. Enabling the encryption provider configuration for the `kube-apiserver`. 7 | 8 | ## Preparing HashiCorp Vault 9 | The `vault-kms-plugin` requires a Vault Authentication, that allows encrypting and decrypting data with a given Transit Key. 10 | 11 | ### Transit Engine 12 | !!! tip 13 | You can also perform these steps using Vaults UI or Vaults Terraform Provider (recommended). 14 | 15 | The following steps enable a transit engine `transit` and create transit key `kms`: 16 | ```bash 17 | $> export VAULT_ADDR="https://vault.tld.de" # change to your Vaults API Address 18 | $> export VAULT_TOKEN="hvs.XXXX" # change to a token allowed to create a transit engine and a transit key 19 | $> vault secrets enable transit 20 | $> vault write -f transit/keys/kms 21 | ``` 22 | 23 | !!! warning 24 | Absolutely make sure to either Backup (snapshot) your Vault in order to recover from failure, or export your KMS key prior usage (option: `allow-plaintext-backup`). 25 | 26 | **If you loose the KMS key or recreate it, the kube-apiserver will not be able to decrypt any secrets.** 27 | 28 | 29 | ### Vault Policy 30 | The following Vault Policy lists the API paths required for the `vault-kubernetes-kms` plugin: 31 | 32 | !!! note 33 | This Policy assumes the transit engine is mounted at `transit` with a key named `kms`. 34 | In case your configuration differs, you will have to update the policy accordingly. 35 | 36 | ```hcl 37 | # kms-policy.hcl 38 | # lookup the current tokens ttl for token renewal, is also in Vaults default policy 39 | path "auth/token/lookup-self" { 40 | capabilities = ["read"] 41 | } 42 | 43 | # encrypt any data using the transit key 44 | path "transit/encrypt/kms" { 45 | capabilities = [ "update" ] 46 | } 47 | 48 | # decrypt any data using the transit key 49 | path "transit/decrypt/kms" { 50 | capabilities = [ "update" ] 51 | } 52 | 53 | # get the transit keys key versions for KMS key rotation 54 | path "transit/keys/kms" { 55 | capabilities = [ "read" ] 56 | } 57 | ``` 58 | 59 | You can create the policy using `vault policy write kms ./kms-policy.hcl`. 60 | 61 | ### Vault Auth 62 | `vault-kubernetes-kms` suppors Token & Approle Auth. Kubernetes Auth was removed (see [falcosuessgott/vault-kubernetes-kms#81](https://github.com/FalcoSuessgott/vault-kubernetes-kms/issues/81)), since a static pod cannot reference any other API objects, such as Service Account, which are required for Kubernetes Auth. 63 | 64 | ### Approle Auth 65 | 66 | ```bash 67 | # Follow https://developer.hashicorp.com/vault/docs/auth/approle 68 | # enable approle and create a role 69 | $> vault auth enable approle 70 | $> vault write auth/approle/role/kms token_num_uses=0 token_period=3600 token_policies=kms 71 | 72 | # get the role ID from the output of 73 | $> vault read auth/approle/role/kms/role-id 74 | 75 | # get the secret ID from the output of 76 | $> vault write -f auth/approle/role/kms/secret-id 77 | ``` 78 | 79 | ### Token Auth 80 | It is recommended, that the Vault token used for authentication is **an orphaned and periodic token**. Periodic tokens can be renewed within the period. An orphan token does not have a parent token and will not be revoked when the token that created it expires. 81 | 82 | ```bash 83 | # get the token from the output 84 | $> vault token create -orphan -policy="kms" -period=24h 85 | ``` 86 | 87 | ## Deploying `vault-kubernetes-kms` 88 | #### Container Images 89 | `vault-kubernetes-kms` is published on: 90 | 91 | - ghcr.io: [https://github.com/FalcoSuessgott/vault-kubernetes-kms/pkgs/container/vault-kubernetes-kms](https://github.com/FalcoSuessgott/vault-kubernetes-kms/pkgs/container/vault-kubernetes-kms) 92 | - Docker Hub: [https://hub.docker.com/r/falcosuessgott/vault-kubernetes-kms](https://hub.docker.com/r/falcosuessgott/vault-kubernetes-kms) 93 | 94 | As of now, only `amd` (x86_64) images are published. If you need `arm` images, raise an issue. 95 | 96 | 97 | !!! info 98 | The plugin creates a Unix-Socket that is referenced in a `EncryptionConfiguration` manifest, which the `kube-apiserver` points to. 99 | 100 | **That means, that the `kube-apiserver` will not properly start if the plugin is not up & running. To ensure the plugin is running before the `kube-apiserver` it has to be deployed as a static Pod.** To do so, we use `priorityClassName: system-node-critical` in the plugins manifest, to mark the Pod as critical ([https://kubernetes.io/docs/tasks/administer-cluster/guaranteed-scheduling-critical-addon-pods/#marking-pod-as-critical](https://kubernetes.io/docs/tasks/administer-cluster/guaranteed-scheduling-critical-addon-pods/#marking-pod-as-critical)). 101 | 102 | 103 | ### CLI Args & Environment Variables 104 | List of required and optional CLI args/env vars. **Furthermore, all of Vaults [Env Vars](https://developer.hashicorp.com/vault/docs/commands#environment-variables) are supported**: 105 | 106 | **Vault Server**: 107 | 108 | * **(Required)**: `-vault-address` (`VAULT_KMS_VAULT_ADDR`) 109 | * **(Optional)**: `-vault-namespace` (`VAULT_KMS_VAULT_NAMESPACE`) 110 | 111 | **Vault Transit Engine**: 112 | 113 | * **(Optional)**: `-transit-mount` (`VAULT_KMS_TRANSIT_MOUNT`); default: `"transit"` 114 | * **(Optional)**: `-transit-key` (`VAULT_KMS_TRANSIT_KEY`); default: `"kms"` 115 | 116 | **If Vault Token Auth**: 117 | 118 | * **(Required)**: `-auth-method="token"` (`VAULT_KMS_AUTH_METHOD`) 119 | * **(Required)**: `-token` (`VAULT_KMS_VAULT_TOKEN`) 120 | 121 | **If Vault Approle Auth**: 122 | 123 | * **(Required)**: `-auth-method="approle"` (`VAULT_KMS_AUTH_METHOD`) 124 | * **(Required)**: `-approle-role-id` (`VAULT_KMS_APPROLE_ROLE_ID`) 125 | * **(Required)**: `-approle-secret-id` (`VAULT_KMS_APPROLE_SECRET_ID`) 126 | * **(Optional)**: `-approle-mount` (`VAULT_KMS_APPROLE_MOUNT`); default: `"approle"` 127 | 128 | **Lease Refreshing Settings**: 129 | 130 | * **(Optional)**: `-token-refresh-interval` (`VAULT_KMS_TOKEN_REFRESH_INTERVAL`); default: `"60s"` 131 | * **(Optional)**: `-token-renewal` (`VAULT_KMS_TOKEN_RENEWAL`); default: `"3600"` 132 | 133 | !!! warning 134 | `vault_kubernetes_kms` automatically renewals the lease to avoid expired/revoked leases. 135 | 136 | It does so by periodically comparing the current TTL with the tokens creation TTL (See [https://developer.hashicorp.com/vault/tutorials/get-started/introduction-tokens#token-metadata](https://developer.hashicorp.com/vault/tutorials/get-started/introduction-tokens#token-metadata)). The check will be run periodically in the interval specified with `-token-refresh-interval` (default: `60s`). 137 | 138 | **If the current TTL is less than half of the creation TTL, then the current lease will be renewed for the amount of seconds defined in `-token-renewal` (default: `3600` seconds, `1h`).** 139 | 140 | **If, for whatever reason the token renewal fails, then the configured auth method will be executed again**. Meanwhile this works well with Approle authentication, token auth will most likely fail, due to the token being revoked. In that case, you should make sure to provide a [periodic token](https://falcosuessgott.github.io/vault-kubernetes-kms/configuration/#token-auth). 141 | 142 | **General**: 143 | 144 | * **(Optional)**: `-socket` (`VAULT_KMS_SOCKET`); default: `unix:///opt/kms/vaultkms.socket"` 145 | * **(Optional)**: `-force-socket-overwrite` (`FORCE_SOCKET_OVERWRITE`); default: `false`. 146 | 147 | !!! warning 148 | Use `-force-socket-overwrite` with caution. **This will delete whatever filetype exists at the value specified in `-socket` path on the Control plane node**. 149 | 150 | When `vault-kubernetes-kms` crashes, it is not guaranteed that the socket-file will always be removed. For those scenarios `-force-socket-overwrite` was introduced to allow a smooth re-deployment of the plugin and not having to manually delete the stale socket file on the control plane node. 151 | 152 | * **(Optional)**: `-debug` (`VAULT_KMS_DEBUG`) 153 | * **(Optional)**: `-health-port` (`VAULT_KMS_HEALTH_PORT`); default: `":8080"` 154 | * **(Optional)**: `-disable-v1` (`VAULT_KMS_DISABLE_V1`); default: `"true"` 155 | 156 | 157 | ### Example Vault Token Auth 158 | 159 | ```yaml 160 | apiVersion: v1 161 | kind: Pod 162 | metadata: 163 | name: vault-kubernetes-kms 164 | namespace: kube-system 165 | spec: 166 | priorityClassName: system-node-critical 167 | hostNetwork: true 168 | containers: 169 | - name: vault-kubernetes-kms 170 | image: falcosuessgott/vault-kubernetes-kms:latest 171 | # either specify CLI Args or env vars (look above) 172 | command: 173 | - /vault-kubernetes-kms 174 | - -vault-address=https://vault.server.de 175 | - -auth-method=token 176 | - -token=hvs.ABC123 177 | volumeMounts: 178 | - name: kms 179 | mountPath: /opt/kms 180 | livenessProbe: 181 | httpGet: 182 | path: /health 183 | port: 8080 184 | readinessProbe: 185 | httpGet: 186 | path: /live 187 | port: 8080 188 | resources: 189 | requests: 190 | cpu: 100m 191 | memory: 128Mi 192 | limits: 193 | cpu: 2 194 | memory: 1Gi 195 | volumes: 196 | - name: kms 197 | hostPath: 198 | path: /opt/kms 199 | ``` 200 | 201 | ### Example Vault Approle Auth 202 | 203 | ```yaml 204 | apiVersion: v1 205 | kind: Pod 206 | metadata: 207 | name: vault-kubernetes-kms 208 | namespace: kube-system 209 | spec: 210 | priorityClassName: system-node-critical 211 | hostNetwork: true 212 | containers: 213 | - name: vault-kubernetes-kms 214 | image: falcosuessgott/vault-kubernetes-kms:latest 215 | # either specify CLI Args or env vars (look above) 216 | command: 217 | - /vault-kubernetes-kms 218 | - -vault-address=https://vault.server.de 219 | - -auth-method=approle 220 | - -approle-role-id=XXXX 221 | - -approle-secret-id=XXXX 222 | volumeMounts: 223 | - name: kms 224 | mountPath: /opt/kms 225 | livenessProbe: 226 | httpGet: 227 | path: /health 228 | port: 8080 229 | readinessProbe: 230 | httpGet: 231 | path: /live 232 | port: 8080 233 | resources: 234 | requests: 235 | cpu: 100m 236 | memory: 128Mi 237 | limits: 238 | cpu: 2 239 | memory: 1Gi 240 | volumes: 241 | - name: kms 242 | hostPath: 243 | path: /opt/kms 244 | ``` 245 | 246 | ### Example TLS Configuration 247 | It is recommended, to specify the CA cert that issued the vault server certificate, to connect to the Vault Server using HTTPS. 248 | 249 | To do sou you would have to copy the PEM-encoded CA Cert on the node where the plugin is running and then adjust the manifest to mount that directory as a volume and then use that path and specify it in the [`"VAULT_CACERT"`](https://developer.hashicorp.com/vault/docs/commands#vault_cacert) environment variable. 250 | 251 | !!! note 252 | Note that you cant reference a secret here, because static Pod cant reference any other Kubernetes API Objects. 253 | 254 | Example: 255 | 256 | ```yaml 257 | apiVersion: v1 258 | kind: Pod 259 | metadata: 260 | name: vault-kubernetes-kms 261 | namespace: kube-system 262 | spec: 263 | priorityClassName: system-node-critical 264 | hostNetwork: true 265 | containers: 266 | - name: vault-kubernetes-kms 267 | image: falcosuessgott/vault-kubernetes-kms:latest 268 | # either specify CLI Args or env vars (look above) 269 | command: 270 | - /vault-kubernetes-kms 271 | - --vault-address=https://vault.server.de 272 | - -auth-method=token 273 | - -token=XXXX 274 | env: 275 | # add vaults CA file via env vars 276 | - name: VAULT_CACERT 277 | value: /opt/kms/ca.crt 278 | volumeMounts: 279 | # mount the volume 280 | - name: kms 281 | mountPath: /opt/kms 282 | livenessProbe: 283 | httpGet: 284 | path: /health 285 | port: 8080 286 | readinessProbe: 287 | httpGet: 288 | path: /live 289 | port: 8080 290 | resources: 291 | requests: 292 | cpu: 100m 293 | memory: 128Mi 294 | limits: 295 | cpu: 2 296 | memory: 1Gi 297 | volumes: 298 | - name: kms 299 | hostPath: 300 | # CA cert is located on node at /opt/kms/ca.crt 301 | path: /opt/kms 302 | ``` 303 | 304 | After applying you check the pods logs for any errors: 305 | 306 | ```bash 307 | $> kubectl logs -n kube-system vault-kubernetes-kms 308 | {"level":"info","timestamp":"2024-01-31T13:18:24.852Z","caller":"cmd/main.go:111","message":"starting kms plugin","socket":"unix:///opt/vaultkms.socket","debug":false,"vault-address":"http://host.minikube.internal:8200","vault-namespace":"","vault-token":"","vault-k8s-mount":"kubernetes","vault-k8s-role":"kms","vault-transit-mount":"transit","vault-transit-key":"kms"} 309 | {"level":"info","timestamp":"2024-01-31T13:18:24.898Z","caller":"cmd/main.go:144","message":"Successfully authenticated to vault"} 310 | {"level":"info","timestamp":"2024-01-31T13:18:24.898Z","caller":"cmd/main.go:151","message":"Successfully created unix socket","socket":"/opt/vaultkms.socket"} 311 | {"level":"info","timestamp":"2024-01-31T13:18:24.898Z","caller":"cmd/main.go:158","message":"Listening for connection"} 312 | {"level":"info","timestamp":"2024-01-31T13:18:24.898Z","caller":"cmd/main.go:169","message":"Successfully registered kms plugin"} 313 | ``` 314 | 315 | ## Enabling the encryption provider configuration for the `kube-apiserver`. 316 | ### Determine which KMS version to use 317 | Since the `vault-kms-plugin` supports both KMS versions, you would have to determine, which KMS Plugin version works for your setup: 318 | 319 | From the [Kubernetes documentation](https://kubernetes.io/docs/tasks/administer-cluster/kms-provider/#before-you-begin): 320 | 321 | !!! note 322 | The version of Kubernetes that you need depends on which KMS API version you have selected. Kubernetes recommends using KMS v2. 323 | 324 | **If you selected KMS API v2, you should use Kubernetes v1.29** (if you are running a different version of Kubernetes that also supports the v2 KMS API, switch to the documentation for that version of Kubernetes). 325 | 326 | **If you selected KMS API v1 to support clusters prior to version v1.27 or if you have a legacy KMS plugin that only supports KMS v1, any supported Kubernetes version will work**. This API is deprecated as of Kubernetes v1.28. Kubernetes does not recommend the use of this API. 327 | 328 | ### Encryption Provider configuration 329 | Copy the appropriate encryption provider configuration to your control plane nodes (e.g. `/opt/kms/encryption_provider_config.yml`): 330 | 331 | #### KMS Plugin v1 332 | ```yaml 333 | {!../scripts/encryption_provider_config_v1.yml!} 334 | ``` 335 | 336 | 337 | #### KMS Plugin v2 338 | ```yaml 339 | {!../scripts/encryption_provider_config_v2.yml!} 340 | ``` 341 | 342 | ### Modify the `kube-api-server` Manifest 343 | Last but not least, you would have to enable the encryption provider config for the `kube-apiserver`. 344 | This steps depends on wether your control plane components run as a systemd daemon or as static Pod on your control plane nodes (usually located at `/etc/kubernetes/manifests`). 345 | 346 | **Either way, the following changes need to be done:** 347 | 348 | ```yaml 349 | # ... 350 | spec: 351 | containers: 352 | - command: 353 | - kube-apiserver 354 | # enabling the encryption provider config 355 | - --encryption-provider-config=/opt/kms/encryption_provider_config.yml 356 | # ... 357 | ``` 358 | 359 | Also you will have to mount the `/opt` directory, for accessing the socket, that is created by the plugin and the encryption provider config: 360 | 361 | ```yaml 362 | # .... 363 | volumeMounts: 364 | - name: kms 365 | mountPath: /opt/kms 366 | volumes: 367 | - name: kms 368 | hostPath: 369 | path: /opt/kms 370 | # .... 371 | ``` 372 | 373 | Once changes are made, the `kube-apiserver` will restart (if static pod) or you restart the SystemD Unit. 374 | 375 | You then can use `watch` to monitor the pods: 376 | 377 | ```bash 378 | $> watch kubectl get pod -n kube-system 379 | NAME READY STATUS RESTARTS AGE 380 | coredns-76f75df574-dwtfv 1/1 Running 0 151m 381 | etcd-minikube 1/1 Running 0 152m 382 | kube-apiserver-minikube 0/1 Running 1 50s # restarted 383 | kube-controller-manager-minikube 1/1 Running 0 152m 384 | kube-proxy-hqpmw 1/1 Running 0 151m 385 | kube-scheduler-minikube 1/1 Running 0 152m 386 | storage-provisioner 1/1 Running 7 (118m ago) 152m 387 | vault-kubernetes-kms 1/1 Running 0 49m 388 | ``` 389 | 390 | You should now see in the plugin logs that encryption and decryption requests are coming: 391 | 392 | ```bash 393 | $> kubectl logs -n kube-system vault-kubernetes-kms 394 | {"level":"info","timestamp":"2024-01-31T13:31:29.159Z","caller":"kms/plugin.go:112","message":"encryption request","request_id":"f1eb6db8-390e-4bd4-8481-c56e46c1d685"} 395 | ``` 396 | 397 | ## Verify Secret Encryption 398 | Finally, create a secret to verify everything works correctly: 399 | 400 | ```bash 401 | $> kubectl create secret generic secret-encrypted -n default --from-literal=key=value 402 | secret/secret-encrypted created 403 | ``` 404 | 405 | **If the secret creation fails, something does not work!** 406 | 407 | You could also check the etcd storage for the encrypted data: 408 | 409 | ```bash 410 | # this works only on minikube, you would have to update the params according to your cluster config 411 | $> kubectl -n kube-system exec etcd-minikube -- sh -c "ETCDCTL_API=3 etcdctl 412 | --endpoints=https://127.0.0.1:2379 \ 413 | --cert /var/lib/minikube/certs/etcd/server.crt \ 414 | --key /var/lib/minikube/certs/etcd/server.key \ 415 | --cacert /var/lib/minikube/certs/etcd/ca.crt \ 416 | get /registry/secrets/default/secret-encrypted" | hexdump -C 417 | 00000000 2f 72 65 67 69 73 74 72 79 2f 73 65 63 72 65 74 |/registry/secret| 418 | 00000010 73 2f 64 65 66 61 75 6c 74 2f 73 65 63 72 65 74 |s/default/secret| 419 | 00000020 2d 65 6e 63 72 79 70 74 65 64 0a 6b 38 73 3a 65 |-encrypted.k8s:e| 420 | 00000030 6e 63 3a 6b 6d 73 3a 76 32 3a 76 61 75 6c 74 3a |nc:kms:v2:vault:| 421 | 00000040 0a a4 02 2f 0b 6f 44 78 f8 9b 2c 23 b7 d8 99 e0 |.../.oDx..,#....| 422 | 00000050 6f 2b 71 48 00 27 08 31 58 c2 2d 9c 8c 41 54 87 |o+qH.'.1X.-..AT.| 423 | 00000060 cd 38 7e 90 78 ea 5d 3d 81 5e d4 67 ac f9 11 25 |.8~.x.]=.^.g...%| 424 | 00000070 ca eb 68 f3 ae 43 e0 eb ce 0f fa dc d2 97 91 bb |..h..C..........| 425 | 00000080 e4 04 2f fe 7e f7 83 0f ef cc 4c 5e 41 f2 3f 42 |../.~.....L^A.?B| 426 | 00000090 5a 47 4d e4 3b 6d dc 78 e2 a3 65 f8 bb 84 88 e5 |ZGM.;m.x..e.....| 427 | 000000a0 9f 34 1f 53 d2 2a 59 8f ac 77 a4 58 57 e9 4b 06 |.4.S.*Y..w.XW.K.| 428 | 000000b0 f8 e9 80 f1 cf 96 aa 50 1a 24 1c 6a f6 6c e7 2d |.......P.$.j.l.-| 429 | 000000c0 58 ec 30 13 27 6c 4d 43 f5 60 07 8d 11 6f 43 4c |X.0.'lMC.`...oCL| 430 | 000000d0 ae 2b f0 69 01 18 05 a0 22 9b e9 9b 10 c6 83 7f |.+.i....".......| 431 | 000000e0 bb 5c 3e 89 cb 33 68 52 fc 16 c0 37 0a f9 8e 5d |.\>..3hR...7...]| 432 | 000000f0 7c 88 4f cd 02 f1 94 c9 69 52 d7 bc 61 7d b0 aa ||.O.....iR..a}..| 433 | 00000100 bd 4e 7b a1 d9 91 79 17 c8 2a 3d ec 1c a0 60 8e |.N{...y..*=...`.| 434 | 00000110 73 1c 1e 4e 1b 09 81 fb 3a 2b 6c 1c a4 87 7c 3f |s..N....:+l...|?| 435 | 00000120 f2 6a 21 1b f8 42 d4 33 57 64 da be 47 43 a8 92 |.j!..B.3Wd..GC..| 436 | 00000130 09 95 61 1b cd 97 5c 30 f1 e5 bf 21 ba 82 21 68 |..a...\0...!..!h| 437 | 00000140 3a 14 8b e9 0e 8a 83 6b ed de 24 f3 5b fd 02 f0 |:......k..$.[...| 438 | 00000150 bd 22 b1 ea f3 15 13 9d c2 a9 01 cf 36 78 5a 77 |."..........6xZw| 439 | 00000160 fd 83 fe 46 0e 49 bf 12 0a 31 37 30 36 37 30 37 |...F.I...1706707| 440 | 00000170 37 37 34 1a 59 76 61 75 6c 74 3a 76 31 3a 68 66 |774.Yvault:v1:hf| 441 | 00000180 54 61 73 58 37 39 4c 63 38 37 68 7a 38 48 77 31 |TasX79Lc87hz8Hw1| 442 | 00000190 6a 4e 6d 57 6a 6f 65 56 39 4d 55 73 43 4c 2f 74 |jNmWjoeV9MUsCL/t| 443 | 000001a0 6d 34 6d 78 4e 6a 41 46 2b 51 51 4a 72 36 4c 6b |m4mxNjAF+QQJr6Lk| 444 | 000001b0 36 52 69 6a 32 7a 62 57 73 57 44 44 70 65 30 6b |6Rij2zbWsWDDpe0k| 445 | 000001c0 39 68 59 72 4a 4b 39 2f 55 6c 30 69 79 42 28 01 |9hYrJK9/Ul0iyB(.| 446 | 000001d0 0a |.| 447 | 000001d1 448 | ``` 449 | -------------------------------------------------------------------------------- /docs/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FalcoSuessgott/vault-kubernetes-kms/4c69c5305d01488decb1387e3a4d69913d1169de/docs/dashboard.png -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | This guide walks you through the required steps of building and running this plugin locally with and without Kubernetes. 3 | 4 | ## Local Development without Kubernetes 5 | You don't have to deploy the plugin in a Kubernetes Cluster, you can just execute it locally, given you have a Vault running: 6 | 7 | ```bash 8 | $> make setup-vault 9 | $> go run main.go -vault-address=http://127.0.0.1:8200 -auth-method=token -token=root -socket=unix:///tmp/kms.socket 10 | {"level":"info","timestamp":"2024-08-25T15:36:19.233+1000","caller":"cmd/plugin.go:154","message":"starting kms plugin","auth-method":"token","socket":"unix:///tmp/kms.socket","debug":false,"vault-address":"http://127.0.0.1:8200","vault-namespace":"","transit-engine":"transit","transit-key":"kms","health-port":":8080","disable-v1":false} 11 | {"level":"info","timestamp":"2024-08-25T15:36:19.235+1000","caller":"cmd/plugin.go:167","message":"Successfully authenticated to vault"} 12 | {"level":"info","timestamp":"2024-08-25T15:36:19.235+1000","caller":"cmd/plugin.go:174","message":"Successfully dialed to unix domain socket","socket":"unix:///tmp/kms.socket"} 13 | {"level":"info","timestamp":"2024-08-25T15:36:19.235+1000","caller":"cmd/plugin.go:184","message":"Successfully registered kms plugin v1"} 14 | {"level":"info","timestamp":"2024-08-25T15:36:19.235+1000","caller":"cmd/plugin.go:191","message":"Successfully registered kms plugin v2"} 15 | ``` 16 | 17 | In order to send encryption and decryption requests you can use the client CLI tool in `cmd/v2_Client/main.go`. This tool simply connects to the plugin and encrypts a given string and decrypts it back to its plaintext version: 18 | 19 | ```bash 20 | $> go run cmd/v2_client/main.go encrypt this string 21 | "encrypt this string" -> "dmF1bHQ6djE6VzJMcHp4UmJMdHV4TWNnUnVWMWJQQzBHMWZ0VkwvZFVUMldLRzQ0RUtCa1VJcjVwVjgxMFd3T29pRmVhQzVNPQ==" -> "encrypt this string" 22 | ``` 23 | 24 | ## Local Development with Kubernetes 25 | 26 | The following steps describe how to build & run the vault-kubernetes-kms completely locally using `docker`, `vault` & `kind`. 27 | 28 | ### Requirements 29 | Obviously you will need all the tools mentioned above installed. Also this setup is only tested on Linux and MacOS. 30 | 31 | ### Components 32 | Basically, we will need: 33 | 34 | 1. A local Vault server initialized & unsealed and with a transit engine enabled as well as a transit key created. 35 | 2. A local (docker) registry so kind can pull the currently unreleased `vault-kubernetes-kms` image. 36 | 3. A local Kubernetes Cluster (kind) configured to use the local registry as well as the required settings for the kube-apiservers encryption provider config. 37 | 38 | #### 1. Local Vault Server using `vault` 39 | The following snippets sets up a local vault development server and creates a transit engine as well as a transit key. 40 | 41 | This script is located in `scripts/vault.sh` and is available via `make setup-vault`: 42 | 43 | ```bash 44 | {!../scripts/vault.sh!} 45 | ``` 46 | 47 | 48 | #### 2. Local Container/Docker Registry using `docker` 49 | The following snippet, starts a local container registry, builds the current commits `vault-kubernetes-kms` image and tags & pushes the image to the local registry. 50 | 51 | This script is located in `scripts/local-registry.sh` and is available via `make setup-registry`: 52 | 53 | ```bash 54 | {!../scripts/local-registry.sh!} 55 | ``` 56 | 57 | #### 3. Local Kubernetes Cluster using `kind` 58 | Last but not least, we combine the above mentioned tools and consume them with `kind` 59 | 60 | The following `kind`-config configures the local running registry, copies the encryption provider config and the `vault-kubernetes-kms` static pod manifest to the Kubernetes host and patches the `kube-apiserver` for using the provided encryption provider config. 61 | 62 | This can be run via `make setup-kind`, which runs `kind create cluster --name=kms --config scripts/kind-config.yaml` under the hood: 63 | 64 | ```yaml 65 | {!../scripts/kind-config_v2.yaml!} 66 | ``` 67 | 68 | **the `vault-kubernetes-kms` manifest:** 69 | 70 | for development purposes, we use the vault dev servers configured root token (`"root"`): 71 | 72 | ```yaml 73 | {!../scripts/vault-kubernetes-kms.yml!} 74 | ``` 75 | 76 | ## Putting it together 77 | So if you wanna run all components locally and build the current commits plugin, it would look like this: 78 | 79 | ```bash 80 | $> make setup-vault 81 | $> make setup-registry 82 | $> make setup-kind 83 | kind delete cluster --name=kms || true 84 | Deleting cluster "kms" ... 85 | Deleted nodes: ["kms-control-plane"] 86 | kind create cluster --name=kms --config scripts/kind-config.yaml 87 | Creating cluster "kms" ... 88 | ✓ Ensuring node image (kindest/node:v1.29.2) 🖼 89 | ✓ Preparing nodes 📦 90 | ✓ Writing configuration 📜 91 | ✓ Starting control-plane 🕹️ 92 | ✓ Installing CNI 🔌 93 | ✓ Installing StorageClass 💾 94 | Set kubectl context to "kind-kms" 95 | You can now use your cluster with: 96 | 97 | # testing kubectl 98 | $> kubectl get pod -A 99 | NAMESPACE NAME READY STATUS RESTARTS AGE 100 | kube-system coredns-76f75df574-7pzq4 1/1 Running 0 17m 101 | kube-system coredns-76f75df574-pkqrj 1/1 Running 0 17m 102 | kube-system etcd-kms-control-plane 1/1 Running 0 17m 103 | kube-system kindnet-w2hgj 1/1 Running 0 17m 104 | kube-system kube-apiserver-kms-control-plane 1/1 Running 0 17m 105 | kube-system kube-controller-manager-kms-control-plane 1/1 Running 0 17m 106 | kube-system kube-proxy-w66mx 1/1 Running 0 17m 107 | kube-system kube-scheduler-kms-control-plane 1/1 Running 0 17m 108 | kube-system vault-kubernetes-kms-kms-control-plane 1/1 Running 0 17m 109 | local-path-storage local-path-provisioner-7577fdbbfb-rmqq8 1/1 Running 0 17m 110 | 111 | # creating a kubernetes secret 112 | $> kubectl create secret generic secret -n default --from-literal=key=value 113 | secret/secret created 114 | 115 | # checking encryption value in etcd 116 | $> kubectl -n kube-system exec etcd-kms-control-plane -- sh -c "ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \ 117 | --cert /etc/kubernetes/pki/etcd/server.crt \ 118 | --key /etc/kubernetes/pki/etcd/server.key \ 119 | --cacert /etc/kubernetes/pki/etcd/ca.crt \ 120 | get /registry/secrets/default/secret" | hexdump -C 121 | 00000000 2f 72 65 67 69 73 74 72 79 2f 73 65 63 72 65 74 |/registry/secret| 122 | 00000010 73 2f 64 65 66 61 75 6c 74 2f 73 65 63 72 65 74 |s/default/secret| 123 | 00000020 2d 65 6e 63 72 79 70 74 65 64 0a 6b 38 73 3a 65 |-encrypted.k8s:e| 124 | 00000030 6e 63 3a 6b 6d 73 3a 76 32 3a 76 61 75 6c 74 2d |nc:kms:v2:vault-| 125 | 00000040 6b 75 62 65 72 6e 65 74 65 73 2d 6b 6d 73 3a 0a |kubernetes-kms:.| 126 | 00000050 a4 02 7f fe e1 bb 63 29 71 62 b6 1f c0 be d5 a0 |......c)qb......| 127 | 00000060 a8 38 0b e6 a1 bc 4b bb 16 ff 3f d3 3f 14 e4 be |.8....K...?.?...| 128 | 00000070 7e fa 53 de d5 06 75 08 3a fd 5f fb e9 a3 b1 29 |~.S...u.:._....)| 129 | 00000080 e2 9f 26 1c ef bb 1b 24 37 bc f3 ab 9c df 46 c4 |..&....$7.....F.| 130 | 00000090 8f 47 33 e5 c0 76 54 3b e7 f4 3b da 0d bf 80 e0 |.G3..vT;..;.....| 131 | 000000a0 52 88 cd 1a 6f c6 ec 7f bb 51 4b ef 0c c7 b6 8f |R...o....QK.....| 132 | 000000b0 31 2d 6b 96 3d 37 ee cb f0 56 83 40 d8 b4 21 75 |1-k.=7...V.@..!u| 133 | 000000c0 31 78 e7 ab ec 5f 6e f7 bf 84 86 34 2a aa 65 1b |1x..._n....4*.e.| 134 | 000000d0 8a 2b ce 6c ae 6f b6 df 11 5b ec 14 9d b9 00 74 |.+.l.o...[.....t| 135 | 000000e0 9d 0c 01 11 c4 67 48 67 3d d3 8f 58 1a 0d da 34 |.....gHg=..X...4| 136 | 000000f0 0d 55 19 91 cc 7e db c3 36 a2 6d 2f ea 28 10 ab |.U...~..6.m/.(..| 137 | 00000100 9b 1e 71 a9 d4 b1 74 6b 2f cc ef aa 30 d9 1a b8 |..q...tk/...0...| 138 | 00000110 68 30 3b 5b c5 3a 32 69 6a 75 4d 43 68 1f 33 23 |h0;[.:2ijuMCh.3#| 139 | 00000120 af 56 8c 15 c9 17 cb 8a 46 fc 9f 5a 24 da 25 16 |.V......F..Z$.%.| 140 | 00000130 15 31 ce 41 59 6b b8 c6 7d 5e b3 ee 07 a7 65 3b |.1.AYk..}^....e;| 141 | 00000140 a8 f2 8a ab e7 d0 37 bc 9c e6 e6 33 71 57 c5 6c |......7....3qW.l| 142 | 00000150 09 ff e9 65 c9 8c 9f aa 1c e2 df a4 ad fc a0 02 |...e............| 143 | 00000160 2b 6d 93 5e 44 20 64 28 d7 3f e1 98 eb 84 ab 22 |+m.^D d(.?....."| 144 | 00000170 82 92 7a b6 b2 b8 12 0a 31 37 31 31 32 34 32 33 |..z.....17112423| 145 | 00000180 36 34 1a 59 76 61 75 6c 74 3a 76 31 3a 6d 42 41 |64.Yvault:v1:mBA| 146 | 00000190 4a 47 56 56 35 72 46 78 36 47 4c 4f 62 33 46 50 |JGVV5rFx6GLOb3FP| 147 | 000001a0 37 4a 38 73 5a 79 4a 38 2f 68 36 61 48 2b 46 57 |7J8sZyJ8/h6aH+FW| 148 | 000001b0 55 46 2f 67 53 68 30 65 41 31 4e 51 45 47 6e 30 |UF/gSh0eA1NQEGn0| 149 | 000001c0 5a 30 38 66 6a 59 45 53 30 4c 31 79 35 45 49 50 |Z08fjYES0L1y5EIP| 150 | 000001d0 33 67 4c 72 77 61 35 4b 61 44 35 43 63 28 01 0a |3gLrwa5KaD5Cc(..| 151 | 000001e0 152 | 153 | # receiving the secret 154 | $> kubectl get secret secret -o json | jq '.data | map_values(@base64d)' 155 | { 156 | "key": "value" 157 | } 158 | ``` 159 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # vault-kms-plugin 2 | A Kubernetes KMS Plugin that uses [HashiCorp Vaults](https://developer.hashicorp.com/vault) [Transit Engine](https://developer.hashicorp.com/vault/docs/secrets/transit) for securely encrypting Secrets, Config Maps and other Kubernetes Objects in etcd at rest (on disk). 3 | 4 | [![E2E](https://github.com/FalcoSuessgott/vault-kubernetes-kms/actions/workflows/e2e.yml/badge.svg)](https://github.com/FalcoSuessgott/vault-kubernetes-kms/actions/workflows/e2e.yml) 5 | drawing drawing drawing 6 | 7 | 8 | ## Why 9 | HashiCorp Vault already offers useful [Kubernetes integrations](https://developer.hashicorp.com/vault/docs/platform/k8s), such as the Vault Secrets Operator for populating Kubernetes secrets from Vault Secrets or the Vault Agent Injector for injecting Vault secrets into a Pod using a sidecar container approach. 10 | 11 | Wouldn't it be nice if you could also use your Vault server to encrypt Kubernetes secrets before they are written into etcd? The `vault-kubernetes-kms` plugin does exactly this! 12 | 13 | Since the key used for encrypting secrets is not stored in Kubernetes, an attacker who intends to get unauthorized access to the plaintext values would need to compromise etcd and the Vault server. 14 | ## How does it work? 15 | ![img](arch.svg) 16 | 17 | 18 | `vault-kubernetes-kms` is supposed to run as a static pod on every control plane node or on that node where the `kube-apiserver` will run. 19 | 20 | The plugin creates a Unix-Socket and receive encryption requests through that socket from the `kube-apiserver`. The plugin will then use the specified Vault transit encryption key to encrypt the data and send it back to the `kube-apiserver`, who will then store the encrypted response in `etcd`. 21 | 22 | To do so, you will have to enable Data at Rest encryption, by configuring the `kube-apiserver` to use a `EncryptionConfiguration` (See [https://falcosuessgott.github.io/vault-kubernetes-kms/configuration/](https://falcosuessgott.github.io/vault-kubernetes-kms/configuration/) for more details). 23 | 24 | :warning: As a result of that, **the `kube-apiserver` requires the `vault-kubernetes-kms` plugin to be up & running before the `kube-apiserver` starts**. To ensure this, setting a priority class in the plugins manifest (`"priorityClassName: system-node-critical"`) is recommended. :warning: 25 | 26 | :warning: **`vault-kubernetes-kms` is in early stage! Running it in Production is not yet recommended. Im looking for early adopters in order to gather important feedback.** :warning: 27 | 28 | ## Features 29 | * support [Vault Token Auth](https://developer.hashicorp.com/vault/docs/auth/token) (not recommended for production), [AppRole](https://developer.hashicorp.com/vault/docs/auth/approle) and [Vault Kubernetes Auth](https://developer.hashicorp.com/vault/docs/auth/kubernetes) using the Plugins Service Account 30 | * support Kubernetes [KMS Plugin v1 (deprecated since `v1.28.0`) & v2 (stable in `v1.29.0`)](https://kubernetes.io/docs/tasks/administer-cluster/kms-provider/#before-you-begin) 31 | * [automatic Token Renewal for avoiding Token expiry](https://falcosuessgott.github.io/vault-kubernetes-kms/configuration/#cli-args-environment-variables) 32 | * [Exposes useful Prometheus Metrics](https://falcosuessgott.github.io/vault-kubernetes-kms/metrics/#prometheus-metrics) 33 | -------------------------------------------------------------------------------- /docs/integration.md: -------------------------------------------------------------------------------- 1 | # Integrations 2 | Collection of snippets to deploy the `vault-kubernetes-kms` plugin 3 | 4 | ## kubeadm 5 | * [https://kubernetes.io/docs/reference/config-api/kubeadm-config.v1beta3/#kubeadm-k8s-io-v1beta3-ClusterConfiguration](https://kubernetes.io/docs/reference/config-api/kubeadm-config.v1beta3/#kubeadm-k8s-io-v1beta3-ClusterConfiguration): 6 | 7 | ```yaml 8 | kind: ClusterConfiguration 9 | apiServer: 10 | extraArgs: 11 | encryption-provider-config: "/etc/kubernetes/encryption_provider_config_v2.yaml" 12 | extraVolumes: 13 | - name: encryption-config 14 | hostPath: "/etc/kubernetes/encryption_provider_config_v2.yaml" 15 | mountPath: "/etc/kubernetes/encryption_provider_config_v2.yaml" 16 | readOnly: true 17 | pathType: File 18 | - name: socket 19 | hostPath: "/opt/kms" 20 | mountPath: "/opt/kms" 21 | ``` 22 | 23 | ## kind 24 | * [https://kind.sigs.k8s.io/docs/user/configuration/](https://kind.sigs.k8s.io/docs/user/configuration/) 25 | 26 | ```yaml 27 | kind: Cluster 28 | apiVersion: kind.x-k8s.io/v1alpha4 29 | nodes: 30 | - role: control-plane 31 | extraMounts: 32 | # mount encryption provider config available on all cp nodes 33 | - containerPath: /etc/kubernetes/encryption_provider_config_v2.yaml 34 | hostPath: scripts/encryption_provider_config_v2.yml 35 | readOnly: true 36 | propagation: None 37 | # vault-kubernetes-kms as a static Pod 38 | - containerPath: /etc/kubernetes/manifests/vault-kubernetes-kms.yaml 39 | hostPath: scripts/vault-kubernetes-kms.yml 40 | readOnly: true 41 | propagation: None 42 | # patch kube-apiserver 43 | kubeadmConfigPatches: 44 | - | 45 | kind: ClusterConfiguration 46 | apiServer: 47 | extraArgs: 48 | encryption-provider-config: "/etc/kubernetes/encryption_provider_config_v2.yaml" 49 | extraVolumes: 50 | - name: encryption-config 51 | hostPath: "/etc/kubernetes/encryption_provider_config_v2.yaml" 52 | mountPath: "/etc/kubernetes/encryption_provider_config_v2.yaml" 53 | readOnly: true 54 | pathType: File 55 | - name: socket 56 | hostPath: "/opt/kms" 57 | mountPath: "/opt/kms" 58 | ``` 59 | 60 | ## kops 61 | * [https://kops.sigs.k8s.io/manifests_and_customizing_via_api/](https://kops.sigs.k8s.io/manifests_and_customizing_via_api/) 62 | 63 | ```yaml 64 | kind: Cluster 65 | spec: 66 | # patch kube-apiserver 67 | encryptionConfig: true 68 | # mount encryption provider config available on all cp nodes 69 | fileAssets: 70 | - name: scripts/encryption_provider_config_v2.yml 71 | path: /etc/kubernetes/encryption_provider_config_v2.yaml 72 | roles: 73 | - Master 74 | content: | 75 | kind: EncryptionConfiguration 76 | apiVersion: apiserver.config.k8s.io/v1 77 | resources: 78 | - resources: 79 | - secrets 80 | providers: 81 | - kms: 82 | apiVersion: v2 83 | name: vault-kubernetes-kms 84 | endpoint: unix:///opt/kms/vaultkms.socket 85 | - identity: {} 86 | # vault-kubernetes-kms as a static Pod 87 | - name: scripts/vault-kubernetes-kms.yml 88 | path: /etc/kubernetes/manifests/vault-kubernetes-kms.yaml 89 | roles: 90 | - Master 91 | content: | 92 | apiVersion: v1 93 | kind: Pod 94 | metadata: 95 | name: vault-kubernetes-kms 96 | namespace: kube-system 97 | spec: 98 | priorityClassName: system-node-critical 99 | hostNetwork: true 100 | containers: 101 | - name: vault-kubernetes-kms 102 | image: falcosuessgott/vault-kubernetes-kms:latest 103 | imagePullPolicy: IfNotPresent 104 | command: 105 | - /vault-kubernetes-kms 106 | - -vault-address=http://172.17.0.1:8200 107 | - -auth-method=token 108 | - -token=root 109 | volumeMounts: 110 | # mount /opt/kms host directory 111 | - name: kms 112 | mountPath: /opt/kms 113 | resources: 114 | requests: 115 | cpu: 100m 116 | memory: 128Mi 117 | limits: 118 | cpu: "2" 119 | memory: 1Gi 120 | volumes: 121 | # mount /opt/kms host directory 122 | - name: kms 123 | hostPath: 124 | path: /opt/kms 125 | ``` 126 | -------------------------------------------------------------------------------- /docs/metrics.md: -------------------------------------------------------------------------------- 1 | # Prometheus Metrics 2 | Beginning with `v1.0.0` `vault-kubernetes-kms` exposes metrics under `:8080/metrics` (change with `-health-port` or setting `HEALTH_PORT`). 3 | 4 | The following metrics are available: 5 | 6 | ## Available Prometheus Metrics 7 | | Metric Name | Type | Description | 8 | |---------------------------------------------------------------------|-----------|-----------------------------------------------------| 9 | | `vault_kubernetes_kms_decryption_operation_duration_seconds_bucket` | Histogram | duration of decryption operations in seconds | 10 | | `vault_kubernetes_kms_encryption_operation_duration_seconds_bucket` | Histogram | duration of encryption operations in seconds | 11 | | `vault_kubernetes_kms_decryption_operation_errors_total` | Counter | total number of errors during decryption operations | 12 | | `vault_kubernetes_kms_encryption_operation_errors_total` | Counter | total number of errors during encryption operations | 13 | | `vault_kubernetes_kms_token_expiry_seconds` | Gauge | time remaining until the current token expires | 14 | | `vault_kubernetes_kms_token_renewals_total` | Counter | total number of token renewals | 15 | 16 | Including the metrics defined in the [Prometheus Process Collector](https://github.com/prometheus/client_golang/blob/main/prometheus/process_collector.go#L38) (when running on `Linux`). 17 | 18 | Those metrics allow you to define your own Grafana Dashboard: 19 | 20 | ![img](./dashboard.png) 21 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | This Guide will walk you through the required steps of installing and configuring the `vault-kms-plugin` for Kubernetes. It currently uses token based authentication and HTTP communication, which is not secure enough when running in production. 3 | 4 | !!! tip 5 | Checkout [https://falcosuessgott.github.io/hashicorp-vault-playground/home/](https://falcosuessgott.github.io/hashicorp-vault-playground/home/) a project that helps you quickly setting up HashiCorp Vault locally with many useful Kubernetes Labs already pre configured. 6 | 7 | !!! warning 8 | This guide uses the new version of the Kubernetes KMS Plugin API, which was introduced in Kubernetes v1.29.0 ([https://kubernetes.io/docs/tasks/administer-cluster/kms-provider/#kms-v2](https://kubernetes.io/docs/tasks/administer-cluster/kms-provider/#kms-v2)). 9 | 10 | ## Requirements 11 | In order to run this guide, you will need to have `kind`, `kubectl` and `vault` installed on your system. This guide has been tested on MacOS and Linux. 12 | 13 | !!! note 14 | `vault-kubernetes-kms` is only published as `amd` (x86_64) images. 15 | 16 | You will make sure, you actually pull `amd` images. You can test it, by using `docker run -it ubuntu /usr/bin/uname -p` which, should output `86_64`. 17 | 18 | If you need `arm` images, raise an issue. 19 | 20 | ## Clone the repository 21 | ```bash 22 | $> git clone --depth 1 https://github.com/FalcoSuessgott/vault-kubernetes-kms.git 23 | $> cd vault-kubernetes-kms 24 | ``` 25 | 26 | ## Setup a Vault in development mode 27 | ```bash 28 | # invokes ./scripts/setup.vault.sh 29 | $> make setup-vault 30 | 31 | # point your vault CLI to the local Vault server 32 | $> export VAULT_ADDR="http://127.0.0.1:8200" 33 | $> export VAULT_SKIP_VERIFY="true" 34 | $> export VAULT_TOKEN="root" 35 | $> vault status 36 | Key Value 37 | --- ----- 38 | Seal Type shamir 39 | Initialized true 40 | Sealed false 41 | Total Shares 1 42 | Threshold 1 43 | Version 1.15.6 44 | Build Date 2024-02-28T17:07:34Z 45 | Storage Type inmem 46 | Cluster Name vault-cluster-32a0c10b 47 | Cluster ID 2081a49b-8372-3857-3754-b576e0877682 48 | HA Enabled false 49 | 50 | # a transit engine `transit` & key `kms` has been created 51 | $> vault list transit/keys 52 | Keys 53 | ---- 54 | kms 55 | ``` 56 | 57 | ## setup vault with encryption provider config usage 58 | Now, we have a local running Vault server, lets start a local running Kubernetes cluster using `kind`, which will deploy the `vault-kubernetes-kms` plugin as a static pod on the control plane as well as its required `encryption_provider_config` (see [https://falcosuessgott.github.io/vault-kubernetes-kms/configuration/](https://falcosuessgott.github.io/vault-kubernetes-kms/configuration/) for the required configuration steps): 59 | 60 | ```bash 61 | # can take up to 2 minutes 62 | $> kind create cluster --name=kms --config assets/kind-config.yaml 63 | Creating cluster "kms" ... 64 | ✓ Ensuring node image (kindest/node:v1.29.2) 🖼 65 | ✓ Preparing nodes 📦 66 | ✓ Writing configuration 📜 67 | ✓ Starting control-plane 🕹️ 68 | ✓ Installing CNI 🔌 69 | ✓ Installing StorageClass 💾 70 | Set kubectl context to "kind-kms" 71 | You can now use your cluster with: 72 | 73 | kubectl cluster-info --context kind-kms 74 | 75 | Have a nice day! 👋 76 | 77 | $> kubectl get pod -n kube-system 78 | NAME READY STATUS RESTARTS AGE 79 | coredns-76f75df574-q9nvb 1/1 Running 0 97s 80 | coredns-76f75df574-vwmxz 1/1 Running 0 97s 81 | etcd-kms-control-plane 1/1 Running 0 2m 82 | kindnet-wngbr 1/1 Running 0 98s 83 | kube-apiserver-kms-control-plane 1/1 Running 0 118s 84 | kube-controller-manager-kms-control-plane 1/1 Running 0 118s 85 | kube-proxy-rvl9z 1/1 Running 0 98s 86 | kube-scheduler-kms-control-plane 1/1 Running 0 2m 87 | vault-kubernetes-kms-kms-control-plane 1/1 Running 0 116s # vaul-kubernetes-kms pod 88 | ``` 89 | 90 | ## Creating Kubernetes secrets encrypted on etcd disk 91 | Time for creating new Kubernetes secrets and check how they are now stored in etcd storage due to a kms encryption provider configured: 92 | 93 | ```bash 94 | # create any secret 95 | $> kubectl create secret generic secret-encrypted -n default --from-literal=key=value 96 | secret/secret-encrypted created 97 | 98 | # show the secret 99 | §> kubectl get secret secret-encrypted -o json | jq '.data | map_values(@base64d)' 100 | { 101 | "key": "value" 102 | } 103 | 104 | # show secret in etcd storage 105 | $> kubectl -n kube-system exec etcd-kms-control-plane -- sh -c "ETCDCTL_API=3 etcdctl \ 106 | --endpoints=https://127.0.0.1:2379 \ 107 | --cert /etc/kubernetes/pki/etcd/server.crt \ 108 | --key /etc/kubernetes/pki/etcd/server.key \ 109 | --cacert /etc/kubernetes/pki/etcd/ca.crt \ 110 | get /registry/secrets/default/secret-encrypted" | hexdump -C 111 | 00000000 2f 72 65 67 69 73 74 72 79 2f 73 65 63 72 65 74 |/registry/secret| 112 | 00000010 73 2f 64 65 66 61 75 6c 74 2f 73 65 63 72 65 74 |s/default/secret| 113 | 00000020 2d 65 6e 63 72 79 70 74 65 64 0a 6b 38 73 3a 65 |-encrypted.k8s:e| 114 | 00000030 6e 63 3a 6b 6d 73 3a 76 32 3a 76 61 75 6c 74 2d |nc:kms:v2:vault-| 115 | 00000040 6b 75 62 65 72 6e 65 74 65 73 2d 6b 6d 73 3a 0a |kubernetes-kms:.| 116 | 00000050 a4 02 7f fe e1 bb 63 29 71 62 b6 1f c0 be d5 a0 |......c)qb......| 117 | 00000060 a8 38 0b e6 a1 bc 4b bb 16 ff 3f d3 3f 14 e4 be |.8....K...?.?...| 118 | 00000070 7e fa 53 de d5 06 75 08 3a fd 5f fb e9 a3 b1 29 |~.S...u.:._....)| 119 | 00000080 e2 9f 26 1c ef bb 1b 24 37 bc f3 ab 9c df 46 c4 |..&....$7.....F.| 120 | 00000090 8f 47 33 e5 c0 76 54 3b e7 f4 3b da 0d bf 80 e0 |.G3..vT;..;.....| 121 | 000000a0 52 88 cd 1a 6f c6 ec 7f bb 51 4b ef 0c c7 b6 8f |R...o....QK.....| 122 | 000000b0 31 2d 6b 96 3d 37 ee cb f0 56 83 40 d8 b4 21 75 |1-k.=7...V.@..!u| 123 | 000000c0 31 78 e7 ab ec 5f 6e f7 bf 84 86 34 2a aa 65 1b |1x..._n....4*.e.| 124 | 000000d0 8a 2b ce 6c ae 6f b6 df 11 5b ec 14 9d b9 00 74 |.+.l.o...[.....t| 125 | 000000e0 9d 0c 01 11 c4 67 48 67 3d d3 8f 58 1a 0d da 34 |.....gHg=..X...4| 126 | 000000f0 0d 55 19 91 cc 7e db c3 36 a2 6d 2f ea 28 10 ab |.U...~..6.m/.(..| 127 | 00000100 9b 1e 71 a9 d4 b1 74 6b 2f cc ef aa 30 d9 1a b8 |..q...tk/...0...| 128 | 00000110 68 30 3b 5b c5 3a 32 69 6a 75 4d 43 68 1f 33 23 |h0;[.:2ijuMCh.3#| 129 | 00000120 af 56 8c 15 c9 17 cb 8a 46 fc 9f 5a 24 da 25 16 |.V......F..Z$.%.| 130 | 00000130 15 31 ce 41 59 6b b8 c6 7d 5e b3 ee 07 a7 65 3b |.1.AYk..}^....e;| 131 | 00000140 a8 f2 8a ab e7 d0 37 bc 9c e6 e6 33 71 57 c5 6c |......7....3qW.l| 132 | 00000150 09 ff e9 65 c9 8c 9f aa 1c e2 df a4 ad fc a0 02 |...e............| 133 | 00000160 2b 6d 93 5e 44 20 64 28 d7 3f e1 98 eb 84 ab 22 |+m.^D d(.?....."| 134 | 00000170 82 92 7a b6 b2 b8 12 0a 31 37 31 31 32 34 32 33 |..z.....17112423| 135 | 00000180 36 34 1a 59 76 61 75 6c 74 3a 76 31 3a 6d 42 41 |64.Yvault:v1:mBA| # encrypted value 136 | 00000190 4a 47 56 56 35 72 46 78 36 47 4c 4f 62 33 46 50 |JGVV5rFx6GLOb3FP| 137 | 000001a0 37 4a 38 73 5a 79 4a 38 2f 68 36 61 48 2b 46 57 |7J8sZyJ8/h6aH+FW| 138 | 000001b0 55 46 2f 67 53 68 30 65 41 31 4e 51 45 47 6e 30 |UF/gSh0eA1NQEGn0| 139 | 000001c0 5a 30 38 66 6a 59 45 53 30 4c 31 79 35 45 49 50 |Z08fjYES0L1y5EIP| 140 | 000001d0 33 67 4c 72 77 61 35 4b 61 44 35 43 63 28 01 0a |3gLrwa5KaD5Cc(..| 141 | 000001e0 142 | ``` 143 | 144 | ## Encrypt all existing secrets 145 | You can encrypt all previous existing secrets using: 146 | 147 | ```bash 148 | $> kubectl get secrets --all-namespaces -o json | kubectl replace -f - 149 | ``` 150 | 151 | ## Verify decryption after restart 152 | If we restart the `kube-apiserver` the secrets have been Successfully decrypted: 153 | 154 | ```bash 155 | $> kubectl delete pod/etcd-kms-control-plane -n kube-system 156 | pod "kube-apiserver-minikube" deleted 157 | 158 | # secret has been successfully decrypted 159 | $> kubectl get secret secret-encrypted -o json | jq '.data | map_values(@base64d)' 160 | { 161 | "key": "value" 162 | } 163 | ``` 164 | 165 | ## Teardown 166 | ```bash 167 | # kind cluster 168 | $> kind delete cluster -n kms 169 | 170 | # vault 171 | $> kill $(pgrep -x vault) 172 | ``` 173 | 174 | ## Some last thoughts 175 | For production usage you should consider: 176 | 177 | * use HTTPS for the communication between Kubernetes & HashiCorp Vault (see [https://falcosuessgott.github.io/vault-kubernetes-kms/configuration/](https://falcosuessgott.github.io/vault-kubernetes-kms/configuration/)) 178 | * deploy the `vault-kubernetes-kms` plugin as a static pod on all control plane nodes 179 | * automate the deployment using your preferred automation method (see [https://falcosuessgott.github.io/vault-kubernetes-kms/integration/](https://falcosuessgott.github.io/vault-kubernetes-kms/integration/)) 180 | -------------------------------------------------------------------------------- /docs/sign.md: -------------------------------------------------------------------------------- 1 | # Artifact signing and SBOMs 2 | Since `v0.2.0` `vault-kubernetes-kms` release artifacts are signed using `cosign`. We also publish a SBOMs. 3 | 4 | ## SBOMs 5 | SBOMs are published for every release. See [https://github.com/FalcoSuessgott/vault-kubernetes-kms/releases](https://github.com/FalcoSuessgott/vault-kubernetes-kms/releases) 6 | 7 | ## Verify Container signatures 8 | `vault-kubernetes` Container Images are signed using `cosign` and ephemeral certificates signed during Github Actions runs. To verify a Container signature you can use the following oneliner: 9 | 10 | ```bash 11 | $> cosign verify ghcr.io/falcosuessgott/vault-kubernetes-kms:v0.2.1 \ 12 | --certificate-identity="https://github.com/FalcoSuessgott/vault-kubernetes-kms/.github/workflows/goreleaser.yml@refs/tags/v0.2.1" \ 13 | --certificate-oidc-issuer="https://token.actions.githubusercontent.com" | jq . 14 | 15 | Verification for ghcr.io/falcosuessgott/vault-kubernetes-kms:v0.2.1 -- 16 | The following checks were performed on each of these signatures: 17 | - The cosign claims were validated 18 | - Existence of the claims in the transparency log was verified offline 19 | - The code-signing certificate was verified using trusted certificate authority certificates 20 | [ 21 | { 22 | "critical": { 23 | "identity": { 24 | "docker-reference": "ghcr.io/falcosuessgott/vault-kubernetes-kms" 25 | }, 26 | "image": { 27 | "docker-manifest-digest": "sha256:f84b3e0acc178e1b8bcf13bb7868b93ef67b9fceff76ddfaaec85238dd026b31" 28 | }, 29 | "type": "cosign container image signature" 30 | }, 31 | "optional": { 32 | "1.3.6.1.4.1.57264.1.1": "https://token.actions.githubusercontent.com", 33 | "1.3.6.1.4.1.57264.1.2": "push", 34 | "1.3.6.1.4.1.57264.1.3": "74d0643af3e6528bdb7346de4f046e509e50a115", 35 | "1.3.6.1.4.1.57264.1.4": "goreleaser", 36 | "1.3.6.1.4.1.57264.1.5": "FalcoSuessgott/vault-kubernetes-kms", 37 | "1.3.6.1.4.1.57264.1.6": "refs/tags/v0.2.1", 38 | "Bundle": { 39 | "SignedEntryTimestamp": "MEUCID1CweAvlouvESD7+poEWehgdA4QGrSCk92BAFjdPEovAiEA+HipvN0melZl0/QjzHV7r3jgzibZ3gi0GsIIynhkrKo=", 40 | "Payload": { 41 | "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJkZmQyMzczMmVjYzBkM2RlNzA1ODcyZjhlNDFkYWM0YTc3ODJkYmYyYzgxOWUwYTM5NmE5NTA2MzE4ZWY5YzIzIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUNMbnBKdEoyL2wreW9ROUNaekhQaHdwQUhvVnhBb1h2TllYbnErS0pacWZ3SWhBSm1pTXRVeWtyNklsQ256NFY2V3dSQjUzQ1RCdkJTVjRWYmVRdS92R0p1dSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVaEtha05EUW5GNVowRjNTVUpCWjBsVlF6QmFjbVZsV1RGdVdFSlZWekpMVFdGM0x6Um9hVzFCWjJ3d2QwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDlFUlRKTlJHZDRUMVJKZVZkb1kwNU5hbEYzVDBSRk1rMUVaM2xQVkVsNVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZKVkU0eVowYzNlbUZxVmtsNFNrRlpiVEZ3WWtneVRGZ3hWbFZLY1daa05sQmhhRmdLTDNsTE0xQlNhbmRPTkZVeGVtWlJMMmMxTkhoemIweHZaelJvUlhoaWJUVktlVk4xV0dzMGEwazFhVzlyTnpKTlZrdFBRMEpqYzNkbloxaElUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZDVTBVeUNqRmlhbkowTW5kcVZtOUpNMVpSYVd4blNFeFpVME5KZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDJSbldVUldVakJTUVZGSUwwSkhkM2RoYjFwdllVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVERCYWFHSkhUblpWTTFac1l6Tk9iZ3BpTTFJd1RETmFhR1JYZURCTVYzUXhXVzFXZVdKdFZqQmFXRTEwWVRJeGVreDVOVzVoV0ZKdlpGZEpkbVF5T1hsaE1scHpZak5rZWt3eVpIWmpiVlp6Q2xwWFJucGFXRWwxWlZjeGMxRklTbXhhYmsxMlpFZEdibU41T1RKTlF6UjVUR3BGZDA5UldVdExkMWxDUWtGSFJIWjZRVUpCVVZGeVlVaFNNR05JVFRZS1RIazVNR0l5ZEd4aWFUVm9XVE5TY0dJeU5YcE1iV1J3WkVkb01WbHVWbnBhV0VwcVlqSTFNRnBYTlRCTWJVNTJZbFJCVTBKbmIzSkNaMFZGUVZsUEx3cE5RVVZEUWtGU2QyUllUbTlOUkZsSFEybHpSMEZSVVVKbk56aDNRVkZOUlV0RVl6QmFSRUV5VGtST2FGcHFUbXhPYWxWNVQwZEthMWxxWTNwT1JGcHJDbHBVVW0xTlJGRXlXbFJWZDA5WFZURk5SMFY0VFZSVmQwZEJXVXRMZDFsQ1FrRkhSSFo2UVVKQ1FWRkxXakk1ZVZwWGVHeFpXRTVzWTJwQmVFSm5iM0lLUW1kRlJVRlpUeTlOUVVWR1FrTk9SMWxYZUdwaU1VNHhXbGhPZWxveU9UQmtRemt5V1ZoV2MyUkRNWEprVjBwc1kyMDFiR1JIVm5wTVYzUjBZM3BCWlFwQ1oyOXlRbWRGUlVGWlR5OU5RVVZIUWtKQ2VWcFhXbnBNTTFKb1dqTk5kbVJxUVhWTmFUUjRUVVJ6UjBOcGMwZEJVVkZDWnpjNGQwRlJaMFZNVVhkeUNtRklVakJqU0UwMlRIazVNR0l5ZEd4aWFUVm9XVE5TY0dJeU5YcE1iV1J3WkVkb01WbHVWbnBhV0VwcVlqSTFNRnBYTlRCTWJVNTJZbFJDTkVKbmIzSUtRbWRGUlVGWlR5OU5RVVZLUWtkdlRXRkhhREJrU0VKNlQyazRkbG95YkRCaFNGWnBURzFPZG1KVE9VZFpWM2hxWWpGT01WcFlUbnBhTWprd1pFTTVNZ3BaV0ZaelpFTXhjbVJYU214amJUVnNaRWRXZWt4WGRIUmplVGgxV2pKc01HRklWbWxNTTJSMlkyMTBiV0pIT1ROamVUbHVZak5LYkdKSFZtaGpNbFo1Q2t4dWJIUmlSVUo1V2xkYWVrd3pVbWhhTTAxMlpHcEJkVTFwTkhoTlJHZEhRMmx6UjBGUlVVSm5OemgzUVZGdlJVdG5kMjlPZWxKclRVUlpNRTB5Um0wS1RUSlZNazVVU1RSWmJWSnBUbnBOTUU1dFVteE9SMWwzVGtSYWJFNVVRVFZhVkZWM1dWUkZlRTVVUVdSQ1oyOXlRbWRGUlVGWlR5OU5RVVZNUWtFNFRRcEVWMlJ3WkVkb01WbHBNVzlpTTA0d1dsZFJkMUpuV1V0TGQxbENRa0ZIUkhaNlFVSkVRVkUwUkVSYWIyUklVbmRqZW05MlRESmtjR1JIYURGWmFUVnFDbUl5TUhaU2JVWnpXVEk1VkdSWFZucGpNbVIyWkVoUmRtUnRSakZpU0ZGMFlUTldhVnBZU25WYVdGSnNZM2t4Y21KWVRYZFBRVmxMUzNkWlFrSkJSMFFLZG5wQlFrUlJVWEZFUTJjelRrZFJkMDVxVVhwWlYxbDZXbFJaTVUxcWFHbGFSMGt6VFhwUk1scEhWVEJhYWtFd1RtMVZNVTFFYkd4T1ZFSm9UVlJGTVFwTlEwRkhRMmx6UjBGUlVVSm5OemgzUVZFMFJVVm5kMUZqYlZadFkzazVNRmxYWkhwTU0xbDNUR3BKZFUxVVFWcENaMjl5UW1kRlJVRlpUeTlOUVVWUUNrSkJjMDFEVkdNd1QwUm5lRTlFVVROT1JFRjRRbWR2Y2tKblJVVkJXVTh2VFVGRlVVSkRUVTFKVjJnd1pFaENlazlwT0haYU1td3dZVWhXYVV4dFRuWUtZbE01UjFsWGVHcGlNVTR4V2xoT2Vsb3lPVEJrUkVGWlFtZHZja0puUlVWQldVOHZUVUZGVWtKQmIwMURSRVUwVFVSVmVFNUVZM2xOU0dkSFEybHpSd3BCVVZGQ1p6YzRkMEZTU1VWaFozaHZZVWhTTUdOSVRUWk1lVGx1WVZoU2IyUlhTWFZaTWpsMFREQmFhR0pIVG5aVk0xWnNZek5PYm1JelVqQk1NMXBvQ21SWGVEQk1WM1F4V1cxV2VXSnRWakJhV0UxMFlUSXhla3g1Tlc1aFdGSnZaRmRKZG1ReU9YbGhNbHB6WWpOa2Vrd3laSFpqYlZaeldsZEdlbHBZU1hVS1pWY3hjMUZJU214YWJrMTJaRWRHYm1ONU9USk5RelI1VEdwRmQwOUJXVXRMZDFsQ1FrRkhSSFo2UVVKRmQxRnhSRU5uTTA1SFVYZE9hbEY2V1ZkWmVncGFWRmt4VFdwb2FWcEhTVE5OZWxFeVdrZFZNRnBxUVRCT2JWVXhUVVJzYkU1VVFtaE5WRVV4VFVKUlIwTnBjMGRCVVZGQ1p6YzRkMEZTVVVWQ1ozZEZDbU5JVm5waFJFSnhRbWR2Y2tKblJVVkJXVTh2VFVGRlZrSkdkMDFYYldnd1pFaENlazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemxIV1ZkNGFtSXhUakVLV2xoT2Vsb3lPVEJrUXpreVdWaFdjMlJETVhKa1YwcHNZMjAxYkdSSFZucE1WM1IwWTNrNWFGa3pVbkJpTWpWNlRETktNV0p1VFhaTlZFRXdUVlJaTlFwTlJHTjZUWHBaZGxsWVVqQmFWekYzWkVoTmRrMVVRVmRDWjI5eVFtZEZSVUZaVHk5TlFVVlhRa0ZuVFVKdVFqRlpiWGh3V1hwRFFtbFJXVXRMZDFsQ0NrSkJTRmRsVVVsRlFXZFNOMEpJYTBGa2QwSXhRVTR3T1UxSGNrZDRlRVY1V1hoclpVaEtiRzVPZDB0cFUydzJORE5xZVhRdk5HVkxZMjlCZGt0bE5rOEtRVUZCUW10V2NFWkZlVEJCUVVGUlJFRkZXWGRTUVVsblRtRjRUa0pWYVd4dE5EVnVNR1pVY2xOeGREbEVhR0ZYYXk4NVowRnZlVWhhZEdaWVprUk1aZ292Vm1kRFNVVkpRVFk0YUZrNGEzUmhaRTB5YTNkS1IybzJTSGQ1ZGpSS1NIQktkRGdyU1RSTE5tWkRjRlZUV1ZOTlFXOUhRME54UjFOTk5EbENRVTFFQ2tFeVowRk5SMVZEVFVOU05UWXJkRW94WlVOTE5HZElUVFU1WVZVeUwwcHlRazlHVlZCbFNtTjNkR2RCT1RJNU0yNDNNVkI0TTJrd1FqSXljVVY0YkU0S2FuWkpPRkZTWjFGdmQwbDRRVWxxZDBGbk1HaE9SM0p3Wkc0eWRHWmpZMVZUZWxCaGRHNXVSWG81TkZwNU5VRm1Sa05hTW5SQmVETk9lRVYwUzFST09BbzVSemN3VDJOSWNuZG1Na05vZHowOUNpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwSyJ9fX19", 42 | "integratedTime": 1723796362, 43 | "logIndex": 121966596, 44 | "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d" 45 | } 46 | }, 47 | "Issuer": "https://token.actions.githubusercontent.com", 48 | "Subject": "https://github.com/FalcoSuessgott/vault-kubernetes-kms/.github/workflows/goreleaser.yml@refs/tags/v0.2.1", 49 | "githubWorkflowName": "goreleaser", 50 | "githubWorkflowRef": "refs/tags/v0.2.1", 51 | "githubWorkflowRepository": "FalcoSuessgott/vault-kubernetes-kms", 52 | "githubWorkflowSha": "74d0643af3e6528bdb7346de4f046e509e50a115", 53 | "githubWorkflowTrigger": "push" 54 | } 55 | }, 56 | { 57 | "critical": { 58 | "identity": { 59 | "docker-reference": "ghcr.io/falcosuessgott/vault-kubernetes-kms" 60 | }, 61 | "image": { 62 | "docker-manifest-digest": "sha256:f84b3e0acc178e1b8bcf13bb7868b93ef67b9fceff76ddfaaec85238dd026b31" 63 | }, 64 | "type": "cosign container image signature" 65 | }, 66 | "optional": { 67 | "1.3.6.1.4.1.57264.1.1": "https://token.actions.githubusercontent.com", 68 | "1.3.6.1.4.1.57264.1.2": "push", 69 | "1.3.6.1.4.1.57264.1.3": "74d0643af3e6528bdb7346de4f046e509e50a115", 70 | "1.3.6.1.4.1.57264.1.4": "goreleaser", 71 | "1.3.6.1.4.1.57264.1.5": "FalcoSuessgott/vault-kubernetes-kms", 72 | "1.3.6.1.4.1.57264.1.6": "refs/tags/v0.2.1", 73 | "Bundle": { 74 | "SignedEntryTimestamp": "MEQCIGgxn2BBR1cXbokbpNQeA9WtLiAk5sf0qmcSsEwCzq6FAiB7D/epTTwqkJGmaPAa86e0DDksYxlEup190+EZU6wxKA==", 75 | "Payload": { 76 | "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJkZmQyMzczMmVjYzBkM2RlNzA1ODcyZjhlNDFkYWM0YTc3ODJkYmYyYzgxOWUwYTM5NmE5NTA2MzE4ZWY5YzIzIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJUURSa1grZkdSTzZLRFJObDh0bmpoYmwyalp3UllESUlkdStvcDlwNEZ4UEF3SWdSYWR4U1hDN2lYWWRyZVQ2YUtlem5YQzliemtQZkNNb2E3TkNucS9tbUg0PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVaEtla05EUW5FeVowRjNTVUpCWjBsVllWZHVaR2d2VEhkRmFuYzBXbFpCVERoQ01sTnNWR2hNVWpKVmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDlFUlRKTlJHZDRUMVJKTUZkb1kwNU5hbEYzVDBSRk1rMUVaM2xQVkVrd1YycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZYU0ZvM1ozaFdjREU0T1hCTGNWbHdlazF1ZVV0SVRHTnRLM0JWZUUxUlZVVm5TbGNLVjI1NFIydFVhR2R2SzBscGFFYzBLMVZZVUhKd2VrOXlTR3hJTlRsSlRYbDNielkzZW1nclVuTnBkWGQ0YVRCdWVVdFBRMEpqZDNkbloxaEpUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlYzV2xKV0NuWnZOamxpWWsxNE0wTnFWREYxUkRCUVdsTTJWbUpuZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDJSbldVUldVakJTUVZGSUwwSkhkM2RoYjFwdllVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVERCYWFHSkhUblpWTTFac1l6Tk9iZ3BpTTFJd1RETmFhR1JYZURCTVYzUXhXVzFXZVdKdFZqQmFXRTEwWVRJeGVreDVOVzVoV0ZKdlpGZEpkbVF5T1hsaE1scHpZak5rZWt3eVpIWmpiVlp6Q2xwWFJucGFXRWwxWlZjeGMxRklTbXhhYmsxMlpFZEdibU41T1RKTlF6UjVUR3BGZDA5UldVdExkMWxDUWtGSFJIWjZRVUpCVVZGeVlVaFNNR05JVFRZS1RIazVNR0l5ZEd4aWFUVm9XVE5TY0dJeU5YcE1iV1J3WkVkb01WbHVWbnBhV0VwcVlqSTFNRnBYTlRCTWJVNTJZbFJCVTBKbmIzSkNaMFZGUVZsUEx3cE5RVVZEUWtGU2QyUllUbTlOUkZsSFEybHpSMEZSVVVKbk56aDNRVkZOUlV0RVl6QmFSRUV5VGtST2FGcHFUbXhPYWxWNVQwZEthMWxxWTNwT1JGcHJDbHBVVW0xTlJGRXlXbFJWZDA5WFZURk5SMFY0VFZSVmQwZEJXVXRMZDFsQ1FrRkhSSFo2UVVKQ1FWRkxXakk1ZVZwWGVHeFpXRTVzWTJwQmVFSm5iM0lLUW1kRlJVRlpUeTlOUVVWR1FrTk9SMWxYZUdwaU1VNHhXbGhPZWxveU9UQmtRemt5V1ZoV2MyUkRNWEprVjBwc1kyMDFiR1JIVm5wTVYzUjBZM3BCWlFwQ1oyOXlRbWRGUlVGWlR5OU5RVVZIUWtKQ2VWcFhXbnBNTTFKb1dqTk5kbVJxUVhWTmFUUjRUVVJ6UjBOcGMwZEJVVkZDWnpjNGQwRlJaMFZNVVhkeUNtRklVakJqU0UwMlRIazVNR0l5ZEd4aWFUVm9XVE5TY0dJeU5YcE1iV1J3WkVkb01WbHVWbnBhV0VwcVlqSTFNRnBYTlRCTWJVNTJZbFJDTkVKbmIzSUtRbWRGUlVGWlR5OU5RVVZLUWtkdlRXRkhhREJrU0VKNlQyazRkbG95YkRCaFNGWnBURzFPZG1KVE9VZFpWM2hxWWpGT01WcFlUbnBhTWprd1pFTTVNZ3BaV0ZaelpFTXhjbVJYU214amJUVnNaRWRXZWt4WGRIUmplVGgxV2pKc01HRklWbWxNTTJSMlkyMTBiV0pIT1ROamVUbHVZak5LYkdKSFZtaGpNbFo1Q2t4dWJIUmlSVUo1V2xkYWVrd3pVbWhhTTAxMlpHcEJkVTFwTkhoTlJHZEhRMmx6UjBGUlVVSm5OemgzUVZGdlJVdG5kMjlPZWxKclRVUlpNRTB5Um0wS1RUSlZNazVVU1RSWmJWSnBUbnBOTUU1dFVteE9SMWwzVGtSYWJFNVVRVFZhVkZWM1dWUkZlRTVVUVdSQ1oyOXlRbWRGUlVGWlR5OU5RVVZNUWtFNFRRcEVWMlJ3WkVkb01WbHBNVzlpTTA0d1dsZFJkMUpuV1V0TGQxbENRa0ZIUkhaNlFVSkVRVkUwUkVSYWIyUklVbmRqZW05MlRESmtjR1JIYURGWmFUVnFDbUl5TUhaU2JVWnpXVEk1VkdSWFZucGpNbVIyWkVoUmRtUnRSakZpU0ZGMFlUTldhVnBZU25WYVdGSnNZM2t4Y21KWVRYZFBRVmxMUzNkWlFrSkJSMFFLZG5wQlFrUlJVWEZFUTJjelRrZFJkMDVxVVhwWlYxbDZXbFJaTVUxcWFHbGFSMGt6VFhwUk1scEhWVEJhYWtFd1RtMVZNVTFFYkd4T1ZFSm9UVlJGTVFwTlEwRkhRMmx6UjBGUlVVSm5OemgzUVZFMFJVVm5kMUZqYlZadFkzazVNRmxYWkhwTU0xbDNUR3BKZFUxVVFWcENaMjl5UW1kRlJVRlpUeTlOUVVWUUNrSkJjMDFEVkdNd1QwUm5lRTlFVVROT1JFRjRRbWR2Y2tKblJVVkJXVTh2VFVGRlVVSkRUVTFKVjJnd1pFaENlazlwT0haYU1td3dZVWhXYVV4dFRuWUtZbE01UjFsWGVHcGlNVTR4V2xoT2Vsb3lPVEJrUkVGWlFtZHZja0puUlVWQldVOHZUVUZGVWtKQmIwMURSRVUwVFVSVmVFNUVZM2xOU0dkSFEybHpSd3BCVVZGQ1p6YzRkMEZTU1VWaFozaHZZVWhTTUdOSVRUWk1lVGx1WVZoU2IyUlhTWFZaTWpsMFREQmFhR0pIVG5aVk0xWnNZek5PYm1JelVqQk1NMXBvQ21SWGVEQk1WM1F4V1cxV2VXSnRWakJhV0UxMFlUSXhla3g1Tlc1aFdGSnZaRmRKZG1ReU9YbGhNbHB6WWpOa2Vrd3laSFpqYlZaeldsZEdlbHBZU1hVS1pWY3hjMUZJU214YWJrMTJaRWRHYm1ONU9USk5RelI1VEdwRmQwOUJXVXRMZDFsQ1FrRkhSSFo2UVVKRmQxRnhSRU5uTTA1SFVYZE9hbEY2V1ZkWmVncGFWRmt4VFdwb2FWcEhTVE5OZWxFeVdrZFZNRnBxUVRCT2JWVXhUVVJzYkU1VVFtaE5WRVV4VFVKUlIwTnBjMGRCVVZGQ1p6YzRkMEZTVVVWQ1ozZEZDbU5JVm5waFJFSnhRbWR2Y2tKblJVVkJXVTh2VFVGRlZrSkdkMDFYYldnd1pFaENlazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemxIV1ZkNGFtSXhUakVLV2xoT2Vsb3lPVEJrUXpreVdWaFdjMlJETVhKa1YwcHNZMjAxYkdSSFZucE1WM1IwWTNrNWFGa3pVbkJpTWpWNlRETktNV0p1VFhaTlZFRXdUVlJaTlFwTlJHTjZUWHBaZGxsWVVqQmFWekYzWkVoTmRrMVVRVmRDWjI5eVFtZEZSVUZaVHk5TlFVVlhRa0ZuVFVKdVFqRlpiWGh3V1hwRFFtbG5XVXRMZDFsQ0NrSkJTRmRsVVVsRlFXZFNPRUpJYjBGbFFVSXlRVTR3T1UxSGNrZDRlRVY1V1hoclpVaEtiRzVPZDB0cFUydzJORE5xZVhRdk5HVkxZMjlCZGt0bE5rOEtRVUZCUW10V2NFWklVVTFCUVVGUlJFRkZZM2RTVVVsblQya3pMekoyVkV3eE5HRTJjVVkxUkdzNE0xSkRhWEJtTmtWSVVUVlRjR3BvY2xCUWRqVlNPQXBYV1VWRFNWRkVibE5vZHpoMFJXUnNXV1JoT1N0SldrMU9iMEV2YmpkMlVsZFJTa2xKV0dneWJHZGFVRUV4U0ZSMWFrRkxRbWRuY1docmFrOVFVVkZFQ2tGM1RtOUJSRUpzUVdwRlFUQmxjRmxJVkhWTmJtRllUVlJDUWt0cmJGVlVha1J4ZDFoNFJTOVRSbmRYY1RSM00zUktPU3RoTUdsMEwwcDNTV2RpUW1ZS1JDOXFjMlJYV1Uxck9XeFdRV3BDZEV4YVZVRXdOSGd6ZUdVcmVtTkhPRTUzY1dOVE5XMUNWbTlZZVU5dWJtUkpkRmhxUVdsb1lXWnpkRzg0YzNBek1BcDZia1ZZTVdWMlZGbDFWekpRYVZFOUNpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwSyJ9fX19", 77 | "integratedTime": 1723796365, 78 | "logIndex": 121966609, 79 | "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d" 80 | } 81 | }, 82 | "Issuer": "https://token.actions.githubusercontent.com", 83 | "Subject": "https://github.com/FalcoSuessgott/vault-kubernetes-kms/.github/workflows/goreleaser.yml@refs/tags/v0.2.1", 84 | "githubWorkflowName": "goreleaser", 85 | "githubWorkflowRef": "refs/tags/v0.2.1", 86 | "githubWorkflowRepository": "FalcoSuessgott/vault-kubernetes-kms", 87 | "githubWorkflowSha": "74d0643af3e6528bdb7346de4f046e509e50a115", 88 | "githubWorkflowTrigger": "push" 89 | } 90 | } 91 | ] 92 | ``` 93 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | ## Rollback 4 | -> Follow the official [Kubernetes documentation](https://kubernetes.io/docs/tasks/administer-cluster/decrypt-data/#decrypting-all-data) for decryption all data again. 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/FalcoSuessgott/vault-kubernetes-kms 2 | 3 | go 1.23.3 4 | 5 | require ( 6 | github.com/caarlos0/env/v6 v6.10.1 7 | github.com/hashicorp/vault/api v1.16.0 8 | github.com/prometheus/client_golang v1.22.0 9 | github.com/stretchr/testify v1.10.0 10 | github.com/testcontainers/testcontainers-go v0.37.0 11 | github.com/testcontainers/testcontainers-go/modules/vault v0.37.0 12 | go.uber.org/zap v1.27.0 13 | google.golang.org/grpc v1.72.2 14 | gotest.tools/gotestsum v1.12.2 15 | k8s.io/kms v0.31.4 16 | ) 17 | 18 | require ( 19 | dario.cat/mergo v1.0.1 // indirect 20 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 21 | github.com/Microsoft/go-winio v0.6.2 // indirect 22 | github.com/beorn7/perks v1.0.1 // indirect 23 | github.com/bitfield/gotestdox v0.2.2 // indirect 24 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 25 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 26 | github.com/containerd/log v0.1.0 // indirect 27 | github.com/containerd/platforms v0.2.1 // indirect 28 | github.com/cpuguy83/dockercfg v0.3.2 // indirect 29 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 30 | github.com/distribution/reference v0.6.0 // indirect 31 | github.com/dnephin/pflag v1.0.7 // indirect 32 | github.com/docker/docker v28.0.1+incompatible // indirect 33 | github.com/docker/go-connections v0.5.0 // indirect 34 | github.com/docker/go-units v0.5.0 // indirect 35 | github.com/ebitengine/purego v0.8.2 // indirect 36 | github.com/fatih/color v1.18.0 // indirect 37 | github.com/felixge/httpsnoop v1.0.4 // indirect 38 | github.com/fsnotify/fsnotify v1.8.0 // indirect 39 | github.com/go-jose/go-jose/v4 v4.0.5 // indirect 40 | github.com/go-logr/logr v1.4.2 // indirect 41 | github.com/go-logr/stdr v1.2.2 // indirect 42 | github.com/go-ole/go-ole v1.3.0 // indirect 43 | github.com/go-test/deep v1.1.0 // indirect 44 | github.com/gogo/protobuf v1.3.2 // indirect 45 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 46 | github.com/google/uuid v1.6.0 // indirect 47 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect 48 | github.com/hashicorp/errwrap v1.1.0 // indirect 49 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 50 | github.com/hashicorp/go-multierror v1.1.1 // indirect 51 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 52 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 53 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 // indirect 54 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 55 | github.com/hashicorp/go-sockaddr v1.0.6 // indirect 56 | github.com/hashicorp/hcl v1.0.1-vault-5 // indirect 57 | github.com/klauspost/compress v1.18.0 // indirect 58 | github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect 59 | github.com/magiconair/properties v1.8.10 // indirect 60 | github.com/mattn/go-colorable v0.1.13 // indirect 61 | github.com/mattn/go-isatty v0.0.20 // indirect 62 | github.com/mitchellh/go-homedir v1.1.0 // indirect 63 | github.com/mitchellh/mapstructure v1.5.0 // indirect 64 | github.com/moby/docker-image-spec v1.3.1 // indirect 65 | github.com/moby/patternmatcher v0.6.0 // indirect 66 | github.com/moby/sys/sequential v0.5.0 // indirect 67 | github.com/moby/sys/user v0.1.0 // indirect 68 | github.com/moby/sys/userns v0.1.0 // indirect 69 | github.com/moby/term v0.5.0 // indirect 70 | github.com/morikuni/aec v1.0.0 // indirect 71 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 72 | github.com/opencontainers/go-digest v1.0.0 // indirect 73 | github.com/opencontainers/image-spec v1.1.1 // indirect 74 | github.com/pkg/errors v0.9.1 // indirect 75 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 76 | github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect 77 | github.com/prometheus/client_model v0.6.1 // indirect 78 | github.com/prometheus/common v0.62.0 // indirect 79 | github.com/prometheus/procfs v0.15.1 // indirect 80 | github.com/ryanuber/go-glob v1.0.0 // indirect 81 | github.com/shirou/gopsutil/v4 v4.25.1 // indirect 82 | github.com/sirupsen/logrus v1.9.3 // indirect 83 | github.com/tklauser/go-sysconf v0.3.13 // indirect 84 | github.com/tklauser/numcpus v0.7.0 // indirect 85 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 86 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 87 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 88 | go.opentelemetry.io/otel v1.35.0 // indirect 89 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 90 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 91 | go.uber.org/multierr v1.10.0 // indirect 92 | golang.org/x/crypto v0.37.0 // indirect 93 | golang.org/x/mod v0.20.0 // indirect 94 | golang.org/x/net v0.38.0 // indirect 95 | golang.org/x/sync v0.13.0 // indirect 96 | golang.org/x/sys v0.32.0 // indirect 97 | golang.org/x/term v0.31.0 // indirect 98 | golang.org/x/text v0.24.0 // indirect 99 | golang.org/x/time v0.5.0 // indirect 100 | golang.org/x/tools v0.24.0 // indirect 101 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect 102 | google.golang.org/protobuf v1.36.5 // indirect 103 | gopkg.in/yaml.v3 v3.0.1 // indirect 104 | ) 105 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 2 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= 4 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 5 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= 6 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 7 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 8 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 9 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 10 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 11 | github.com/bitfield/gotestdox v0.2.2 h1:x6RcPAbBbErKLnapz1QeAlf3ospg8efBsedU93CDsnE= 12 | github.com/bitfield/gotestdox v0.2.2/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8eqRJ3N+9pY= 13 | github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II= 14 | github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= 15 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 16 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 17 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 18 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 19 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 20 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 21 | github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= 22 | github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= 23 | github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= 24 | github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= 25 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 26 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 27 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 29 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 30 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 31 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 32 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 33 | github.com/dnephin/pflag v1.0.7 h1:oxONGlWxhmUct0YzKTgrpQv9AUA1wtPBn7zuSjJqptk= 34 | github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE= 35 | github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0= 36 | github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 37 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 38 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 39 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 40 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 41 | github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= 42 | github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 43 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 44 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 45 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 46 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 47 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 48 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 49 | github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= 50 | github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= 51 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 52 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 53 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 54 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 55 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 56 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 57 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 58 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 59 | github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= 60 | github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 61 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 62 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 63 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 64 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 65 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 66 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 67 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 68 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 69 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 70 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 71 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 72 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= 73 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= 74 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 75 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 76 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 77 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 78 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 79 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 80 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 81 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 82 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 83 | github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= 84 | github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= 85 | github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= 86 | github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= 87 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 h1:iBt4Ew4XEGLfh6/bPk4rSYmuZJGizr6/x/AEizP0CQc= 88 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8/go.mod h1:aiJI+PIApBRQG7FZTEBx5GiiX+HbOHilUdNxUZi4eV0= 89 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= 90 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= 91 | github.com/hashicorp/go-sockaddr v1.0.6 h1:RSG8rKU28VTUTvEKghe5gIhIQpv8evvNpnDEyqO4u9I= 92 | github.com/hashicorp/go-sockaddr v1.0.6/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI= 93 | github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= 94 | github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= 95 | github.com/hashicorp/vault-client-go v0.4.3 h1:zG7STGVgn/VK6rnZc0k8PGbfv2x/sJExRKHSUg3ljWc= 96 | github.com/hashicorp/vault-client-go v0.4.3/go.mod h1:4tDw7Uhq5XOxS1fO+oMtotHL7j4sB9cp0T7U6m4FzDY= 97 | github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4= 98 | github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA= 99 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 100 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 101 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 102 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 103 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 104 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 105 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 106 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 107 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 108 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 109 | github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed h1:036IscGBfJsFIgJQzlui7nK1Ncm0tp2ktmPj8xO4N/0= 110 | github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= 111 | github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= 112 | github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 113 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 114 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 115 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 116 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 117 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 118 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 119 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 120 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 121 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 122 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 123 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 124 | github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= 125 | github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= 126 | github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= 127 | github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= 128 | github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= 129 | github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= 130 | github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= 131 | github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= 132 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 133 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 134 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 135 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 136 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 137 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 138 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 139 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 140 | github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= 141 | github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 142 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 143 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 144 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 145 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 146 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 147 | github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= 148 | github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 149 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 150 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 151 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 152 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 153 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 154 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 155 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 156 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 157 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 158 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 159 | github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= 160 | github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= 161 | github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs= 162 | github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= 163 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 164 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 165 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 166 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 167 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 168 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 169 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 170 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 171 | github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg= 172 | github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM= 173 | github.com/testcontainers/testcontainers-go/modules/vault v0.37.0 h1:4vMGutogL0vz+PV3LKlxEVKwO2AndirEkfvoTp0SHyI= 174 | github.com/testcontainers/testcontainers-go/modules/vault v0.37.0/go.mod h1:BzVDYWZ5KZcsAN4KR/OIjM74TJt4NahOPKaKKJBI2dI= 175 | github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= 176 | github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 177 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 178 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 179 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 180 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 181 | github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= 182 | github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= 183 | github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= 184 | github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY= 185 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 186 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 187 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= 188 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 189 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 190 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 191 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= 192 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= 193 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 194 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 195 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= 196 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= 197 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= 198 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= 199 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 200 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 201 | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 202 | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 203 | go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= 204 | go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= 205 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 206 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 207 | go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= 208 | go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= 209 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 210 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 211 | go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= 212 | go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 213 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 214 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 215 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 216 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 217 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 218 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 219 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 220 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 221 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 222 | golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= 223 | golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 224 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 225 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 226 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 227 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 228 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 229 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 230 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 231 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 232 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 233 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 234 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 235 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 236 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 237 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 238 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 239 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 240 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 241 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 242 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 243 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 244 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 245 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 246 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 247 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 248 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 249 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 250 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 251 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 252 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 253 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 254 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 255 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 256 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 257 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 258 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 259 | golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= 260 | golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= 261 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 262 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 263 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 264 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 265 | google.golang.org/genproto v0.0.0-20230526203410-71b5a4ffd15e h1:Ao9GzfUMPH3zjVfzXG5rlWlk+Q8MXWKwWpwVQE1MXfw= 266 | google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= 267 | google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= 268 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= 269 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= 270 | google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= 271 | google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 272 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 273 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 274 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 275 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 276 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 277 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 278 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 279 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 280 | gotest.tools/gotestsum v1.12.2 h1:eli4tu9Q2D/ogDsEGSr8XfQfl7mT0JsGOG6DFtUiZ/Q= 281 | gotest.tools/gotestsum v1.12.2/go.mod h1:kjRtCglPZVsSU0hFHX3M5VWBM6Y63emHuB14ER1/sow= 282 | gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= 283 | gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= 284 | k8s.io/kms v0.31.4 h1:DVk9T1PHxG7IUMfWs1sDhBTbzGnM7lhMJO8lOzOzTIs= 285 | k8s.io/kms v0.31.4/go.mod h1:OZKwl1fan3n3N5FFxnW5C4V3ygrah/3YXeJWS3O6+94= 286 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/FalcoSuessgott/vault-kubernetes-kms/cmd" 8 | ) 9 | 10 | var version = "0.0.1-dev" 11 | 12 | func main() { 13 | if err := cmd.NewPlugin(version); err != nil { 14 | fmt.Fprintf(os.Stderr, "%v\n", err) 15 | 16 | os.Exit(1) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: vault-kms-plugin 2 | site_description: "Encrypt Kubernetes Secrets using Hashicorp Vault as the KMS Provider" 3 | site_author: FalcoSuessgott 4 | 5 | repo_name: FalcoSuessgott/vault-kms-plugin 6 | repo_url: https://github.com/FalcoSuessgott/vault-kubernetes-kms 7 | 8 | docs_dir: docs/ 9 | 10 | plugins: 11 | - search 12 | - git-authors 13 | - git-revision-date-localized: 14 | locale: en 15 | enable_creation_date: false 16 | 17 | markdown_extensions: 18 | - pymdownx.superfences: 19 | custom_fences: 20 | - name: mermaid 21 | class: mermaid 22 | - pymdownx.tabbed: 23 | alternate_style: true 24 | - pymdownx.highlight: 25 | anchor_linenums: true 26 | line_spans: __span 27 | pygments_lang_class: true 28 | - pymdownx.snippets 29 | - pymdownx.emoji: 30 | emoji_index: !!python/name:materialx.emoji.twemoji 31 | emoji_generator: !!python/name:materialx.emoji.to_svg 32 | - pymdownx.inlinehilite 33 | - admonition 34 | - def_list 35 | - footnotes 36 | - attr_list 37 | - md_in_html 38 | - tables 39 | - pymdownx.tasklist: 40 | custom_checkbox: true 41 | - footnotes 42 | - pymdownx.tabbed: 43 | alternate_style: true 44 | - toc: 45 | permalink: true 46 | - markdown_include.include: 47 | base_path: docs 48 | 49 | nav: 50 | - vault-kubernetes-kms: 51 | - index.md 52 | - quickstart.md 53 | - configuration.md 54 | - concepts.md 55 | - sign.md 56 | - metrics.md 57 | - integration.md 58 | - troubleshooting.md 59 | - development.md 60 | theme: 61 | icon: 62 | edit: material/pencil 63 | view: material/eye 64 | repo: fontawesome/brands/github 65 | name: material 66 | 67 | language: en 68 | palette: 69 | # Palette toggle for light mode 70 | - scheme: default 71 | primary: blue 72 | accent: indigo 73 | toggle: 74 | icon: material/eye 75 | name: Switch to dark mode 76 | # Palette toggle for dark mode 77 | - scheme: slate 78 | primary: blue 79 | accent: indigo 80 | toggle: 81 | icon: material/eye-outline 82 | name: Switch to light mode 83 | features: 84 | - navigation.tabs 85 | - navigation.tabs.sticky 86 | - navigation.sections 87 | - navigation.indexes 88 | - content.code.copy 89 | - content.action.edit 90 | - navigation.top 91 | - navigation.expand 92 | - navigation.footer 93 | -------------------------------------------------------------------------------- /pkg/logging/zap.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "go.uber.org/zap/zapcore" 6 | ) 7 | 8 | // NewStandardLogger creates a new zap.Logger based on common configuration 9 | // https://github.com/kubernetes-sigs/aws-encryption-provider/blob/master/pkg/logging/zap.go 10 | // nolint: mnd 11 | func NewStandardLogger(logLevel zapcore.Level) (*zap.Logger, error) { 12 | return zap.Config{ 13 | Level: zap.NewAtomicLevelAt(logLevel), 14 | Development: false, 15 | Sampling: &zap.SamplingConfig{ 16 | Initial: 100, 17 | Thereafter: 100, 18 | }, 19 | Encoding: "json", 20 | EncoderConfig: zapcore.EncoderConfig{ 21 | TimeKey: "timestamp", 22 | LevelKey: "level", 23 | NameKey: "logger", 24 | CallerKey: "caller", 25 | MessageKey: "message", 26 | StacktraceKey: "stacktrace", 27 | EncodeLevel: zapcore.LowercaseLevelEncoder, 28 | EncodeTime: zapcore.ISO8601TimeEncoder, 29 | EncodeDuration: zapcore.SecondsDurationEncoder, 30 | EncodeCaller: zapcore.ShortCallerEncoder, 31 | }, 32 | OutputPaths: []string{"stdout"}, 33 | ErrorOutputPaths: []string{"stderr"}, 34 | }.Build() 35 | } 36 | -------------------------------------------------------------------------------- /pkg/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/collectors" 6 | ) 7 | 8 | const MetricsPrefix = "vault_kubernetes_kms" 9 | 10 | // nolint: mnd 11 | var defaultBuckets = prometheus.ExponentialBuckets(0.001, 2, 11) 12 | 13 | var metricsPrefix = func(s string) string { 14 | return MetricsPrefix + "_" + s 15 | } 16 | 17 | func RegisterPrometheusMetrics() *prometheus.Registry { 18 | promReg := prometheus.NewRegistry() 19 | 20 | promReg.MustRegister( 21 | // note: The process collector only collects metrics on Linux OS. 22 | collectors.NewProcessCollector(collectors.ProcessCollectorOpts{ 23 | Namespace: MetricsPrefix, 24 | }), 25 | EncryptionErrorsTotal, 26 | DecryptionErrorsTotal, 27 | EncryptionOperationDurationSeconds, 28 | DecryptionOperationDurationSeconds, 29 | VaultTokenRenewalTotal, 30 | VaultTokenExpirySeconds, 31 | ) 32 | 33 | return promReg 34 | } 35 | 36 | var ( 37 | EncryptionOperationDurationSeconds = prometheus.NewHistogram( 38 | prometheus.HistogramOpts{ 39 | Name: metricsPrefix("encryption_operation_duration_seconds"), 40 | Help: "duration of encryption operations", 41 | Buckets: defaultBuckets, 42 | }, 43 | ) 44 | 45 | DecryptionOperationDurationSeconds = prometheus.NewHistogram( 46 | prometheus.HistogramOpts{ 47 | Name: metricsPrefix("decryption_operation_duration_seconds"), 48 | Help: "duration of decryption operations", 49 | Buckets: defaultBuckets, 50 | }, 51 | ) 52 | 53 | EncryptionErrorsTotal = prometheus.NewCounter( 54 | prometheus.CounterOpts{ 55 | Name: metricsPrefix("encryption_operation_errors_total"), 56 | Help: "total number of errors during encryption operations", 57 | }, 58 | ) 59 | 60 | DecryptionErrorsTotal = prometheus.NewCounter( 61 | prometheus.CounterOpts{ 62 | Name: metricsPrefix("decryption_operation_errors_total"), 63 | Help: "total number of errors during decryption operations", 64 | }, 65 | ) 66 | 67 | VaultTokenRenewalTotal = prometheus.NewCounter( 68 | prometheus.CounterOpts{ 69 | Name: metricsPrefix("token_renewals_total"), 70 | Help: "total number of token renewals", 71 | }, 72 | ) 73 | 74 | VaultTokenExpirySeconds = prometheus.NewGauge( 75 | prometheus.GaugeOpts{ 76 | Name: metricsPrefix("token_expiry_seconds"), 77 | Help: "time remaining until the current token expires", 78 | }, 79 | ) 80 | ) 81 | -------------------------------------------------------------------------------- /pkg/plugin/plugin_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "runtime" 7 | "testing" 8 | "time" 9 | 10 | "github.com/FalcoSuessgott/vault-kubernetes-kms/pkg/socket" 11 | "github.com/FalcoSuessgott/vault-kubernetes-kms/pkg/testutils" 12 | "github.com/FalcoSuessgott/vault-kubernetes-kms/pkg/vault" 13 | "github.com/stretchr/testify/suite" 14 | "google.golang.org/grpc" 15 | v1beta1 "k8s.io/kms/apis/v1beta1" 16 | v2 "k8s.io/kms/apis/v2" 17 | ) 18 | 19 | var socketPath = "/opt/vault/vaultkms.sock" 20 | 21 | type PluginSuite struct { 22 | suite.Suite 23 | 24 | tc *testutils.TestContainer 25 | vault *vault.Client 26 | } 27 | 28 | func TestVaultSuite(t *testing.T) { 29 | // github actions doesn't offer the docker sock, which we require for testing 30 | if runtime.GOOS != "windows" { 31 | suite.Run(t, new(PluginSuite)) 32 | } 33 | } 34 | 35 | func (p *PluginSuite) SetupAllSuite() { 36 | // create unix socket 37 | _, err := socket.NewSocket(socketPath) 38 | if err != nil { 39 | p.T().Fatal("cannot create socket: %w", err) 40 | } 41 | } 42 | 43 | func (p *PluginSuite) SetupSubTest() { 44 | tc, err := testutils.StartTestContainer( 45 | "secrets enable transit", 46 | "write -f transit/keys/kms", 47 | ) 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | 52 | p.tc = tc 53 | 54 | vault, err := vault.NewClient( 55 | vault.WithVaultAddress(tc.URI), 56 | vault.WithTokenAuth(tc.Token), 57 | vault.WithTransit("transit", "kms"), 58 | ) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | 63 | p.vault = vault 64 | } 65 | 66 | func (p *PluginSuite) TearDownSubTest() { 67 | if err := p.tc.Terminate(); err != nil { 68 | log.Fatal(err) 69 | } 70 | } 71 | 72 | // nolint: funlen, dupl 73 | func (p *PluginSuite) TestPluginEncryptDecrypt() { 74 | testCases := []struct { 75 | name string 76 | data []byte 77 | v1 bool 78 | err bool 79 | }{ 80 | { 81 | name: "simple v2 encrypt decrypt", 82 | data: []byte("simple string"), 83 | v1: false, 84 | }, 85 | { 86 | name: "simple v1 encrypt decrypt", 87 | data: []byte("simple string"), 88 | v1: true, 89 | }, 90 | } 91 | 92 | for _, tc := range testCases { 93 | grpc := grpc.NewServer() 94 | 95 | p.Run(tc.name, func() { 96 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 97 | p.T().Cleanup(cancel) 98 | 99 | // v1 100 | if tc.v1 { 101 | pluginV1 := NewPluginV1(p.vault) 102 | 103 | pluginV1.Register(grpc) 104 | 105 | //nolint: staticcheck 106 | vResp, err := pluginV1.Version(ctx, &v1beta1.VersionRequest{}) 107 | p.Require().NoError(err, tc.name) 108 | 109 | //nolint: staticcheck 110 | p.Require().Equal(&v1beta1.VersionResponse{ 111 | Version: "v1beta1", 112 | RuntimeName: "vault", 113 | RuntimeVersion: "0.0.1", 114 | }, vResp, tc.name) 115 | 116 | // encrypt 117 | //nolint: staticcheck 118 | encryptRequest := &v1beta1.EncryptRequest{ 119 | Plain: tc.data, 120 | } 121 | 122 | resp, err := pluginV1.Encrypt(ctx, encryptRequest) 123 | p.Require().NoError(err, tc.name) 124 | 125 | // decrypt 126 | //nolint: staticcheck 127 | decryptRequest := &v1beta1.DecryptRequest{ 128 | Cipher: resp.GetCipher(), 129 | } 130 | 131 | res, err := pluginV1.Decrypt(ctx, decryptRequest) 132 | p.Require().NoError(err, tc.name) 133 | 134 | // compare result 135 | p.Require().Equal(tc.data, res.GetPlain(), tc.name) 136 | } else { 137 | pluginV2 := NewPluginV2(p.vault) 138 | 139 | pluginV2.Register(grpc) 140 | 141 | vResp, err := pluginV2.Status(ctx, &v2.StatusRequest{}) 142 | p.Require().NoError(err, tc.name) 143 | 144 | //nolint: staticcheck 145 | p.Require().Equal(&v2.StatusResponse{ 146 | Version: "v2", 147 | Healthz: "ok", 148 | KeyId: "1", 149 | }, vResp, tc.name) 150 | 151 | // encrypt 152 | encryptRequest := &v2.EncryptRequest{ 153 | Plaintext: tc.data, 154 | } 155 | 156 | resp, err := pluginV2.Encrypt(ctx, encryptRequest) 157 | p.Require().NoError(err, tc.name) 158 | 159 | // decrypt 160 | decryptRequest := &v2.DecryptRequest{ 161 | Ciphertext: resp.GetCiphertext(), 162 | } 163 | 164 | res, err := pluginV2.Decrypt(ctx, decryptRequest) 165 | p.Require().NoError(err, tc.name) 166 | 167 | // compare result 168 | p.Require().Equal(tc.data, res.GetPlaintext(), tc.name) 169 | } 170 | }) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /pkg/plugin/plugin_v1.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/FalcoSuessgott/vault-kubernetes-kms/pkg/metrics" 8 | "github.com/FalcoSuessgott/vault-kubernetes-kms/pkg/vault" 9 | "github.com/prometheus/client_golang/prometheus" 10 | "go.uber.org/zap" 11 | "google.golang.org/grpc" 12 | pb "k8s.io/kms/apis/v1beta1" 13 | ) 14 | 15 | // PluginV1 a kms plugin wrapper. 16 | type PluginV1 struct { 17 | *vault.Client 18 | } 19 | 20 | // NewPluginV1 returns a kms wrapper. 21 | func NewPluginV1(vc *vault.Client) *PluginV1 { 22 | return &PluginV1{vc} 23 | } 24 | 25 | // nolint: staticcheck 26 | func (p *PluginV1) Version(ctx context.Context, request *pb.VersionRequest) (*pb.VersionResponse, error) { 27 | return &pb.VersionResponse{ 28 | Version: "v1beta1", 29 | RuntimeName: "vault", 30 | RuntimeVersion: "0.0.1", 31 | }, nil 32 | } 33 | 34 | // Health sends a simple plaintext for encryption and then compares the decrypted value. 35 | // nolint: staticcheck 36 | func (p *PluginV1) Health() error { 37 | health := "health" 38 | 39 | enc, err := p.Encrypt(context.Background(), &pb.EncryptRequest{ 40 | Plain: []byte(health), 41 | }) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | dec, err := p.Decrypt(context.Background(), &pb.DecryptRequest{ 47 | Cipher: enc.GetCipher(), 48 | }) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if health != string(dec.GetPlain()) { 54 | zap.L().Info("v1 health status failed") 55 | 56 | return errors.New("v1 health check failed") 57 | } 58 | 59 | return nil 60 | } 61 | 62 | // nolint: staticcheck 63 | func (p *PluginV1) Encrypt(ctx context.Context, request *pb.EncryptRequest) (*pb.EncryptResponse, error) { 64 | timer := prometheus.NewTimer(metrics.EncryptionOperationDurationSeconds) 65 | 66 | resp, _, err := p.Client.Encrypt(ctx, request.GetPlain()) 67 | if err != nil { 68 | metrics.EncryptionErrorsTotal.Inc() 69 | 70 | return nil, err 71 | } 72 | 73 | zap.L().Info("v1 encryption request") 74 | 75 | timer.ObserveDuration() 76 | 77 | return &pb.EncryptResponse{ 78 | Cipher: resp, 79 | }, nil 80 | } 81 | 82 | // nolint: staticcheck 83 | func (p *PluginV1) Decrypt(ctx context.Context, request *pb.DecryptRequest) (*pb.DecryptResponse, error) { 84 | timer := prometheus.NewTimer(metrics.DecryptionOperationDurationSeconds) 85 | 86 | resp, err := p.Client.Decrypt(ctx, request.GetCipher()) 87 | if err != nil { 88 | metrics.DecryptionErrorsTotal.Inc() 89 | 90 | return nil, err 91 | } 92 | 93 | zap.L().Info("v1 decryption request") 94 | 95 | timer.ObserveDuration() 96 | 97 | return &pb.DecryptResponse{ 98 | Plain: resp, 99 | }, nil 100 | } 101 | 102 | // nolint: staticcheck 103 | func (p *PluginV1) Register(s *grpc.Server) { 104 | pb.RegisterKeyManagementServiceServer(s, p) 105 | } 106 | -------------------------------------------------------------------------------- /pkg/plugin/plugin_v2.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/FalcoSuessgott/vault-kubernetes-kms/pkg/metrics" 10 | "github.com/FalcoSuessgott/vault-kubernetes-kms/pkg/vault" 11 | "github.com/prometheus/client_golang/prometheus" 12 | "go.uber.org/zap" 13 | "google.golang.org/grpc" 14 | pb "k8s.io/kms/apis/v2" 15 | ) 16 | 17 | // PluginV2 a kms plugin wrapper. 18 | type PluginV2 struct { 19 | *vault.Client 20 | } 21 | 22 | // PluginV2 returns a kms wrapper. 23 | func NewPluginV2(vc *vault.Client) *PluginV2 { 24 | return &PluginV2{vc} 25 | } 26 | 27 | // Status performs a simple health check and returns ok if encryption / decryption was successful 28 | // https://kubernetes.io/docs/tasks/administer-cluster/kms-provider/#developing-a-kms-plugin-gRPC-server-notes-kms-v2 29 | func (p *PluginV2) Status(ctx context.Context, _ *pb.StatusRequest) (*pb.StatusResponse, error) { 30 | health := "ok" 31 | 32 | kv, err := p.Client.GetKeyVersion(ctx) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | //nolint: contextcheck 38 | if err := p.Health(); err != nil { 39 | health = "err" 40 | 41 | zap.L().Info(err.Error()) 42 | } 43 | 44 | zap.L().Info("health status", 45 | zap.String("key_id", kv), 46 | zap.String("healthz", health), 47 | zap.String("version", "v2"), 48 | ) 49 | 50 | return &pb.StatusResponse{ 51 | Version: "v2", 52 | Healthz: "ok", 53 | KeyId: kv, 54 | }, nil 55 | } 56 | 57 | // Health sends a simple plaintext for encryption and then compares the decrypted value. 58 | func (p *PluginV2) Health() error { 59 | health := "health" 60 | 61 | start := time.Now().Unix() 62 | 63 | enc, err := p.Encrypt(context.Background(), &pb.EncryptRequest{ 64 | Plaintext: []byte(health), 65 | Uid: strconv.FormatInt(start, 10), 66 | }) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | dec, err := p.Decrypt(context.Background(), &pb.DecryptRequest{ 72 | Ciphertext: enc.GetCiphertext(), 73 | Uid: strconv.FormatInt(start, 10), 74 | }) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | if health != string(dec.GetPlaintext()) { 80 | zap.L().Info("v2 health status failed") 81 | 82 | return errors.New("v2 health check failed") 83 | } 84 | 85 | return nil 86 | } 87 | 88 | func (p *PluginV2) Encrypt(ctx context.Context, request *pb.EncryptRequest) (*pb.EncryptResponse, error) { 89 | timer := prometheus.NewTimer(metrics.EncryptionOperationDurationSeconds) 90 | 91 | resp, id, err := p.Client.Encrypt(ctx, request.GetPlaintext()) 92 | if err != nil { 93 | metrics.EncryptionErrorsTotal.Inc() 94 | 95 | return nil, err 96 | } 97 | 98 | zap.L().Info("v2 encryption request", zap.String("request_id", request.GetUid())) 99 | 100 | timer.ObserveDuration() 101 | 102 | return &pb.EncryptResponse{ 103 | Ciphertext: resp, 104 | KeyId: id, 105 | }, nil 106 | } 107 | 108 | func (p *PluginV2) Decrypt(ctx context.Context, request *pb.DecryptRequest) (*pb.DecryptResponse, error) { 109 | timer := prometheus.NewTimer(metrics.DecryptionOperationDurationSeconds) 110 | 111 | resp, err := p.Client.Decrypt(ctx, request.GetCiphertext()) 112 | if err != nil { 113 | metrics.DecryptionErrorsTotal.Inc() 114 | 115 | return nil, err 116 | } 117 | 118 | zap.L().Info("v2 decryption request", zap.String("request_id", request.GetUid())) 119 | 120 | timer.ObserveDuration() 121 | 122 | return &pb.DecryptResponse{ 123 | Plaintext: resp, 124 | }, nil 125 | } 126 | 127 | func (p *PluginV2) Register(s *grpc.Server) { 128 | pb.RegisterKeyManagementServiceServer(s, p) 129 | } 130 | -------------------------------------------------------------------------------- /pkg/probes/probes.go: -------------------------------------------------------------------------------- 1 | package probes 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "go.uber.org/zap" 8 | ) 9 | 10 | // Prober interface. 11 | type Prober interface { 12 | Health() error 13 | } 14 | 15 | // HealthZ performs a health check for each prober and returns OK if all checks were successful. 16 | func HealthZ(prober []Prober) http.HandlerFunc { 17 | return func(w http.ResponseWriter, r *http.Request) { 18 | for _, p := range prober { 19 | if p == nil { 20 | return 21 | } 22 | 23 | if err := p.Health(); err != nil { 24 | w.WriteHeader(http.StatusInternalServerError) 25 | fmt.Fprint(w, err) 26 | 27 | zap.L().Error("health check failed", zap.Error(err)) 28 | 29 | return 30 | } 31 | } 32 | 33 | w.WriteHeader(http.StatusOK) 34 | fmt.Fprint(w, http.StatusText(http.StatusOK)) 35 | 36 | zap.L().Debug("health checks succeeded") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pkg/probes/probes_test.go: -------------------------------------------------------------------------------- 1 | package probes 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | type ErrorProber struct{} 13 | 14 | func (p *ErrorProber) Health() error { return errors.New("probe failed") } 15 | 16 | type SuccessProber struct{} 17 | 18 | func (p *SuccessProber) Health() error { return nil } 19 | 20 | func TestHealthZ(t *testing.T) { 21 | t.Run("error", func(t *testing.T) { 22 | prober := []Prober{&ErrorProber{}} 23 | 24 | hf := HealthZ(prober) 25 | 26 | req := httptest.NewRequest(http.MethodGet, "https://google.de", nil) 27 | w := httptest.NewRecorder() 28 | hf(w, req) 29 | 30 | require.Equal(t, http.StatusInternalServerError, w.Code) 31 | require.Equal(t, "probe failed", w.Body.String()) 32 | }) 33 | 34 | t.Run("succcess", func(t *testing.T) { 35 | prober := []Prober{&SuccessProber{}} 36 | 37 | hf := HealthZ(prober) 38 | 39 | req := httptest.NewRequest(http.MethodGet, "https://google.de", nil) 40 | w := httptest.NewRecorder() 41 | hf(w, req) 42 | 43 | require.Equal(t, http.StatusOK, w.Code) 44 | require.Equal(t, http.StatusText(http.StatusOK), w.Body.String()) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/socket/socket.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "os" 8 | "strings" 9 | 10 | "go.uber.org/zap" 11 | ) 12 | 13 | // Socket represents a unix socket. 14 | type Socket struct { 15 | Network string 16 | Path string 17 | } 18 | 19 | // NewSocket returns a new unix socket. 20 | func NewSocket(str string) (*Socket, error) { 21 | socket := &Socket{} 22 | 23 | if strings.HasPrefix(strings.ToLower(str), "unix://") { 24 | //nolint: mnd 25 | s := strings.SplitN(str, "://", 2) 26 | if s[1] != "" { 27 | socket.Network, socket.Path = s[0], s[1] 28 | } 29 | } 30 | 31 | if socket.Path == "" { 32 | return nil, errors.New("path missing") 33 | } 34 | 35 | return socket, nil 36 | } 37 | 38 | // Listen listens on the current socket for connections. 39 | func (s *Socket) Listen(force bool) (net.Listener, error) { 40 | // Remove the socket file if it already exists. 41 | if _, err := os.Stat(s.Path); err == nil { 42 | zap.L().Info("Socket already exists", zap.String("path", s.Path)) 43 | 44 | if force { 45 | if err := os.Remove(s.Path); err != nil { 46 | return nil, fmt.Errorf("failed to remove unix socket: %w", err) 47 | } 48 | 49 | zap.L().Info("Socket overwrite is enabled. Successfully removed socket", zap.String("path", s.Path)) 50 | } 51 | } 52 | 53 | return net.Listen(s.Network, s.Path) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/socket/socket_test.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestNewSocket(t *testing.T) { 13 | testCases := []struct { 14 | name string 15 | str string 16 | exp *Socket 17 | err bool 18 | }{ 19 | { 20 | name: "basic", 21 | str: "unix:///opt/vaultkms.socket", 22 | exp: &Socket{ 23 | Network: "unix", 24 | Path: "/opt/vaultkms.socket", 25 | }, 26 | }, 27 | { 28 | name: "invalid", 29 | str: "/opt/vaultkms.socket", 30 | err: true, 31 | }, 32 | } 33 | 34 | for _, tc := range testCases { 35 | s, err := NewSocket(tc.str) 36 | 37 | if tc.err { 38 | require.Error(t, err, tc.name) 39 | } else { 40 | require.NoError(t, err, tc.name) 41 | 42 | assert.Equal(t, tc.exp, s, tc.name) 43 | } 44 | } 45 | } 46 | 47 | func TestForce(t *testing.T) { 48 | s := &Socket{"unix", "/tmp/vaultkms_test.socket"} 49 | 50 | //nolint: errcheck 51 | go s.Listen(false) 52 | 53 | time.Sleep(3 * time.Second) 54 | 55 | _, err := s.Listen(false) 56 | require.Error(t, err, "socket exists, no force, should error") 57 | 58 | time.Sleep(3 * time.Second) 59 | 60 | _, err = s.Listen(true) 61 | require.NoError(t, err, "socket exists, force, should not error") 62 | 63 | t.Cleanup(func() { 64 | os.Remove(s.Path) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /pkg/testutils/testutils.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "strings" 9 | 10 | "github.com/testcontainers/testcontainers-go" 11 | "github.com/testcontainers/testcontainers-go/modules/vault" 12 | ) 13 | 14 | var ( 15 | image = "hashicorp/vault:1.16.0" 16 | token = "root" 17 | ) 18 | 19 | // TestContainer vault dev container wrapper. 20 | type TestContainer struct { 21 | Container testcontainers.Container 22 | URI string 23 | Token string 24 | } 25 | 26 | // StartTestContainer Starts a fresh vault in development mode. 27 | func StartTestContainer(commands ...string) (*TestContainer, error) { 28 | ctx := context.Background() 29 | 30 | vaultContainer, err := vault.Run(ctx, 31 | image, 32 | vault.WithToken(token), 33 | vault.WithInitCommand(commands...), 34 | ) 35 | if err != nil { 36 | return nil, fmt.Errorf("failed to start container: %w", err) 37 | } 38 | 39 | uri, err := vaultContainer.HttpHostAddress(ctx) 40 | if err != nil { 41 | return nil, fmt.Errorf("error returning container mapped port: %w", err) 42 | } 43 | 44 | return &TestContainer{ 45 | Container: vaultContainer, 46 | URI: uri, 47 | Token: token, 48 | }, nil 49 | } 50 | 51 | func (v *TestContainer) GetApproleCreds(mount, role string) (string, string, error) { 52 | _, r, err := v.Container.Exec(context.Background(), []string{"vault", "read", "-field=role_id", fmt.Sprintf("auth/%s/role/%s/role-id", mount, role)}) 53 | if err != nil { 54 | return "", "", fmt.Errorf("error creating role_id: %w", err) 55 | } 56 | 57 | roleID, err := io.ReadAll(r) 58 | if err != nil { 59 | return "", "", fmt.Errorf("error reading role_id: %w", err) 60 | } 61 | 62 | _, r, err = v.Container.Exec(context.Background(), []string{"vault", "write", "-field=secret_id", "-force", fmt.Sprintf("auth/%s/role/%s/secret-id", mount, role)}) 63 | if err != nil { 64 | return "", "", fmt.Errorf("error creating secret_id: %w", err) 65 | } 66 | 67 | secretID, err := io.ReadAll(r) 68 | if err != nil { 69 | return "", "", fmt.Errorf("error reading secret_id: %w", err) 70 | } 71 | 72 | // removing the first 8 bytes, which is the shell prompt 73 | return string(roleID[8:]), string(secretID[8:]), nil 74 | } 75 | 76 | // nolint: perfsprint 77 | func (v *TestContainer) GetToken(policy string, ttl string) (string, error) { 78 | return v.RunCommand("vault token create -field=token -policy=" + policy + " -ttl=" + ttl) 79 | } 80 | 81 | func (v *TestContainer) RunCommand(cmd string) (string, error) { 82 | log.Println("running command: ", cmd) 83 | 84 | _, r, err := v.Container.Exec(context.Background(), strings.Split(cmd, " ")) 85 | if err != nil { 86 | return "", fmt.Errorf("error creating root token: %w", err) 87 | } 88 | 89 | rootToken, err := io.ReadAll(r) 90 | if err != nil { 91 | return "", fmt.Errorf("error reading root token: %w", err) 92 | } 93 | 94 | log.Println("output: ", string(rootToken[8:])) 95 | 96 | // removing the first 8 bytes, which is the shell prompt 97 | return string(rootToken[8:]), nil 98 | } 99 | 100 | // Terminate terminates the testcontainer. 101 | func (v *TestContainer) Terminate() error { 102 | return v.Container.Terminate(context.Background()) 103 | } 104 | -------------------------------------------------------------------------------- /pkg/testutils/testutils_test.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/FalcoSuessgott/vault-kubernetes-kms/pkg/vault" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestVaultConnection(t *testing.T) { 11 | // create vault 12 | tc, err := StartTestContainer("secrets enable transit", 13 | "write -f transit/keys/kms", 14 | "auth enable approle", 15 | "write auth/approle/role/kms token_ttl=1h", 16 | ) 17 | require.NoError(t, err, "start") 18 | 19 | // create token 20 | token, err := tc.GetToken("default", "1h") 21 | require.NoError(t, err, "token creation") 22 | 23 | tokenVault, err := vault.NewClient( 24 | vault.WithVaultAddress(tc.URI), 25 | vault.WithTokenAuth(token), 26 | vault.WithTransit("transit", "kms"), 27 | ) 28 | require.NoError(t, err, "token login") 29 | 30 | // check unsealed / init 31 | health, err := tokenVault.Client.Sys().Health() 32 | require.NoError(t, err, "health") 33 | require.True(t, health.Initialized, "initialized") 34 | require.False(t, health.Sealed, "unsealed") 35 | 36 | // test approle 37 | roleID, secretID, err := tc.GetApproleCreds("approle", "kms") 38 | require.NoError(t, err, "approle creation") 39 | 40 | _, err = vault.NewClient( 41 | vault.WithVaultAddress(tc.URI), 42 | vault.WithTokenAuth(tc.Token), 43 | vault.WithAppRoleAuth("approle", roleID, secretID), 44 | vault.WithTransit("transit", "kms"), 45 | ) 46 | 47 | require.NoError(t, err, "health") 48 | 49 | // teardown 50 | require.NoError(t, tc.Terminate(), "terminate") 51 | } 52 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/caarlos0/env/v6" 5 | ) 6 | 7 | // ParseEnvs parses the environment variables and sets the options. 8 | func ParseEnvs(prefix string, i interface{}) error { 9 | opts := env.Options{ 10 | Prefix: prefix, 11 | } 12 | 13 | if err := env.Parse(i, opts); err != nil { 14 | return err 15 | } 16 | 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /pkg/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestParseEnvs(t *testing.T) { 10 | type test struct { 11 | Test string `env:"TEST"` 12 | } 13 | 14 | o := &test{} 15 | exp := "test" 16 | 17 | t.Setenv("test_TEST", exp) 18 | 19 | require.NoError(t, ParseEnvs("test_", o)) 20 | require.Equal(t, exp, o.Test) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/vault/client.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/vault/api" 7 | ) 8 | 9 | // Client Vault API wrapper. 10 | type Client struct { 11 | *api.Client 12 | 13 | Token string 14 | 15 | AppRoleMount string 16 | AppRoleID string 17 | AppRoleSecretID string 18 | 19 | AuthMethodFunc Option 20 | 21 | TokenRenewalSeconds int 22 | 23 | TransitEngine string 24 | TransitKey string 25 | } 26 | 27 | // Option vault client connection option. 28 | type Option func(*Client) error 29 | 30 | // NewClient returns a new vault client wrapper. 31 | func NewClient(opts ...Option) (*Client, error) { 32 | cfg := api.DefaultConfig() 33 | 34 | // read all vault env vars 35 | c, err := api.NewClient(cfg) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | client := &Client{Client: c} 41 | 42 | for _, opt := range opts { 43 | if err := opt(client); err != nil { 44 | return nil, err 45 | } 46 | } 47 | 48 | // perform a self lookup to verify the token 49 | _, err = c.Auth().Token().LookupSelf() 50 | if err != nil { 51 | return nil, fmt.Errorf("failed to connect to vault: %w", err) 52 | } 53 | 54 | return client, nil 55 | } 56 | 57 | // WithVaultAddress sets the specified address. 58 | func WithVaultAddress(address string) Option { 59 | return func(c *Client) error { 60 | return c.SetAddress(address) 61 | } 62 | } 63 | 64 | // WithVaultNamespace sets the specified namespace. 65 | func WithVaultNamespace(namespace string) Option { 66 | return func(c *Client) error { 67 | if namespace != "" { 68 | c.SetNamespace(namespace) 69 | } 70 | 71 | return nil 72 | } 73 | } 74 | 75 | // WithTransit sets transit parameters. 76 | func WithTransit(mount, key string) Option { 77 | return func(c *Client) error { 78 | c.TransitEngine = mount 79 | c.TransitKey = key 80 | 81 | return nil 82 | } 83 | } 84 | 85 | // WithTokenAuth sets the specified token. 86 | func WithTokenAuth(token string) Option { 87 | return func(c *Client) error { 88 | c.Token = token 89 | 90 | if token != "" { 91 | c.SetToken(token) 92 | } 93 | 94 | if c.AuthMethodFunc == nil { 95 | c.AuthMethodFunc = WithTokenAuth(token) 96 | } 97 | 98 | return nil 99 | } 100 | } 101 | 102 | // WithTokenAuth sets the specified token. 103 | func WithTokenRenewalSeconds(seconds int) Option { 104 | return func(c *Client) error { 105 | c.TokenRenewalSeconds = seconds 106 | 107 | return nil 108 | } 109 | } 110 | 111 | // WitAppRoleAuth performs a approle auth login. 112 | func WithAppRoleAuth(mount, roleID, secretID string) Option { 113 | return func(c *Client) error { 114 | c.AppRoleID = roleID 115 | c.AppRoleMount = mount 116 | c.AppRoleSecretID = secretID 117 | 118 | opts := map[string]interface{}{ 119 | "role_id": roleID, 120 | "secret_id": secretID, 121 | } 122 | 123 | s, err := c.Logical().Write(fmt.Sprintf(authLoginPath, mount), opts) 124 | if err != nil { 125 | return fmt.Errorf("error performing approle auth: %w", err) 126 | } 127 | 128 | c.SetToken(s.Auth.ClientToken) 129 | 130 | if c.AuthMethodFunc == nil { 131 | c.AuthMethodFunc = WithAppRoleAuth(mount, roleID, secretID) 132 | } 133 | 134 | return nil 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /pkg/vault/client_test.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "runtime" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/FalcoSuessgott/vault-kubernetes-kms/pkg/testutils" 11 | "github.com/stretchr/testify/suite" 12 | ) 13 | 14 | type VaultSuite struct { 15 | suite.Suite 16 | 17 | tc *testutils.TestContainer 18 | vault *Client 19 | } 20 | 21 | func (s *VaultSuite) TearDownSubTest() { 22 | if err := s.tc.Terminate(); err != nil { 23 | log.Fatal(err) 24 | } 25 | } 26 | 27 | func (s *VaultSuite) SetupSubTest() { 28 | tc, err := testutils.StartTestContainer( 29 | "secrets enable transit", 30 | "write -f transit/keys/kms", 31 | ) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | s.tc = tc 37 | 38 | vault, err := NewClient( 39 | WithVaultAddress(tc.URI), 40 | WithTokenAuth(tc.Token), 41 | WithTransit("transit", "kms"), 42 | ) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | s.vault = vault 48 | } 49 | 50 | // nolint: funlen 51 | func (s *VaultSuite) TestAuthMethods() { 52 | testCases := []struct { 53 | name string 54 | prepCmd []string 55 | auth func() (Option, error) 56 | err bool 57 | }{ 58 | { 59 | name: "basic approle auth", 60 | prepCmd: []string{ 61 | "vault auth enable approle", 62 | "vault write auth/approle/role/kms token_ttl=1h", 63 | }, 64 | auth: func() (Option, error) { 65 | roleID, secretID, err := s.tc.GetApproleCreds("approle", "kms") 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return WithAppRoleAuth("approle", roleID, secretID), nil 71 | }, 72 | }, 73 | { 74 | name: "invalid approle auth", 75 | err: true, 76 | auth: func() (Option, error) { 77 | return WithAppRoleAuth("approle", "invalid", "invalid"), nil 78 | }, 79 | }, 80 | { 81 | name: "token auth", 82 | auth: func() (Option, error) { 83 | token, err := s.tc.GetToken("default", "1h") 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | return WithTokenAuth(token), nil 89 | }, 90 | }, 91 | { 92 | name: "invalid token auth", 93 | auth: func() (Option, error) { 94 | return WithTokenAuth("invalidtoken"), nil 95 | }, 96 | err: true, 97 | }, 98 | } 99 | 100 | for _, tc := range testCases { 101 | s.Run(tc.name, func() { 102 | // prep vault 103 | for _, cmd := range tc.prepCmd { 104 | _, _, err := s.tc.Container.Exec(context.Background(), strings.Split(cmd, " ")) 105 | s.Require().NoError(err, tc.name) 106 | } 107 | 108 | // perform auth 109 | auth, err := tc.auth() 110 | s.Require().NoError(err, "auth "+tc.name) 111 | 112 | _, err = NewClient( 113 | WithVaultAddress(s.tc.URI), 114 | WithTokenAuth(s.tc.Token), 115 | auth, 116 | ) 117 | 118 | // assert 119 | s.Require().Equal(tc.err, err != nil, tc.name) 120 | }) 121 | } 122 | } 123 | 124 | func TestVaultSuite(t *testing.T) { 125 | // github actions doesn't offer the docker sock, which we require for testing 126 | if runtime.GOOS != "windows" { 127 | suite.Run(t, new(VaultSuite)) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /pkg/vault/const.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | const ( 4 | authLoginPath = "auth/%s/login" 5 | 6 | encryptDataPath = "%s/encrypt/%s" 7 | decryptDataPath = "%s/decrypt/%s" 8 | 9 | mountEnginePath = "sys/mounts/%s" 10 | transitKeyPath = "%s/keys/%s" 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/vault/lease.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "time" 7 | 8 | "github.com/FalcoSuessgott/vault-kubernetes-kms/pkg/metrics" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | // LeaseRefresher periodically checks the ttl of the current lease and attempts to renew it if the ttl is less than half of the creation ttl. 13 | // if the token renewal fails, a new login with the configured auth method is performed 14 | // this func is supposed to run as a goroutine. 15 | // nolint: funlen, gocognit, cyclop 16 | func (c *Client) LeaseRefresher(ctx context.Context, interval time.Duration) { 17 | ticker := time.NewTicker(interval) 18 | defer ticker.Stop() 19 | 20 | for { 21 | select { 22 | case <-ticker.C: 23 | token, err := c.Auth().Token().LookupSelf() 24 | if err != nil { 25 | zap.L().Error("failed to lookup token", zap.Error(err)) 26 | 27 | continue 28 | } 29 | 30 | creationTTL, ok := token.Data["creation_ttl"].(json.Number) 31 | if !ok { 32 | zap.L().Error("failed to assert creation_ttl type") 33 | 34 | continue 35 | } 36 | 37 | ttl, ok := token.Data["ttl"].(json.Number) 38 | if !ok { 39 | zap.L().Error("failed to assert ttl type") 40 | 41 | continue 42 | } 43 | 44 | creationTTLFloat, err := creationTTL.Float64() 45 | if err != nil { 46 | zap.L().Error("failed to parse creation_ttl", zap.Error(err)) 47 | 48 | continue 49 | } 50 | 51 | ttlFloat, err := ttl.Float64() 52 | if err != nil { 53 | zap.L().Error("failed to parse ttl", zap.Error(err)) 54 | 55 | continue 56 | } 57 | 58 | metrics.VaultTokenExpirySeconds.Set(ttlFloat) 59 | 60 | zap.L().Info("checking token renewal", zap.Float64("creation_ttl", creationTTLFloat), zap.Float64("ttl", ttlFloat)) 61 | 62 | //nolint: nestif 63 | if ttlFloat < creationTTLFloat/2 { 64 | zap.L().Info("attempting token renewal", zap.Int("renewal_seconds", c.TokenRenewalSeconds)) 65 | 66 | if _, err := c.Auth().Token().RenewSelf(c.TokenRenewalSeconds); err != nil { 67 | zap.L().Error("failed to renew token, performing new authentication", zap.Error(err)) 68 | 69 | if err := c.AuthMethodFunc(c); err != nil { 70 | zap.L().Error("failed to authenticate", zap.Error(err)) 71 | } else { 72 | zap.L().Info("successfully re-authenticated") 73 | } 74 | } else { 75 | zap.L().Info("successfully refreshed token") 76 | } 77 | 78 | metrics.VaultTokenRenewalTotal.Inc() 79 | } else { 80 | zap.L().Info("skipping token renewal") 81 | } 82 | 83 | case <-ctx.Done(): 84 | zap.L().Info("token refresher shutting down") 85 | 86 | return 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /pkg/vault/lease_test.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | func (s *VaultSuite) TestTokenRefresher() { 9 | // here we simply create a token with a TTL of 7sec and start the token refresher 10 | // after 20sec we check if the token is still valid 11 | s.Run("token refresher", func() { 12 | ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second) 13 | defer cancel() 14 | 15 | token, err := s.tc.GetToken("default", "7s") 16 | s.Require().NoError(err, "token creation failed") 17 | 18 | vc, err := NewClient( 19 | WithVaultAddress(s.tc.URI), 20 | WithTokenAuth(token), 21 | ) 22 | 23 | s.Require().NoError(err, "client") 24 | 25 | go vc.LeaseRefresher(ctx, 3*time.Second) 26 | 27 | time.Sleep(15 * time.Second) 28 | 29 | _, err = s.tc.RunCommand("vault token lookup " + token) 30 | s.Require().NoError(err, "token lookup") 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/vault/transit.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | // Encrypt takes any data and encrypts it using the specified vaults transit engine. 12 | func (c *Client) Encrypt(ctx context.Context, data []byte) ([]byte, string, error) { 13 | p := fmt.Sprintf(encryptDataPath, c.TransitEngine, c.TransitKey) 14 | 15 | opts := map[string]interface{}{ 16 | "plaintext": base64.StdEncoding.EncodeToString(data), 17 | } 18 | 19 | resp, err := c.Logical().WriteWithContext(ctx, p, opts) 20 | if err != nil { 21 | return nil, "", err 22 | } 23 | 24 | res, ok := resp.Data["ciphertext"].(string) 25 | if !ok { 26 | return nil, "", errors.New("invalid response") 27 | } 28 | 29 | kv, err := c.GetKeyVersion(ctx) 30 | if err != nil { 31 | return nil, "", err 32 | } 33 | 34 | return []byte(res), kv, nil 35 | } 36 | 37 | // Decrypt takes any encrypted data and decrypts it using the specified vaults transit engine. 38 | func (c *Client) Decrypt(ctx context.Context, data []byte) ([]byte, error) { 39 | p := fmt.Sprintf(decryptDataPath, c.TransitEngine, c.TransitKey) 40 | 41 | opts := map[string]interface{}{ 42 | "ciphertext": string(data), 43 | } 44 | 45 | resp, err := c.Logical().WriteWithContext(ctx, p, opts) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | res, ok := resp.Data["plaintext"].(string) 51 | if !ok { 52 | return nil, errors.New("invalid response") 53 | } 54 | 55 | decoded, err := base64.StdEncoding.DecodeString(res) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | return decoded, nil 61 | } 62 | 63 | // GetKeyVersions returns the latest_version aka the timestamp the key version was created. 64 | // https://developer.hashicorp.com/vault/api-docs/secret/transit#read-key 65 | func (c *Client) GetKeyVersion(ctx context.Context) (string, error) { 66 | p := fmt.Sprintf(transitKeyPath, c.TransitEngine, c.TransitKey) 67 | 68 | resp, err := c.Logical().ReadWithContext(ctx, p) 69 | if err != nil { 70 | return "", err 71 | } 72 | 73 | if resp == nil { 74 | return "", fmt.Errorf("could not read transit key: %s/%s. Check transit engine and key and permissions", c.TransitEngine, c.TransitKey) 75 | } 76 | 77 | kv, ok := resp.Data["latest_version"].(json.Number) 78 | if !ok { 79 | return "", fmt.Errorf("could not get latest_version of transit key: %s/%s", c.TransitEngine, c.TransitKey) 80 | } 81 | 82 | return kv.String(), nil 83 | } 84 | -------------------------------------------------------------------------------- /pkg/vault/transit_test.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func (s *VaultSuite) TestTransitEncryptDecrypt() { 11 | testCases := []struct { 12 | name string 13 | data []byte 14 | err bool 15 | }{ 16 | { 17 | name: "encrypt decrypt", 18 | data: []byte("simple string"), 19 | }, 20 | } 21 | 22 | for _, tc := range testCases { 23 | s.Run(tc.name, func() { 24 | // encrypt data 25 | enc, _, err := s.vault.Encrypt(context.Background(), tc.data) 26 | require.NoError(s.Suite.T(), err, tc.name) 27 | 28 | // decrypt data 29 | dec, err := s.vault.Decrypt(context.Background(), enc) 30 | require.NoError(s.Suite.T(), err, tc.name) 31 | 32 | // data should match decrypted text 33 | assert.Equal(s.Suite.T(), tc.data, dec, tc.name) 34 | }) 35 | } 36 | } 37 | 38 | func (s *VaultSuite) TestTransitKeyVersion() { 39 | testCases := []struct { 40 | name string 41 | transit Option 42 | exp string 43 | err bool 44 | }{ 45 | { 46 | name: "should work", 47 | transit: WithTransit("transit", "kms"), 48 | exp: "1", 49 | }, 50 | { 51 | name: "should fail", 52 | transit: WithTransit("doesnot", "exist"), 53 | err: true, 54 | }, 55 | } 56 | 57 | for _, tc := range testCases { 58 | s.Run(tc.name, func() { 59 | vault, err := NewClient( 60 | WithVaultAddress(s.tc.URI), 61 | WithTokenAuth(s.tc.Token), 62 | tc.transit, 63 | ) 64 | 65 | s.Suite.Require().NoError(err) 66 | 67 | v, err := vault.GetKeyVersion(context.Background()) 68 | 69 | if tc.err { 70 | s.Suite.Require().Error(err, tc.name) 71 | } else { 72 | s.Suite.Require().NoError(err, tc.name) 73 | s.Suite.Require().Equal(tc.exp, v, "version "+tc.name) 74 | } 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | markdown 2 | mkdocs 3 | mkdocs-material 4 | mkdocs-macros-plugin 5 | mkdocs_puml 6 | mkdocs-include-dir-to-nav 7 | mkdocs-with-pdf 8 | mkdocs-git-revision-date-localized-plugin 9 | mkdocs-git-authors-plugin 10 | requests 11 | weasyprint 12 | markdown-include 13 | -------------------------------------------------------------------------------- /scripts/encryption_provider_config_v1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: EncryptionConfiguration 3 | apiVersion: apiserver.config.k8s.io/v1 4 | resources: 5 | - resources: 6 | - secrets 7 | providers: 8 | - kms: 9 | name: vault 10 | endpoint: unix:///opt/kms/vaultkms.socket 11 | - identity: {} 12 | -------------------------------------------------------------------------------- /scripts/encryption_provider_config_v2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: EncryptionConfiguration 3 | apiVersion: apiserver.config.k8s.io/v1 4 | resources: 5 | - resources: 6 | - secrets 7 | providers: 8 | - kms: 9 | apiVersion: v2 10 | name: vault-kubernetes-kms 11 | endpoint: unix:///opt/kms/vaultkms.socket 12 | - identity: {} 13 | -------------------------------------------------------------------------------- /scripts/grafana_values.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/grafana/helm-charts/blob/main/charts/grafana/values.yaml 2 | datasources: 3 | datasources.yaml: 4 | apiVersion: 1 5 | datasources: 6 | - name: Prometheus 7 | type: prometheus 8 | url: http://prometheus-server:80 9 | -------------------------------------------------------------------------------- /scripts/kind-config_v1.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | containerdConfigPatches: 4 | # add a local docker registry to containerd 5 | # the registry is run via a separated docker container 6 | - |- 7 | [plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:5000"] 8 | endpoint = ["http://registry:5000"] 9 | nodes: 10 | - role: control-plane 11 | extraMounts: 12 | # mount encryption provider config available on all cp nodes 13 | - containerPath: /etc/kubernetes/encryption_provider_config_v1.yaml 14 | hostPath: scripts/encryption_provider_config_v1.yml 15 | readOnly: true 16 | propagation: None 17 | # vault-kubernetes-kms as a static Pod 18 | - containerPath: /etc/kubernetes/manifests/vault-kubernetes-kms.yaml 19 | hostPath: scripts/vault-kubernetes-kms.yml 20 | readOnly: true 21 | propagation: None 22 | # patch kube-apiserver 23 | kubeadmConfigPatches: 24 | - | 25 | kind: ClusterConfiguration 26 | apiServer: 27 | extraArgs: 28 | encryption-provider-config: "/etc/kubernetes/encryption_provider_config_v1.yaml" 29 | extraVolumes: 30 | - name: encryption-config 31 | hostPath: "/etc/kubernetes/encryption_provider_config_v1.yaml" 32 | mountPath: "/etc/kubernetes/encryption_provider_config_v1.yaml" 33 | readOnly: true 34 | pathType: File 35 | - name: socket 36 | hostPath: "/opt/kms" 37 | mountPath: "/opt/kms" 38 | -------------------------------------------------------------------------------- /scripts/kind-config_v2.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | containerdConfigPatches: 4 | # add a local docker registry to containerd 5 | # the registry is run via a separated docker container 6 | - |- 7 | [plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:5000"] 8 | endpoint = ["http://registry:5000"] 9 | nodes: 10 | - role: control-plane 11 | extraMounts: 12 | # mount encryption provider config available on all cp nodes 13 | - containerPath: /etc/kubernetes/encryption_provider_config_v2.yaml 14 | hostPath: scripts/encryption_provider_config_v2.yml 15 | readOnly: true 16 | propagation: None 17 | # vault-kubernetes-kms as a static Pod 18 | - containerPath: /etc/kubernetes/manifests/vault-kubernetes-kms.yaml 19 | hostPath: scripts/vault-kubernetes-kms.yml 20 | readOnly: true 21 | propagation: None 22 | # patch kube-apiserver 23 | kubeadmConfigPatches: 24 | - | 25 | kind: ClusterConfiguration 26 | apiServer: 27 | extraArgs: 28 | encryption-provider-config: "/etc/kubernetes/encryption_provider_config_v2.yaml" 29 | extraVolumes: 30 | - name: encryption-config 31 | hostPath: "/etc/kubernetes/encryption_provider_config_v2.yaml" 32 | mountPath: "/etc/kubernetes/encryption_provider_config_v2.yaml" 33 | readOnly: true 34 | pathType: File 35 | - name: socket 36 | hostPath: "/opt/kms" 37 | mountPath: "/opt/kms" 38 | -------------------------------------------------------------------------------- /scripts/local-registry.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xeu 3 | 4 | command -v docker >/dev/null 2>&1 || { echo "docker is not installed. Aborting." >&2; exit 1; } 5 | 6 | REGISTRY_NAME=registry 7 | REGISTRY_PORT=5000 8 | IMAGE_NAME=vault-kubernetes-kms 9 | 10 | echo "====> create kind docker network" 11 | docker network create kind || true 12 | 13 | echo "====> creating registry container unless it already exists" 14 | [[ $(docker ps -f "name=${REGISTRY_NAME}" --format '{{.Names}}') == $REGISTRY_NAME ]] || docker run -d --restart=always -p "${REGISTRY_PORT}:5000" --name "${REGISTRY_NAME}" registry:2 15 | 16 | echo "====> building container ..." 17 | docker build --no-cache -t "${IMAGE_NAME}:latest" . 18 | 19 | echo "====> tagging container ..." 20 | docker tag "${IMAGE_NAME}:latest" "localhost:${REGISTRY_PORT}/${IMAGE_NAME}:latest" 21 | 22 | echo "====> pushing container to local registry ...." 23 | docker push "localhost:${REGISTRY_PORT}/${IMAGE_NAME}:latest" 24 | 25 | echo "====> connecting registry to kind ...." 26 | docker network connect kind "${REGISTRY_NAME}" || true 27 | -------------------------------------------------------------------------------- /scripts/prometheus_values.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/prometheus-community/helm-charts/blob/main/charts/prometheus/values.yaml 2 | alertmanager: 3 | enabled: false 4 | 5 | kube-state-metrics: 6 | enabled: false 7 | 8 | prometheus-node-exporter: 9 | enabled: false 10 | 11 | prometheus-pushgateway: 12 | enabled: false 13 | 14 | extraScrapeConfigs: | 15 | - job_name: vault-kubernetes-kms 16 | metrics_path: /metrics 17 | static_configs: 18 | - targets: ["vault-kubernetes-kms.kube-system.svc.cluster.local:80"] 19 | -------------------------------------------------------------------------------- /scripts/svc.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: vault-kubernetes-kms 5 | namespace: kube-system 6 | spec: 7 | selector: 8 | app: vault-kubernetes-kms 9 | ports: 10 | - protocol: TCP 11 | port: 80 12 | targetPort: 8080 13 | -------------------------------------------------------------------------------- /scripts/vault-kubernetes-kms.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: vault-kubernetes-kms 5 | namespace: kube-system 6 | labels: 7 | app: vault-kubernetes-kms 8 | spec: 9 | priorityClassName: system-node-critical 10 | hostNetwork: true 11 | containers: 12 | - name: vault-kubernetes-kms 13 | image: localhost:5000/vault-kubernetes-kms:latest 14 | imagePullPolicy: IfNotPresent 15 | command: 16 | - /vault-kubernetes-kms 17 | - -vault-address=http://172.17.0.1:8200 18 | - -auth-method=token 19 | - -token=root 20 | volumeMounts: 21 | # mount /opt/kms host directory 22 | - name: kms 23 | mountPath: /opt/kms 24 | livenessProbe: 25 | httpGet: 26 | path: /health 27 | port: 8080 28 | readinessProbe: 29 | httpGet: 30 | path: /live 31 | port: 8080 32 | resources: 33 | requests: 34 | cpu: 100m 35 | memory: 128Mi 36 | limits: 37 | cpu: 2 38 | memory: 1Gi 39 | volumes: 40 | # mount /opt/kms host directory 41 | - name: kms 42 | hostPath: 43 | path: /opt/kms 44 | -------------------------------------------------------------------------------- /scripts/vault.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x 3 | 4 | command -v vault >/dev/null 2>&1 || { echo "vault is not installed. Aborting." >&2; exit 1; } 5 | 6 | # kill any remaining vault instances 7 | kill $(pgrep -x vault) || true 8 | 9 | # start developemnt vault 10 | nohup vault server -dev -dev-listen-address=0.0.0.0:8200 -dev-root-token-id=root 2> /dev/null & 11 | sleep 3 12 | 13 | # auth to vault 14 | export VAULT_ADDR="http://127.0.0.1:8200" 15 | export VAULT_SKIP_VERIFY="true" 16 | export VAULT_TOKEN="root" 17 | 18 | # enable transit engine 19 | vault secrets enable transit 20 | vault write -f transit/keys/kms 21 | 22 | # write vault policy 23 | vault policy write kms - <