├── .copywrite.hcl ├── .github ├── actions │ └── integration-test │ │ └── action.yml ├── dependabot.yaml └── workflows │ ├── build.yml │ ├── jira.yaml │ └── tests.yaml ├── .gitignore ├── .go-version ├── .release ├── ci.hcl ├── release-metadata.hcl ├── security-scan.hcl └── vault-csi-provider-artifacts.hcl ├── CHANGELOG.md ├── CODEOWNERS ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── deployment └── vault-csi-provider.yaml ├── go.mod ├── go.sum ├── internal ├── auth │ └── kubernetes_jwt.go ├── client │ ├── client.go │ └── client_test.go ├── clientcache │ ├── cache_key.go │ ├── client_cache.go │ └── client_cache_test.go ├── config │ ├── config.go │ ├── config_test.go │ └── testdata │ │ └── example-parameters-string.txt ├── hmac │ ├── hmac.go │ └── hmac_test.go ├── provider │ ├── provider.go │ └── provider_test.go ├── server │ └── server.go └── version │ ├── version.go │ └── version_test.go ├── main.go ├── main_test.go ├── manifest_staging └── deployment │ └── vault-csi-provider.yaml ├── test └── bats │ ├── _helpers.bash │ ├── configs │ ├── cluster-resources.yaml │ ├── kind │ │ └── config.yaml │ ├── nginx-kv-env-var.yaml │ ├── nginx-kv-multiple-volumes.yaml │ ├── nginx │ │ ├── Chart.yaml │ │ └── templates │ │ │ ├── nginx.yaml │ │ │ └── serviceaccount.yaml │ ├── postgres-creation-statements.sql │ ├── postgres.yaml │ ├── vault-all-secretproviderclass.yaml │ ├── vault-db-secretproviderclass.yaml │ ├── vault-kv-custom-audience-secretproviderclass.yaml │ ├── vault-kv-namespace-secretproviderclass.yaml │ ├── vault-kv-secretproviderclass-jwt-auth.yaml │ ├── vault-kv-secretproviderclass.yaml │ ├── vault-kv-sync-multiple-secretproviderclass.yaml │ ├── vault-kv-sync-secretproviderclass.yaml │ ├── vault-pki-secretproviderclass.yaml │ ├── vault-policy-db.hcl │ ├── vault-policy-kv-custom-audience.hcl │ ├── vault-policy-kv-namespace.hcl │ ├── vault-policy-kv.hcl │ ├── vault-policy-pki.hcl │ └── vault │ │ ├── Chart.yaml │ │ ├── templates │ │ ├── bootstrap-configmap.yaml │ │ └── tls-secrets.yaml │ │ └── vault.values.yaml │ └── provider.bats └── tools ├── go.mod ├── go.sum └── tools.go /.copywrite.hcl: -------------------------------------------------------------------------------- 1 | schema_version = 1 2 | 3 | project { 4 | license = "BUSL-1.1" 5 | copyright_year = 2024 6 | 7 | # (OPTIONAL) A list of globs that should not have copyright/license headers. 8 | # Supports doublestar glob patterns for more flexibility in defining which 9 | # files or folders should be ignored 10 | header_ignore = [ 11 | # "vendors/**", 12 | # "**autogen**", 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.github/actions/integration-test/action.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | name: Integration test 5 | description: Run the integration tests against a single version of k8s and Vault 6 | inputs: 7 | k8s-version: 8 | description: 'Kubernetes version to use for the kind cluster' 9 | required: true 10 | vault-version: 11 | description: 'Vault version to use for the tests' 12 | required: true 13 | tarball-file: 14 | description: 'Name of the tarball file artifact to download' 15 | required: true 16 | vault-license: 17 | description: 'Vault license to use for enterprise tests' 18 | required: true 19 | kind-cluster-name: 20 | description: 'Name of the kind cluster to create and test against' 21 | default: 'kind' 22 | runs: 23 | using: "composite" 24 | steps: 25 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 26 | - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 27 | with: 28 | node-version: ${{ env.NODE_VERSION }} 29 | - run: npm install -g bats@${{ env.BATS_VERSION }} 30 | shell: bash 31 | - run: bats -v 32 | shell: bash 33 | 34 | - name: Create Kind Cluster 35 | uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0 36 | with: 37 | cluster_name: ${{ inputs.kind-cluster-name }} 38 | config: test/bats/configs/kind/config.yaml 39 | node_image: kindest/node:v${{ inputs.k8s-version }} 40 | version: ${{ env.KIND_VERSION }} 41 | 42 | - name: Create kind export log root 43 | id: create_kind_export_log_root 44 | shell: bash 45 | run: | 46 | vault_flavor=ent 47 | log_artifact_name="kind-${{ inputs.kind-cluster-name }}-$(git rev-parse --short ${{ github.sha }})-${{ inputs.k8s-version }}-${{ inputs.vault-version }}-${vault_flavor}-helm-logs" 48 | log_root="/tmp/${log_artifact_name}" 49 | mkdir -p "${log_root}" 50 | echo "log_root=${log_root}" >> $GITHUB_OUTPUT 51 | echo "log_artifact_name=${log_artifact_name}" >> $GITHUB_OUTPUT 52 | 53 | - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 54 | with: 55 | name: vault-csi-provider-image 56 | 57 | - name: Load vault-csi-provider dev image 58 | shell: bash 59 | run: docker image load --input ${{ inputs.tarball-file }} 60 | 61 | - name: bats tests 62 | shell: bash 63 | env: 64 | VAULT_LICENSE: ${{ inputs.vault-license }} 65 | run: make e2e-teardown e2e-setup e2e-test DISPLAY_SETUP_TEARDOWN_LOGS=true VAULT_VERSION="${{ inputs.vault-version }}" 66 | 67 | - name: export kind cluster logs 68 | if: always() 69 | shell: bash 70 | run: | 71 | kind export logs --name ${{ inputs.kind-cluster-name }} ${{ steps.create_kind_export_log_root.outputs.log_root }} 72 | 73 | - name: Store kind cluster logs 74 | if: success() 75 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 76 | with: 77 | name: ${{ steps.create_kind_export_log_root.outputs.log_artifact_name }} 78 | path: ${{ steps.create_kind_export_log_root.outputs.log_root }} 79 | 80 | - name: Store kind cluster logs failure 81 | if: failure() 82 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 83 | with: 84 | name: ${{ steps.create_kind_export_log_root.outputs.log_artifact_name }}-failed 85 | path: ${{ steps.create_kind_export_log_root.outputs.log_root }} 86 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | # Dependabot config that checks version updates for go.mod packages and docker 5 | # images, and also checks only for security updates for GitHub actions. 6 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 7 | 8 | version: 2 9 | updates: 10 | - package-ecosystem: "gomod" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | labels: ["dependencies"] 15 | groups: 16 | gomod-breaking: 17 | update-types: 18 | - major 19 | gomod-backward-compatible: 20 | update-types: 21 | - minor 22 | - patch 23 | - package-ecosystem: "github-actions" 24 | directory: "/" 25 | schedule: 26 | interval: "weekly" 27 | labels: ["dependencies"] 28 | groups: 29 | github-actions-breaking: 30 | update-types: 31 | - major 32 | github-actions-backward-compatible: 33 | update-types: 34 | - minor 35 | - patch 36 | # only update internal github actions, external github actions are handled 37 | # by https://github.com/hashicorp/security-tsccr/tree/main/automation 38 | allow: 39 | - dependency-name: "hashicorp/*" 40 | - package-ecosystem: "docker" 41 | directory: "/" 42 | schedule: 43 | interval: "weekly" 44 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | workflow_dispatch: 6 | inputs: 7 | version: 8 | description: "Version to build, e.g. 0.1.0" 9 | type: string 10 | required: false 11 | 12 | env: 13 | PKG_NAME: "vault-csi-provider" 14 | 15 | jobs: 16 | get-product-version: 17 | runs-on: ubuntu-latest 18 | outputs: 19 | product-version: ${{ steps.get-product-version.outputs.product-version }} 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | - name: get product version 23 | id: get-product-version 24 | run: | 25 | VERSION="${{ github.event.inputs.version || format('0.0.0-dev+{0}', github.sha) }}" 26 | echo "Using version ${VERSION}" 27 | echo "product-version=${VERSION}" >> $GITHUB_OUTPUT 28 | generate-metadata-file: 29 | needs: get-product-version 30 | runs-on: ubuntu-latest 31 | outputs: 32 | filepath: ${{ steps.generate-metadata-file.outputs.filepath }} 33 | steps: 34 | - name: 'Checkout directory' 35 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 36 | - name: Generate metadata file 37 | id: generate-metadata-file 38 | uses: hashicorp/actions-generate-metadata@v1 39 | with: 40 | version: ${{ needs.get-product-version.outputs.product-version }} 41 | product: ${{ env.PKG_NAME }} 42 | 43 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 44 | with: 45 | name: metadata.json 46 | path: ${{ steps.generate-metadata-file.outputs.filepath }} 47 | 48 | build: 49 | needs: 50 | - get-product-version 51 | runs-on: ubuntu-latest 52 | strategy: 53 | matrix: 54 | arch: ["arm", "arm64", "386", "amd64"] 55 | fail-fast: true 56 | 57 | name: Go linux ${{ matrix.arch }} build 58 | 59 | steps: 60 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 61 | 62 | - name: Setup go 63 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 64 | with: 65 | go-version-file: .go-version 66 | 67 | - name: Build 68 | id: build-binary 69 | env: 70 | GOOS: "linux" 71 | GOARCH: ${{ matrix.arch }} 72 | VERSION: ${{ needs.get-product-version.outputs.product-version }} 73 | shell: bash 74 | run: | 75 | BUILD_DIR=dist 76 | make build BUILD_DIR="${BUILD_DIR}" 77 | OUT_DIR='build/out' 78 | mkdir -p "${OUT_DIR}" 79 | cp -a LICENSE "${BUILD_DIR}/LICENSE.txt" 80 | ZIP_FILE="${OUT_DIR}/${{ env.PKG_NAME }}_${{ needs.get-product-version.outputs.product-version }}_linux_${{ matrix.arch }}.zip" 81 | zip -r -j "${ZIP_FILE}" "${BUILD_DIR}/" 82 | echo "path=${ZIP_FILE}" >> $GITHUB_OUTPUT 83 | echo "name=$(basename ${ZIP_FILE})" >> $GITHUB_OUTPUT 84 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 85 | with: 86 | name: ${{ steps.build-binary.outputs.name }} 87 | path: ${{ steps.build-binary.outputs.path }} 88 | 89 | build-docker: 90 | name: Docker ${{ matrix.arch }} build 91 | needs: 92 | - get-product-version 93 | - build 94 | runs-on: ubuntu-latest 95 | strategy: 96 | matrix: 97 | arch: ["arm", "arm64", "386", "amd64"] 98 | env: 99 | repo: ${{github.event.repository.name}} 100 | version: ${{needs.get-product-version.outputs.product-version}} 101 | 102 | steps: 103 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 104 | - name: Docker Build (Action) 105 | uses: hashicorp/actions-docker-build@v2 106 | with: 107 | version: ${{env.version}} 108 | target: default 109 | arch: ${{matrix.arch}} 110 | tags: | 111 | docker.io/hashicorp/${{env.repo}}:${{env.version}} 112 | public.ecr.aws/hashicorp/${{env.repo}}:${{env.version}} 113 | -------------------------------------------------------------------------------- /.github/workflows/jira.yaml: -------------------------------------------------------------------------------- 1 | name: Jira Sync 2 | on: 3 | issues: 4 | types: [opened, closed, deleted, reopened] 5 | pull_request_target: 6 | types: [opened, closed, reopened] 7 | issue_comment: # Also triggers when commenting on a PR from the conversation view 8 | types: [created] 9 | jobs: 10 | sync: 11 | uses: hashicorp/vault-workflows-common/.github/workflows/jira.yaml@main 12 | secrets: 13 | JIRA_SYNC_BASE_URL: ${{ secrets.JIRA_SYNC_BASE_URL }} 14 | JIRA_SYNC_USER_EMAIL: ${{ secrets.JIRA_SYNC_USER_EMAIL }} 15 | JIRA_SYNC_API_TOKEN: ${{ secrets.JIRA_SYNC_API_TOKEN }} 16 | with: 17 | teams-array: '["vault-eco"]' 18 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, workflow_dispatch] 4 | 5 | env: 6 | KIND_VERSION: "v0.27.0" 7 | BATS_VERSION: "1.11.1" 8 | NODE_VERSION: "19.8.1" 9 | TARBALL_FILE: vault-csi-provider.docker.tar 10 | 11 | jobs: 12 | versions: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - run: echo "setting versions" 16 | outputs: 17 | # JSON encoded array of k8s versions. 18 | K8S_VERSIONS: '["1.32.3", "1.31.6", "1.30.10", "1.29.14", "1.28.15"]' 19 | VAULT_N: "1.19.0" 20 | VAULT_N_1: "1.18.5" 21 | VAULT_N_2: "1.17.12" 22 | VAULT_LTS_1: "1.16.16" 23 | copyright: 24 | uses: hashicorp/vault-workflows-common/.github/workflows/copyright-headers.yaml@main 25 | go-checks: 26 | uses: hashicorp/vault-workflows-common/.github/workflows/go-checks.yaml@main 27 | lint: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 31 | - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 32 | with: 33 | go-version-file: .go-version 34 | 35 | - name: Install tools 36 | run: make bootstrap 37 | 38 | - name: Lint 39 | run: make lint GOLANGCI_LINT_FORMAT=colored-line-number 40 | 41 | build-and-test: 42 | runs-on: ubuntu-latest 43 | outputs: 44 | TARBALL_FILE: ${{ env.TARBALL_FILE }} 45 | steps: 46 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 47 | - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 48 | with: 49 | go-version-file: .go-version 50 | 51 | - name: Build 52 | run: | 53 | make e2e-image 54 | docker save --output "${TARBALL_FILE}" e2e/vault-csi-provider:latest 55 | 56 | - name: Test 57 | run: make test 58 | 59 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 60 | with: 61 | name: vault-csi-provider-image 62 | path: ${{ env.TARBALL_FILE }} 63 | 64 | latest-vault: 65 | name: vault:${{ matrix.vault-version }} kind:${{ matrix.k8s-version }} 66 | runs-on: ubuntu-latest 67 | needs: 68 | - versions 69 | - lint 70 | - build-and-test 71 | strategy: 72 | fail-fast: false 73 | matrix: 74 | vault-version: 75 | - ${{ needs.versions.outputs.VAULT_N }} 76 | k8s-version: ${{ fromJson(needs.versions.outputs.K8S_VERSIONS) }} 77 | 78 | steps: 79 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 80 | - uses: ./.github/actions/integration-test 81 | name: vault:${{ matrix.vault-version }} kind:${{ matrix.k8s-version }} 82 | with: 83 | k8s-version: ${{ matrix.k8s-version }} 84 | vault-version: ${{ matrix.vault-version }} 85 | tarball-file: ${{ needs.build-and-test.outputs.TARBALL_FILE }} 86 | vault-license: ${{ secrets.VAULT_LICENSE_CI }} 87 | 88 | latest-k8s: 89 | name: vault:${{ matrix.vault-version }} kind:${{ matrix.k8s-version }} 90 | needs: 91 | - versions 92 | - lint 93 | - build-and-test 94 | strategy: 95 | fail-fast: false 96 | matrix: 97 | k8s-version: 98 | - ${{ fromJson(needs.versions.outputs.K8S_VERSIONS)[0] }} 99 | vault-version: 100 | - ${{ needs.versions.outputs.VAULT_N_1 }} 101 | - ${{ needs.versions.outputs.VAULT_N_2 }} 102 | - ${{ needs.versions.outputs.VAULT_LTS_1 }} 103 | runs-on: ubuntu-latest 104 | steps: 105 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 106 | - uses: ./.github/actions/integration-test 107 | name: vault:${{ matrix.vault-version }} kind:${{ matrix.k8s-version }} 108 | with: 109 | k8s-version: ${{ matrix.k8s-version }} 110 | vault-version: ${{ matrix.vault-version }} 111 | tarball-file: ${{ needs.build-and-test.outputs.TARBALL_FILE }} 112 | vault-license: ${{ secrets.VAULT_LICENSE_CI }} 113 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | .idea 3 | scratch/ 4 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.23.7 2 | -------------------------------------------------------------------------------- /.release/ci.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | schema = "1" 5 | 6 | project "vault-csi-provider" { 7 | team = "vault" 8 | slack { 9 | // #vault-releases channel 10 | notification_channel = "C03RXFX5M4L" // #feed-vault-releases 11 | } 12 | github { 13 | organization = "hashicorp" 14 | repository = "vault-csi-provider" 15 | release_branches = ["main"] 16 | } 17 | } 18 | 19 | event "merge" { 20 | // "entrypoint" to use if build is not run automatically 21 | // i.e. send "merge" complete signal to orchestrator to trigger build 22 | } 23 | 24 | event "build" { 25 | depends = ["merge"] 26 | action "build" { 27 | organization = "hashicorp" 28 | repository = "vault-csi-provider" 29 | workflow = "build" 30 | } 31 | } 32 | 33 | event "prepare" { 34 | depends = ["build"] 35 | action "prepare" { 36 | organization = "hashicorp" 37 | repository = "crt-workflows-common" 38 | workflow = "prepare" 39 | depends = ["build"] 40 | } 41 | 42 | notification { 43 | on = "fail" 44 | } 45 | } 46 | 47 | ## These are promotion and post-publish events 48 | ## they should be added to the end of the file after the verify event stanza. 49 | 50 | event "trigger-staging" { 51 | // This event is dispatched by the bob trigger-promotion command 52 | // and is required - do not delete. 53 | } 54 | 55 | event "promote-staging" { 56 | depends = ["trigger-staging"] 57 | action "promote-staging" { 58 | organization = "hashicorp" 59 | repository = "crt-workflows-common" 60 | workflow = "promote-staging" 61 | config = "release-metadata.hcl" 62 | } 63 | 64 | notification { 65 | on = "always" 66 | } 67 | } 68 | 69 | event "promote-staging-docker" { 70 | depends = ["promote-staging"] 71 | action "promote-staging-docker" { 72 | organization = "hashicorp" 73 | repository = "crt-workflows-common" 74 | workflow = "promote-staging-docker" 75 | } 76 | 77 | notification { 78 | on = "always" 79 | } 80 | } 81 | 82 | event "trigger-production" { 83 | // This event is dispatched by the bob trigger-promotion command 84 | // and is required - do not delete. 85 | } 86 | 87 | event "promote-production" { 88 | depends = ["trigger-production"] 89 | action "promote-production" { 90 | organization = "hashicorp" 91 | repository = "crt-workflows-common" 92 | workflow = "promote-production" 93 | } 94 | 95 | notification { 96 | on = "always" 97 | } 98 | } 99 | 100 | event "promote-production-docker" { 101 | depends = ["promote-production"] 102 | action "promote-production-docker" { 103 | organization = "hashicorp" 104 | repository = "crt-workflows-common" 105 | workflow = "promote-production-docker" 106 | } 107 | 108 | notification { 109 | on = "always" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /.release/release-metadata.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | url_docker_registry_dockerhub = "https://registry.hub.docker.com/r/hashicorp/vault-csi-provider" 5 | url_license = "https://github.com/hashicorp/vault-csi-provider/blob/main/LICENSE" 6 | url_project_website = "https://www.vaultproject.io/docs/platform/k8s/csi" 7 | url_source_repository = "https://github.com/hashicorp/vault-csi-provider" 8 | -------------------------------------------------------------------------------- /.release/security-scan.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | container { 5 | dependencies = true 6 | alpine_secdb = true 7 | secrets = true 8 | } 9 | 10 | binary { 11 | secrets = true 12 | go_modules = true 13 | osv = true 14 | oss_index = false 15 | nvd = false 16 | } -------------------------------------------------------------------------------- /.release/vault-csi-provider-artifacts.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | schema = 1 5 | artifacts { 6 | zip = [ 7 | "vault-csi-provider_${version}_linux_386.zip", 8 | "vault-csi-provider_${version}_linux_amd64.zip", 9 | "vault-csi-provider_${version}_linux_arm.zip", 10 | "vault-csi-provider_${version}_linux_arm64.zip", 11 | ] 12 | container = [ 13 | "vault-csi-provider_default_linux_386_${version}_${commit_sha}.docker.tar", 14 | "vault-csi-provider_default_linux_amd64_${version}_${commit_sha}.docker.tar", 15 | "vault-csi-provider_default_linux_arm64_${version}_${commit_sha}.docker.tar", 16 | "vault-csi-provider_default_linux_arm_${version}_${commit_sha}.docker.tar", 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | CHANGES: 4 | 5 | * Build with Go v1.23.7 6 | * Test with K8s 1.28-1.32 7 | * Test with Vault 1.16-1.19 8 | * Updated Docker base image from alpine 3.20.2 -> 3.21.0 9 | * Updated dependencies: 10 | * github.com/hashicorp/vault/api v1.14.0 -> v1.16.0 11 | * golang.org/x/crypto v0.26.0 => v0.36.0 12 | * golang.org/x/net v0.28.0 => v0.37.0 13 | * golang.org/x/oauth2 v0.20.0 => v0.28.0 14 | * golang.org/x/sys v0.23.0 => v0.31.0 15 | * golang.org/x/term v0.23.0 => v0.30.0 16 | * golang.org/x/text v0.17.0 => v0.23.0 17 | * google.golang.org/grpc v1.65.0 -> v1.71.0 18 | * k8s.io/api v0.30.3 -> v0.32.2 19 | * k8s.io/apimachinery v0.30.3 -> v0.32.2 20 | * k8s.io/client-go v0.30.3 -> v0.32.2 21 | * k8s.io/utils v0.0.0-20230726121419-3b25d923346b -> v0.0.0-20241104100929-3ea5e8cea738 22 | * sigs.k8s.io/secrets-store-csi-driver v1.4.4 -> v1.4.8 23 | 24 | ## 1.5.0 (August 8th, 2024) 25 | 26 | FEATURES: 27 | 28 | * Add ability to tune log levels with `-log-level` flag. [[GH-295](https://github.com/hashicorp/vault-csi-provider/pull/295)] 29 | 30 | CHANGES: 31 | 32 | * Build with Go v1.22.6 33 | * Updated Docker base image from alpine 3.20.1 -> 3.20.2 34 | * Updated dependencies: 35 | * k8s.io/api v0.30.2 -> v0.30.3 36 | * k8s.io/apimachinery v0.30.2 -> v0.30.3 37 | * k8s.io/client-go v0.30.2 -> v0.30.3 38 | * golang.org/x/crypto v0.24.0 -> v0.26.0 39 | * golang.org/x/net v0.26.0 -> v0.28.0 40 | * golang.org/x/sys v0.21.0 -> v0.23.0 41 | * golang.org/x/term v0.21.0 -> v0.23.0 42 | * golang.org/x/text v0.16.0 -> v0.17.0 43 | 44 | ## 1.4.3 (July 3rd, 2024) 45 | 46 | CHANGES: 47 | 48 | * Build with Go v1.22.5 49 | * Test with K8s 1.26-1.30 50 | * Test with Vault 1.15-1.17 51 | * Updated Docker base image from alpine 3.19.1 -> 3.20.1 52 | * Updated dependencies: 53 | * github.com/hashicorp/go-hclog v1.6.2 -> v1.6.3 54 | * github.com/hashicorp/vault/api v1.12.2 -> v1.14.0 55 | * golang.org/x/crypto v0.21.0 -> v0.24.0 56 | * golang.org/x/net v0.22.0 -> v0.26.0 57 | * golang.org/x/sys v0.18.0 -> v0.21.0 58 | * golang.org/x/term v0.18.0 -> v0.21.0 59 | * golang.org/x/text v0.14.0 -> v0.16.0 60 | * google.golang.org/grpc v1.62.1 -> v1.65.0 61 | * k8s.io/api v0.29.3 -> v0.30.2 62 | * k8s.io/apimachinery v0.29.3 -> v0.30.2 63 | * k8s.io/client-go v0.29.3 -> v0.30.2 64 | * sigs.k8s.io/secrets-store-csi-driver v1.4.2 -> v1.4.4 65 | 66 | ## 1.4.2 (March 27th, 2024) 67 | 68 | CHANGES: 69 | 70 | * Build with Go v1.22.1 71 | * Test with K8s 1.25-1.29 72 | * Test with Vault 1.13-1.15 73 | * Updated Docker base image from alpine 3.18.4 -> 3.19.1 74 | * Updated dependencies: 75 | * github.com/go-jose/go-jose/v3 v3.0.1 -> 3.0.3 76 | * github.com/hashicorp/go-hclog v1.5.0 -> v1.6.2 77 | * github.com/hashicorp/golang-lru/v2 v2.0.2 -> v2.0.7 78 | * github.com/hashicorp/vault/api v1.9.0 -> v1.12.2 79 | * github.com/stretchr/testify v1.8.2 -> v1.9.0 80 | * golang.org/x/crypto v0.14.0 -> v0.21.0 81 | * golang.org/x/net v0.17.0 -> v0.22.0 82 | * golang.org/x/sys v0.13.0 -> v0.18.0 83 | * golang.org/x/term v0.13.0 -> v0.18.0 84 | * google.golang.org/grpc v1.56.3 -> v1.62.1 85 | * google.golang.org/protobuf v1.30.0 -> v1.33.0 86 | * k8s.io/api v0.26.3 -> v0.29.3 87 | * k8s.io/apimachinery v0.26.3 -> v0.29.3 88 | * k8s.io/client-go v0.26.3 -> v0.29.3 89 | * k8s.io/utils v0.0.0-20230313181309-38a27ef9d749 -> v0.0.0-20230726121419-3b25d923346b 90 | * sigs.k8s.io/secrets-store-csi-driver v1.3.3 -> v1.4.2 91 | 92 | ## 1.4.1 (October 26th, 2023) 93 | 94 | CHANGES: 95 | 96 | * Build with Go v1.21.3 97 | * Test with K8s 1.24-1.28 98 | * Updated Docker base image from alpine 3.17.3 -> 3.18.4 99 | * Updated dependencies: 100 | * golang.org/x/crypto v0.6.0 -> v0.14.0 101 | * golang.org/x/net v0.8.0 -> v0.17.0 102 | * golang.org/x/sys v0.6.0 -> v0.13.0 103 | * golang.org/x/term v0.6.0 -> v0.13.0 104 | * golang.org/x/text v0.8.0 -> v0.13.0 105 | * google.golang.org/grpc v1.54.0 -> v1.56.3 106 | * sigs.k8s.io/secrets-store-csi-driver v1.3.2 -> v1.3.3 107 | 108 | ## 1.4.0 (April 28th, 2023) 109 | 110 | CHANGES: 111 | 112 | * SecretProviderClass objects now also accept `spec.parameters.vaultAuthMountPath` as an alternative to `spec.parameters.vaultKubernetesMountPath`. [[GH-210](https://github.com/hashicorp/vault-csi-provider/pull/210)] 113 | 114 | FEATURES: 115 | 116 | * The Provider will cache a Vault token per requesting pod in memory and re-use it until it expires. [[GH-202](https://github.com/hashicorp/vault-csi-provider/pull/202)] 117 | * JWT auth is supported by setting role name and auth mount path in the same way as for Kubernetes auth. [[GH-210](https://github.com/hashicorp/vault-csi-provider/pull/210)] 118 | 119 | ## 1.3.0 (April 5th, 2023) 120 | 121 | CHANGES: 122 | 123 | * Vault CSI Provider will use service account tokens passed from the Secrets Store CSI Driver instead of generating one if an appropriate token is provided. [[GH-163](https://github.com/hashicorp/vault-csi-provider/pull/163)] 124 | * The Secrets Store CSI driver needs to be configured to generate tokens with the correct audience for this feature. Vault CSI Provider 125 | will look for a token with the audience specified in the SecretProviderClass, or otherwise "vault". To configure the driver to generate 126 | a token with the correct audience, use the 127 | [`tokenRequests`](https://github.com/kubernetes-sigs/secrets-store-csi-driver/tree/main/charts/secrets-store-csi-driver#configuration) 128 | option from the _driver_ helm chart via the flag `--set tokenRequests[0].audience="vault"`. See 129 | [CSI TokenRequests documentation](https://kubernetes-csi.github.io/docs/token-requests.html) for further details. 130 | * Vault CSI Provider now creates a Kubernetes secret with an HMAC key to produce consistent hashes for secret versions. [[GH-198](https://github.com/hashicorp/vault-csi-provider/pull/198)] 131 | * Requires RBAC permissions to create secrets, and read the same specific secret back. Versions are not generated otherwise and a warning 132 | is logged on each mount that fails to generate a version. 133 | * Supports creating the secret with custom name via `-hmac-secret-name` 134 | * Updated Docker base image from alpine 3.16.3 -> 3.17.3 135 | * Build with Go v1.20.3 136 | * Updated dependencies: 137 | * github.com/hashicorp/go-hclog v1.3.1 -> v1.5.0 138 | * github.com/hashicorp/vault/api v1.8.2 -> v1.9.0 139 | * github.com/stretchr/testify v1.8.1 -> v1.8.2 140 | * google.golang.org/grpc v1.50.1 -> v1.54.0 141 | * k8s.io/api v0.25.4 -> v0.26.3 142 | * k8s.io/apimachinery v0.25.4 -> v0.26.3 143 | * k8s.io/client-go v0.25.4 -> v0.26.3 144 | * k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed -> v0.0.0-20230313181309-38a27ef9d749 145 | * sigs.k8s.io/secrets-store-csi-driver v1.2.4 -> v1.3.2 146 | * Tests are now run against Kubernetes versions: 1.22.17, 1.23.17, 1.24.12, 1.25.8, 1.26.3 147 | 148 | IMPROVEMENTS: 149 | 150 | * Support utf-8 (default), hex, and base64 encoded secrets [[GH-194](https://github.com/hashicorp/vault-csi-provider/pull/194)] 151 | 152 | ## 1.2.1 (November 21st, 2022) 153 | 154 | CHANGES: 155 | 156 | * Updated dependencies: 157 | * github.com/hashicorp/go-hclog v1.0.0 -> v1.3.1 158 | * github.com/hashicorp/vault/api v1.2.0 -> v1.8.2 159 | * github.com/stretchr/testify v1.7.2 -> v1.8.1 160 | * google.golang.org/grpc v1.41.0 -> v1.50.1 161 | * k8s.io/api v0.22.2 -> v0.25.4 162 | * k8s.io/apimachinery v0.22.2 -> v0.25.4 163 | * k8s.io/client-go v0.22.2 -> v0.25.4 164 | * sigs.k8s.io/secrets-store-csi-driver v1.0.0 -> v1.2.4 165 | * golang.org/x/net v0.0.0-20220722155237-a158d28d115b -> v0.0.0-20221012135044-0b7e1fb9d458 166 | * golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f -> v0.0.0-20220728004956-3c1f35247d10 167 | * golang.org/x/text v0.3.7 -> v0.3.8 168 | * Updated Docker base image from alpine 3.15.0 -> 3.16.3 169 | 170 | ## 1.2.0 (August 8th, 2022) 171 | 172 | CHANGES: 173 | 174 | * Duplicate object names now trigger an error instead of silently overwriting files. [[GH-148](https://github.com/hashicorp/vault-csi-provider/pull/148)] 175 | 176 | BUGS: 177 | 178 | * `VAULT_ADDR` environment variable can now be used to set the Vault address. [[GH-160](https://github.com/hashicorp/vault-csi-provider/pull/160)] 179 | * Secret mounting correctly fails now if the secret path exists but the requested secret key does not. [[GH-166](https://github.com/hashicorp/vault-csi-provider/issues/166)] 180 | 181 | IMPROVEMENTS: 182 | 183 | * Secret versions are now reported as a hash of their input and contents instead of hardcoded to 0. [[GH-148](https://github.com/hashicorp/vault-csi-provider/pull/148)] 184 | * Bump github.com/stretchr/testify from v1.7.0 to v1.7.2. [[GH-161](https://github.com/hashicorp/vault-csi-provider/pull/161)] 185 | * Bump gopkg.in/yaml.v3 from v3.0.0-20210107192922-496545a6307b to v3.0.1. [[GH-161](https://github.com/hashicorp/vault-csi-provider/pull/161)] 186 | 187 | ## 1.1.0 (April 26th, 2022) 188 | 189 | IMPROVEMENTS: 190 | 191 | * New flags to configure default Vault namespace and TLS details. [[GH-138](https://github.com/hashicorp/vault-csi-provider/pull/138)] 192 | * `-vault-namespace` 193 | * `-vault-tls-ca-cert` 194 | * `-vault-tls-ca-directory` 195 | * `-vault-tls-server-name` 196 | * `-vault-tls-client-cert` 197 | * `-vault-tls-client-key` 198 | * `-vault-tls-skip-verify` 199 | * Add an optional SecretProviderClass parameter `audience` to customize the `aud` claim in the JWT [[GH-144](https://github.com/hashicorp/vault-csi-provider/pull/144)] 200 | * New SecretProviderClass field `filePermission` can be used per-secret to set the file permissions it is written with. [[GH-139](https://github.com/hashicorp/vault-csi-provider/pull/139)] 201 | 202 | ## 1.0.0 (January 25th, 2022) 203 | 204 | CHANGES: 205 | 206 | * `-write-secrets` flag removed. All secrets are now written to the filesystem by the CSI secrets store driver. [[GH-133](https://github.com/hashicorp/vault-csi-provider/pull/133)] 207 | * **NOTE:** CSI secrets store driver v0.0.21+ is required. 208 | * `-health_addr` flag removed, use `-health-addr` instead. [[GH-133](https://github.com/hashicorp/vault-csi-provider/pull/133)] 209 | * Warning logs are no longer printed when deprecated SecretProviderClass fields `kubernetesServiceAccountPath` and `vaultCAPem` are used. [[GH-134](https://github.com/hashicorp/vault-csi-provider/pull/134)] 210 | 211 | ## 0.4.0 (January 12th, 2022) 212 | 213 | CHANGES: 214 | 215 | * `-write-secrets` flag now defaults to `false`, delegating file writes to the driver. [[GH-127](https://github.com/hashicorp/vault-csi-provider/pull/127)] 216 | * **Note:** `-write-secrets` is deprecated and will be removed in the next major version. 217 | 218 | FEATURES: 219 | 220 | * Support extracting JSON values using `secretKey` in the SecretProviderClass [[GH-126](https://github.com/hashicorp/vault-csi-provider/pull/126)] 221 | 222 | ## 0.3.0 (June 7th, 2021) 223 | 224 | FEATURES: 225 | 226 | * Support for changing the default Vault address and Kubernetes mount path via CLI flag to the vault-csi-provider binary [[GH-96](https://github.com/hashicorp/vault-csi-provider/pull/96)] 227 | * Support for sending secret contents to driver for writing via `-write-secrets=false` [[GH-89](https://github.com/hashicorp/vault-csi-provider/pull/89)] 228 | * **Note:** `-write-secrets=false` will become the default from v0.4.0 and require secrets-store-csi-driver v0.0.21+ 229 | 230 | CHANGES: 231 | 232 | * `-health_addr` flag is marked deprecated and replaced by `-health-addr`. Slated for removal in v0.5.0 [[GH-100](https://github.com/hashicorp/vault-csi-provider/pull/100)] 233 | 234 | BUGS: 235 | 236 | * Added missing error handling when transforming SecretProviderClass config to a Vault request [[GH-97](https://github.com/hashicorp/vault-csi-provider/pull/97)] 237 | 238 | ## 0.2.0 (April 14th, 2021) 239 | 240 | FEATURES: 241 | 242 | * Support for Vault namespaces, via `vaultNamespace` option in SecretProviderClass parameters [[GH-84](https://github.com/hashicorp/vault-csi-provider/pull/84)] 243 | 244 | ## 0.1.0 (March 24th, 2021) 245 | 246 | CHANGES: 247 | 248 | * All secret engines are now supported [[GH-63](https://github.com/hashicorp/vault-csi-provider/pull/63)] 249 | * **This makes several breaking changes to the configuration of the SecretProviderClass' `objects` entry** 250 | * There is no top-level `array` entry under `objects` 251 | * `objectVersion` is now ignored 252 | * `objectPath` is renamed to `secretPath` 253 | * `secretKey`, `secretArgs` and `method` are newly available options 254 | * `objectName` no longer determines which key is read from the secret's data 255 | * If `secretKey` is set, that is the key from the secret's data that will be written 256 | * If `secretKey` is not set, the whole JSON response from Vault will be written 257 | * `vaultSkipTLSVerify` is no longer required to be set to `"true"` if the `vaultAddress` scheme is not `https` 258 | * The provider will now authenticate to Vault as the requesting pod's service account [[GH-64](https://github.com/hashicorp/vault-csi-provider/pull/64)] 259 | * **This is likely a breaking change for existing deployments being upgraded** 260 | * vault-csi-provider service account now requires cluster-wide permission to create service account tokens 261 | * auth/kubernetes mounts in Vault will now need to bind ACL policies to the requesting pods' 262 | service accounts instead of the provider's service account. 263 | * `spec.parameters.kubernetesServiceAccountPath` is now ignored and will log a warning if set 264 | * The provider now supports mTLS [[GH-65](https://github.com/hashicorp/vault-csi-provider/pull/65)] 265 | * `spec.parameters.vaultCAPem` is now ignored and will log a warning if set. **This is a breaking change** 266 | * `spec.parameters.vaultTLSClientCertPath` and `spec.parameters.vaultTLSClientKeyPath` are newly available options 267 | 268 | IMPROVEMENTS 269 | 270 | * The provider now uses the `hashicorp/vault/api` package to communicate with Vault [[GH-61](https://github.com/hashicorp/vault-csi-provider/pull/61)] 271 | * `-version` flag will now print the version of Go used to build the provider [[GH-62](https://github.com/hashicorp/vault-csi-provider/pull/62)] 272 | * CircleCI linting, tests and integration tests added [[GH-60](https://github.com/hashicorp/vault-csi-provider/pull/60)] 273 | 274 | ## 0.0.7 (January 20th, 2021) 275 | 276 | CHANGES: 277 | 278 | * Switch provider to gRPC. [[GH-54](https://github.com/hashicorp/vault-csi-provider/pull/54)] 279 | * Note this requires at least v0.0.14 of the driver, and the driver should have 'vault' included in `--grpcSupportedProviders`. 280 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hashicorp/vault-ecosystem 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | # This Dockerfile contains multiple targets. 5 | # Use 'docker build --target= .' to build one. 6 | 7 | ARG GO_VERSION=latest 8 | 9 | # devbuild compiles the binary 10 | # ----------------------------------- 11 | FROM docker.mirror.hashicorp.services/golang:${GO_VERSION} AS devbuild 12 | ENV CGO_ENABLED=0 13 | # Leave the GOPATH 14 | WORKDIR /build 15 | COPY . ./ 16 | RUN go build -o vault-csi-provider 17 | 18 | # dev runs the binary from devbuild 19 | # ----------------------------------- 20 | FROM docker.mirror.hashicorp.services/alpine:3.21.3 AS dev 21 | COPY --from=devbuild /build/vault-csi-provider /bin/ 22 | ENTRYPOINT [ "/bin/vault-csi-provider" ] 23 | 24 | # Default release image. 25 | # ----------------------------------- 26 | FROM docker.mirror.hashicorp.services/alpine:3.21.3 AS default 27 | 28 | ARG PRODUCT_VERSION 29 | ARG PRODUCT_REVISION 30 | ARG PRODUCT_NAME=vault-csi-provider 31 | ARG TARGETOS 32 | ARG TARGETARCH 33 | 34 | LABEL name="Vault CSI Provider" \ 35 | maintainer="Vault Team " \ 36 | vendor="HashiCorp" \ 37 | version=$PRODUCT_VERSION \ 38 | release=$PRODUCT_VERSION \ 39 | revision=$PRODUCT_REVISION \ 40 | org.opencontainers.image.licenses="BUSL-1.1" \ 41 | summary="HashiCorp Vault Provider for Secret Store CSI Driver for Kubernetes" \ 42 | description="Provides a CSI provider for Kubernetes to interact with HashiCorp Vault." 43 | 44 | RUN set -eux && \ 45 | apk update && \ 46 | apk upgrade --no-cache libcrypto3 47 | 48 | # Copy license to conform to HC IPS-002 49 | COPY LICENSE /usr/share/doc/$PRODUCT_NAME/LICENSE.txt 50 | 51 | COPY dist/$TARGETOS/$TARGETARCH/vault-csi-provider /bin/ 52 | ENTRYPOINT [ "/bin/vault-csi-provider" ] 53 | 54 | # =================================== 55 | # 56 | # Set default target to 'dev'. 57 | # 58 | # =================================== 59 | FROM dev 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved. 2 | "Business Source License" is a trademark of MariaDB Corporation Ab. 3 | 4 | Parameters 5 | 6 | Licensor: HashiCorp, Inc. 7 | Licensed Work: Vault CSI Provider Version 1.4.1 or later. 8 | The Licensed Work is (c) 2024 HashiCorp, Inc. 9 | Additional Use Grant: You may make production use of the Licensed Work, provided 10 | Your use does not include offering the Licensed Work to third 11 | parties on a hosted or embedded basis in order to compete with 12 | HashiCorp's paid version(s) of the Licensed Work. For purposes 13 | of this license: 14 | 15 | A "competitive offering" is a Product that is offered to third 16 | parties on a paid basis, including through paid support 17 | arrangements, that significantly overlaps with the capabilities 18 | of HashiCorp's paid version(s) of the Licensed Work. If Your 19 | Product is not a competitive offering when You first make it 20 | generally available, it will not become a competitive offering 21 | later due to HashiCorp releasing a new version of the Licensed 22 | Work with additional capabilities. In addition, Products that 23 | are not provided on a paid basis are not competitive. 24 | 25 | "Product" means software that is offered to end users to manage 26 | in their own environments or offered as a service on a hosted 27 | basis. 28 | 29 | "Embedded" means including the source code or executable code 30 | from the Licensed Work in a competitive offering. "Embedded" 31 | also means packaging the competitive offering in such a way 32 | that the Licensed Work must be accessed or downloaded for the 33 | competitive offering to operate. 34 | 35 | Hosting or using the Licensed Work(s) for internal purposes 36 | within an organization is not considered a competitive 37 | offering. HashiCorp considers your organization to include all 38 | of your affiliates under common control. 39 | 40 | For binding interpretive guidance on using HashiCorp products 41 | under the Business Source License, please visit our FAQ. 42 | (https://www.hashicorp.com/license-faq) 43 | Change Date: Four years from the date the Licensed Work is published. 44 | Change License: MPL 2.0 45 | 46 | For information about alternative licensing arrangements for the Licensed Work, 47 | please contact licensing@hashicorp.com. 48 | 49 | Notice 50 | 51 | Business Source License 1.1 52 | 53 | Terms 54 | 55 | The Licensor hereby grants you the right to copy, modify, create derivative 56 | works, redistribute, and make non-production use of the Licensed Work. The 57 | Licensor may make an Additional Use Grant, above, permitting limited production use. 58 | 59 | Effective on the Change Date, or the fourth anniversary of the first publicly 60 | available distribution of a specific version of the Licensed Work under this 61 | License, whichever comes first, the Licensor hereby grants you rights under 62 | the terms of the Change License, and the rights granted in the paragraph 63 | above terminate. 64 | 65 | If your use of the Licensed Work does not comply with the requirements 66 | currently in effect as described in this License, you must purchase a 67 | commercial license from the Licensor, its affiliated entities, or authorized 68 | resellers, or you must refrain from using the Licensed Work. 69 | 70 | All copies of the original and modified Licensed Work, and derivative works 71 | of the Licensed Work, are subject to this License. This License applies 72 | separately for each version of the Licensed Work and the Change Date may vary 73 | for each version of the Licensed Work released by Licensor. 74 | 75 | You must conspicuously display this License on each original or modified copy 76 | of the Licensed Work. If you receive the Licensed Work in original or 77 | modified form from a third party, the terms and conditions set forth in this 78 | License apply to your use of that work. 79 | 80 | Any use of the Licensed Work in violation of this License will automatically 81 | terminate your rights under this License for the current and all other 82 | versions of the Licensed Work. 83 | 84 | This License does not grant you any right in any trademark or logo of 85 | Licensor or its affiliates (provided that you may use a trademark or logo of 86 | Licensor as expressly required by this License). 87 | 88 | TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON 89 | AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, 90 | EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF 91 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND 92 | TITLE. 93 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILD_DIR ?= dist 2 | REGISTRY_NAME?=docker.io/hashicorp 3 | IMAGE_NAME=vault-csi-provider 4 | VERSION?=0.0.0-dev 5 | IMAGE_TAG=$(REGISTRY_NAME)/$(IMAGE_NAME):$(VERSION) 6 | IMAGE_TAG_LATEST=$(REGISTRY_NAME)/$(IMAGE_NAME):latest 7 | # https://reproducible-builds.org/docs/source-date-epoch/ 8 | DATE_FMT=+%Y-%m-%d-%H:%M 9 | SOURCE_DATE_EPOCH ?= $(shell git log -1 --pretty=%ct) 10 | ifdef SOURCE_DATE_EPOCH 11 | BUILD_DATE ?= $(shell date -u -d "@$(SOURCE_DATE_EPOCH)" $(DATE_FMT) 2>/dev/null || date -u -r "$(SOURCE_DATE_EPOCH)" $(DATE_FMT) 2>/dev/null || date -u $(DATE_FMT)) 12 | else 13 | BUILD_DATE ?= $(shell date $(DATE_FMT)) 14 | endif 15 | PKG=github.com/hashicorp/vault-csi-provider/internal/version 16 | LDFLAGS?="-X '$(PKG).BuildVersion=$(VERSION)' \ 17 | -X '$(PKG).BuildDate=$(BUILD_DATE)' \ 18 | -X '$(PKG).GoVersion=$(shell go version)'" 19 | CSI_DRIVER_VERSION=1.4.8 20 | VAULT_HELM_VERSION=0.29.1 21 | VAULT_VERSION=1.19.0 22 | GOLANGCI_LINT_FORMAT?=colored-line-number 23 | 24 | VAULT_VERSION_ARGS=--set server.image.tag=$(VAULT_VERSION) --set csi.agent.image.tag=$(VAULT_VERSION) 25 | ifdef VAULT_LICENSE 26 | VAULT_VERSION_ARGS=--set server.image.repository=docker.mirror.hashicorp.services/hashicorp/vault-enterprise \ 27 | --set server.image.tag=$(VAULT_VERSION)-ent \ 28 | --set server.enterpriseLicense.secretName=vault-ent-license \ 29 | --set csi.agent.image.repository=docker.mirror.hashicorp.services/hashicorp/vault-enterprise \ 30 | --set csi.agent.image.tag=$(VAULT_VERSION)-ent 31 | 32 | endif 33 | 34 | .PHONY: default build test bootstrap fmt lint image e2e-image e2e-setup e2e-teardown e2e-test mod setup-kind promote-staging-manifest copyright clean 35 | 36 | GO111MODULE?=on 37 | export GO111MODULE 38 | 39 | default: test 40 | 41 | bootstrap: 42 | @echo "Downloading tools..." 43 | @go generate -tags tools tools/tools.go 44 | 45 | fmt: 46 | gofumpt -l -w . 47 | 48 | lint: 49 | golangci-lint run \ 50 | --disable-all \ 51 | --timeout=10m \ 52 | --out-format=$(GOLANGCI_LINT_FORMAT) \ 53 | --enable=gofmt \ 54 | --enable=gosimple \ 55 | --enable=govet \ 56 | --enable=errcheck \ 57 | --enable=ineffassign \ 58 | --enable=unused 59 | 60 | build: clean 61 | CGO_ENABLED=0 go build \ 62 | -ldflags $(LDFLAGS) \ 63 | -o $(BUILD_DIR)/ \ 64 | . 65 | 66 | test: 67 | go test ./... 68 | 69 | image: 70 | docker build \ 71 | --build-arg GO_VERSION=$(shell cat .go-version) \ 72 | --target dev \ 73 | --no-cache \ 74 | --tag $(IMAGE_TAG) \ 75 | . 76 | 77 | e2e-image: 78 | REGISTRY_NAME="e2e" VERSION="latest" make image 79 | 80 | setup-kind: 81 | kind create cluster 82 | 83 | e2e-setup: 84 | kind load docker-image e2e/vault-csi-provider:latest 85 | kubectl apply -f test/bats/configs/cluster-resources.yaml 86 | helm install secrets-store-csi-driver secrets-store-csi-driver \ 87 | --repo https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts --version=$(CSI_DRIVER_VERSION) \ 88 | --wait --timeout=5m \ 89 | --namespace=csi \ 90 | --set linux.image.pullPolicy="IfNotPresent" \ 91 | --set syncSecret.enabled=true \ 92 | --set tokenRequests[0].audience="vault" 93 | @if [ -n "$(VAULT_LICENSE)" ]; then\ 94 | kubectl create --namespace=csi secret generic vault-ent-license --from-literal="license=$(VAULT_LICENSE)";\ 95 | fi 96 | helm install vault-bootstrap test/bats/configs/vault \ 97 | --namespace=csi 98 | helm install vault vault \ 99 | --repo https://helm.releases.hashicorp.com --version=$(VAULT_HELM_VERSION) \ 100 | --wait --timeout=5m \ 101 | --namespace=csi \ 102 | --values=test/bats/configs/vault/vault.values.yaml \ 103 | $(VAULT_VERSION_ARGS) 104 | kubectl wait --namespace=csi --for=condition=Ready --timeout=5m pod -l app.kubernetes.io/name=vault 105 | kubectl exec -i --namespace=csi vault-0 -- /bin/sh /mnt/bootstrap/bootstrap.sh 106 | kubectl wait --namespace=csi --for=condition=Ready --timeout=5m pod -l app.kubernetes.io/name=vault-csi-provider 107 | 108 | e2e-teardown: 109 | helm uninstall --namespace=csi vault || true 110 | helm uninstall --namespace=csi vault-bootstrap || true 111 | helm uninstall --namespace=csi secrets-store-csi-driver || true 112 | kubectl delete --ignore-not-found -f test/bats/configs/cluster-resources.yaml 113 | 114 | e2e-test: 115 | bats test/bats/provider.bats 116 | 117 | mod: 118 | @go mod tidy 119 | 120 | promote-staging-manifest: #promote staging manifests to release dir 121 | @rm -rf deployment 122 | @cp -r manifest_staging/deployment . 123 | 124 | copyright: 125 | copywrite headers 126 | 127 | clean: 128 | -rm -rf $(BUILD_DIR) 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HashiCorp Vault Provider for Secrets Store CSI Driver 2 | 3 | > :warning: **Please note**: We take Vault's security and our users' trust very seriously. If 4 | you believe you have found a security issue in Vault CSI Provider, _please responsibly disclose_ 5 | by contacting us at [security@hashicorp.com](mailto:security@hashicorp.com). 6 | 7 | HashiCorp [Vault](https://vaultproject.io) provider for the [Secrets Store CSI driver](https://github.com/kubernetes-sigs/secrets-store-csi-driver) allows you to get secrets stored in 8 | Vault and use the Secrets Store CSI driver interface to mount them into Kubernetes pods. 9 | 10 | ## Installation 11 | 12 | ### Prerequisites 13 | 14 | * Supported Kubernetes version, see the [documentation](https://developer.hashicorp.com/vault/docs/platform/k8s/csi#supported-kubernetes-versions) (runs on Linux nodes only) 15 | * [Secrets store CSI driver](https://secrets-store-csi-driver.sigs.k8s.io/getting-started/installation.html) installed 16 | 17 | ### Using helm 18 | 19 | The recommended installation method is via helm 3: 20 | 21 | ```bash 22 | helm repo add hashicorp https://helm.releases.hashicorp.com 23 | # Just installs Vault CSI provider. Adjust `server.enabled` and `injector.enabled` 24 | # if you also want helm to install Vault and the Vault Agent injector. 25 | helm install vault hashicorp/vault \ 26 | --set "server.enabled=false" \ 27 | --set "injector.enabled=false" \ 28 | --set "csi.enabled=true" 29 | ``` 30 | 31 | ### Using yaml 32 | 33 | You can also install using the deployment config in the `deployment` folder: 34 | 35 | ```bash 36 | kubectl apply -f deployment/vault-csi-provider.yaml 37 | ``` 38 | 39 | ## Usage 40 | 41 | See the [learn tutorial](https://learn.hashicorp.com/tutorials/vault/kubernetes-secret-store-driver) 42 | and [documentation pages](https://www.vaultproject.io/docs/platform/k8s/csi) for 43 | full details of deploying, configuring and using Vault CSI provider. The 44 | integration tests in [test/bats/provider.bats](./test/bats/provider.bats) also 45 | provide a good set of fully worked and tested examples to build on. 46 | 47 | ## Troubleshooting 48 | 49 | To troubleshoot issues with Vault CSI provider, look at logs from the Vault CSI 50 | provider pod running on the same node as your application pod: 51 | 52 | ```bash 53 | kubectl get pods -o wide 54 | # find the Vault CSI provider pod running on the same node as your application pod 55 | 56 | kubectl logs vault-csi-provider-7x44t 57 | ``` 58 | 59 | **Warning** 60 | The `-debug=true` flag has been deprecated, please use `-log-level=debug` instead. 61 | Available log levels are `info`, `debug`, `trace`, `warn`, `error`, and `off`. 62 | 63 | ## Developing 64 | 65 | The Makefile has targets to automate building and testing: 66 | 67 | ```bash 68 | make build test 69 | ``` 70 | 71 | The project also uses some linting and formatting tools. To install the tools: 72 | 73 | ```bash 74 | make bootstrap 75 | ``` 76 | 77 | You can then run the additional checks: 78 | 79 | ```bash 80 | make fmt lint mod 81 | ``` 82 | 83 | To run a full set of integration tests on a local kind cluster, ensure you have 84 | the following additional dependencies installed: 85 | 86 | * `docker` 87 | * [`kind`](https://github.com/kubernetes-sigs/kind) 88 | * [`kubectl`](https://kubernetes.io/docs/tasks/tools/) 89 | * [`helm`](https://helm.sh/docs/intro/install/) 90 | * [`bats`](https://bats-core.readthedocs.io/en/stable/installation.html) 91 | 92 | You can then run: 93 | 94 | ```bash 95 | make setup-kind e2e-image e2e-setup e2e-test 96 | ``` 97 | 98 | Finally tidy up the resources created in the kind cluster with: 99 | 100 | ```bash 101 | make e2e-teardown 102 | ``` 103 | -------------------------------------------------------------------------------- /deployment/vault-csi-provider.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | apiVersion: v1 5 | kind: Namespace 6 | metadata: 7 | name: csi 8 | --- 9 | apiVersion: v1 10 | kind: ServiceAccount 11 | metadata: 12 | name: vault-csi-provider 13 | namespace: csi 14 | --- 15 | apiVersion: rbac.authorization.k8s.io/v1 16 | kind: ClusterRole 17 | metadata: 18 | name: vault-csi-provider-clusterrole 19 | rules: 20 | - apiGroups: 21 | - "" 22 | resources: 23 | - serviceaccounts/token 24 | verbs: 25 | - create 26 | --- 27 | apiVersion: rbac.authorization.k8s.io/v1 28 | kind: ClusterRoleBinding 29 | metadata: 30 | name: vault-csi-provider-clusterrolebinding 31 | roleRef: 32 | apiGroup: rbac.authorization.k8s.io 33 | kind: ClusterRole 34 | name: vault-csi-provider-clusterrole 35 | subjects: 36 | - kind: ServiceAccount 37 | name: vault-csi-provider 38 | namespace: csi 39 | --- 40 | apiVersion: rbac.authorization.k8s.io/v1 41 | kind: Role 42 | metadata: 43 | name: vault-csi-provider-role 44 | rules: 45 | - apiGroups: [""] 46 | resources: ["secrets"] 47 | verbs: ["get"] 48 | resourceNames: 49 | - vault-csi-provider-hmac-key 50 | # 'create' permissions cannot be restricted by resource name: 51 | # https://kubernetes.io/docs/reference/access-authn-authz/rbac/#referring-to-resources 52 | - apiGroups: [""] 53 | resources: ["secrets"] 54 | verbs: ["create"] 55 | --- 56 | apiVersion: rbac.authorization.k8s.io/v1 57 | kind: RoleBinding 58 | metadata: 59 | name: vault-csi-provider-rolebinding 60 | roleRef: 61 | apiGroup: rbac.authorization.k8s.io 62 | kind: Role 63 | name: vault-csi-provider-role 64 | subjects: 65 | - kind: ServiceAccount 66 | name: vault-csi-provider 67 | namespace: csi 68 | --- 69 | apiVersion: apps/v1 70 | kind: DaemonSet 71 | metadata: 72 | labels: 73 | app.kubernetes.io/name: vault-csi-provider 74 | name: vault-csi-provider 75 | namespace: csi 76 | spec: 77 | updateStrategy: 78 | type: RollingUpdate 79 | selector: 80 | matchLabels: 81 | app.kubernetes.io/name: vault-csi-provider 82 | template: 83 | metadata: 84 | labels: 85 | app.kubernetes.io/name: vault-csi-provider 86 | spec: 87 | serviceAccountName: vault-csi-provider 88 | tolerations: 89 | containers: 90 | - name: provider-vault-installer 91 | image: hashicorp/vault-csi-provider:1.5.0 92 | imagePullPolicy: Always 93 | args: 94 | - -endpoint=/provider/vault.sock 95 | - -log-level=info 96 | resources: 97 | requests: 98 | cpu: 50m 99 | memory: 100Mi 100 | limits: 101 | cpu: 50m 102 | memory: 100Mi 103 | volumeMounts: 104 | - name: providervol 105 | mountPath: "/provider" 106 | livenessProbe: 107 | httpGet: 108 | path: "/health/ready" 109 | port: 8080 110 | scheme: "HTTP" 111 | failureThreshold: 2 112 | initialDelaySeconds: 5 113 | periodSeconds: 5 114 | successThreshold: 1 115 | timeoutSeconds: 3 116 | readinessProbe: 117 | httpGet: 118 | path: "/health/ready" 119 | port: 8080 120 | scheme: "HTTP" 121 | failureThreshold: 2 122 | initialDelaySeconds: 5 123 | periodSeconds: 5 124 | successThreshold: 1 125 | timeoutSeconds: 3 126 | volumes: 127 | - name: providervol 128 | hostPath: 129 | path: "/etc/kubernetes/secrets-store-csi-providers" 130 | nodeSelector: 131 | kubernetes.io/os: linux 132 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/vault-csi-provider 2 | 3 | go 1.23.7 4 | 5 | require ( 6 | github.com/hashicorp/go-hclog v1.6.3 7 | github.com/hashicorp/golang-lru/v2 v2.0.7 8 | github.com/hashicorp/vault/api v1.16.0 9 | github.com/stretchr/testify v1.10.0 10 | google.golang.org/grpc v1.71.1 11 | gopkg.in/yaml.v3 v3.0.1 12 | k8s.io/api v0.32.3 13 | k8s.io/apimachinery v0.32.3 14 | k8s.io/client-go v0.32.3 15 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 16 | sigs.k8s.io/secrets-store-csi-driver v1.4.8 17 | ) 18 | 19 | require ( 20 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 21 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 22 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 23 | github.com/fatih/color v1.16.0 // indirect 24 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 25 | github.com/go-jose/go-jose/v4 v4.0.5 // indirect 26 | github.com/go-logr/logr v1.4.2 // indirect 27 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 28 | github.com/go-openapi/jsonreference v0.20.2 // indirect 29 | github.com/go-openapi/swag v0.23.0 // indirect 30 | github.com/gogo/protobuf v1.3.2 // indirect 31 | github.com/golang/protobuf v1.5.4 // indirect 32 | github.com/google/gnostic-models v0.6.8 // indirect 33 | github.com/google/go-cmp v0.6.0 // indirect 34 | github.com/google/gofuzz v1.2.0 // indirect 35 | github.com/google/uuid v1.6.0 // indirect 36 | github.com/hashicorp/errwrap v1.1.0 // indirect 37 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 38 | github.com/hashicorp/go-multierror v1.1.1 // indirect 39 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 40 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 41 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect 42 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 43 | github.com/hashicorp/go-sockaddr v1.0.2 // indirect 44 | github.com/hashicorp/hcl v1.0.0 // indirect 45 | github.com/josharian/intern v1.0.0 // indirect 46 | github.com/json-iterator/go v1.1.12 // indirect 47 | github.com/mailru/easyjson v0.7.7 // indirect 48 | github.com/mattn/go-colorable v0.1.13 // indirect 49 | github.com/mattn/go-isatty v0.0.20 // indirect 50 | github.com/mitchellh/go-homedir v1.1.0 // indirect 51 | github.com/mitchellh/mapstructure v1.5.0 // indirect 52 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 53 | github.com/modern-go/reflect2 v1.0.2 // indirect 54 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 55 | github.com/pkg/errors v0.9.1 // indirect 56 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 57 | github.com/ryanuber/go-glob v1.0.0 // indirect 58 | github.com/x448/float16 v0.8.4 // indirect 59 | golang.org/x/crypto v0.36.0 // indirect 60 | golang.org/x/net v0.37.0 // indirect 61 | golang.org/x/oauth2 v0.28.0 // indirect 62 | golang.org/x/sys v0.31.0 // indirect 63 | golang.org/x/term v0.30.0 // indirect 64 | golang.org/x/text v0.23.0 // indirect 65 | golang.org/x/time v0.7.0 // indirect 66 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect 67 | google.golang.org/protobuf v1.36.4 // indirect 68 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 69 | gopkg.in/inf.v0 v0.9.1 // indirect 70 | k8s.io/klog/v2 v2.130.1 // indirect 71 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 72 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 73 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 74 | sigs.k8s.io/yaml v1.4.0 // indirect 75 | ) 76 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 2 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 3 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 4 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 5 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 9 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 11 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 12 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 13 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 14 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 15 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 16 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 17 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 18 | github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= 19 | github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= 20 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 21 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 22 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 23 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 24 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 25 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 26 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 27 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 28 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 29 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 30 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 31 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 32 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 33 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 34 | github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= 35 | github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 36 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 37 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 38 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 39 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 40 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 41 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 42 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 43 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 44 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 45 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 46 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 47 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 48 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 49 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 50 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 51 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 52 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 53 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 54 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 55 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 56 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 57 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 58 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 59 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 60 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 61 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 62 | github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= 63 | github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= 64 | github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= 65 | github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= 66 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= 67 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= 68 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= 69 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= 70 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= 71 | github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= 72 | github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= 73 | github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 74 | github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 75 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 76 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 77 | github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4= 78 | github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA= 79 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 80 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 81 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 82 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 83 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 84 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 85 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 86 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 87 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 88 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 89 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 90 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 91 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 92 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 93 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 94 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 95 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 96 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 97 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 98 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 99 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 100 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 101 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 102 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 103 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 104 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 105 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 106 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 107 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 108 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 109 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 110 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 111 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 112 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 113 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 114 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 115 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 116 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 117 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 118 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 119 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 120 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 121 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 122 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 123 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 124 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 125 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 126 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 127 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 128 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 129 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 130 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 131 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 132 | github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 133 | github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= 134 | github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= 135 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 136 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 137 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 138 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 139 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 140 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 141 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 142 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 143 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 144 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 145 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 146 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 147 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 148 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 149 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 150 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 151 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 152 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 153 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 154 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 155 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 156 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 157 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 158 | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 159 | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 160 | go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= 161 | go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= 162 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 163 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 164 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 165 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 166 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 167 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 168 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 169 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 170 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 171 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 172 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 173 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 174 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 175 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= 176 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 177 | golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= 178 | golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 179 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 180 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 181 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 182 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 183 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 184 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 185 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 186 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 187 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 188 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 189 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 190 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 191 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 192 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 193 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 194 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 195 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 196 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 197 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 198 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 199 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 200 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 201 | golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= 202 | golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 203 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 204 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 205 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 206 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 207 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 208 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 209 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 210 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 211 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 212 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 213 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= 214 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= 215 | google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= 216 | google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= 217 | google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= 218 | google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 219 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 220 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 221 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 222 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 223 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 224 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 225 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 226 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 227 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 228 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 229 | k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= 230 | k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= 231 | k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= 232 | k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 233 | k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= 234 | k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= 235 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 236 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 237 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= 238 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= 239 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= 240 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 241 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= 242 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= 243 | sigs.k8s.io/secrets-store-csi-driver v1.4.8 h1:YmL0lx9HMYqeZCnLyOZRMuGAZXmP/e42UGCCAnMKjgE= 244 | sigs.k8s.io/secrets-store-csi-driver v1.4.8/go.mod h1:IawZyjzh3xGt6hHdckJUf3ls04O0zG5H550PEZz/beo= 245 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= 246 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= 247 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 248 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 249 | -------------------------------------------------------------------------------- /internal/auth/kubernetes_jwt.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: BUSL-1.1 3 | 4 | package auth 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "time" 10 | 11 | "github.com/hashicorp/go-hclog" 12 | "github.com/hashicorp/vault-csi-provider/internal/config" 13 | authenticationv1 "k8s.io/api/authentication/v1" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/client-go/kubernetes" 16 | ) 17 | 18 | // KubernetesJWTAuth implements both Kubernetes and JWT auth, as both have 19 | // exactly the same login endpoint. If their login endpoints ever diverge, this 20 | // struct may need splitting. 21 | type KubernetesJWTAuth struct { 22 | logger hclog.Logger 23 | k8sClient kubernetes.Interface 24 | params config.Parameters 25 | defaultMountPath string 26 | } 27 | 28 | func NewKubernetesJWTAuth(logger hclog.Logger, k8sClient kubernetes.Interface, params config.Parameters, defaultMountPath string) *KubernetesJWTAuth { 29 | return &KubernetesJWTAuth{ 30 | logger: logger, 31 | k8sClient: k8sClient, 32 | params: params, 33 | defaultMountPath: defaultMountPath, 34 | } 35 | } 36 | 37 | // AuthRequest returns the request path and body required to authenticate 38 | // using the configured auth role in Vault. If no appropriate JWT is provided 39 | // in the CSI mount request, it will create a new one. 40 | func (k *KubernetesJWTAuth) AuthRequest(ctx context.Context) (path string, body map[string]string, err error) { 41 | jwt := k.params.PodInfo.ServiceAccountToken 42 | if jwt == "" { 43 | k.logger.Debug("no suitable token found in mount request, using self-generated service account JWT") 44 | var err error 45 | jwt, err = k.createJWTToken(ctx, k.params.PodInfo, k.params.Audience) 46 | if err != nil { 47 | return "", nil, err 48 | } 49 | } else { 50 | k.logger.Debug("using token from mount request for login") 51 | } 52 | 53 | mountPath := k.params.VaultAuthMountPath 54 | if mountPath == "" { 55 | mountPath = k.defaultMountPath 56 | } 57 | 58 | return fmt.Sprintf("/v1/auth/%s/login", mountPath), map[string]string{ 59 | "jwt": jwt, 60 | "role": k.params.VaultRoleName, 61 | }, nil 62 | } 63 | 64 | func (k *KubernetesJWTAuth) createJWTToken(ctx context.Context, podInfo config.PodInfo, audience string) (string, error) { 65 | k.logger.Debug("creating service account token bound to pod", 66 | "namespace", podInfo.Namespace, 67 | "serviceAccountName", podInfo.ServiceAccountName, 68 | "podUID", podInfo.UID, 69 | "audience", audience) 70 | 71 | ttl := int64((15 * time.Minute).Seconds()) 72 | audiences := []string{} 73 | if audience != "" { 74 | audiences = []string{audience} 75 | } 76 | resp, err := k.k8sClient.CoreV1().ServiceAccounts(podInfo.Namespace).CreateToken(ctx, podInfo.ServiceAccountName, &authenticationv1.TokenRequest{ 77 | Spec: authenticationv1.TokenRequestSpec{ 78 | ExpirationSeconds: &ttl, 79 | Audiences: audiences, 80 | BoundObjectRef: &authenticationv1.BoundObjectReference{ 81 | Kind: "Pod", 82 | APIVersion: "v1", 83 | Name: podInfo.Name, 84 | UID: podInfo.UID, 85 | }, 86 | }, 87 | }, metav1.CreateOptions{}) 88 | if err != nil { 89 | return "", fmt.Errorf("failed to create a service account token for requesting pod %v: %w", podInfo, err) 90 | } 91 | 92 | k.logger.Debug("service account token creation successful") 93 | return resp.Status.Token, nil 94 | } 95 | -------------------------------------------------------------------------------- /internal/client/client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: BUSL-1.1 3 | 4 | package client 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | "net/url" 12 | "strings" 13 | "sync" 14 | 15 | "github.com/hashicorp/go-hclog" 16 | "github.com/hashicorp/vault-csi-provider/internal/auth" 17 | "github.com/hashicorp/vault-csi-provider/internal/config" 18 | "github.com/hashicorp/vault/api" 19 | ) 20 | 21 | type Client struct { 22 | logger hclog.Logger 23 | inner *api.Client 24 | 25 | mtx sync.Mutex 26 | } 27 | 28 | // New creates a Vault client configured for a specific SecretProviderClass (SPC). 29 | // Config is read from environment variables first, then flags, then the SPC in 30 | // ascending order of precedence. 31 | func New(logger hclog.Logger, spcParameters config.Parameters, flagsConfig config.FlagsConfig) (*Client, error) { 32 | cfg := api.DefaultConfig() 33 | if cfg.Error != nil { 34 | return nil, cfg.Error 35 | } 36 | if err := overlayConfig(cfg, flagsConfig.VaultAddr, flagsConfig.TLSConfig()); err != nil { 37 | return nil, err 38 | } 39 | if err := overlayConfig(cfg, spcParameters.VaultAddress, spcParameters.VaultTLSConfig); err != nil { 40 | return nil, err 41 | } 42 | 43 | inner, err := api.NewClient(cfg) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | // Set Vault namespace if configured. 49 | if flagsConfig.VaultNamespace != "" { 50 | inner.SetNamespace(flagsConfig.VaultNamespace) 51 | } 52 | if spcParameters.VaultNamespace != "" { 53 | inner.SetNamespace(spcParameters.VaultNamespace) 54 | } 55 | 56 | return &Client{ 57 | logger: logger, 58 | inner: inner, 59 | }, nil 60 | } 61 | 62 | func overlayConfig(cfg *api.Config, vaultAddr string, tlsConfig api.TLSConfig) error { 63 | err := cfg.ConfigureTLS(&tlsConfig) 64 | if err != nil { 65 | return err 66 | } 67 | if vaultAddr != "" { 68 | cfg.Address = vaultAddr 69 | } 70 | 71 | return nil 72 | } 73 | 74 | // RequestSecret fetches a single secret response from Vault. It will trigger 75 | // an initial authentication attempt if the client doesn't already have a Vault 76 | // token. Otherwise, if it gets a 403 response from Vault it will attempt 77 | // to reauthenticate and retry fetching the secret, on the assumption that 78 | // the pre-existing token may have expired. 79 | // 80 | // We follow this pattern because we assume Vault Agent is caching and renewing 81 | // our auth token, and we have no universal way to check it's still valid and 82 | // in the Agent's cache before making a request. 83 | func (c *Client) RequestSecret(ctx context.Context, authMethod *auth.KubernetesJWTAuth, secretConfig config.Secret) (*api.Secret, error) { 84 | // Ensure we have a token available. 85 | authed, err := c.auth(ctx, authMethod, "") 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | req, err := c.generateSecretRequest(secretConfig) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | c.logger.Debug("Requesting secret", "secretConfig", secretConfig, "method", req.Method, "path", req.URL.Path, "params", req.Params) 96 | 97 | var resp *api.Response 98 | for i := 0; i < 2; i++ { 99 | resp, err = c.doInternal(ctx, req) 100 | if err != nil { 101 | var apiErr *api.ResponseError 102 | if !authed && i == 0 && errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusForbidden { 103 | // This may just mean our token has expired. 104 | // Retry and ensure the next request uses a new token. 105 | if _, authErr := c.auth(ctx, authMethod, req.ClientToken); authErr != nil { 106 | return nil, fmt.Errorf("failed to fetch secret: %w; and failed to reauthenticate: %w", err, authErr) 107 | } 108 | req.ClientToken = c.inner.Token() 109 | continue 110 | } 111 | return nil, fmt.Errorf("error requesting secret: %w", err) 112 | } 113 | 114 | break 115 | } 116 | 117 | if resp == nil { 118 | return nil, fmt.Errorf("failed to fetch secret object %s", secretConfig.ObjectName) 119 | } 120 | return api.ParseSecret(resp.Body) 121 | } 122 | 123 | // auth handles authenticating to Vault and setting the client's token. 124 | // All requests from one client share the same token. This function serializes 125 | // authentications so that when a token expires, multiple consumers asking it 126 | // to reauthenticate at the same time only trigger one new authentication with 127 | // Vault. 128 | func (c *Client) auth(ctx context.Context, authMethod *auth.KubernetesJWTAuth, failedToken string) (authed bool, err error) { 129 | c.mtx.Lock() 130 | defer c.mtx.Unlock() 131 | 132 | // If we already have a token and it's not the failed one we've been told 133 | // to replace, then there's no work to do. 134 | if c.inner.Token() != "" && c.inner.Token() != failedToken { 135 | return false, nil 136 | } 137 | 138 | c.logger.Debug("performing vault login") 139 | path, body, err := authMethod.AuthRequest(ctx) 140 | if err != nil { 141 | return false, err 142 | } 143 | 144 | req := c.inner.NewRequest(http.MethodPost, path) 145 | if err := req.SetJSONBody(body); err != nil { 146 | return false, err 147 | } 148 | 149 | resp, err := c.doInternal(ctx, req) 150 | if err != nil { 151 | return false, fmt.Errorf("failed to login: %w", err) 152 | } 153 | secret, err := api.ParseSecret(resp.Body) 154 | if err != nil { 155 | return false, fmt.Errorf("failed to parse login response: %w", err) 156 | } 157 | 158 | c.logger.Debug("vault login successful") 159 | c.inner.SetToken(secret.Auth.ClientToken) 160 | 161 | return true, nil 162 | } 163 | 164 | func (c *Client) doInternal(ctx context.Context, req *api.Request) (*api.Response, error) { 165 | resp, err := c.inner.RawRequestWithContext(ctx, req) 166 | if err != nil { 167 | return nil, err 168 | } 169 | if resp == nil { 170 | return nil, fmt.Errorf("received empty response from %q", req.URL.Path) 171 | } 172 | 173 | return resp, nil 174 | } 175 | 176 | func (c *Client) generateSecretRequest(secret config.Secret) (*api.Request, error) { 177 | secretPath := ensureV1Prefix(secret.SecretPath) 178 | queryIndex := strings.Index(secretPath, "?") 179 | var queryParams map[string][]string 180 | if queryIndex != -1 { 181 | var err error 182 | queryParams, err = url.ParseQuery(secretPath[queryIndex+1:]) 183 | if err != nil { 184 | return nil, fmt.Errorf("failed to parse query parameters from secretPath %q for objectName %q: %w", secretPath, secret.ObjectName, err) 185 | } 186 | secretPath = secretPath[:queryIndex] 187 | } 188 | method := http.MethodGet 189 | if secret.Method != "" { 190 | method = secret.Method 191 | } 192 | 193 | req := c.inner.NewRequest(method, secretPath) 194 | if queryParams != nil { 195 | req.Params = queryParams 196 | } 197 | if secret.SecretArgs != nil { 198 | err := req.SetJSONBody(secret.SecretArgs) 199 | if err != nil { 200 | return nil, err 201 | } 202 | } 203 | 204 | return req, nil 205 | } 206 | 207 | func ensureV1Prefix(s string) string { 208 | switch { 209 | case strings.HasPrefix(s, "/v1/"): 210 | return s 211 | case strings.HasPrefix(s, "v1/"): 212 | return "/" + s 213 | case strings.HasPrefix(s, "/"): 214 | return "/v1" + s 215 | default: 216 | return "/v1/" + s 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /internal/client/client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: BUSL-1.1 3 | 4 | package client 5 | 6 | import ( 7 | "context" 8 | "crypto/rand" 9 | "crypto/rsa" 10 | "crypto/x509" 11 | "crypto/x509/pkix" 12 | "encoding/json" 13 | "encoding/pem" 14 | "fmt" 15 | "math" 16 | "math/big" 17 | "net/http" 18 | "net/http/httptest" 19 | "os" 20 | "path" 21 | "path/filepath" 22 | "testing" 23 | "time" 24 | 25 | "github.com/hashicorp/go-hclog" 26 | "github.com/hashicorp/vault-csi-provider/internal/auth" 27 | "github.com/hashicorp/vault-csi-provider/internal/config" 28 | "github.com/hashicorp/vault/api" 29 | "github.com/stretchr/testify/assert" 30 | "github.com/stretchr/testify/require" 31 | corev1 "k8s.io/api/core/v1" 32 | "k8s.io/client-go/kubernetes/fake" 33 | ) 34 | 35 | var caPath = filepath.Join("testdata", "ca.pem") 36 | 37 | func TestNew(t *testing.T) { 38 | err := os.Mkdir("testdata", 0o755) 39 | if err != nil && !os.IsExist(err) { 40 | t.Fatal("failed to make testdata folder", err) 41 | } 42 | defer func() { 43 | require.NoError(t, os.RemoveAll("testdata")) 44 | }() 45 | generateCA(t, caPath) 46 | 47 | for _, tc := range []struct { 48 | name string 49 | cfg api.TLSConfig 50 | }{ 51 | { 52 | name: "file", 53 | cfg: api.TLSConfig{ 54 | CACert: caPath, 55 | }, 56 | }, 57 | { 58 | name: "directory", 59 | cfg: api.TLSConfig{ 60 | CAPath: "testdata", 61 | }, 62 | }, 63 | } { 64 | _, err = New(hclog.NewNullLogger(), config.Parameters{ 65 | VaultTLSConfig: tc.cfg, 66 | }, config.FlagsConfig{}) 67 | require.NoError(t, err, tc.name) 68 | } 69 | } 70 | 71 | func TestConfigPrecedence(t *testing.T) { 72 | if originalVaultAddr, isSet := os.LookupEnv(api.EnvVaultAddress); isSet { 73 | defer os.Setenv(api.EnvVaultAddress, originalVaultAddr) 74 | } 75 | t.Setenv(api.EnvVaultAddress, "from-env") 76 | 77 | client, err := New(hclog.NewNullLogger(), config.Parameters{}, config.FlagsConfig{}) 78 | require.NoError(t, err) 79 | assert.Equal(t, "from-env", client.inner.Address()) 80 | 81 | client, err = New(hclog.NewNullLogger(), config.Parameters{}, config.FlagsConfig{ 82 | VaultAddr: "from-flags", 83 | }) 84 | require.NoError(t, err) 85 | assert.Equal(t, "from-flags", client.inner.Address()) 86 | 87 | client, err = New(hclog.NewNullLogger(), config.Parameters{ 88 | VaultAddress: "from-parameters", 89 | }, config.FlagsConfig{ 90 | VaultAddr: "from-flags", 91 | }) 92 | require.NoError(t, err) 93 | assert.Equal(t, "from-parameters", client.inner.Address()) 94 | } 95 | 96 | func TestNew_Error(t *testing.T) { 97 | dir := t.TempDir() 98 | err := os.WriteFile(path.Join(dir, "not-a-ca.pem"), []byte("Hello"), 0o644) 99 | require.NoError(t, err) 100 | 101 | _, err = New(hclog.NewNullLogger(), config.Parameters{ 102 | VaultTLSConfig: api.TLSConfig{ 103 | CAPath: dir, 104 | }, 105 | }, config.FlagsConfig{}) 106 | require.Error(t, err) 107 | } 108 | 109 | func TestRequestSecret_OnlyAuthenticatesOnce(t *testing.T) { 110 | var auths, reads int 111 | mockVaultServer := httptest.NewServer(http.HandlerFunc(mockVaultHandler(t, &auths, &reads))) 112 | flagsConfig := config.FlagsConfig{ 113 | VaultAddr: mockVaultServer.URL, 114 | } 115 | defer mockVaultServer.Close() 116 | 117 | k8sClient := fake.NewSimpleClientset( 118 | &corev1.ServiceAccount{}, 119 | ) 120 | authMethod := auth.NewKubernetesJWTAuth(hclog.Default(), k8sClient, config.Parameters{}, "") 121 | client, err := New(hclog.Default(), config.Parameters{}, flagsConfig) 122 | require.NoError(t, err) 123 | 124 | for name, tc := range map[string]struct { 125 | initialToken string 126 | expectedReads int 127 | }{ 128 | "unauthenticated": {"", 1}, 129 | "expired token": {"footoken", 2}, 130 | } { 131 | t.Run(name, func(t *testing.T) { 132 | auths = 0 133 | reads = 0 134 | client.inner.SetToken(tc.initialToken) 135 | 136 | _, err = client.RequestSecret(context.Background(), authMethod, config.Secret{}) 137 | assert.Error(t, err) 138 | t.Log(err) 139 | 140 | assert.Equal(t, 1, auths) 141 | assert.Equal(t, tc.expectedReads, reads) 142 | }) 143 | } 144 | } 145 | 146 | func mockVaultHandler(t *testing.T, auths, reads *int) func(w http.ResponseWriter, req *http.Request) { 147 | t.Helper() 148 | 149 | return func(w http.ResponseWriter, req *http.Request) { 150 | t.Helper() 151 | switch req.Method { 152 | case http.MethodPost: 153 | *auths++ 154 | // Assume all POSTs are login requests and return a token. 155 | body, err := json.Marshal(&api.Secret{ 156 | Auth: &api.SecretAuth{ 157 | ClientToken: fmt.Sprintf("my-vault-client-token-%d", *auths), 158 | }, 159 | }) 160 | require.NoError(t, err) 161 | _, err = w.Write(body) 162 | require.NoError(t, err) 163 | case http.MethodGet: 164 | *reads++ 165 | // Return 403 for all secret reads to test out the retry logic. 166 | body, err := json.Marshal(&api.ErrorResponse{ 167 | Errors: []string{"permission denied"}, 168 | }) 169 | require.NoError(t, err) 170 | w.WriteHeader(http.StatusForbidden) 171 | _, err = w.Write(body) 172 | require.NoError(t, err) 173 | } 174 | } 175 | } 176 | 177 | func generateCA(t *testing.T, path string) { 178 | // Based on https://golang.org/src/crypto/tls/generate_cert.go. 179 | key, err := rsa.GenerateKey(rand.Reader, 4096) 180 | require.NoError(t, err) 181 | serialNumber, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt32)) 182 | require.NoError(t, err) 183 | caTemplate := x509.Certificate{ 184 | IsCA: true, 185 | SerialNumber: serialNumber, 186 | Subject: pkix.Name{ 187 | Organization: []string{"Tests'R'Us"}, 188 | }, 189 | NotBefore: time.Now().Add(-time.Minute), 190 | NotAfter: time.Now().Add(time.Hour), 191 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign, 192 | BasicConstraintsValid: true, 193 | } 194 | bytes, err := x509.CreateCertificate(rand.Reader, &caTemplate, &caTemplate, &key.PublicKey, key) 195 | require.NoError(t, err) 196 | certOut, err := os.Create(path) 197 | require.NoError(t, err) 198 | defer func() { 199 | require.NoError(t, certOut.Close()) 200 | }() 201 | err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: bytes}) 202 | require.NoError(t, err) 203 | } 204 | 205 | func TestEnsureV1Prefix(t *testing.T) { 206 | for _, tc := range []struct { 207 | name string 208 | input string 209 | expected string 210 | }{ 211 | {"no prefix", "secret/foo", "/v1/secret/foo"}, 212 | {"leading slash", "/secret/foo", "/v1/secret/foo"}, 213 | {"leading v1", "v1/secret/foo", "/v1/secret/foo"}, 214 | {"leading /v1/", "/v1/secret/foo", "/v1/secret/foo"}, 215 | // These will mostly be invalid paths, but testing reasonable behaviour. 216 | {"empty string", "", "/v1/"}, 217 | {"just /v1/", "/v1/", "/v1/"}, 218 | {"leading 1", "1/secret/foo", "/v1/1/secret/foo"}, 219 | {"2* /v1/", "/v1/v1/", "/v1/v1/"}, 220 | {"v2", "/v2/secret/foo", "/v1/v2/secret/foo"}, 221 | } { 222 | assert.Equal(t, tc.expected, ensureV1Prefix(tc.input), tc.name) 223 | } 224 | } 225 | 226 | func TestGenerateRequest(t *testing.T) { 227 | type expected struct { 228 | method string 229 | path string 230 | params string 231 | body string 232 | } 233 | client, err := New(hclog.NewNullLogger(), config.Parameters{}, config.FlagsConfig{}) 234 | require.NoError(t, err) 235 | for _, tc := range []struct { 236 | name string 237 | secret config.Secret 238 | expected expected 239 | }{ 240 | { 241 | name: "base case", 242 | secret: config.Secret{ 243 | SecretPath: "secret/foo", 244 | }, 245 | expected: expected{http.MethodGet, "/v1/secret/foo", "", ""}, 246 | }, 247 | { 248 | name: "zero-length query string", 249 | secret: config.Secret{ 250 | SecretPath: "secret/foo?", 251 | }, 252 | expected: expected{http.MethodGet, "/v1/secret/foo", "", ""}, 253 | }, 254 | { 255 | name: "query string", 256 | secret: config.Secret{ 257 | SecretPath: "secret/foo?bar=true&baz=maybe&zap=0", 258 | }, 259 | expected: expected{http.MethodGet, "/v1/secret/foo", "bar=true&baz=maybe&zap=0", ""}, 260 | }, 261 | { 262 | name: "method specified", 263 | secret: config.Secret{ 264 | SecretPath: "secret/foo", 265 | Method: "PUT", 266 | }, 267 | expected: expected{"PUT", "/v1/secret/foo", "", ""}, 268 | }, 269 | { 270 | name: "body specified", 271 | secret: config.Secret{ 272 | SecretPath: "secret/foo", 273 | Method: http.MethodPost, 274 | SecretArgs: map[string]interface{}{ 275 | "bar": true, 276 | "baz": 10, 277 | "zap": "a string", 278 | }, 279 | }, 280 | expected: expected{http.MethodPost, "/v1/secret/foo", "", `{"bar":true,"baz":10,"zap":"a string"}`}, 281 | }, 282 | } { 283 | t.Run(tc.name, func(t *testing.T) { 284 | req, err := client.generateSecretRequest(tc.secret) 285 | require.NoError(t, err) 286 | assert.Equal(t, tc.expected.method, req.Method) 287 | assert.Equal(t, tc.expected.path, req.URL.Path) 288 | assert.Equal(t, tc.expected.params, req.Params.Encode()) 289 | assert.Equal(t, tc.expected.body, string(req.BodyBytes)) 290 | }) 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /internal/clientcache/cache_key.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: BUSL-1.1 3 | 4 | package clientcache 5 | 6 | import ( 7 | "encoding/json" 8 | 9 | "github.com/hashicorp/vault-csi-provider/internal/config" 10 | ) 11 | 12 | type cacheKey string 13 | 14 | func makeCacheKey(params config.Parameters) (cacheKey, error) { 15 | // Zero out the configurables that should not cause a cache miss when they change. 16 | params.PodInfo.ServiceAccountToken = "" 17 | params.Secrets = nil 18 | 19 | paramsBytes, err := json.Marshal(¶ms) 20 | if err != nil { 21 | return "", err 22 | } 23 | 24 | return cacheKey(paramsBytes), nil 25 | } 26 | -------------------------------------------------------------------------------- /internal/clientcache/client_cache.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: BUSL-1.1 3 | 4 | package clientcache 5 | 6 | import ( 7 | "sync" 8 | 9 | "github.com/hashicorp/go-hclog" 10 | lru "github.com/hashicorp/golang-lru/v2" 11 | vaultclient "github.com/hashicorp/vault-csi-provider/internal/client" 12 | "github.com/hashicorp/vault-csi-provider/internal/config" 13 | ) 14 | 15 | type ClientCache struct { 16 | logger hclog.Logger 17 | 18 | mtx sync.Mutex 19 | cache *lru.Cache[cacheKey, *vaultclient.Client] 20 | } 21 | 22 | // NewClientCache intializes a new client cache. The cache's lifetime 23 | // should be tied to the provider process (i.e. longer than a single 24 | // mount request) so that Vault tokens stored in the clients are cached 25 | // and reused across different mount requests for the same pod. 26 | func NewClientCache(logger hclog.Logger, size int) (*ClientCache, error) { 27 | var cache *lru.Cache[cacheKey, *vaultclient.Client] 28 | var err error 29 | if size > 0 { 30 | logger.Info("Creating Vault client cache", "size", size) 31 | cache, err = lru.New[cacheKey, *vaultclient.Client](size) 32 | if err != nil { 33 | return nil, err 34 | } 35 | } else { 36 | logger.Info("Disabling Vault client cache", "size", size) 37 | } 38 | 39 | return &ClientCache{ 40 | logger: logger, 41 | cache: cache, 42 | }, nil 43 | } 44 | 45 | func (c *ClientCache) GetOrCreateClient(params config.Parameters, flagsConfig config.FlagsConfig) (*vaultclient.Client, error) { 46 | if c.cache == nil { 47 | return vaultclient.New(c.logger, params, flagsConfig) 48 | } 49 | 50 | key, err := makeCacheKey(params) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | c.mtx.Lock() 56 | defer c.mtx.Unlock() 57 | 58 | if cachedClient, ok := c.cache.Get(key); ok { 59 | return cachedClient, nil 60 | } 61 | 62 | client, err := vaultclient.New(c.logger, params, flagsConfig) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | c.cache.Add(key, client) 68 | return client, nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/clientcache/client_cache_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: BUSL-1.1 3 | 4 | package clientcache 5 | 6 | import ( 7 | "net/http" 8 | "sync" 9 | "testing" 10 | 11 | "github.com/hashicorp/go-hclog" 12 | "github.com/hashicorp/vault-csi-provider/internal/config" 13 | "github.com/hashicorp/vault/api" 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | func TestParallelCacheAccess(t *testing.T) { 19 | cache, err := NewClientCache(hclog.Default(), 1000) 20 | require.NoError(t, err) 21 | 22 | var startWG, endWG sync.WaitGroup 23 | startWG.Add(1) 24 | for i := 0; i < 100; i++ { 25 | endWG.Add(1) 26 | go func() { 27 | defer endWG.Done() 28 | startWG.Wait() 29 | _, err := cache.GetOrCreateClient(config.Parameters{}, config.FlagsConfig{}) 30 | require.NoError(t, err) 31 | }() 32 | } 33 | 34 | // Unblock all the goroutines at once. 35 | startWG.Done() 36 | endWG.Wait() 37 | assert.Equal(t, 1, cache.cache.Len()) 38 | } 39 | 40 | func TestCacheKeyedOnCorrectFields(t *testing.T) { 41 | cache, err := NewClientCache(hclog.Default(), 10) 42 | require.NoError(t, err) 43 | params := config.Parameters{ 44 | VaultRoleName: "example-role", 45 | VaultAddress: "http://vault:8200", 46 | VaultTLSConfig: api.TLSConfig{ 47 | Insecure: true, 48 | }, 49 | Secrets: []config.Secret{ 50 | { 51 | ObjectName: "bar1", 52 | SecretPath: "v1/secret/foo1", 53 | Method: http.MethodGet, 54 | }, 55 | { 56 | ObjectName: "bar2", 57 | SecretPath: "v1/secret/foo2", 58 | Method: http.MethodGet, 59 | }, 60 | }, 61 | PodInfo: config.PodInfo{ 62 | Name: "nginx-secrets-store-inline", 63 | UID: "9aeb260f-d64a-426c-9872-95b6bab37e00", 64 | Namespace: "test", 65 | ServiceAccountName: "default", 66 | ServiceAccountToken: "footoken", 67 | }, 68 | Audience: "testaudience", 69 | } 70 | 71 | _, err = cache.GetOrCreateClient(params, config.FlagsConfig{}) 72 | require.NoError(t, err) 73 | assert.Equal(t, 1, cache.cache.Len()) 74 | 75 | // Shouldn't have modified the original params struct 76 | assert.Equal(t, "footoken", params.PodInfo.ServiceAccountToken) 77 | assert.Len(t, params.Secrets, 2) 78 | 79 | params.Secrets = append(params.Secrets, config.Secret{}) 80 | params.PodInfo.ServiceAccountToken = "bartoken" 81 | 82 | _, err = cache.GetOrCreateClient(params, config.FlagsConfig{}) 83 | require.NoError(t, err) 84 | assert.Equal(t, 1, cache.cache.Len()) 85 | 86 | // Still shouldn't have modified the updated params struct 87 | assert.Equal(t, "bartoken", params.PodInfo.ServiceAccountToken) 88 | assert.Len(t, params.Secrets, 3) 89 | 90 | params.PodInfo.UID = "new-uid" 91 | 92 | _, err = cache.GetOrCreateClient(params, config.FlagsConfig{}) 93 | require.NoError(t, err) 94 | assert.Equal(t, 2, cache.cache.Len()) 95 | } 96 | 97 | func TestCache_CanBeDisabled(t *testing.T) { 98 | for name, tc := range map[string]struct { 99 | size int 100 | expectedCaching bool 101 | }{ 102 | "-10": {-10, false}, 103 | "-1": {-1, false}, 104 | "0": {0, false}, 105 | "1": {1, true}, 106 | } { 107 | t.Run(name, func(t *testing.T) { 108 | cache, err := NewClientCache(hclog.Default(), tc.size) 109 | require.NoError(t, err) 110 | params := config.Parameters{} 111 | flags := config.FlagsConfig{} 112 | 113 | c1, err := cache.GetOrCreateClient(params, flags) 114 | require.NoError(t, err) 115 | c2, err := cache.GetOrCreateClient(params, flags) 116 | require.NoError(t, err) 117 | if tc.expectedCaching { 118 | assert.Equal(t, 1, cache.cache.Len()) 119 | assert.Equal(t, c1, c2) 120 | } else { 121 | assert.Nil(t, cache.cache) 122 | assert.NotEqual(t, c1, c2) 123 | } 124 | }) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: BUSL-1.1 3 | 4 | package config 5 | 6 | import ( 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "os" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/hashicorp/vault/api" 15 | "gopkg.in/yaml.v3" 16 | "k8s.io/apimachinery/pkg/types" 17 | ) 18 | 19 | // Config represents all of the provider's configurable behaviour from the SecretProviderClass, 20 | // transmitted in the MountRequest proto message: 21 | // * Parameters from the `Attributes` field. 22 | // * Plus the rest of the proto fields we consume. 23 | // See sigs.k8s.io/secrets-store-csi-driver/provider/v1alpha1/service.pb.go 24 | type Config struct { 25 | Parameters Parameters 26 | TargetPath string 27 | FilePermission os.FileMode 28 | } 29 | 30 | type FlagsConfig struct { 31 | Endpoint string 32 | Debug bool 33 | LogLevel string 34 | Version bool 35 | HealthAddr string 36 | 37 | HMACSecretName string 38 | 39 | CacheSize int 40 | 41 | VaultAddr string 42 | VaultMount string 43 | VaultNamespace string 44 | 45 | TLSCACertPath string 46 | TLSCADirectory string 47 | TLSServerName string 48 | TLSClientCert string 49 | TLSClientKey string 50 | TLSSkipVerify bool 51 | } 52 | 53 | func (fc FlagsConfig) TLSConfig() api.TLSConfig { 54 | return api.TLSConfig{ 55 | CACert: fc.TLSCACertPath, 56 | CAPath: fc.TLSCADirectory, 57 | ClientCert: fc.TLSClientCert, 58 | ClientKey: fc.TLSClientKey, 59 | TLSServerName: fc.TLSServerName, 60 | Insecure: fc.TLSSkipVerify, 61 | } 62 | } 63 | 64 | // Parameters stores the parameters specified in a mount request's `Attributes` field. 65 | // It consists of the parameters section from the SecretProviderClass being mounted 66 | // and pod metadata provided by the driver. 67 | // 68 | // Top-level values that aren't strings are not directly deserialisable because 69 | // they are defined as literal string types: 70 | // https://github.com/kubernetes-sigs/secrets-store-csi-driver/blob/0ba9810d41cc2dc336c68251d45ebac19f2e7f28/apis/v1alpha1/secretproviderclass_types.go#L59 71 | // 72 | // So we just deserialise by hand to avoid complexity and two passes. 73 | type Parameters struct { 74 | VaultAddress string 75 | VaultRoleName string 76 | VaultAuthMountPath string 77 | VaultNamespace string 78 | VaultTLSConfig api.TLSConfig 79 | Secrets []Secret 80 | PodInfo PodInfo 81 | Audience string 82 | } 83 | 84 | type PodInfo struct { 85 | Name string 86 | UID types.UID 87 | Namespace string 88 | ServiceAccountName string 89 | ServiceAccountToken string 90 | } 91 | 92 | type Secret struct { 93 | ObjectName string `yaml:"objectName,omitempty"` 94 | SecretPath string `yaml:"secretPath,omitempty"` 95 | SecretKey string `yaml:"secretKey,omitempty"` 96 | Method string `yaml:"method,omitempty"` 97 | SecretArgs map[string]interface{} `yaml:"secretArgs,omitempty"` 98 | FilePermission os.FileMode `yaml:"filePermission,omitempty"` 99 | Encoding string `yaml:"encoding,omitempty"` 100 | } 101 | 102 | func Parse(parametersStr, targetPath, permissionStr string) (Config, error) { 103 | config := Config{ 104 | TargetPath: targetPath, 105 | } 106 | 107 | var err error 108 | config.Parameters, err = parseParameters(parametersStr) 109 | if err != nil { 110 | return Config{}, err 111 | } 112 | 113 | if err := json.Unmarshal([]byte(permissionStr), &config.FilePermission); err != nil { 114 | return Config{}, err 115 | } 116 | 117 | if err := config.validate(); err != nil { 118 | return Config{}, err 119 | } 120 | 121 | return config, nil 122 | } 123 | 124 | func parseParameters(parametersStr string) (Parameters, error) { 125 | var params map[string]string 126 | err := json.Unmarshal([]byte(parametersStr), ¶ms) 127 | if err != nil { 128 | return Parameters{}, err 129 | } 130 | 131 | var parameters Parameters 132 | parameters.VaultRoleName = params["roleName"] 133 | parameters.VaultAddress = params["vaultAddress"] 134 | parameters.VaultNamespace = params["vaultNamespace"] 135 | parameters.VaultTLSConfig.CACert = params["vaultCACertPath"] 136 | parameters.VaultTLSConfig.CAPath = params["vaultCADirectory"] 137 | parameters.VaultTLSConfig.TLSServerName = params["vaultTLSServerName"] 138 | parameters.VaultTLSConfig.ClientCert = params["vaultTLSClientCertPath"] 139 | parameters.VaultTLSConfig.ClientKey = params["vaultTLSClientKeyPath"] 140 | k8sMountPath, k8sSet := params["vaultKubernetesMountPath"] 141 | authMountPath, authSet := params["vaultAuthMountPath"] 142 | switch { 143 | case k8sSet && authSet: 144 | return Parameters{}, fmt.Errorf("cannot set both vaultKubernetesMountPath and vaultAuthMountPath") 145 | case k8sSet: 146 | parameters.VaultAuthMountPath = k8sMountPath 147 | case authSet: 148 | parameters.VaultAuthMountPath = authMountPath 149 | } 150 | parameters.PodInfo.Name = params["csi.storage.k8s.io/pod.name"] 151 | parameters.PodInfo.UID = types.UID(params["csi.storage.k8s.io/pod.uid"]) 152 | parameters.PodInfo.Namespace = params["csi.storage.k8s.io/pod.namespace"] 153 | parameters.PodInfo.ServiceAccountName = params["csi.storage.k8s.io/serviceAccount.name"] 154 | parameters.Audience = params["audience"] 155 | if skipTLS, ok := params["vaultSkipTLSVerify"]; ok { 156 | value, err := strconv.ParseBool(skipTLS) 157 | if err == nil { 158 | parameters.VaultTLSConfig.Insecure = value 159 | } else { 160 | return Parameters{}, err 161 | } 162 | } 163 | 164 | secretsYaml := params["objects"] 165 | err = yaml.Unmarshal([]byte(secretsYaml), ¶meters.Secrets) 166 | if err != nil { 167 | return Parameters{}, err 168 | } 169 | 170 | tokensJSON := params["csi.storage.k8s.io/serviceAccount.tokens"] 171 | if tokensJSON != "" { 172 | // The csi.storage.k8s.io/serviceAccount.tokens field is a JSON object 173 | // marshalled into a string. The object keys are audience name (string) 174 | // and the values are embedded objects with "token" and 175 | // "expirationTimestamp" fields for the corresponding audience. 176 | var tokens map[string]struct { 177 | Token string `json:"token"` 178 | ExpirationTimestamp string `json:"expirationTimestamp"` 179 | } 180 | if err := json.Unmarshal([]byte(tokensJSON), &tokens); err != nil { 181 | return Parameters{}, fmt.Errorf("failed to unmarshal service account tokens: %w", err) 182 | } 183 | 184 | audience := "vault" 185 | if parameters.Audience != "" { 186 | audience = parameters.Audience 187 | } 188 | if token, ok := tokens[audience]; ok { 189 | parameters.PodInfo.ServiceAccountToken = token.Token 190 | } 191 | } 192 | 193 | return parameters, nil 194 | } 195 | 196 | func (c *Config) validate() error { 197 | // Some basic validation checks. 198 | if c.TargetPath == "" { 199 | return errors.New("missing target path field") 200 | } 201 | if c.Parameters.VaultRoleName == "" { 202 | return errors.New("missing 'roleName' in SecretProviderClass definition") 203 | } 204 | if len(c.Parameters.Secrets) == 0 { 205 | return errors.New("no secrets configured - the provider will not read any secret material") 206 | } 207 | 208 | objectNames := map[string]struct{}{} 209 | conflicts := []string{} 210 | for _, secret := range c.Parameters.Secrets { 211 | if _, exists := objectNames[secret.ObjectName]; exists { 212 | conflicts = append(conflicts, secret.ObjectName) 213 | } 214 | 215 | objectNames[secret.ObjectName] = struct{}{} 216 | } 217 | 218 | if len(conflicts) > 0 { 219 | return fmt.Errorf("each `objectName` within a SecretProviderClass must be unique, "+ 220 | "but the following keys were duplicated: %s", strings.Join(conflicts, ", ")) 221 | } 222 | 223 | return nil 224 | } 225 | -------------------------------------------------------------------------------- /internal/config/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: BUSL-1.1 3 | 4 | package config 5 | 6 | import ( 7 | "encoding/json" 8 | "io/ioutil" 9 | "net/http" 10 | "path/filepath" 11 | "testing" 12 | 13 | "github.com/hashicorp/vault/api" 14 | "github.com/stretchr/testify/require" 15 | "gopkg.in/yaml.v3" 16 | ) 17 | 18 | const ( 19 | objects = "-\n secretPath: \"v1/secret/foo1\"\n objectName: \"bar1\"\n filePermission: 0600" 20 | certsSPCYaml = `apiVersion: secrets-store.csi.x-k8s.io/v1 21 | kind: SecretProviderClass 22 | metadata: 23 | name: vault-foo 24 | spec: 25 | provider: vault 26 | parameters: 27 | objects: | 28 | - objectName: "test-certs" 29 | secretPath: "pki/issue/example-dot-com" 30 | secretKey: "certificate" 31 | secretArgs: 32 | common_name: "test.example.com" 33 | ip_sans: "127.0.0.1" 34 | exclude_cn_from_sans: true 35 | method: "PUT" 36 | - objectName: "internal-certs" 37 | secretPath: "pki/issue/example-dot-com" 38 | secretArgs: 39 | common_name: "internal.example.com" 40 | method: "PUT" 41 | ` 42 | ) 43 | 44 | func TestParseParametersFromYaml(t *testing.T) { 45 | // Test starts with a minimal simulation of the processing the driver does 46 | // with each SecretProviderClass yaml. 47 | var secretProviderClass struct { 48 | Spec struct { 49 | Parameters map[string]string `yaml:"parameters"` 50 | } `yaml:"spec"` 51 | } 52 | err := yaml.Unmarshal([]byte(certsSPCYaml), &secretProviderClass) 53 | require.NoError(t, err) 54 | paramsBytes, err := json.Marshal(secretProviderClass.Spec.Parameters) 55 | require.NoError(t, err) 56 | 57 | // This is now the form the provider receives the data in. 58 | params, err := parseParameters(string(paramsBytes)) 59 | require.NoError(t, err) 60 | 61 | require.Equal(t, Parameters{ 62 | Secrets: []Secret{ 63 | { 64 | ObjectName: "test-certs", 65 | SecretPath: "pki/issue/example-dot-com", 66 | SecretKey: "certificate", 67 | SecretArgs: map[string]interface{}{ 68 | "common_name": "test.example.com", 69 | "ip_sans": "127.0.0.1", 70 | "exclude_cn_from_sans": true, 71 | }, 72 | Method: "PUT", 73 | }, 74 | { 75 | ObjectName: "internal-certs", 76 | SecretPath: "pki/issue/example-dot-com", 77 | SecretArgs: map[string]interface{}{ 78 | "common_name": "internal.example.com", 79 | }, 80 | Method: "PUT", 81 | }, 82 | }, 83 | }, params) 84 | } 85 | 86 | func TestParseParameters(t *testing.T) { 87 | // This file's contents are copied directly from a driver mount request. 88 | parametersStr, err := ioutil.ReadFile(filepath.Join("testdata", "example-parameters-string.txt")) 89 | require.NoError(t, err) 90 | actual, err := parseParameters(string(parametersStr)) 91 | require.NoError(t, err) 92 | expected := Parameters{ 93 | VaultRoleName: "example-role", 94 | VaultAddress: "http://vault:8200", 95 | VaultTLSConfig: api.TLSConfig{ 96 | Insecure: true, 97 | }, 98 | Secrets: []Secret{ 99 | {"bar1", "v1/secret/foo1", "", http.MethodGet, nil, 0, ""}, 100 | {"bar2", "v1/secret/foo2", "", "", nil, 0, ""}, 101 | }, 102 | PodInfo: PodInfo{ 103 | Name: "nginx-secrets-store-inline", 104 | UID: "9aeb260f-d64a-426c-9872-95b6bab37e00", 105 | Namespace: "test", 106 | ServiceAccountName: "default", 107 | }, 108 | Audience: "testaudience", 109 | } 110 | require.Equal(t, expected, actual) 111 | } 112 | 113 | func TestParseConfig(t *testing.T) { 114 | const roleName = "example-role" 115 | const targetPath = "/some/path" 116 | for _, tc := range []struct { 117 | name string 118 | targetPath string 119 | parameters map[string]string 120 | expected Config 121 | }{ 122 | { 123 | name: "defaults", 124 | targetPath: targetPath, 125 | parameters: map[string]string{ 126 | "roleName": "example-role", 127 | "vaultSkipTLSVerify": "true", 128 | "objects": objects, 129 | }, 130 | expected: Config{ 131 | TargetPath: targetPath, 132 | FilePermission: 420, 133 | Parameters: func() Parameters { 134 | expected := Parameters{} 135 | expected.VaultRoleName = roleName 136 | expected.VaultTLSConfig.Insecure = true 137 | expected.Secrets = []Secret{ 138 | {"bar1", "v1/secret/foo1", "", "", nil, 0o600, ""}, 139 | } 140 | return expected 141 | }(), 142 | }, 143 | }, 144 | { 145 | name: "set all options", 146 | targetPath: targetPath, 147 | parameters: map[string]string{ 148 | "roleName": "example-role", 149 | "vaultSkipTLSVerify": "true", 150 | "vaultAddress": "my-vault-address", 151 | "vaultNamespace": "my-vault-namespace", 152 | "vaultKubernetesMountPath": "my-mount-path", 153 | "vaultCACertPath": "my-ca-cert-path", 154 | "vaultCADirectory": "my-ca-directory", 155 | "vaultTLSServerName": "mytls-server-name", 156 | "vaultTLSClientCertPath": "my-tls-client-cert-path", 157 | "vaultTLSClientKeyPath": "my-tls-client-key-path", 158 | "csi.storage.k8s.io/pod.name": "my-pod-name", 159 | "csi.storage.k8s.io/pod.uid": "my-pod-uid", 160 | "csi.storage.k8s.io/pod.namespace": "my-pod-namespace", 161 | "csi.storage.k8s.io/serviceAccount.name": "my-pod-sa-name", 162 | "csi.storage.k8s.io/serviceAccount.tokens": `{"my-aud": {"token": "my-pod-sa-token", "expirationTimestamp": "bar"}, "other-aud": {"token": "unused-token"}}`, 163 | "objects": objects, 164 | "audience": "my-aud", 165 | }, 166 | expected: Config{ 167 | TargetPath: targetPath, 168 | FilePermission: 420, 169 | Parameters: Parameters{ 170 | VaultRoleName: roleName, 171 | VaultAddress: "my-vault-address", 172 | VaultNamespace: "my-vault-namespace", 173 | VaultAuthMountPath: "my-mount-path", 174 | Secrets: []Secret{ 175 | {"bar1", "v1/secret/foo1", "", "", nil, 0o600, ""}, 176 | }, 177 | VaultTLSConfig: api.TLSConfig{ 178 | CACert: "my-ca-cert-path", 179 | CAPath: "my-ca-directory", 180 | ClientCert: "my-tls-client-cert-path", 181 | ClientKey: "my-tls-client-key-path", 182 | TLSServerName: "mytls-server-name", 183 | Insecure: true, 184 | }, 185 | PodInfo: PodInfo{ 186 | "my-pod-name", 187 | "my-pod-uid", 188 | "my-pod-namespace", 189 | "my-pod-sa-name", 190 | "my-pod-sa-token", 191 | }, 192 | Audience: "my-aud", 193 | }, 194 | }, 195 | }, 196 | } { 197 | parametersStr, err := json.Marshal(tc.parameters) 198 | require.NoError(t, err) 199 | cfg, err := Parse(string(parametersStr), tc.targetPath, "420") 200 | require.NoError(t, err, tc.name) 201 | require.Equal(t, tc.expected, cfg) 202 | } 203 | } 204 | 205 | func TestParseConfig_Errors(t *testing.T) { 206 | for name, tc := range map[string]struct { 207 | name string 208 | targetPath string 209 | parameters map[string]string 210 | }{ 211 | "no roleName": { 212 | parameters: map[string]string{ 213 | "vaultSkipTLSVerify": "true", 214 | "objects": objects, 215 | }, 216 | }, 217 | "no secrets configured": { 218 | parameters: map[string]string{ 219 | "roleName": "example-role", 220 | "vaultSkipTLSVerify": "true", 221 | "objects": "", 222 | }, 223 | }, 224 | "both vaultAuthMountPath and vaultKubernetesMountPath specified": { 225 | parameters: map[string]string{ 226 | "roleName": "example-role", 227 | "vaultSkipTLSVerify": "true", 228 | "vaultAuthMountPath": "foo", 229 | "vaultKubernetesMountPath": "bar", 230 | "objects": objects, 231 | }, 232 | }, 233 | } { 234 | t.Run(name, func(t *testing.T) { 235 | parametersStr, err := json.Marshal(tc.parameters) 236 | require.NoError(t, err) 237 | _, err = Parse(string(parametersStr), "/some/path", "420") 238 | require.Error(t, err, tc.name) 239 | }) 240 | } 241 | } 242 | 243 | func TestValidateConfig(t *testing.T) { 244 | minimumValid := Config{ 245 | TargetPath: "a", 246 | Parameters: Parameters{ 247 | VaultAddress: "http://127.0.0.1:8200", 248 | VaultRoleName: "b", 249 | Secrets: []Secret{{}}, 250 | }, 251 | } 252 | for _, tc := range []struct { 253 | name string 254 | cfg Config 255 | cfgValid bool 256 | }{ 257 | { 258 | name: "minimum valid", 259 | cfgValid: true, 260 | cfg: minimumValid, 261 | }, 262 | { 263 | name: "No role name", 264 | cfg: func() Config { 265 | cfg := minimumValid 266 | cfg.Parameters.VaultRoleName = "" 267 | return cfg 268 | }(), 269 | }, 270 | { 271 | name: "No target path", 272 | cfg: func() Config { 273 | cfg := minimumValid 274 | cfg.TargetPath = "" 275 | return cfg 276 | }(), 277 | }, 278 | { 279 | name: "No secrets configured", 280 | cfg: func() Config { 281 | cfg := minimumValid 282 | cfg.Parameters.Secrets = []Secret{} 283 | return cfg 284 | }(), 285 | }, 286 | { 287 | name: "Duplicate objectName", 288 | cfg: func() Config { 289 | cfg := minimumValid 290 | cfg.Parameters.Secrets = []Secret{ 291 | {ObjectName: "foo", SecretPath: "path/one"}, 292 | {ObjectName: "foo", SecretPath: "path/two"}, 293 | } 294 | return cfg 295 | }(), 296 | }, 297 | } { 298 | err := tc.cfg.validate() 299 | if tc.cfgValid { 300 | require.NoError(t, err, tc.name) 301 | } else { 302 | require.Error(t, err, tc.name) 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /internal/config/testdata/example-parameters-string.txt: -------------------------------------------------------------------------------- 1 | { 2 | "csi.storage.k8s.io/pod.name":"nginx-secrets-store-inline", 3 | "csi.storage.k8s.io/pod.namespace":"test", 4 | "csi.storage.k8s.io/pod.uid":"9aeb260f-d64a-426c-9872-95b6bab37e00", 5 | "csi.storage.k8s.io/serviceAccount.name":"default", 6 | "objects":"- secretPath: \"v1/secret/foo1\"\n objectName: \"bar1\"\n method: \"GET\"\n- secretPath: \"v1/secret/foo2\"\n objectName: \"bar2\"", 7 | "roleName":"example-role", 8 | "vaultAddress":"http://vault:8200", 9 | "vaultSkipTLSVerify":"true", 10 | "audience":"testaudience" 11 | } -------------------------------------------------------------------------------- /internal/hmac/hmac.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: BUSL-1.1 3 | 4 | package hmac 5 | 6 | import ( 7 | "context" 8 | "crypto/rand" 9 | "errors" 10 | "fmt" 11 | 12 | corev1 "k8s.io/api/core/v1" 13 | apierrors "k8s.io/apimachinery/pkg/api/errors" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/client-go/kubernetes" 16 | ) 17 | 18 | const ( 19 | hmacKeyName = "key" 20 | hmacKeyLength = 32 21 | ) 22 | 23 | var errDeleteSecret = errors.New("delete the kubernetes secret to trigger an automatic regeneration") 24 | 25 | func NewHMACGenerator(client kubernetes.Interface, secretSpec *corev1.Secret) *HMACGenerator { 26 | return &HMACGenerator{ 27 | client: client, 28 | secretSpec: secretSpec, 29 | } 30 | } 31 | 32 | type HMACGenerator struct { 33 | client kubernetes.Interface 34 | secretSpec *corev1.Secret 35 | } 36 | 37 | // GetOrCreateHMACKey will try to read an HMAC key from a Kubernetes secret and 38 | // race with other pods to create it if not found. The HMAC key is persisted to 39 | // a Kubernetes secret to ensure all pods are deterministically producing the 40 | // same version hashes when given the same inputs. 41 | func (g *HMACGenerator) GetOrCreateHMACKey(ctx context.Context) ([]byte, error) { 42 | // Fast path - most of the time the secret will already be created. 43 | secret, err := g.client.CoreV1().Secrets(g.secretSpec.Namespace).Get(ctx, g.secretSpec.Name, metav1.GetOptions{}) 44 | if err == nil { 45 | return hmacKeyFromSecret(secret) 46 | } 47 | if !apierrors.IsNotFound(err) { 48 | return nil, err 49 | } 50 | 51 | // Secret not found. We'll join the race to create it. 52 | hmacKeyCandidate := make([]byte, hmacKeyLength) 53 | _, err = rand.Read(hmacKeyCandidate) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | // Make a copy of the secretSpec to avoid a data race. 59 | secretSpec := *g.secretSpec 60 | secretSpec.Data = map[string][]byte{ 61 | hmacKeyName: hmacKeyCandidate, 62 | } 63 | 64 | var persistedHMACSecret *corev1.Secret 65 | 66 | // Try to create first 67 | persistedHMACSecret, err = g.client.CoreV1().Secrets(secretSpec.Namespace).Create(ctx, &secretSpec, metav1.CreateOptions{}) 68 | switch { 69 | case err == nil: 70 | // We created the secret, nothing to handle. 71 | case apierrors.IsAlreadyExists(err): 72 | // We lost the race to create the secret. Read the existing secret instead. 73 | persistedHMACSecret, err = g.client.CoreV1().Secrets(secretSpec.Namespace).Get(ctx, secretSpec.Name, metav1.GetOptions{}) 74 | if err != nil { 75 | return nil, err 76 | } 77 | default: 78 | // Unexpected error case. 79 | return nil, err 80 | } 81 | 82 | return hmacKeyFromSecret(persistedHMACSecret) 83 | } 84 | 85 | func hmacKeyFromSecret(secret *corev1.Secret) ([]byte, error) { 86 | hmacKey, ok := secret.Data[hmacKeyName] 87 | if !ok { 88 | return nil, fmt.Errorf("expected secret %q to have a key %q; %w", secret.Name, hmacKeyName, errDeleteSecret) 89 | } 90 | 91 | if len(hmacKey) == 0 { 92 | return nil, fmt.Errorf("expected secret %q to have a non-zero HMAC key; %w", secret.Name, errDeleteSecret) 93 | } 94 | 95 | return hmacKey, nil 96 | } 97 | -------------------------------------------------------------------------------- /internal/hmac/hmac_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: BUSL-1.1 3 | 4 | package hmac 5 | 6 | import ( 7 | "context" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | corev1 "k8s.io/api/core/v1" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | "k8s.io/apimachinery/pkg/runtime/schema" 17 | "k8s.io/client-go/kubernetes/fake" 18 | k8stesting "k8s.io/client-go/testing" 19 | ) 20 | 21 | const ( 22 | secretName = "test-secret" 23 | secretNamespace = "test-namespace" 24 | ) 25 | 26 | var secretSpec = &corev1.Secret{ 27 | ObjectMeta: metav1.ObjectMeta{ 28 | Name: secretName, 29 | Namespace: secretNamespace, 30 | }, 31 | Data: map[string][]byte{ 32 | hmacKeyName: []byte(strings.Repeat("a", 32)), 33 | }, 34 | } 35 | 36 | func setup(t *testing.T) (*HMACGenerator, *fake.Clientset) { 37 | client := fake.NewSimpleClientset() 38 | return NewHMACGenerator(client, secretSpec), client 39 | } 40 | 41 | func TestGenerateSecretIfNoneExists(t *testing.T) { 42 | gen, client := setup(t) 43 | 44 | // Add counter functions. 45 | createCount := countAPICalls(client, "create", "secrets") 46 | getCount := countAPICalls(client, "get", "secrets") 47 | 48 | // Get an HMAC key, which should create the k8s secret. 49 | key, err := gen.GetOrCreateHMACKey(context.Background()) 50 | require.NoError(t, err) 51 | assert.Len(t, key, hmacKeyLength) 52 | assert.Equal(t, 1, *createCount) 53 | assert.Equal(t, 1, *getCount) 54 | assert.NotEqual(t, string(secretSpec.Data[hmacKeyName]), string(key)) 55 | assert.NotEmpty(t, string(key)) 56 | } 57 | 58 | func TestReadSecretIfAlreadyExists(t *testing.T) { 59 | gen, client := setup(t) 60 | 61 | ctx := context.Background() 62 | _, err := client.CoreV1().Secrets(secretNamespace).Create(ctx, secretSpec, metav1.CreateOptions{}) 63 | require.NoError(t, err) 64 | 65 | // Add counter functions. 66 | createCount := countAPICalls(client, "create", "secrets") 67 | getCount := countAPICalls(client, "get", "secrets") 68 | 69 | // Get an HMAC key, which should read the existing k8s secret. 70 | key, err := gen.GetOrCreateHMACKey(ctx) 71 | require.NoError(t, err) 72 | assert.Len(t, key, hmacKeyLength) 73 | assert.Equal(t, 0, *createCount) 74 | assert.Equal(t, 1, *getCount) 75 | assert.Equal(t, string(secretSpec.Data[hmacKeyName]), string(key)) 76 | } 77 | 78 | func TestGracefullyHandlesLosingTheRace(t *testing.T) { 79 | gen, client := setup(t) 80 | 81 | ctx := context.Background() 82 | 83 | client.PrependReactor("create", "secrets", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { 84 | // Intercept the create call and create the secret just before. 85 | err = client.Tracker().Create(schema.GroupVersionResource{ 86 | Group: "", 87 | Version: "v1", 88 | Resource: "secrets", 89 | }, secretSpec, secretNamespace) 90 | require.NoError(t, err) 91 | return false, nil, nil 92 | }) 93 | createCount := countAPICalls(client, "create", "secrets") 94 | getCount := countAPICalls(client, "get", "secrets") 95 | 96 | // Get an HMAC key, which should initially find no secret, and then lose the race for creating it. 97 | key, err := gen.GetOrCreateHMACKey(ctx) 98 | require.NoError(t, err) 99 | assert.Len(t, key, hmacKeyLength) 100 | assert.Equal(t, 1, *createCount) 101 | assert.Equal(t, 2, *getCount) 102 | assert.Equal(t, string(secretSpec.Data[hmacKeyName]), string(key)) 103 | } 104 | 105 | // Counts the number of times an API is called. 106 | func countAPICalls(client *fake.Clientset, verb string, resource string) *int { 107 | i := 0 108 | client.PrependReactor(verb, resource, func(_ k8stesting.Action) (handled bool, ret runtime.Object, err error) { 109 | i++ 110 | return false, nil, nil 111 | }) 112 | return &i 113 | } 114 | -------------------------------------------------------------------------------- /internal/provider/provider.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: BUSL-1.1 3 | 4 | package provider 5 | 6 | import ( 7 | "context" 8 | "crypto/hmac" 9 | "crypto/sha256" 10 | "encoding/base64" 11 | "encoding/hex" 12 | "encoding/json" 13 | "fmt" 14 | "strings" 15 | 16 | "github.com/hashicorp/go-hclog" 17 | "github.com/hashicorp/vault-csi-provider/internal/auth" 18 | vaultclient "github.com/hashicorp/vault-csi-provider/internal/client" 19 | "github.com/hashicorp/vault-csi-provider/internal/clientcache" 20 | "github.com/hashicorp/vault-csi-provider/internal/config" 21 | hmacgen "github.com/hashicorp/vault-csi-provider/internal/hmac" 22 | "github.com/hashicorp/vault/api" 23 | pb "sigs.k8s.io/secrets-store-csi-driver/provider/v1alpha1" 24 | ) 25 | 26 | // provider implements the secrets-store-csi-driver provider interface 27 | // and communicates with the Vault API. 28 | type provider struct { 29 | logger hclog.Logger 30 | vaultResponseCache map[vaultResponseCacheKey]*api.Secret 31 | 32 | // Allows mocking Kubernetes API for tests. 33 | authMethod *auth.KubernetesJWTAuth 34 | hmacGenerator *hmacgen.HMACGenerator 35 | clientCache *clientcache.ClientCache 36 | } 37 | 38 | func NewProvider(logger hclog.Logger, authMethod *auth.KubernetesJWTAuth, hmacGenerator *hmacgen.HMACGenerator, clientCache *clientcache.ClientCache) *provider { 39 | p := &provider{ 40 | logger: logger, 41 | vaultResponseCache: make(map[vaultResponseCacheKey]*api.Secret), 42 | 43 | authMethod: authMethod, 44 | hmacGenerator: hmacGenerator, 45 | clientCache: clientCache, 46 | } 47 | 48 | return p 49 | } 50 | 51 | type vaultResponseCacheKey struct { 52 | secretPath string 53 | method string 54 | } 55 | 56 | const ( 57 | EncodingBase64 string = "base64" 58 | EncodingHex string = "hex" 59 | EncodingUtf8 string = "utf-8" 60 | ) 61 | 62 | func keyFromData(rootData map[string]interface{}, secretKey string) ([]byte, error) { 63 | // Automatically parse through to embedded .data.data map if it's present 64 | // and the correct type (e.g. for kv v2). 65 | var data map[string]interface{} 66 | d, ok := rootData["data"] 67 | if ok { 68 | data, ok = d.(map[string]interface{}) 69 | } 70 | if !ok { 71 | data = rootData 72 | } 73 | 74 | // Fail early if a the key does not exist in the secret 75 | if _, ok := data[secretKey]; !ok { 76 | return nil, fmt.Errorf("key %q does not exist at the secret path", secretKey) 77 | } 78 | 79 | // Special-case the most common format of strings so the contents are 80 | // returned plainly without quotes that json.Marshal would add. 81 | if content, ok := data[secretKey].(string); ok { 82 | return []byte(content), nil 83 | } 84 | 85 | // Arbitrary data can be returned in the data field of an API secret struct. 86 | // It's already been Unmarshalled from the response, so in theory, 87 | // marshalling should never realistically fail, but don't log the error just 88 | // in case, as it could contain secret contents if it does somehow fail. 89 | if content, err := json.Marshal(data[secretKey]); err == nil { 90 | return content, nil 91 | } 92 | 93 | return nil, fmt.Errorf("failed to extract secret content as string or JSON from key %q", secretKey) 94 | } 95 | 96 | func decodeValue(data []byte, encoding string) ([]byte, error) { 97 | if len(encoding) == 0 || strings.EqualFold(encoding, EncodingUtf8) { 98 | return data, nil 99 | } else if strings.EqualFold(encoding, EncodingBase64) { 100 | return base64.StdEncoding.DecodeString(string(data)) 101 | } else if strings.EqualFold(encoding, EncodingHex) { 102 | return hex.DecodeString(string(data)) 103 | } 104 | 105 | return nil, fmt.Errorf("invalid encoding type. Should be utf-8, base64, or hex") 106 | } 107 | 108 | func (p *provider) getSecret(ctx context.Context, client *vaultclient.Client, secretConfig config.Secret) ([]byte, error) { 109 | var secret *api.Secret 110 | var cached bool 111 | key := vaultResponseCacheKey{secretPath: secretConfig.SecretPath, method: secretConfig.Method} 112 | if secret, cached = p.vaultResponseCache[key]; !cached { 113 | var err error 114 | secret, err = client.RequestSecret(ctx, p.authMethod, secretConfig) 115 | if err != nil { 116 | return nil, fmt.Errorf("couldn't read secret %q: %w", secretConfig.ObjectName, err) 117 | } 118 | if secret == nil || secret.Data == nil { 119 | return nil, fmt.Errorf("empty response from %q, warnings: %v", secretConfig.SecretPath, secret.Warnings) 120 | } 121 | 122 | for _, w := range secret.Warnings { 123 | p.logger.Warn("Warning in response from Vault API", "warning", w) 124 | } 125 | 126 | p.vaultResponseCache[key] = secret 127 | } else { 128 | p.logger.Debug("Secret fetched from cache", "secretConfig", secretConfig) 129 | } 130 | 131 | // If no secretKey specified, we return the whole response as a JSON object. 132 | if secretConfig.SecretKey == "" { 133 | content, err := json.Marshal(secret) 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | return content, nil 139 | } 140 | 141 | value, err := keyFromData(secret.Data, secretConfig.SecretKey) 142 | if err != nil { 143 | return nil, fmt.Errorf("{%s}: {%w}", secretConfig.SecretPath, err) 144 | } 145 | 146 | decodedVal, decodeErr := decodeValue(value, secretConfig.Encoding) 147 | if decodeErr != nil { 148 | return nil, fmt.Errorf("{%s}: {%w}", secretConfig.SecretPath, decodeErr) 149 | } 150 | 151 | return decodedVal, nil 152 | } 153 | 154 | // MountSecretsStoreObjectContent mounts content of the vault object to target path 155 | func (p *provider) HandleMountRequest(ctx context.Context, cfg config.Config, flagsConfig config.FlagsConfig) (*pb.MountResponse, error) { 156 | hmacKey, err := p.hmacGenerator.GetOrCreateHMACKey(ctx) 157 | if err != nil { 158 | p.logger.Warn("Error generating HMAC key. Mounted secrets will not be assigned a version", "error", err) 159 | } 160 | client, err := p.clientCache.GetOrCreateClient(cfg.Parameters, flagsConfig) 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | var files []*pb.File 166 | var objectVersions []*pb.ObjectVersion 167 | for _, secret := range cfg.Parameters.Secrets { 168 | content, err := p.getSecret(ctx, client, secret) 169 | if err != nil { 170 | return nil, err 171 | } 172 | 173 | version, err := generateObjectVersion(secret, hmacKey, content) 174 | if err != nil { 175 | return nil, fmt.Errorf("failed to generate version for object name %q: %w", secret.ObjectName, err) 176 | } 177 | 178 | filePermission := int32(cfg.FilePermission) 179 | if secret.FilePermission != 0 { 180 | filePermission = int32(secret.FilePermission) 181 | } 182 | files = append(files, &pb.File{Path: secret.ObjectName, Mode: filePermission, Contents: content}) 183 | objectVersions = append(objectVersions, version) 184 | p.logger.Info("secret added to mount response", "directory", cfg.TargetPath, "file", secret.ObjectName) 185 | } 186 | 187 | return &pb.MountResponse{ 188 | Files: files, 189 | ObjectVersion: objectVersions, 190 | }, nil 191 | } 192 | 193 | func generateObjectVersion(secret config.Secret, hmacKey []byte, content []byte) (*pb.ObjectVersion, error) { 194 | // If something went wrong with generating the HMAC key, we log the error and 195 | // treat generating the version as best-effort instead, as delivering the secret 196 | // is generally more critical to workloads than assigning a version for it. 197 | if hmacKey == nil { 198 | return &pb.ObjectVersion{ 199 | Id: secret.ObjectName, 200 | Version: "", 201 | }, nil 202 | } 203 | 204 | // We include the secret config in the hash input to avoid leaking information 205 | // about different secrets that could have the same content. 206 | hash := hmac.New(sha256.New, hmacKey) 207 | cfg, err := json.Marshal(secret) 208 | if err != nil { 209 | return nil, err 210 | } 211 | if _, err := hash.Write(cfg); err != nil { 212 | return nil, err 213 | } 214 | if _, err := hash.Write(content); err != nil { 215 | return nil, err 216 | } 217 | 218 | return &pb.ObjectVersion{ 219 | Id: secret.ObjectName, 220 | Version: base64.URLEncoding.EncodeToString(hash.Sum(nil)), 221 | }, nil 222 | } 223 | -------------------------------------------------------------------------------- /internal/provider/provider_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: BUSL-1.1 3 | 4 | package provider 5 | 6 | import ( 7 | "context" 8 | "encoding/base64" 9 | "encoding/json" 10 | "fmt" 11 | "net/http" 12 | "net/http/httptest" 13 | "testing" 14 | 15 | "github.com/hashicorp/go-hclog" 16 | "github.com/hashicorp/vault-csi-provider/internal/auth" 17 | "github.com/hashicorp/vault-csi-provider/internal/clientcache" 18 | "github.com/hashicorp/vault-csi-provider/internal/config" 19 | "github.com/hashicorp/vault-csi-provider/internal/hmac" 20 | "github.com/hashicorp/vault/api" 21 | "github.com/stretchr/testify/assert" 22 | "github.com/stretchr/testify/require" 23 | corev1 "k8s.io/api/core/v1" 24 | "k8s.io/client-go/kubernetes/fake" 25 | "sigs.k8s.io/secrets-store-csi-driver/provider/v1alpha1" 26 | pb "sigs.k8s.io/secrets-store-csi-driver/provider/v1alpha1" 27 | ) 28 | 29 | func TestKeyFromData(t *testing.T) { 30 | data := map[string]interface{}{ 31 | "foo": "bar", 32 | "baz": "zap", 33 | } 34 | dataWithDataString := map[string]interface{}{ 35 | "foo": "bar", 36 | "baz": "zap", 37 | "data": "hello", 38 | } 39 | dataWithDataField := map[string]interface{}{ 40 | "data": map[string]interface{}{ 41 | "foo": "bar", 42 | "baz": "zap", 43 | }, 44 | } 45 | dataWithNonStringValue := map[string]interface{}{ 46 | "foo": 10, 47 | "baz": "zap", 48 | } 49 | dataWithJSON := map[string]interface{}{ 50 | "data": map[string]interface{}{ 51 | "foo": map[string]interface{}{ 52 | "bar": "hop", 53 | "baz": "zap", 54 | "cheeses": map[string]interface{}{ 55 | "brie": 9, 56 | "cheddar": "8", 57 | }, 58 | }, 59 | "baz": "zap", 60 | }, 61 | } 62 | dataWithArray := map[string]interface{}{ 63 | "values": []interface{}{6, "stilton", true}, 64 | } 65 | for _, tc := range []struct { 66 | name string 67 | key string 68 | data map[string]interface{} 69 | expected []byte 70 | }{ 71 | { 72 | name: "base case", 73 | key: "foo", 74 | data: data, 75 | expected: []byte("bar"), 76 | }, 77 | { 78 | name: "string data", 79 | key: "data", 80 | data: dataWithDataString, 81 | expected: []byte("hello"), 82 | }, 83 | { 84 | name: "kv v2 embedded data field", 85 | key: "foo", 86 | data: dataWithDataField, 87 | expected: []byte("bar"), 88 | }, 89 | { 90 | name: "kv v2 embedded data field", 91 | key: "foo", 92 | data: dataWithNonStringValue, 93 | expected: []byte("10"), 94 | }, 95 | { 96 | name: "json data", 97 | key: "foo", 98 | data: dataWithJSON, 99 | expected: []byte(`{"bar":"hop","baz":"zap","cheeses":{"brie":9,"cheddar":"8"}}`), 100 | }, 101 | { 102 | name: "json array", 103 | key: "values", 104 | data: dataWithArray, 105 | expected: []byte(`[6,"stilton",true]`), 106 | }, 107 | } { 108 | content, err := keyFromData(tc.data, tc.key) 109 | require.NoError(t, err, tc.name) 110 | assert.Equal(t, tc.expected, content) 111 | } 112 | } 113 | 114 | func TestKeyFromDataMissingKey(t *testing.T) { 115 | data := map[string]interface{}{ 116 | "foo": "bar", 117 | "baz": "zap", 118 | } 119 | dataWithDataString := map[string]interface{}{ 120 | "foo": "bar", 121 | "baz": "zap", 122 | "data": "hello", 123 | } 124 | dataWithDataField := map[string]interface{}{ 125 | "data": map[string]interface{}{ 126 | "foo": "bar", 127 | "baz": "zap", 128 | }, 129 | } 130 | for _, tc := range []struct { 131 | name string 132 | key string 133 | data map[string]interface{} 134 | }{ 135 | { 136 | name: "base case", 137 | key: "non-existing", 138 | data: data, 139 | }, 140 | { 141 | name: "string data", 142 | key: "non-existing", 143 | data: dataWithDataString, 144 | }, 145 | { 146 | name: "kv v2 embedded data field", 147 | key: "non-existing", 148 | data: dataWithDataField, 149 | }, 150 | } { 151 | _, err := keyFromData(tc.data, tc.key) 152 | require.Error(t, err) 153 | } 154 | } 155 | 156 | func TestHandleMountRequest(t *testing.T) { 157 | spcConfig := config.Config{ 158 | TargetPath: "some/unused/path", 159 | FilePermission: 0, 160 | Parameters: config.Parameters{ 161 | VaultRoleName: "my-vault-role", 162 | Secrets: []config.Secret{ 163 | { 164 | ObjectName: "object-one", 165 | SecretPath: "path/one", 166 | SecretKey: "the-key", 167 | Method: "", 168 | SecretArgs: nil, 169 | Encoding: "", 170 | }, 171 | { 172 | ObjectName: "object-two", 173 | SecretPath: "path/two", 174 | SecretKey: "", 175 | Method: "", 176 | SecretArgs: nil, 177 | Encoding: "", 178 | }, 179 | { 180 | ObjectName: "object-three", 181 | SecretPath: "path/three", 182 | SecretKey: "the-key", 183 | Method: "", 184 | SecretArgs: nil, 185 | Encoding: "base64", 186 | }, 187 | }, 188 | }, 189 | } 190 | 191 | // TEST 192 | expectedFiles := []*pb.File{ 193 | { 194 | Path: "object-one", 195 | Mode: 0, 196 | Contents: []byte("secret v1 from: /v1/path/one"), 197 | }, 198 | { 199 | Path: "object-two", 200 | Mode: 0, 201 | Contents: []byte(`{"request_id":"","lease_id":"","lease_duration":0,"renewable":false,"data":{"the-key":"secret v1 from: /v1/path/two"},"warnings":null}`), 202 | }, 203 | { 204 | Path: "object-three", 205 | Mode: 0, 206 | Contents: []byte("secret v1 from: /v1/path/three"), 207 | }, 208 | } 209 | expectedVersionIDs := []string{"object-one", "object-two", "object-three"} 210 | versionsSeen := map[string]struct{}{} 211 | 212 | // SETUP 213 | mockVaultServer := httptest.NewServer(http.HandlerFunc(mockVaultHandler( 214 | map[string]func(numberOfCalls int) (string, interface{}){ 215 | "/v1/path/one": func(numberOfCalls int) (string, interface{}) { 216 | return "the-key", fmt.Sprintf("secret v%d from: /v1/path/one", numberOfCalls) 217 | }, 218 | "/v1/path/two": func(numberOfCalls int) (string, interface{}) { 219 | return "the-key", fmt.Sprintf("secret v%d from: /v1/path/two", numberOfCalls) 220 | }, 221 | "/v1/path/three": func(numberOfCalls int) (string, interface{}) { 222 | return "the-key", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("secret v%d from: /v1/path/three", numberOfCalls))) 223 | }, 224 | }, 225 | ))) 226 | flagsConfig := config.FlagsConfig{ 227 | VaultAddr: mockVaultServer.URL, 228 | } 229 | defer mockVaultServer.Close() 230 | 231 | k8sClient := fake.NewSimpleClientset( 232 | &corev1.ServiceAccount{}, 233 | ) 234 | authMethod := auth.NewKubernetesJWTAuth(hclog.Default(), k8sClient, spcConfig.Parameters, "") 235 | hmacGenerator := hmac.NewHMACGenerator(k8sClient, &corev1.Secret{}) 236 | clientCache, err := clientcache.NewClientCache(hclog.Default(), 10) 237 | require.NoError(t, err) 238 | // While we hit the cache, the secret contents and versions should remain the same. 239 | provider := NewProvider(hclog.Default(), authMethod, hmacGenerator, clientCache) 240 | for i := 0; i < 3; i++ { 241 | resp, err := provider.HandleMountRequest(context.Background(), spcConfig, flagsConfig) 242 | require.NoError(t, err) 243 | 244 | assert.Equal(t, (*v1alpha1.Error)(nil), resp.Error) 245 | assert.Equal(t, expectedFiles, resp.Files) 246 | assert.Equal(t, expectedVersionIDs[i], resp.ObjectVersion[i].Id) 247 | assert.NotEmpty(t, resp.ObjectVersion[i].Version) 248 | _, seen := versionsSeen[resp.ObjectVersion[i].Version] 249 | assert.False(t, seen) 250 | versionsSeen[resp.ObjectVersion[i].Version] = struct{}{} 251 | } 252 | 253 | // The mockVaultHandler function below includes a dynamic counter in the content of secrets. 254 | // That means mounting again with a fresh provider will update the contents of the secrets, which should update the version. 255 | resp, err := NewProvider(hclog.Default(), authMethod, hmacGenerator, clientCache).HandleMountRequest(context.Background(), spcConfig, flagsConfig) 256 | require.NoError(t, err) 257 | 258 | assert.Equal(t, (*v1alpha1.Error)(nil), resp.Error) 259 | expectedFiles[0].Contents = []byte("secret v2 from: /v1/path/one") 260 | expectedFiles[1].Contents = []byte(`{"request_id":"","lease_id":"","lease_duration":0,"renewable":false,"data":{"the-key":"secret v2 from: /v1/path/two"},"warnings":null}`) 261 | expectedFiles[2].Contents = []byte("secret v2 from: /v1/path/three") 262 | assert.Equal(t, expectedFiles, resp.Files) 263 | for i := 0; i < len(expectedFiles); i++ { 264 | assert.Equal(t, expectedVersionIDs[i], resp.ObjectVersion[i].Id) 265 | assert.NotEmpty(t, resp.ObjectVersion[i].Version) 266 | _, seen := versionsSeen[resp.ObjectVersion[i].Version] 267 | assert.False(t, seen) 268 | versionsSeen[resp.ObjectVersion[i].Version] = struct{}{} 269 | } 270 | } 271 | 272 | func mockVaultHandler(pathMapping map[string]func(numberOfCalls int) (string, interface{})) func(w http.ResponseWriter, req *http.Request) { 273 | getsPerPath := map[string]int{} 274 | 275 | return func(w http.ResponseWriter, req *http.Request) { 276 | switch req.Method { 277 | case http.MethodPost: 278 | // Assume all POSTs are login requests and return a token. 279 | body, err := json.Marshal(&api.Secret{ 280 | Auth: &api.SecretAuth{ 281 | ClientToken: "my-vault-client-token", 282 | }, 283 | }) 284 | if err != nil { 285 | http.Error(w, err.Error(), http.StatusInternalServerError) 286 | return 287 | } 288 | _, err = w.Write(body) 289 | if err != nil { 290 | http.Error(w, err.Error(), http.StatusInternalServerError) 291 | return 292 | } 293 | case http.MethodGet: 294 | // Assume all GETs are secret reads and return a derivative of the request path. 295 | path := req.URL.Path 296 | getsPerPath[path]++ 297 | mappingFunc := pathMapping[path] 298 | key, value := mappingFunc(getsPerPath[path]) 299 | body, err := json.Marshal(&api.Secret{ 300 | Data: map[string]interface{}{ 301 | key: value, 302 | }, 303 | }) 304 | if err != nil { 305 | http.Error(w, err.Error(), http.StatusInternalServerError) 306 | return 307 | } 308 | _, err = w.Write(body) 309 | if err != nil { 310 | http.Error(w, err.Error(), http.StatusInternalServerError) 311 | return 312 | } 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /internal/server/server.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: BUSL-1.1 3 | 4 | package server 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | "github.com/hashicorp/go-hclog" 11 | "github.com/hashicorp/vault-csi-provider/internal/auth" 12 | "github.com/hashicorp/vault-csi-provider/internal/clientcache" 13 | "github.com/hashicorp/vault-csi-provider/internal/config" 14 | "github.com/hashicorp/vault-csi-provider/internal/hmac" 15 | "github.com/hashicorp/vault-csi-provider/internal/provider" 16 | "github.com/hashicorp/vault-csi-provider/internal/version" 17 | "k8s.io/client-go/kubernetes" 18 | pb "sigs.k8s.io/secrets-store-csi-driver/provider/v1alpha1" 19 | ) 20 | 21 | var _ pb.CSIDriverProviderServer = (*Server)(nil) 22 | 23 | // Server implements the secrets-store-csi-driver provider gRPC service interface. 24 | type Server struct { 25 | logger hclog.Logger 26 | flagsConfig config.FlagsConfig 27 | k8sClient kubernetes.Interface 28 | hmacGenerator *hmac.HMACGenerator 29 | clientCache *clientcache.ClientCache 30 | } 31 | 32 | func NewServer(logger hclog.Logger, flagsConfig config.FlagsConfig, k8sClient kubernetes.Interface, hmacGenerator *hmac.HMACGenerator, clientCache *clientcache.ClientCache) *Server { 33 | return &Server{ 34 | logger: logger, 35 | flagsConfig: flagsConfig, 36 | k8sClient: k8sClient, 37 | hmacGenerator: hmacGenerator, 38 | clientCache: clientCache, 39 | } 40 | } 41 | 42 | func (s *Server) Version(context.Context, *pb.VersionRequest) (*pb.VersionResponse, error) { 43 | return &pb.VersionResponse{ 44 | Version: "v1alpha1", 45 | RuntimeName: "vault-csi-provider", 46 | RuntimeVersion: version.BuildVersion, 47 | }, nil 48 | } 49 | 50 | func (s *Server) Mount(ctx context.Context, req *pb.MountRequest) (*pb.MountResponse, error) { 51 | cfg, err := config.Parse(req.Attributes, req.TargetPath, req.Permission) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | authMethod := auth.NewKubernetesJWTAuth(s.logger.Named("auth"), s.k8sClient, cfg.Parameters, s.flagsConfig.VaultMount) 57 | provider := provider.NewProvider(s.logger.Named("provider"), authMethod, s.hmacGenerator, s.clientCache) 58 | resp, err := provider.HandleMountRequest(ctx, cfg, s.flagsConfig) 59 | if err != nil { 60 | return nil, fmt.Errorf("error making mount request: %w", err) 61 | } 62 | 63 | return resp, nil 64 | } 65 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: BUSL-1.1 3 | 4 | package version 5 | 6 | import ( 7 | "encoding/json" 8 | ) 9 | 10 | const minDriverVersion = "v0.0.21" 11 | 12 | var ( 13 | BuildDate string 14 | BuildVersion string 15 | GoVersion string 16 | ) 17 | 18 | // providerVersion holds current provider version 19 | type providerVersion struct { 20 | Version string `json:"version"` // Version of the binary. 21 | BuildDate string `json:"buildDate"` // The date the binary was built. 22 | GoVersion string `json:"goVersion"` // Version of Go the binary was built with. 23 | MinDriverVersion string `json:"minDriverVersion"` // Minimum driver version the provider works with. 24 | } 25 | 26 | func GetVersion() (string, error) { 27 | pv := providerVersion{ 28 | Version: BuildVersion, 29 | BuildDate: BuildDate, 30 | GoVersion: GoVersion, 31 | MinDriverVersion: minDriverVersion, 32 | } 33 | 34 | res, err := json.Marshal(pv) 35 | if err != nil { 36 | return "", err 37 | } 38 | 39 | return string(res), nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/version/version_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: BUSL-1.1 3 | 4 | package version 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestGetVersion(t *testing.T) { 13 | BuildDate = "Now" 14 | BuildVersion = "version" 15 | GoVersion = "go version x.y.z" 16 | 17 | v, err := GetVersion() 18 | if err != nil { 19 | t.Fatalf("expected no error, got %v", err) 20 | } 21 | expected := fmt.Sprintf(`{"version":"version","buildDate":"Now","goVersion":"go version x.y.z","minDriverVersion":"%s"}`, minDriverVersion) 22 | if !strings.EqualFold(v, expected) { 23 | t.Fatalf("string doesn't match, expected %s, got %s", expected, v) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: BUSL-1.1 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "flag" 9 | "fmt" 10 | "net" 11 | "net/http" 12 | "os" 13 | "os/signal" 14 | "syscall" 15 | "time" 16 | 17 | "github.com/hashicorp/go-hclog" 18 | "github.com/hashicorp/vault-csi-provider/internal/clientcache" 19 | "github.com/hashicorp/vault-csi-provider/internal/config" 20 | "github.com/hashicorp/vault-csi-provider/internal/hmac" 21 | providerserver "github.com/hashicorp/vault-csi-provider/internal/server" 22 | "github.com/hashicorp/vault-csi-provider/internal/version" 23 | "google.golang.org/grpc" 24 | "google.golang.org/grpc/status" 25 | corev1 "k8s.io/api/core/v1" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | "k8s.io/client-go/kubernetes" 28 | "k8s.io/client-go/rest" 29 | "k8s.io/utils/pointer" 30 | pb "sigs.k8s.io/secrets-store-csi-driver/provider/v1alpha1" 31 | ) 32 | 33 | const ( 34 | namespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" 35 | ) 36 | 37 | func main() { 38 | logger := hclog.Default() 39 | err := realMain(logger) 40 | if err != nil { 41 | logger.Error("Error running provider", "err", err) 42 | os.Exit(1) 43 | } 44 | } 45 | 46 | func setupLogger(flags config.FlagsConfig) hclog.Logger { 47 | logger := hclog.Default() 48 | var level hclog.Level 49 | if flags.LogLevel != "" { 50 | level = hclog.LevelFromString(flags.LogLevel) 51 | if level == hclog.NoLevel { 52 | level = hclog.Info 53 | } 54 | } else if flags.Debug { 55 | level = hclog.Debug 56 | } 57 | logger.SetLevel(level) 58 | return logger 59 | } 60 | 61 | func realMain(logger hclog.Logger) error { 62 | flags := config.FlagsConfig{} 63 | flag.StringVar(&flags.Endpoint, "endpoint", "/tmp/vault.sock", "Path to socket on which to listen for driver gRPC calls.") 64 | flag.BoolVar(&flags.Debug, "debug", false, "Sets log to debug level. This has been deprecated, please use -log-level=debug instead.") 65 | flag.StringVar(&flags.LogLevel, "log-level", "info", "Sets log level. Options are info, debug, trace, warn, error, and off.") 66 | flag.BoolVar(&flags.Version, "version", false, "Prints the version information.") 67 | flag.StringVar(&flags.HealthAddr, "health-addr", ":8080", "Configure http listener for reporting health.") 68 | 69 | flag.StringVar(&flags.HMACSecretName, "hmac-secret-name", "vault-csi-provider-hmac-key", "Configure the Kubernetes secret name that the provider creates to store an HMAC key for generating secret version hashes") 70 | 71 | flag.IntVar(&flags.CacheSize, "cache-size", 1000, "Set the maximum number of Vault tokens that will be cached in-memory. One Vault token will be stored for each pod on the same node that mounts secrets.") 72 | 73 | flag.StringVar(&flags.VaultAddr, "vault-addr", "", "Default address for connecting to Vault. Can also be specified via the VAULT_ADDR environment variable.") 74 | flag.StringVar(&flags.VaultMount, "vault-mount", "kubernetes", "Default Vault mount path for authentication. Can refer to a Kubernetes or JWT auth mount.") 75 | flag.StringVar(&flags.VaultNamespace, "vault-namespace", "", "Default Vault namespace for Vault requests. Can also be specified via the VAULT_NAMESPACE environment variable.") 76 | 77 | flag.StringVar(&flags.TLSCACertPath, "vault-tls-ca-cert", "", "Path on disk to a single PEM-encoded CA certificate to trust for Vault. Takes precendence over -vault-tls-ca-directory. Can also be specified via the VAULT_CACERT environment variable.") 78 | flag.StringVar(&flags.TLSCADirectory, "vault-tls-ca-directory", "", "Path on disk to a directory of PEM-encoded CA certificates to trust for Vault. Can also be specified via the VAULT_CAPATH environment variable.") 79 | flag.StringVar(&flags.TLSServerName, "vault-tls-server-name", "", "Name to use as the SNI host when connecting to Vault via TLS. Can also be specified via the VAULT_TLS_SERVER_NAME environment variable.") 80 | flag.StringVar(&flags.TLSClientCert, "vault-tls-client-cert", "", "Path on disk to a PEM-encoded client certificate for mTLS communication with Vault. If set, also requires -vault-tls-client-key. Can also be specified via the VAULT_CLIENT_CERT environment variable.") 81 | flag.StringVar(&flags.TLSClientKey, "vault-tls-client-key", "", "Path on disk to a PEM-encoded client key for mTLS communication with Vault. If set, also requires -vault-tls-client-cert. Can also be specified via the VAULT_CLIENT_KEY environment variable.") 82 | flag.BoolVar(&flags.TLSSkipVerify, "vault-tls-skip-verify", false, "Disable verification of TLS certificates. Can also be specified via the VAULT_SKIP_VERIFY environment variable.") 83 | flag.Parse() 84 | 85 | // set log level 86 | logger = setupLogger(flags) 87 | 88 | if flags.Version { 89 | v, err := version.GetVersion() 90 | if err != nil { 91 | return fmt.Errorf("failed to print version, err: %w", err) 92 | } 93 | // print the version and exit 94 | _, err = fmt.Println(v) 95 | return err 96 | } 97 | 98 | logger.Info("Creating new gRPC server") 99 | serverLogger := logger.Named("server") 100 | server := grpc.NewServer( 101 | grpc.UnaryInterceptor(func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 102 | startTime := time.Now() 103 | serverLogger.Info("Processing unary gRPC call", "grpc.method", info.FullMethod) 104 | resp, err := handler(ctx, req) 105 | serverLogger.Info("Finished unary gRPC call", "grpc.method", info.FullMethod, "grpc.time", time.Since(startTime), "grpc.code", status.Code(err), "err", err) 106 | return resp, err 107 | }), 108 | ) 109 | 110 | c := make(chan os.Signal, 1) 111 | signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) 112 | go func() { 113 | sig := <-c 114 | logger.Info(fmt.Sprintf("Caught signal %s, shutting down", sig)) 115 | server.GracefulStop() 116 | }() 117 | 118 | listener, err := listen(logger, flags.Endpoint) 119 | if err != nil { 120 | return err 121 | } 122 | defer listener.Close() 123 | 124 | cfg, err := rest.InClusterConfig() 125 | if err != nil { 126 | return err 127 | } 128 | clientset, err := kubernetes.NewForConfig(cfg) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | namespace, err := os.ReadFile(namespaceFile) 134 | if err != nil { 135 | return fmt.Errorf("failed to read namespace from file: %w", err) 136 | } 137 | hmacSecretSpec := &corev1.Secret{ 138 | ObjectMeta: metav1.ObjectMeta{ 139 | Name: flags.HMACSecretName, 140 | Namespace: string(namespace), 141 | // TODO: Configurable labels and annotations? 142 | }, 143 | Immutable: pointer.Bool(true), 144 | } 145 | hmacGenerator := hmac.NewHMACGenerator(clientset, hmacSecretSpec) 146 | 147 | clientCache, err := clientcache.NewClientCache(serverLogger.Named("vaultclient"), flags.CacheSize) 148 | if err != nil { 149 | return fmt.Errorf("failed to initialize the cache: %w", err) 150 | } 151 | 152 | srv := providerserver.NewServer(serverLogger, flags, clientset, hmacGenerator, clientCache) 153 | pb.RegisterCSIDriverProviderServer(server, srv) 154 | 155 | // Create health handler 156 | mux := http.NewServeMux() 157 | ms := http.Server{ 158 | Addr: flags.HealthAddr, 159 | Handler: mux, 160 | } 161 | defer func() { 162 | err := ms.Shutdown(context.Background()) 163 | if err != nil { 164 | logger.Error("Error shutting down health handler", "err", err) 165 | } 166 | }() 167 | 168 | mux.HandleFunc("/health/ready", func(w http.ResponseWriter, r *http.Request) { 169 | w.WriteHeader(http.StatusOK) 170 | }) 171 | 172 | // Start health handler 173 | go func() { 174 | logger.Info("Starting health handler", "addr", flags.HealthAddr) 175 | if err := ms.ListenAndServe(); err != nil && err != http.ErrServerClosed { 176 | logger.Error("Error with health handler", "error", err) 177 | } 178 | }() 179 | 180 | logger.Info("Starting gRPC server") 181 | err = server.Serve(listener) 182 | if err != nil { 183 | return fmt.Errorf("error running gRPC server: %w", err) 184 | } 185 | 186 | return nil 187 | } 188 | 189 | func listen(logger hclog.Logger, endpoint string) (net.Listener, error) { 190 | // Because the unix socket is created in a host volume (i.e. persistent 191 | // storage), it can persist from previous runs if the pod was not terminated 192 | // cleanly. Check if we need to clean up before creating a listener. 193 | _, err := os.Stat(endpoint) 194 | if err != nil && !os.IsNotExist(err) { 195 | return nil, fmt.Errorf("failed to check for existence of unix socket: %w", err) 196 | } else if err == nil { 197 | logger.Info("Cleaning up pre-existing file at unix socket location", "endpoint", endpoint) 198 | err = os.Remove(endpoint) 199 | if err != nil { 200 | return nil, fmt.Errorf("failed to clean up pre-existing file at unix socket location: %w", err) 201 | } 202 | } 203 | 204 | logger.Info("Opening unix socket", "endpoint", endpoint) 205 | listener, err := net.Listen("unix", endpoint) 206 | if err != nil { 207 | return nil, fmt.Errorf("failed to listen on unix socket at %s: %v", endpoint, err) 208 | } 209 | 210 | return listener, nil 211 | } 212 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: BUSL-1.1 3 | 4 | package main 5 | 6 | import ( 7 | "io/ioutil" 8 | "os" 9 | "path" 10 | "testing" 11 | 12 | "github.com/hashicorp/go-hclog" 13 | 14 | "github.com/hashicorp/vault-csi-provider/internal/config" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | func TestListen(t *testing.T) { 19 | logger := hclog.NewNullLogger() 20 | dir, err := ioutil.TempDir("/tmp", "TestListen") 21 | require.NoError(t, err) 22 | endpoint := path.Join(dir, "vault.sock") 23 | defer func() { 24 | require.NoError(t, os.Remove(endpoint)) 25 | }() 26 | 27 | // Works when no file in the way. 28 | l, err := listen(logger, endpoint) 29 | require.NoError(t, err) 30 | 31 | // Will replace existing file. 32 | require.NoError(t, l.Close()) 33 | _, err = os.Create(endpoint) 34 | require.NoError(t, err) 35 | } 36 | 37 | func TestSetupLogger(t *testing.T) { 38 | tests := []struct { 39 | flags config.FlagsConfig 40 | expected hclog.Level 41 | }{ 42 | {config.FlagsConfig{Debug: true}, hclog.Debug}, // deprecated flag test 43 | {config.FlagsConfig{LogLevel: "trace"}, hclog.Trace}, 44 | {config.FlagsConfig{LogLevel: "debug"}, hclog.Debug}, 45 | {config.FlagsConfig{LogLevel: "info"}, hclog.Info}, 46 | {config.FlagsConfig{LogLevel: "warn"}, hclog.Warn}, 47 | {config.FlagsConfig{LogLevel: "error"}, hclog.Error}, 48 | {config.FlagsConfig{LogLevel: "off"}, hclog.Off}, 49 | {config.FlagsConfig{LogLevel: "no-level"}, hclog.Info}, 50 | {config.FlagsConfig{Debug: true, LogLevel: "warn"}, hclog.Warn}, // if both set, LogLevel should take precedence 51 | } 52 | 53 | for _, tt := range tests { 54 | t.Run(string(tt.expected), func(t *testing.T) { 55 | logger := setupLogger(tt.flags) 56 | 57 | if logger.GetLevel() != tt.expected { 58 | t.Errorf("expected log level %v, got %v", tt.expected, logger.GetLevel()) 59 | } 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /manifest_staging/deployment/vault-csi-provider.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | apiVersion: v1 5 | kind: Namespace 6 | metadata: 7 | name: csi 8 | --- 9 | apiVersion: v1 10 | kind: ServiceAccount 11 | metadata: 12 | name: vault-csi-provider 13 | namespace: csi 14 | --- 15 | apiVersion: rbac.authorization.k8s.io/v1 16 | kind: ClusterRole 17 | metadata: 18 | name: vault-csi-provider-clusterrole 19 | rules: 20 | - apiGroups: 21 | - "" 22 | resources: 23 | - serviceaccounts/token 24 | verbs: 25 | - create 26 | --- 27 | apiVersion: rbac.authorization.k8s.io/v1 28 | kind: ClusterRoleBinding 29 | metadata: 30 | name: vault-csi-provider-clusterrolebinding 31 | roleRef: 32 | apiGroup: rbac.authorization.k8s.io 33 | kind: ClusterRole 34 | name: vault-csi-provider-clusterrole 35 | subjects: 36 | - kind: ServiceAccount 37 | name: vault-csi-provider 38 | namespace: csi 39 | --- 40 | apiVersion: rbac.authorization.k8s.io/v1 41 | kind: Role 42 | metadata: 43 | name: vault-csi-provider-role 44 | rules: 45 | - apiGroups: [""] 46 | resources: ["secrets"] 47 | verbs: ["get"] 48 | resourceNames: 49 | - vault-csi-provider-hmac-key 50 | # 'create' permissions cannot be restricted by resource name: 51 | # https://kubernetes.io/docs/reference/access-authn-authz/rbac/#referring-to-resources 52 | - apiGroups: [""] 53 | resources: ["secrets"] 54 | verbs: ["create"] 55 | --- 56 | apiVersion: rbac.authorization.k8s.io/v1 57 | kind: RoleBinding 58 | metadata: 59 | name: vault-csi-provider-rolebinding 60 | roleRef: 61 | apiGroup: rbac.authorization.k8s.io 62 | kind: Role 63 | name: vault-csi-provider-role 64 | subjects: 65 | - kind: ServiceAccount 66 | name: vault-csi-provider 67 | namespace: csi 68 | --- 69 | apiVersion: apps/v1 70 | kind: DaemonSet 71 | metadata: 72 | labels: 73 | app.kubernetes.io/name: vault-csi-provider 74 | name: vault-csi-provider 75 | namespace: csi 76 | spec: 77 | updateStrategy: 78 | type: RollingUpdate 79 | selector: 80 | matchLabels: 81 | app.kubernetes.io/name: vault-csi-provider 82 | template: 83 | metadata: 84 | labels: 85 | app.kubernetes.io/name: vault-csi-provider 86 | spec: 87 | serviceAccountName: vault-csi-provider 88 | tolerations: 89 | containers: 90 | - name: provider-vault-installer 91 | image: hashicorp/vault-csi-provider:1.5.0 92 | imagePullPolicy: Always 93 | args: 94 | - -endpoint=/provider/vault.sock 95 | - -log-level=info 96 | resources: 97 | requests: 98 | cpu: 50m 99 | memory: 100Mi 100 | limits: 101 | cpu: 50m 102 | memory: 100Mi 103 | volumeMounts: 104 | - name: providervol 105 | mountPath: "/provider" 106 | livenessProbe: 107 | httpGet: 108 | path: "/health/ready" 109 | port: 8080 110 | scheme: "HTTP" 111 | failureThreshold: 2 112 | initialDelaySeconds: 5 113 | periodSeconds: 5 114 | successThreshold: 1 115 | timeoutSeconds: 3 116 | readinessProbe: 117 | httpGet: 118 | path: "/health/ready" 119 | port: 8080 120 | scheme: "HTTP" 121 | failureThreshold: 2 122 | initialDelaySeconds: 5 123 | periodSeconds: 5 124 | successThreshold: 1 125 | timeoutSeconds: 3 126 | volumes: 127 | - name: providervol 128 | hostPath: 129 | path: "/etc/kubernetes/secrets-store-csi-providers" 130 | nodeSelector: 131 | kubernetes.io/os: linux 132 | -------------------------------------------------------------------------------- /test/bats/_helpers.bash: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | wait_for_success() { 5 | echo $1 6 | for i in {0..60}; do 7 | if eval "$1"; then 8 | return 9 | fi 10 | sleep 1 11 | done 12 | # Fail the test. 13 | [ 1 -eq 2 ] 14 | } 15 | 16 | setup_postgres() { 17 | # Setup postgres, pulling the image first to help avoid CI timeouts. 18 | POSTGRES_IMAGE="$(awk '/image:/{print $NF}' $CONFIGS/postgres.yaml)" 19 | docker pull "${POSTGRES_IMAGE}" 20 | kind load docker-image "${POSTGRES_IMAGE}" 21 | POSTGRES_PASSWORD=$(openssl rand -base64 30) 22 | kubectl --namespace=test create secret generic postgres-root \ 23 | --from-literal=POSTGRES_USER="root" \ 24 | --from-literal=POSTGRES_PASSWORD="${POSTGRES_PASSWORD}" 25 | kubectl --namespace=test apply -f $CONFIGS/postgres.yaml 26 | kubectl wait --namespace=test --for=condition=Ready --timeout=10m pod -l app=postgres 27 | 28 | # Configure vault to manage postgres 29 | kubectl --namespace=csi exec vault-0 -- vault secrets enable database 30 | kubectl --namespace=csi exec vault-0 -- vault write database/config/postgres \ 31 | plugin_name="postgresql-database-plugin" \ 32 | allowed_roles="*" \ 33 | connection_url="postgres://{{username}}:{{password}}@postgres.test.svc.cluster.local:5432/db?sslmode=disable" \ 34 | username="root" \ 35 | password="${POSTGRES_PASSWORD}" \ 36 | verify_connection=false 37 | cat $CONFIGS/postgres-creation-statements.sql | kubectl --namespace=csi exec -i vault-0 -- vault write database/roles/test-role \ 38 | db_name="postgres" \ 39 | default_ttl="1h" max_ttl="24h" \ 40 | creation_statements=- 41 | } 42 | -------------------------------------------------------------------------------- /test/bats/configs/cluster-resources.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | --- 5 | apiVersion: v1 6 | kind: Namespace 7 | metadata: 8 | name: csi 9 | --- 10 | apiVersion: rbac.authorization.k8s.io/v1 11 | kind: ClusterRoleBinding 12 | metadata: 13 | name: oidc-reviewer 14 | roleRef: 15 | apiGroup: rbac.authorization.k8s.io 16 | kind: ClusterRole 17 | name: system:service-account-issuer-discovery 18 | subjects: 19 | - apiGroup: rbac.authorization.k8s.io 20 | kind: Group 21 | name: system:unauthenticated 22 | -------------------------------------------------------------------------------- /test/bats/configs/kind/config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | kind: Cluster 5 | apiVersion: kind.x-k8s.io/v1alpha4 6 | # These apiServer settings are included for running the CSI provider on K8s 1.19 7 | kubeadmConfigPatches: 8 | - | 9 | apiVersion: kubeadm.k8s.io/v1beta2 10 | kind: ClusterConfiguration 11 | metadata: 12 | name: config 13 | apiServer: 14 | extraArgs: 15 | "service-account-issuer": "https://kubernetes.default.svc.cluster.local" 16 | "service-account-signing-key-file": "/etc/kubernetes/pki/sa.key" 17 | "service-account-api-audiences": "https://kubernetes.default.svc.cluster.local" -------------------------------------------------------------------------------- /test/bats/configs/nginx-kv-env-var.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | --- 5 | apiVersion: v1 6 | kind: ServiceAccount 7 | metadata: 8 | name: nginx-kv 9 | --- 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | metadata: 13 | name: nginx-deployment 14 | labels: 15 | app: nginx 16 | spec: 17 | replicas: 2 18 | selector: 19 | matchLabels: 20 | app: nginx 21 | template: 22 | metadata: 23 | labels: 24 | app: nginx 25 | spec: 26 | serviceAccountName: nginx-kv 27 | terminationGracePeriodSeconds: 0 28 | terminationGracePeriodSeconds: 0 29 | containers: 30 | - image: docker.mirror.hashicorp.services/nginx 31 | name: nginx 32 | env: 33 | - name: SECRET_USERNAME 34 | valueFrom: 35 | secretKeyRef: 36 | name: kvsecret 37 | key: username 38 | volumeMounts: 39 | - name: secret-volume 40 | mountPath: "/mnt/secrets-store" 41 | readOnly: true 42 | volumes: 43 | - name: secret-volume 44 | csi: 45 | driver: secrets-store.csi.k8s.io 46 | readOnly: true 47 | volumeAttributes: 48 | secretProviderClass: "vault-kv-sync" 49 | -------------------------------------------------------------------------------- /test/bats/configs/nginx-kv-multiple-volumes.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | --- 5 | apiVersion: v1 6 | kind: ServiceAccount 7 | metadata: 8 | name: nginx-kv 9 | --- 10 | kind: Pod 11 | apiVersion: v1 12 | metadata: 13 | name: nginx-multiple-volumes 14 | labels: 15 | app: nginx 16 | spec: 17 | serviceAccountName: nginx-kv 18 | terminationGracePeriodSeconds: 0 19 | containers: 20 | - image: docker.mirror.hashicorp.services/nginx 21 | name: nginx 22 | volumeMounts: 23 | - name: secrets-store-1 24 | mountPath: "/mnt/secrets-store-1" 25 | readOnly: true 26 | - name: secrets-store-2 27 | mountPath: "/mnt/secrets-store-2" 28 | readOnly: true 29 | env: 30 | - name: SECRET_1_USERNAME 31 | valueFrom: 32 | secretKeyRef: 33 | name: kvsecret-1 34 | key: username 35 | - name: SECRET_2_PWD 36 | valueFrom: 37 | secretKeyRef: 38 | name: kvsecret-2 39 | key: pwd 40 | volumes: 41 | - name: secrets-store-1 42 | csi: 43 | driver: secrets-store.csi.k8s.io 44 | readOnly: true 45 | volumeAttributes: 46 | secretProviderClass: "vault-kv-sync-1" 47 | - name: secrets-store-2 48 | csi: 49 | driver: secrets-store.csi.k8s.io 50 | readOnly: true 51 | volumeAttributes: 52 | secretProviderClass: "vault-kv-sync-2" 53 | -------------------------------------------------------------------------------- /test/bats/configs/nginx/Chart.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | apiVersion: v3 5 | name: nginx-test 6 | version: 0-test 7 | description: Testing chart to deploy pods which request secrets with configurable service accounts. 8 | -------------------------------------------------------------------------------- /test/bats/configs/nginx/templates/nginx.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | --- 5 | kind: Pod 6 | apiVersion: v1 7 | metadata: 8 | name: nginx-{{ .Values.engine }} 9 | namespace: {{ .Release.namespace }} 10 | labels: 11 | app: nginx 12 | app.kubernetes.io/managed-by: {{ .Release.Service }} 13 | spec: 14 | serviceAccountName: nginx-{{ .Values.sa }} 15 | terminationGracePeriodSeconds: 0 16 | containers: 17 | - image: docker.mirror.hashicorp.services/nginx 18 | imagePullPolicy: IfNotPresent 19 | name: nginx 20 | volumeMounts: 21 | - name: secrets-store-inline 22 | mountPath: "/mnt/secrets-store" 23 | readOnly: true 24 | volumes: 25 | - name: secrets-store-inline 26 | csi: 27 | driver: secrets-store.csi.k8s.io 28 | readOnly: true 29 | volumeAttributes: 30 | secretProviderClass: "vault-{{ .Values.engine }}" 31 | -------------------------------------------------------------------------------- /test/bats/configs/nginx/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | --- 5 | apiVersion: v1 6 | kind: ServiceAccount 7 | metadata: 8 | name: nginx-{{ .Values.sa }} 9 | namespace: {{ .Release.namespace }} 10 | labels: 11 | app.kubernetes.io/managed-by: {{ .Release.Service }} -------------------------------------------------------------------------------- /test/bats/configs/postgres-creation-statements.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) HashiCorp, Inc. 2 | -- SPDX-License-Identifier: BUSL-1.1 3 | 4 | CREATE ROLE "{{name}}" WITH LOGIN ENCRYPTED PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; 5 | GRANT SELECT ON ALL TABLES IN SCHEMA public TO "{{name}}"; -------------------------------------------------------------------------------- /test/bats/configs/postgres.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | kind: Pod 5 | apiVersion: v1 6 | metadata: 7 | name: postgres 8 | labels: 9 | app: postgres 10 | spec: 11 | terminationGracePeriodSeconds: 0 12 | containers: 13 | - image: docker.mirror.hashicorp.services/postgres:13-alpine 14 | name: postgres 15 | ports: 16 | - containerPort: 5432 17 | env: 18 | - name: POSTGRES_DB 19 | value: db 20 | envFrom: 21 | - secretRef: 22 | name: postgres-root 23 | readinessProbe: 24 | exec: 25 | command: 26 | - pg_isready 27 | - -ddb 28 | - -h127.0.0.1 29 | - -p5432 30 | initialDelaySeconds: 1 31 | periodSeconds: 1 32 | timeoutSeconds: 5 33 | successThreshold: 1 34 | failureThreshold: 1 35 | --- 36 | apiVersion: v1 37 | kind: Service 38 | metadata: 39 | name: postgres 40 | spec: 41 | selector: 42 | app: postgres 43 | ports: 44 | - protocol: TCP 45 | port: 5432 46 | targetPort: 5432 47 | -------------------------------------------------------------------------------- /test/bats/configs/vault-all-secretproviderclass.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | apiVersion: secrets-store.csi.x-k8s.io/v1 5 | kind: SecretProviderClass 6 | metadata: 7 | name: vault-all 8 | spec: 9 | provider: vault 10 | parameters: 11 | roleName: "all-role" 12 | vaultAddress: https://vault:8200 13 | vaultCACertPath: /mnt/tls/ca.crt 14 | vaultTLSClientCertPath: /mnt/tls/client.crt 15 | vaultTLSClientKeyPath: /mnt/tls/client.key 16 | # Referring to the same dynamic creds twice in one secret provider class should 17 | # result in only one read to Vault, to ensure the username and password match. 18 | objects: | 19 | - objectName: "dbUsername" 20 | secretPath: "database/creds/test-role" 21 | secretKey: "username" 22 | - objectName: "dbPassword" 23 | secretPath: "database/creds/test-role" 24 | secretKey: "password" 25 | - objectName: "certs" 26 | secretPath: "pki/issue/example-dot-com" 27 | secretArgs: 28 | common_name: "test.example.com" 29 | ttl: "24h" 30 | method: "PUT" 31 | - objectName: "secret-1" 32 | secretPath: "secret/data/kv1" 33 | secretKey: "bar1" 34 | -------------------------------------------------------------------------------- /test/bats/configs/vault-db-secretproviderclass.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | apiVersion: secrets-store.csi.x-k8s.io/v1 5 | kind: SecretProviderClass 6 | metadata: 7 | name: vault-db 8 | spec: 9 | provider: vault 10 | parameters: 11 | roleName: "db-role" 12 | vaultAddress: https://vault:8200 13 | vaultCACertPath: /mnt/tls/ca.crt 14 | vaultTLSClientCertPath: /mnt/tls/client.crt 15 | vaultTLSClientKeyPath: /mnt/tls/client.key 16 | # Referring to the same dynamic creds twice in one secret provider class should 17 | # result in only one read to Vault, to ensure the username and password match. 18 | objects: | 19 | - objectName: "dbUsername" 20 | secretPath: "database/creds/test-role" 21 | secretKey: "username" 22 | - objectName: "dbPassword" 23 | secretPath: "database/creds/test-role" 24 | secretKey: "password" 25 | -------------------------------------------------------------------------------- /test/bats/configs/vault-kv-custom-audience-secretproviderclass.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | # Use a custom audience 5 | apiVersion: secrets-store.csi.x-k8s.io/v1 6 | kind: SecretProviderClass 7 | metadata: 8 | name: vault-kv-custom-audience 9 | spec: 10 | provider: vault 11 | parameters: 12 | audience: custom-audience 13 | roleName: "kv-custom-audience-role" 14 | objects: | 15 | - objectName: "secret" 16 | secretPath: "secret/data/kv-custom-audience" 17 | secretKey: "bar" 18 | 19 | -------------------------------------------------------------------------------- /test/bats/configs/vault-kv-namespace-secretproviderclass.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | apiVersion: secrets-store.csi.x-k8s.io/v1 5 | kind: SecretProviderClass 6 | metadata: 7 | name: vault-kv-namespace 8 | spec: 9 | provider: vault 10 | parameters: 11 | roleName: "kv-namespace-role" 12 | vaultAddress: https://vault:8200 13 | vaultNamespace: "acceptance" 14 | vaultCACertPath: /mnt/tls/ca.crt 15 | vaultTLSClientCertPath: /mnt/tls/client.crt 16 | vaultTLSClientKeyPath: /mnt/tls/client.key 17 | objects: | 18 | - objectName: "secret-1" 19 | secretPath: "secret/data/kv1-namespace" 20 | secretKey: "greeting" 21 | -------------------------------------------------------------------------------- /test/bats/configs/vault-kv-secretproviderclass-jwt-auth.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | # The "Hello World" Vault SecretProviderClass 5 | apiVersion: secrets-store.csi.x-k8s.io/v1 6 | kind: SecretProviderClass 7 | metadata: 8 | name: vault-kv-jwt-auth 9 | spec: 10 | provider: vault 11 | parameters: 12 | roleName: "jwt-kv-role" 13 | vaultAuthMountPath: "jwt" 14 | objects: | 15 | - objectName: "secret-1" 16 | secretPath: "secret/data/kv1" 17 | secretKey: "bar1" 18 | filePermission: 0600 19 | -------------------------------------------------------------------------------- /test/bats/configs/vault-kv-secretproviderclass.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | # The "Hello World" Vault SecretProviderClass 5 | apiVersion: secrets-store.csi.x-k8s.io/v1 6 | kind: SecretProviderClass 7 | metadata: 8 | name: vault-kv 9 | spec: 10 | provider: vault 11 | parameters: 12 | roleName: "kv-role" 13 | objects: | 14 | - objectName: "secret-1" 15 | secretPath: "secret/data/kv1" 16 | secretKey: "bar1" 17 | filePermission: 0600 18 | - objectName: "secret-2" 19 | secretPath: "secret/data/kv2" 20 | secretKey: "bar2" 21 | -------------------------------------------------------------------------------- /test/bats/configs/vault-kv-sync-multiple-secretproviderclass.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | # Multiple SecretProviderClass resources 5 | apiVersion: secrets-store.csi.x-k8s.io/v1 6 | kind: SecretProviderClass 7 | metadata: 8 | name: vault-kv-sync-1 9 | spec: 10 | provider: vault 11 | secretObjects: 12 | - secretName: kvsecret-1 13 | type: Opaque 14 | data: 15 | - objectName: secret-1 16 | key: username 17 | parameters: 18 | roleName: "kv-role" 19 | vaultAddress: https://vault:8200 20 | vaultCACertPath: /mnt/tls/ca.crt 21 | vaultTLSClientCertPath: /mnt/tls/client.crt 22 | vaultTLSClientKeyPath: /mnt/tls/client.key 23 | objects: | 24 | - objectName: "secret-1" 25 | secretPath: "/secret/data/kv-sync1" 26 | secretKey: "bar1" 27 | --- 28 | apiVersion: secrets-store.csi.x-k8s.io/v1 29 | kind: SecretProviderClass 30 | metadata: 31 | name: vault-kv-sync-2 32 | spec: 33 | provider: vault 34 | secretObjects: 35 | - secretName: kvsecret-2 36 | type: Opaque 37 | data: 38 | - objectName: secret-2 39 | key: pwd 40 | parameters: 41 | roleName: "kv-role" 42 | vaultAddress: https://vault:8200 43 | vaultCACertPath: /mnt/tls/ca.crt 44 | vaultTLSClientCertPath: /mnt/tls/client.crt 45 | vaultTLSClientKeyPath: /mnt/tls/client.key 46 | objects: | 47 | - objectName: "secret-2" 48 | secretPath: "secret/data/kv-sync2" 49 | secretKey: "bar2" 50 | 51 | -------------------------------------------------------------------------------- /test/bats/configs/vault-kv-sync-secretproviderclass.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | # Syncs Vault secrets to k8s secrets. 5 | apiVersion: secrets-store.csi.x-k8s.io/v1 6 | kind: SecretProviderClass 7 | metadata: 8 | name: vault-kv-sync 9 | spec: 10 | provider: vault 11 | secretObjects: 12 | - secretName: kvsecret 13 | type: Opaque 14 | labels: 15 | environment: "test" 16 | data: 17 | - objectName: secret-1 18 | key: pwd 19 | - objectName: secret-2 20 | key: username 21 | - objectName: secret-3 22 | key: username_b64 23 | parameters: 24 | roleName: "kv-role" 25 | vaultAddress: https://vault:8200 26 | vaultCACertPath: /mnt/tls/ca.crt 27 | vaultTLSClientCertPath: /mnt/tls/client.crt 28 | vaultTLSClientKeyPath: /mnt/tls/client.key 29 | objects: | 30 | - objectName: "secret-1" 31 | secretPath: "/v1/secret/data/kv-sync1" 32 | secretKey: "bar1" 33 | - objectName: "secret-2" 34 | secretPath: "v1/secret/data/kv-sync2" 35 | secretKey: "bar2" 36 | - objectName: "secret-3" 37 | secretPath: "/v1/secret/data/kv-sync3" 38 | secretKey: "bar3" 39 | encoding: "base64" -------------------------------------------------------------------------------- /test/bats/configs/vault-pki-secretproviderclass.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | apiVersion: secrets-store.csi.x-k8s.io/v1 5 | kind: SecretProviderClass 6 | metadata: 7 | name: vault-pki 8 | spec: 9 | provider: vault 10 | parameters: 11 | roleName: "pki-role" 12 | vaultAddress: https://vault:8200 13 | vaultCACertPath: /mnt/tls/ca.crt 14 | # N.B. No secretKey means the whole JSON response will be written. 15 | objects: | 16 | - objectName: "certs" 17 | secretPath: "pki/issue/example-dot-com" 18 | secretArgs: 19 | common_name: "test.example.com" 20 | ttl: "24h" 21 | method: "PUT" 22 | -------------------------------------------------------------------------------- /test/bats/configs/vault-policy-db.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | path "database/creds/test-role" { 5 | capabilities = ["read"] 6 | } 7 | -------------------------------------------------------------------------------- /test/bats/configs/vault-policy-kv-custom-audience.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | path "secret/data/kv-custom-audience" { 5 | capabilities = ["read"] 6 | } 7 | -------------------------------------------------------------------------------- /test/bats/configs/vault-policy-kv-namespace.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | path "secret/data/kv1-namespace" { 5 | capabilities = ["read"] 6 | } 7 | -------------------------------------------------------------------------------- /test/bats/configs/vault-policy-kv.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | path "secret/*" { 5 | capabilities = ["read"] 6 | } 7 | -------------------------------------------------------------------------------- /test/bats/configs/vault-policy-pki.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | path "pki/issue/example-dot-com" { 5 | capabilities = ["update"] 6 | } 7 | -------------------------------------------------------------------------------- /test/bats/configs/vault/Chart.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | apiVersion: v3 5 | name: vault-test 6 | version: 0-test 7 | description: Testing chart to deploy self-signed TLS certificates and a bootstrap script for Vault. -------------------------------------------------------------------------------- /test/bats/configs/vault/templates/bootstrap-configmap.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | apiVersion: v1 5 | kind: ConfigMap 6 | metadata: 7 | name: vault-bootstrap 8 | namespace: {{ .Release.namespace }} 9 | labels: 10 | app.kubernetes.io/managed-by: {{ .Release.Service }} 11 | data: 12 | bootstrap.sh: |- 13 | { 14 | vault status 15 | while [[ $? -ne 2 ]]; do sleep 1 && vault status; done 16 | } > /dev/null 17 | vault operator init --key-shares=1 --key-threshold=1 > /tmp/vault_init 18 | unseal=$(cat /tmp/vault_init | grep "Unseal Key 1: " | sed -e "s/Unseal Key 1: //g") 19 | root=$(cat /tmp/vault_init | grep "Initial Root Token:" | sed -e "s/Initial Root Token: //g") 20 | vault operator unseal ${unseal?} > /dev/null 21 | vault login -no-print ${root?} > /dev/null 22 | echo "Successfully bootstrapped vault" 23 | -------------------------------------------------------------------------------- /test/bats/configs/vault/templates/tls-secrets.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | # Generate a self-signed CA, and server and client certificates signed by that CA, 5 | # lasting 31 days and 30 days respectively. 6 | # 7 | # In the context of these certificates, Vault takes the server role and the CSI 8 | # provider will be the client. 9 | {{ $ca := genCA "svc-vault-ca" 31 }} 10 | {{ $dns1:= printf "vault.%s.svc.cluster.local" .Release.Namespace }} 11 | {{ $dns2 := printf "vault.%s.svc" .Release.Namespace }} 12 | {{ $dns3 := printf "vault.%s" .Release.Namespace }} 13 | {{ $dns4 := "vault" }} 14 | {{ $server := genSignedCert $dns1 (list "127.0.0.1") (list $dns1 $dns2 $dns3 $dns4) 30 $ca }} 15 | {{ $client := genSignedCert "" nil (list $dns1 $dns2 $dns3 $dns4) 30 $ca }} 16 | --- 17 | apiVersion: v1 18 | kind: Secret 19 | metadata: 20 | name: vault-server-tls 21 | namespace: {{ .Release.namespace }} 22 | labels: 23 | app.kubernetes.io/managed-by: {{ .Release.Service }} 24 | data: 25 | ca.crt: {{ b64enc $ca.Cert }} 26 | server.crt: {{ b64enc $server.Cert }} 27 | server.key: {{ b64enc $server.Key }} 28 | --- 29 | apiVersion: v1 30 | kind: Secret 31 | metadata: 32 | name: vault-client-tls 33 | namespace: {{ .Release.namespace }} 34 | labels: 35 | app.kubernetes.io/managed-by: {{ .Release.Service }} 36 | data: 37 | ca.crt: {{ b64enc $ca.Cert }} 38 | client.crt: {{ b64enc $client.Cert }} 39 | client.key: {{ b64enc $client.Key }} -------------------------------------------------------------------------------- /test/bats/configs/vault/vault.values.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: BUSL-1.1 3 | 4 | # Values file for vaul-helm chart. 5 | global: 6 | tlsDisable: false 7 | 8 | injector: 9 | enabled: false 10 | 11 | server: 12 | image: 13 | repository: docker.mirror.hashicorp.services/hashicorp/vault 14 | volumes: 15 | - name: vault-server-tls 16 | secret: 17 | secretName: vault-server-tls 18 | - name: vault-bootstrap 19 | configMap: 20 | name: vault-bootstrap 21 | volumeMounts: 22 | - name: vault-server-tls 23 | mountPath: /mnt/tls 24 | readOnly: true 25 | - name: vault-bootstrap 26 | mountPath: /mnt/bootstrap 27 | readOnly: true 28 | 29 | extraEnvironmentVars: 30 | # Ensure that running vault commands in the pod uses the correct CA. 31 | VAULT_CACERT: /mnt/tls/ca.crt 32 | 33 | standalone: 34 | enabled: true 35 | config: | 36 | listener "tcp" { 37 | address = "[::]:8200" 38 | cluster_address = "[::]:8201" 39 | tls_cert_file = "/mnt/tls/server.crt" 40 | tls_key_file = "/mnt/tls/server.key" 41 | tls_client_ca_file = "/mnt/tls/ca.crt" 42 | } 43 | storage "inmem" { 44 | } 45 | 46 | readinessProbe: 47 | path: "/v1/sys/health?standbyok=true&sealedcode=204&uninitcode=204" 48 | 49 | csi: 50 | enabled: true 51 | debug: true 52 | extraArgs: 53 | - -vault-addr=https://vault:8200 54 | - -vault-tls-ca-cert=/mnt/tls/ca.crt 55 | - -vault-tls-client-cert=/mnt/tls/client.crt 56 | - -vault-tls-client-key=/mnt/tls/client.key 57 | 58 | image: 59 | repository: "e2e/vault-csi-provider" 60 | tag: "latest" 61 | pullPolicy: Never 62 | 63 | volumes: 64 | - name: vault-client-tls 65 | secret: 66 | secretName: vault-client-tls 67 | # TODO: Delete this volume when helm chart has it baked in. 68 | - name: metadata 69 | downwardAPI: 70 | items: 71 | - path: "labels" 72 | fieldRef: 73 | fieldPath: metadata.labels 74 | - path: "annotations" 75 | fieldRef: 76 | fieldPath: metadata.annotations 77 | volumeMounts: 78 | - name: vault-client-tls 79 | mountPath: /mnt/tls 80 | readOnly: true 81 | # TODO: Delete this mount when helm chart has it baked in. 82 | - name: metadata 83 | mountPath: "/var/run/metadata/kubernetes.io/pod" 84 | readOnly: true 85 | 86 | agent: 87 | image: 88 | repository: "docker.mirror.hashicorp.services/hashicorp/vault" 89 | -------------------------------------------------------------------------------- /test/bats/provider.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load _helpers 4 | 5 | export SETUP_TEARDOWN_OUTFILE=/dev/null 6 | if [[ -n "${DISPLAY_SETUP_TEARDOWN_LOGS:-}" ]]; then 7 | export SETUP_TEARDOWN_OUTFILE=/dev/stdout 8 | fi 9 | 10 | #SKIP_TEARDOWN=true 11 | CONFIGS=test/bats/configs 12 | 13 | setup(){ 14 | { # Braces used to redirect all setup logs. 15 | # 1. Configure Vault. 16 | 17 | # 1. a) Vault policies 18 | cat $CONFIGS/vault-policy-db.hcl | kubectl --namespace=csi exec -i vault-0 -- vault policy write db-policy - 19 | cat $CONFIGS/vault-policy-kv.hcl | kubectl --namespace=csi exec -i vault-0 -- vault policy write kv-policy - 20 | cat $CONFIGS/vault-policy-pki.hcl | kubectl --namespace=csi exec -i vault-0 -- vault policy write pki-policy - 21 | if [ -n "${VAULT_LICENSE}" ]; then 22 | kubectl --namespace=csi exec vault-0 -- vault namespace create acceptance 23 | cat $CONFIGS/vault-policy-kv-namespace.hcl | kubectl --namespace=csi exec -i vault-0 -- vault policy write -namespace=acceptance kv-namespace-policy - 24 | fi 25 | cat $CONFIGS/vault-policy-kv-custom-audience.hcl | kubectl --namespace=csi exec -i vault-0 -- vault policy write kv-custom-audience-policy - 26 | 27 | # 1. b) i) Setup kubernetes auth engine. 28 | kubectl --namespace=csi exec vault-0 -- vault auth enable kubernetes 29 | kubectl --namespace=csi exec vault-0 -- sh -c 'vault write auth/kubernetes/config \ 30 | kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"' 31 | if [ -n "${VAULT_LICENSE}" ]; then 32 | kubectl --namespace=csi exec vault-0 -- vault auth enable -namespace=acceptance kubernetes 33 | kubectl --namespace=csi exec vault-0 -- sh -c 'vault write -namespace=acceptance auth/kubernetes/config \ 34 | kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"' 35 | fi 36 | kubectl --namespace=csi exec vault-0 -- vault write auth/kubernetes/role/db-role \ 37 | bound_service_account_names=nginx-db \ 38 | bound_service_account_namespaces=test \ 39 | audience=vault \ 40 | policies=db-policy \ 41 | ttl=20m 42 | kubectl --namespace=csi exec vault-0 -- vault write auth/kubernetes/role/kv-role \ 43 | bound_service_account_names=nginx-kv \ 44 | bound_service_account_namespaces=test \ 45 | audience=vault \ 46 | policies=kv-policy \ 47 | ttl=20m 48 | kubectl --namespace=csi exec vault-0 -- vault write auth/kubernetes/role/kv-custom-audience-role \ 49 | audience=custom-audience \ 50 | bound_service_account_names=nginx-kv-custom-audience \ 51 | bound_service_account_namespaces=test \ 52 | policies=kv-custom-audience-policy \ 53 | ttl=20m 54 | if [ -n "${VAULT_LICENSE}" ]; then 55 | kubectl --namespace=csi exec vault-0 -- vault write -namespace=acceptance auth/kubernetes/role/kv-namespace-role \ 56 | bound_service_account_names=nginx-kv-namespace \ 57 | bound_service_account_namespaces=test \ 58 | audience=vault \ 59 | policies=kv-namespace-policy \ 60 | ttl=20m 61 | fi 62 | kubectl --namespace=csi exec vault-0 -- vault write auth/kubernetes/role/pki-role \ 63 | bound_service_account_names=nginx-pki \ 64 | bound_service_account_namespaces=test \ 65 | audience=vault \ 66 | policies=pki-policy \ 67 | ttl=20m 68 | kubectl --namespace=csi exec vault-0 -- vault write auth/kubernetes/role/all-role \ 69 | bound_service_account_names=nginx-all \ 70 | bound_service_account_namespaces=test \ 71 | audience=vault \ 72 | policies=db-policy,kv-policy,pki-policy \ 73 | ttl=20m 74 | 75 | # 1. b) ii) Setup JWT auth 76 | kubectl --namespace=csi exec vault-0 -- vault auth enable jwt 77 | kubectl --namespace=csi exec vault-0 -- vault write auth/jwt/config \ 78 | oidc_discovery_url=https://kubernetes.default.svc.cluster.local \ 79 | oidc_discovery_ca_pem=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt 80 | kubectl --namespace=csi exec vault-0 -- vault write auth/jwt/role/jwt-kv-role \ 81 | role_type="jwt" \ 82 | bound_audiences="vault" \ 83 | user_claim="sub" \ 84 | bound_subject="system:serviceaccount:test:nginx-kv" \ 85 | policies="kv-policy" \ 86 | ttl="1h" 87 | 88 | # 1. c) Setup pki secrets engine. 89 | kubectl --namespace=csi exec vault-0 -- vault secrets enable pki 90 | kubectl --namespace=csi exec vault-0 -- vault write -field=certificate pki/root/generate/internal \ 91 | common_name="example.com" 92 | kubectl --namespace=csi exec vault-0 -- vault write pki/config/urls \ 93 | issuing_certificates="http://127.0.0.1:8200/v1/pki/ca" 94 | kubectl --namespace=csi exec vault-0 -- vault write pki/roles/example-dot-com \ 95 | allowed_domains="example.com" \ 96 | allow_subdomains=true 97 | 98 | # 1. d) Setup kv secrets in Vault. 99 | kubectl --namespace=csi exec vault-0 -- vault secrets enable -path=secret -version=2 kv 100 | kubectl --namespace=csi exec vault-0 -- vault kv put secret/kv1 bar1=hello1 101 | kubectl --namespace=csi exec vault-0 -- vault kv put secret/kv2 bar2=hello2 102 | kubectl --namespace=csi exec vault-0 -- vault kv put secret/kv-sync1 bar1=hello-sync1 103 | kubectl --namespace=csi exec vault-0 -- vault kv put secret/kv-sync2 bar2=hello-sync2 104 | kubectl --namespace=csi exec vault-0 -- vault kv put secret/kv-sync3 bar3=aGVsbG8tc3luYzM= 105 | if [ -n "${VAULT_LICENSE}" ]; then 106 | kubectl --namespace=csi exec vault-0 -- vault secrets enable -namespace=acceptance -path=secret -version=2 kv 107 | kubectl --namespace=csi exec vault-0 -- vault kv put -namespace=acceptance secret/kv1-namespace greeting=hello-namespaces 108 | fi 109 | kubectl --namespace=csi exec vault-0 -- vault kv put secret/kv-custom-audience bar=hello-custom-audience 110 | 111 | # 2. Create shared k8s resources. 112 | kubectl create namespace test 113 | kubectl --namespace=test apply -f $CONFIGS/vault-all-secretproviderclass.yaml 114 | kubectl --namespace=test apply -f $CONFIGS/vault-db-secretproviderclass.yaml 115 | kubectl --namespace=test apply -f $CONFIGS/vault-kv-custom-audience-secretproviderclass.yaml 116 | kubectl --namespace=test apply -f $CONFIGS/vault-kv-namespace-secretproviderclass.yaml 117 | kubectl --namespace=test apply -f $CONFIGS/vault-kv-secretproviderclass.yaml 118 | kubectl --namespace=test apply -f $CONFIGS/vault-kv-secretproviderclass-jwt-auth.yaml 119 | kubectl --namespace=test apply -f $CONFIGS/vault-kv-sync-secretproviderclass.yaml 120 | kubectl --namespace=test apply -f $CONFIGS/vault-kv-sync-multiple-secretproviderclass.yaml 121 | kubectl --namespace=test apply -f $CONFIGS/vault-pki-secretproviderclass.yaml 122 | } > $SETUP_TEARDOWN_OUTFILE 123 | } 124 | 125 | teardown(){ 126 | if [[ -n $SKIP_TEARDOWN ]]; then 127 | echo "Skipping teardown" 128 | return 129 | fi 130 | 131 | { # Braces used to redirect all teardown logs. 132 | 133 | # If the test failed, print some debug output 134 | if [[ "$BATS_ERROR_STATUS" -ne 0 ]]; then 135 | echo "DESCRIBE NGINX PODS" 136 | kubectl describe pod -l app=nginx --all-namespaces=true 137 | echo "PROVIDER LOGS" 138 | kubectl --namespace=csi logs -l app=vault-csi-provider --tail=50 139 | echo "VAULT LOGS" 140 | kubectl --namespace=csi logs vault-0 141 | fi 142 | 143 | # Teardown Vault configuration. 144 | if [ -n "${VAULT_LICENSE}" ]; then 145 | kubectl --namespace=csi exec vault-0 -- vault namespace delete acceptance 146 | fi 147 | kubectl --namespace=csi exec vault-0 -- vault auth disable kubernetes 148 | kubectl --namespace=csi exec vault-0 -- vault auth disable jwt 149 | kubectl --namespace=csi exec vault-0 -- vault secrets disable secret 150 | kubectl --namespace=csi exec vault-0 -- vault secrets disable pki 151 | kubectl --namespace=csi exec vault-0 -- vault secrets disable database 152 | kubectl --namespace=csi exec vault-0 -- vault policy delete example-policy 153 | kubectl --namespace=csi exec vault-0 -- vault kv delete secret/kv1 154 | kubectl --namespace=csi exec vault-0 -- vault kv delete secret/kv2 155 | kubectl --namespace=csi exec vault-0 -- vault kv delete secret/kv-custom-audience 156 | kubectl --namespace=csi exec vault-0 -- vault kv delete secret/kv-sync1 157 | kubectl --namespace=csi exec vault-0 -- vault kv delete secret/kv-sync2 158 | kubectl --namespace=csi exec vault-0 -- vault kv delete secret/kv-sync3 159 | 160 | # Teardown shared k8s resources. 161 | kubectl delete --ignore-not-found namespace test 162 | kubectl delete --ignore-not-found namespace negative-test-ns 163 | } > $SETUP_TEARDOWN_OUTFILE 164 | } 165 | 166 | @test "1 Inline secrets-store-csi volume" { 167 | helm --namespace=test install nginx $CONFIGS/nginx \ 168 | --set engine=kv --set sa=kv \ 169 | --wait --timeout=5m 170 | 171 | result=$(kubectl --namespace=test exec nginx-kv -- cat /mnt/secrets-store/secret-1) 172 | [[ "$result" == "hello1" ]] 173 | 174 | # Check file permission is non-default 175 | result=$(kubectl --namespace=test exec nginx-kv -- stat -c '%a' /mnt/secrets-store/..data/secret-1) 176 | [[ "$result" == "600" ]] 177 | 178 | result=$(kubectl --namespace=test exec nginx-kv -- cat /mnt/secrets-store/secret-2) 179 | [[ "$result" == "hello2" ]] 180 | 181 | # Check file permission is default 182 | result=$(kubectl --namespace=test exec nginx-kv -- stat -c '%a' /mnt/secrets-store/..data/secret-2) 183 | [[ "$result" == "644" ]] 184 | } 185 | 186 | @test "2 Sync with kubernetes secrets" { 187 | # Deploy some pods that should cause k8s secrets to be created. 188 | kubectl --namespace=test apply -f $CONFIGS/nginx-kv-env-var.yaml 189 | 190 | # This line sometimes throws an error 191 | kubectl --namespace=test wait --for=condition=Ready --timeout=5m pod -l app=nginx 192 | 193 | POD=$(kubectl --namespace=test get pod -l app=nginx -o jsonpath="{.items[0].metadata.name}") 194 | result=$(kubectl --namespace=test exec $POD -- cat /mnt/secrets-store/secret-1) 195 | [[ "$result" == "hello-sync1" ]] 196 | 197 | result=$(kubectl --namespace=test exec $POD -- cat /mnt/secrets-store/secret-2) 198 | [[ "$result" == "hello-sync2" ]] 199 | 200 | run kubectl get secret --namespace=test kvsecret 201 | [ "$status" -eq 0 ] 202 | 203 | result=$(kubectl --namespace=test get secret kvsecret -o jsonpath="{.data.pwd}" | base64 -d) 204 | [[ "$result" == "hello-sync1" ]] 205 | 206 | result=$(kubectl --namespace=test exec $POD -- printenv | grep SECRET_USERNAME | awk -F"=" '{ print $2 }' | tr -d '\r\n') 207 | [[ "$result" == "hello-sync2" ]] 208 | 209 | result=$(kubectl --namespace=test get secret kvsecret -o jsonpath="{.data.username_b64}" | base64 -d) 210 | [[ "$result" == "hello-sync3" ]] 211 | 212 | result=$(kubectl --namespace=test get secret kvsecret -o jsonpath="{.metadata.labels.environment}") 213 | [[ "${result//$'\r'}" == "test" ]] 214 | 215 | result=$(kubectl --namespace=test get secret kvsecret -o jsonpath="{.metadata.labels.secrets-store\.csi\.k8s\.io/managed}") 216 | [[ "${result//$'\r'}" == "true" ]] 217 | 218 | # There isn't really an event we can wait for to ensure this has happened. 219 | for i in {0..60}; do 220 | result="$(kubectl --namespace=test get secret kvsecret -o json | jq '.metadata.ownerReferences | length')" 221 | if [[ "$result" -eq 1 ]]; then 222 | break 223 | fi 224 | sleep 1 225 | done 226 | # The secret's owner is the ReplicaSet created by the deployment from $CONFIGS/nginx-kv-env-var.yaml 227 | [[ "$result" -eq 1 ]] 228 | 229 | # Wait for secret deletion in a background process. 230 | kubectl --namespace=test wait --for=delete --timeout=60s secret kvsecret & 231 | WAIT_PID=$! 232 | 233 | # Trigger deletion implicitly by deleting only owners. 234 | kubectl --namespace=test delete -f $CONFIGS/nginx-kv-env-var.yaml 235 | echo "Waiting for kvsecret to get deleted" 236 | wait $WAIT_PID 237 | 238 | # Ensure it actually got deleted. 239 | run kubectl --namespace=test get secret kvsecret 240 | [ "$status" -eq 1 ] 241 | } 242 | 243 | @test "3 SecretProviderClass in different namespace not usable" { 244 | kubectl create namespace negative-test-ns 245 | helm --namespace=negative-test-ns install nginx $CONFIGS/nginx \ 246 | --set engine=kv --set sa=kv 247 | kubectl --namespace=negative-test-ns wait --for=condition=PodScheduled --timeout=60s pod nginx-kv 248 | 249 | wait_for_success "kubectl --namespace=negative-test-ns describe pod nginx-kv | grep 'FailedMount.*failed to get secretproviderclass negative-test-ns/vault-kv.*not found'" 250 | } 251 | 252 | @test "4 Pod with multiple SecretProviderClasses" { 253 | POD=nginx-multiple-volumes 254 | kubectl --namespace=test apply -f $CONFIGS/nginx-kv-multiple-volumes.yaml 255 | kubectl --namespace=test wait --for=condition=Ready --timeout=5m pod $POD 256 | 257 | result=$(kubectl --namespace=test exec $POD -- cat /mnt/secrets-store-1/secret-1) 258 | [[ "$result" == "hello-sync1" ]] 259 | result=$(kubectl --namespace=test exec $POD -- cat /mnt/secrets-store-2/secret-2) 260 | [[ "$result" == "hello-sync2" ]] 261 | 262 | result=$(kubectl --namespace=test get secret kvsecret-1 -o jsonpath="{.data.username}" | base64 -d) 263 | [[ "$result" == "hello-sync1" ]] 264 | result=$(kubectl --namespace=test get secret kvsecret-2 -o jsonpath="{.data.pwd}" | base64 -d) 265 | [[ "$result" == "hello-sync2" ]] 266 | 267 | result=$(kubectl --namespace=test exec $POD -- printenv | grep SECRET_1_USERNAME | awk -F"=" '{ print $2 }' | tr -d '\r\n') 268 | [[ "$result" == "hello-sync1" ]] 269 | result=$(kubectl --namespace=test exec $POD -- printenv | grep SECRET_2_PWD | awk -F"=" '{ print $2 }' | tr -d '\r\n') 270 | [[ "$result" == "hello-sync2" ]] 271 | } 272 | 273 | @test "5 SecretProviderClass with query parameters and PUT method" { 274 | helm --namespace=test install nginx $CONFIGS/nginx \ 275 | --set engine=pki --set sa=pki \ 276 | --wait --timeout=5m 277 | 278 | result=$(kubectl --namespace=test exec nginx-pki -- cat /mnt/secrets-store/certs) 279 | [[ "$result" != "" ]] 280 | 281 | # Ensure we have some valid x509 certificates. 282 | echo "$result" | jq -r '.data.certificate' | openssl x509 -noout 283 | echo "$result" | jq -r '.data.issuing_ca' | openssl x509 -noout 284 | echo "$result" | jq -r '.data.certificate' | openssl x509 -noout -text | grep "test.example.com" 285 | } 286 | 287 | @test "6 Dynamic secrets engine, endpoint is called only once per SecretProviderClass" { 288 | setup_postgres 289 | 290 | # Now deploy a pod that will generate some dynamic credentials. 291 | helm --namespace=test install nginx $CONFIGS/nginx \ 292 | --set engine=db --set sa=db \ 293 | --wait --timeout=5m 294 | 295 | # Read the creds out of the pod and verify they work for a query. 296 | DYNAMIC_USERNAME=$(kubectl --namespace=test exec nginx-db -- cat /mnt/secrets-store/dbUsername) 297 | DYNAMIC_PASSWORD=$(kubectl --namespace=test exec nginx-db -- cat /mnt/secrets-store/dbPassword) 298 | result=$(kubectl --namespace=test exec postgres -- psql postgres://${DYNAMIC_USERNAME}:${DYNAMIC_PASSWORD}@127.0.0.1:5432/db --command="SELECT usename FROM pg_catalog.pg_user" --csv | sed -n '3 p') 299 | 300 | [[ "$result" != "" ]] 301 | [[ "$result" == "${DYNAMIC_USERNAME}" ]] 302 | } 303 | 304 | @test "7 SecretProviderClass with multiple secret types" { 305 | setup_postgres 306 | 307 | helm --namespace=test install nginx $CONFIGS/nginx \ 308 | --set engine=all --set sa=all \ 309 | --wait --timeout=5m 310 | 311 | # Verify dynamic database creds. 312 | DYNAMIC_USERNAME=$(kubectl --namespace=test exec nginx-all -- cat /mnt/secrets-store/dbUsername) 313 | DYNAMIC_PASSWORD=$(kubectl --namespace=test exec nginx-all -- cat /mnt/secrets-store/dbPassword) 314 | result=$(kubectl --namespace=test exec postgres -- psql postgres://${DYNAMIC_USERNAME}:${DYNAMIC_PASSWORD}@127.0.0.1:5432/db --command="SELECT usename FROM pg_catalog.pg_user" --csv | sed -n '3 p') 315 | 316 | [[ "$result" != "" ]] 317 | [[ "$result" == "${DYNAMIC_USERNAME}" ]] 318 | 319 | # Verify kv secret. 320 | result=$(kubectl --namespace=test exec nginx-all -- cat /mnt/secrets-store/secret-1) 321 | [[ "$result" == "hello1" ]] 322 | 323 | # Verify certificates. 324 | result=$(kubectl --namespace=test exec nginx-all -- cat /mnt/secrets-store/certs) 325 | [[ "$result" != "" ]] 326 | 327 | echo "$result" | jq -r '.data.certificate' | openssl x509 -noout 328 | echo "$result" | jq -r '.data.issuing_ca' | openssl x509 -noout 329 | echo "$result" | jq -r '.data.certificate' | openssl x509 -noout -text | grep "test.example.com" 330 | } 331 | 332 | @test "8 Wrong service account does not have access to Vault" { 333 | helm --namespace=test install nginx $CONFIGS/nginx \ 334 | --set engine=kv --set sa=pki 335 | kubectl --namespace=test wait --for=condition=PodScheduled --timeout=60s pod nginx-kv 336 | 337 | wait_for_success "kubectl --namespace=test describe pod nginx-kv | grep 'FailedMount.*failed to mount secrets store objects for pod test/nginx-kv'" 338 | wait_for_success "kubectl --namespace=test describe pod nginx-kv | grep 'service account name not authorized'" 339 | } 340 | 341 | @test "9 Vault Enterprise namespace" { 342 | if [ -z "${VAULT_LICENSE}" ]; then 343 | skip "No Vault license configured, skipping namespace test" 344 | fi 345 | helm --namespace=test install nginx $CONFIGS/nginx \ 346 | --set engine=kv-namespace --set sa=kv-namespace \ 347 | --wait --timeout=5m 348 | 349 | result=$(kubectl --namespace=test exec nginx-kv-namespace -- cat /mnt/secrets-store/secret-1) 350 | [[ "$result" == "hello-namespaces" ]] 351 | } 352 | 353 | @test "10 Custom audience" { 354 | helm --namespace=test install nginx $CONFIGS/nginx \ 355 | --set engine=kv-custom-audience --set sa=kv-custom-audience \ 356 | --wait --timeout=5m 357 | 358 | result=$(kubectl --namespace=test exec nginx-kv-custom-audience -- cat /mnt/secrets-store/secret) 359 | [[ "$result" == "hello-custom-audience" ]] 360 | } 361 | 362 | @test "11 Consistent version hashes" { 363 | helm --namespace=test install nginx $CONFIGS/nginx \ 364 | --set engine=kv --set sa=kv \ 365 | --wait --timeout=5m 366 | 367 | # HMAC secret should exist. 368 | kubectl --namespace=csi get secrets vault-csi-provider-hmac-key 369 | 370 | # Save the status UID and secret versions. 371 | statusUID1=$(kubectl --namespace=test get secretproviderclasspodstatus nginx-kv-test-vault-kv -o jsonpath='{.metadata.uid}') 372 | versions1=$(kubectl --namespace=test get secretproviderclasspodstatus nginx-kv-test-vault-kv -o jsonpath='{.status.objects[*].version}') 373 | 374 | # Recreate the pod, which should remount the secrets and recreate the status object. 375 | helm --namespace=test uninstall nginx 376 | helm --namespace=test install nginx $CONFIGS/nginx \ 377 | --set engine=kv --set sa=kv \ 378 | --wait --timeout=5m 379 | 380 | # Now the uid should be different, but versions should still be the same. 381 | statusUID2=$(kubectl --namespace=test get secretproviderclasspodstatus nginx-kv-test-vault-kv -o jsonpath='{.metadata.uid}') 382 | versions2=$(kubectl --namespace=test get secretproviderclasspodstatus nginx-kv-test-vault-kv -o jsonpath='{.status.objects[*].version}') 383 | 384 | [[ "$statusUID1" != "$statusUID2" ]] 385 | [[ "$versions1" == "$versions2" ]] 386 | 387 | # Finally, delete the HMAC secret and recreate the pod one more time. 388 | # The HMAC secret should get regenerated and the secret versions should then change. 389 | kubectl --namespace=csi delete secret vault-csi-provider-hmac-key 390 | helm --namespace=test uninstall nginx 391 | helm --namespace=test install nginx $CONFIGS/nginx \ 392 | --set engine=kv --set sa=kv \ 393 | --wait --timeout=5m 394 | 395 | kubectl --namespace=csi get secrets vault-csi-provider-hmac-key 396 | 397 | statusUID3=$(kubectl --namespace=test get secretproviderclasspodstatus nginx-kv-test-vault-kv -o jsonpath='{.metadata.uid}') 398 | versions3=$(kubectl --namespace=test get secretproviderclasspodstatus nginx-kv-test-vault-kv -o jsonpath='{.status.objects[*].version}') 399 | 400 | [[ "$statusUID1" != "$statusUID3" ]] 401 | [[ "$statusUID2" != "$statusUID3" ]] 402 | [[ "$versions2" != "$versions3" ]] 403 | } 404 | 405 | @test "12 JWT auth" { 406 | helm --namespace=test install nginx $CONFIGS/nginx \ 407 | --set engine=kv-jwt-auth --set sa=kv \ 408 | --wait --timeout=5m 409 | 410 | result=$(kubectl --namespace=test exec nginx-kv-jwt-auth -- cat /mnt/secrets-store/secret-1) 411 | [[ "$result" == "hello1" ]] 412 | } 413 | -------------------------------------------------------------------------------- /tools/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/vault-csi-provider/tools 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.7 6 | 7 | require ( 8 | github.com/golangci/golangci-lint v1.64.6 9 | github.com/hashicorp/copywrite v0.21.0 10 | mvdan.cc/gofumpt v0.7.0 11 | ) 12 | 13 | require ( 14 | 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect 15 | 4d63.com/gochecknoglobals v0.2.2 // indirect 16 | github.com/4meepo/tagalign v1.4.2 // indirect 17 | github.com/Abirdcfly/dupword v0.1.3 // indirect 18 | github.com/AlecAivazis/survey/v2 v2.3.7 // indirect 19 | github.com/Antonboom/errname v1.0.0 // indirect 20 | github.com/Antonboom/nilnil v1.0.1 // indirect 21 | github.com/Antonboom/testifylint v1.5.2 // indirect 22 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect 23 | github.com/Crocmagnon/fatcontext v0.7.1 // indirect 24 | github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect 25 | github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect 26 | github.com/Masterminds/semver/v3 v3.3.0 // indirect 27 | github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect 28 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect 29 | github.com/alecthomas/go-check-sumtype v0.3.1 // indirect 30 | github.com/alexkohler/nakedret/v2 v2.0.5 // indirect 31 | github.com/alexkohler/prealloc v1.0.0 // indirect 32 | github.com/alingse/asasalint v0.0.11 // indirect 33 | github.com/alingse/nilnesserr v0.1.2 // indirect 34 | github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect 35 | github.com/ashanbrown/forbidigo v1.6.0 // indirect 36 | github.com/ashanbrown/makezero v1.2.0 // indirect 37 | github.com/beorn7/perks v1.0.1 // indirect 38 | github.com/bkielbasa/cyclop v1.2.3 // indirect 39 | github.com/blizzy78/varnamelen v0.8.0 // indirect 40 | github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect 41 | github.com/bombsimon/wsl/v4 v4.5.0 // indirect 42 | github.com/bradleyfalzon/ghinstallation/v2 v2.5.0 // indirect 43 | github.com/breml/bidichk v0.3.2 // indirect 44 | github.com/breml/errchkjson v0.4.0 // indirect 45 | github.com/butuzov/ireturn v0.3.1 // indirect 46 | github.com/butuzov/mirror v1.3.0 // indirect 47 | github.com/catenacyber/perfsprint v0.8.2 // indirect 48 | github.com/ccojocar/zxcvbn-go v1.0.2 // indirect 49 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 50 | github.com/charithe/durationcheck v0.0.10 // indirect 51 | github.com/chavacava/garif v0.1.0 // indirect 52 | github.com/ckaznocha/intrange v0.3.0 // indirect 53 | github.com/cli/go-gh/v2 v2.11.2 // indirect 54 | github.com/cli/safeexec v1.0.0 // indirect 55 | github.com/cloudflare/circl v1.3.7 // indirect 56 | github.com/curioswitch/go-reassign v0.3.0 // indirect 57 | github.com/daixiang0/gci v0.13.5 // indirect 58 | github.com/davecgh/go-spew v1.1.1 // indirect 59 | github.com/denis-tingaikin/go-header v0.5.0 // indirect 60 | github.com/ettle/strcase v0.2.0 // indirect 61 | github.com/fatih/color v1.18.0 // indirect 62 | github.com/fatih/structtag v1.2.0 // indirect 63 | github.com/firefart/nonamedreturns v1.0.5 // indirect 64 | github.com/fsnotify/fsnotify v1.5.4 // indirect 65 | github.com/fzipp/gocyclo v0.6.0 // indirect 66 | github.com/ghostiam/protogetter v0.3.9 // indirect 67 | github.com/go-critic/go-critic v0.12.0 // indirect 68 | github.com/go-openapi/errors v0.20.2 // indirect 69 | github.com/go-openapi/strfmt v0.21.3 // indirect 70 | github.com/go-toolsmith/astcast v1.1.0 // indirect 71 | github.com/go-toolsmith/astcopy v1.1.0 // indirect 72 | github.com/go-toolsmith/astequal v1.2.0 // indirect 73 | github.com/go-toolsmith/astfmt v1.1.0 // indirect 74 | github.com/go-toolsmith/astp v1.1.0 // indirect 75 | github.com/go-toolsmith/strparse v1.1.0 // indirect 76 | github.com/go-toolsmith/typep v1.1.0 // indirect 77 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 78 | github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect 79 | github.com/gobwas/glob v0.2.3 // indirect 80 | github.com/gofrs/flock v0.12.1 // indirect 81 | github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 82 | github.com/golang/protobuf v1.5.3 // indirect 83 | github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect 84 | github.com/golangci/go-printf-func-name v0.1.0 // indirect 85 | github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect 86 | github.com/golangci/misspell v0.6.0 // indirect 87 | github.com/golangci/plugin-module-register v0.1.1 // indirect 88 | github.com/golangci/revgrep v0.8.0 // indirect 89 | github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect 90 | github.com/google/go-cmp v0.7.0 // indirect 91 | github.com/google/go-github/v45 v45.2.0 // indirect 92 | github.com/google/go-github/v53 v53.0.0 // indirect 93 | github.com/google/go-querystring v1.1.0 // indirect 94 | github.com/gordonklaus/ineffassign v0.1.0 // indirect 95 | github.com/gostaticanalysis/analysisutil v0.7.1 // indirect 96 | github.com/gostaticanalysis/comment v1.5.0 // indirect 97 | github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect 98 | github.com/gostaticanalysis/nilerr v0.1.1 // indirect 99 | github.com/hashicorp/go-hclog v1.5.0 // indirect 100 | github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect 101 | github.com/hashicorp/go-version v1.7.0 // indirect 102 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 103 | github.com/hashicorp/hcl v1.0.0 // indirect 104 | github.com/hexops/gotextdiff v1.0.3 // indirect 105 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 106 | github.com/jedib0t/go-pretty v4.3.0+incompatible // indirect 107 | github.com/jedib0t/go-pretty/v6 v6.4.6 // indirect 108 | github.com/jgautheron/goconst v1.7.1 // indirect 109 | github.com/jingyugao/rowserrcheck v1.1.1 // indirect 110 | github.com/jjti/go-spancheck v0.6.4 // indirect 111 | github.com/joho/godotenv v1.3.0 // indirect 112 | github.com/julz/importas v0.2.0 // indirect 113 | github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect 114 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 115 | github.com/kisielk/errcheck v1.9.0 // indirect 116 | github.com/kkHAIKE/contextcheck v1.1.6 // indirect 117 | github.com/knadh/koanf v1.5.0 // indirect 118 | github.com/kulti/thelper v0.6.3 // indirect 119 | github.com/kunwardeep/paralleltest v1.0.10 // indirect 120 | github.com/lasiar/canonicalheader v1.1.2 // indirect 121 | github.com/ldez/exptostd v0.4.2 // indirect 122 | github.com/ldez/gomoddirectives v0.6.1 // indirect 123 | github.com/ldez/grignotin v0.9.0 // indirect 124 | github.com/ldez/tagliatelle v0.7.1 // indirect 125 | github.com/ldez/usetesting v0.4.2 // indirect 126 | github.com/leonklingele/grouper v1.1.2 // indirect 127 | github.com/macabu/inamedparam v0.1.3 // indirect 128 | github.com/magiconair/properties v1.8.6 // indirect 129 | github.com/maratori/testableexamples v1.0.0 // indirect 130 | github.com/maratori/testpackage v1.1.1 // indirect 131 | github.com/matoous/godox v1.1.0 // indirect 132 | github.com/mattn/go-colorable v0.1.14 // indirect 133 | github.com/mattn/go-isatty v0.0.20 // indirect 134 | github.com/mattn/go-runewidth v0.0.16 // indirect 135 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 136 | github.com/mergestat/timediff v0.0.3 // indirect 137 | github.com/mgechev/revive v1.7.0 // indirect 138 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 139 | github.com/mitchellh/copystructure v1.2.0 // indirect 140 | github.com/mitchellh/go-homedir v1.1.0 // indirect 141 | github.com/mitchellh/mapstructure v1.5.0 // indirect 142 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 143 | github.com/moricho/tparallel v0.3.2 // indirect 144 | github.com/nakabonne/nestif v0.3.1 // indirect 145 | github.com/nishanths/exhaustive v0.12.0 // indirect 146 | github.com/nishanths/predeclared v0.2.2 // indirect 147 | github.com/nunnatsa/ginkgolinter v0.19.1 // indirect 148 | github.com/oklog/ulid v1.3.1 // indirect 149 | github.com/olekukonko/tablewriter v0.0.5 // indirect 150 | github.com/pelletier/go-toml v1.9.5 // indirect 151 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 152 | github.com/pmezard/go-difflib v1.0.0 // indirect 153 | github.com/polyfloyd/go-errorlint v1.7.1 // indirect 154 | github.com/prometheus/client_golang v1.12.1 // indirect 155 | github.com/prometheus/client_model v0.2.0 // indirect 156 | github.com/prometheus/common v0.32.1 // indirect 157 | github.com/prometheus/procfs v0.7.3 // indirect 158 | github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect 159 | github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect 160 | github.com/quasilyte/gogrep v0.5.0 // indirect 161 | github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect 162 | github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect 163 | github.com/raeperd/recvcheck v0.2.0 // indirect 164 | github.com/rivo/uniseg v0.4.7 // indirect 165 | github.com/rogpeppe/go-internal v1.14.1 // indirect 166 | github.com/ryancurrah/gomodguard v1.3.5 // indirect 167 | github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect 168 | github.com/samber/lo v1.37.0 // indirect 169 | github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect 170 | github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect 171 | github.com/sashamelentyev/interfacebloat v1.1.0 // indirect 172 | github.com/sashamelentyev/usestdlibvars v1.28.0 // indirect 173 | github.com/securego/gosec/v2 v2.22.1 // indirect 174 | github.com/sirupsen/logrus v1.9.3 // indirect 175 | github.com/sivchari/containedctx v1.0.3 // indirect 176 | github.com/sivchari/tenv v1.12.1 // indirect 177 | github.com/sonatard/noctx v0.1.0 // indirect 178 | github.com/sourcegraph/go-diff v0.7.0 // indirect 179 | github.com/spf13/afero v1.12.0 // indirect 180 | github.com/spf13/cast v1.5.0 // indirect 181 | github.com/spf13/cobra v1.9.1 // indirect 182 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 183 | github.com/spf13/pflag v1.0.6 // indirect 184 | github.com/spf13/viper v1.12.0 // indirect 185 | github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect 186 | github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect 187 | github.com/stretchr/objx v0.5.2 // indirect 188 | github.com/stretchr/testify v1.10.0 // indirect 189 | github.com/subosito/gotenv v1.4.1 // indirect 190 | github.com/tdakkota/asciicheck v0.4.1 // indirect 191 | github.com/tetafro/godot v1.5.0 // indirect 192 | github.com/thanhpk/randstr v1.0.4 // indirect 193 | github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 // indirect 194 | github.com/timonwong/loggercheck v0.10.1 // indirect 195 | github.com/tomarrell/wrapcheck/v2 v2.10.0 // indirect 196 | github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect 197 | github.com/ultraware/funlen v0.2.0 // indirect 198 | github.com/ultraware/whitespace v0.2.0 // indirect 199 | github.com/uudashr/gocognit v1.2.0 // indirect 200 | github.com/uudashr/iface v1.3.1 // indirect 201 | github.com/xen0n/gosmopolitan v1.2.2 // indirect 202 | github.com/yagipy/maintidx v1.0.0 // indirect 203 | github.com/yeya24/promlinter v0.3.0 // indirect 204 | github.com/ykadowak/zerologlint v0.1.5 // indirect 205 | gitlab.com/bosi/decorder v0.4.2 // indirect 206 | go-simpler.org/musttag v0.13.0 // indirect 207 | go-simpler.org/sloglint v0.9.0 // indirect 208 | go.mongodb.org/mongo-driver v1.17.3 // indirect 209 | go.uber.org/atomic v1.7.0 // indirect 210 | go.uber.org/automaxprocs v1.6.0 // indirect 211 | go.uber.org/multierr v1.6.0 // indirect 212 | go.uber.org/zap v1.24.0 // indirect 213 | golang.org/x/crypto v0.36.0 // indirect 214 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect 215 | golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect 216 | golang.org/x/mod v0.23.0 // indirect 217 | golang.org/x/net v0.37.0 // indirect 218 | golang.org/x/oauth2 v0.28.0 // indirect 219 | golang.org/x/sync v0.12.0 // indirect 220 | golang.org/x/sys v0.31.0 // indirect 221 | golang.org/x/term v0.30.0 // indirect 222 | golang.org/x/text v0.23.0 // indirect 223 | golang.org/x/tools v0.30.0 // indirect 224 | google.golang.org/protobuf v1.36.4 // indirect 225 | gopkg.in/ini.v1 v1.67.0 // indirect 226 | gopkg.in/yaml.v2 v2.4.0 // indirect 227 | gopkg.in/yaml.v3 v3.0.1 // indirect 228 | honnef.co/go/tools v0.6.0 // indirect 229 | mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect 230 | ) 231 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: BUSL-1.1 3 | 4 | //go:build tools 5 | 6 | // This file ensures tool dependencies are kept in sync. This is the 7 | // recommended way of doing this according to 8 | // https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 9 | // To install the following tools at the version used by this repo run: 10 | // $ make bootstrap 11 | // or 12 | // $ go generate -tags tools tools/tools.go 13 | 14 | package tools 15 | 16 | //go:generate go install github.com/golangci/golangci-lint/cmd/golangci-lint 17 | //go:generate golangci-lint version 18 | //go:generate go install github.com/hashicorp/copywrite 19 | //go:generate copywrite --version 20 | //go:generate go install mvdan.cc/gofumpt 21 | //go:generate gofumpt --version 22 | import ( 23 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 24 | _ "github.com/hashicorp/copywrite" 25 | _ "mvdan.cc/gofumpt" 26 | ) 27 | --------------------------------------------------------------------------------