├── .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 | [](https://github.com/FalcoSuessgott/vault-kubernetes-kms/actions/workflows/e2e.yml)
8 |
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 | 
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 | [](https://github.com/FalcoSuessgott/vault-kubernetes-kms/actions/workflows/e2e.yml)
5 |
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 | 
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 | 
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 - <