├── .circleci └── config.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── api └── v1beta1 │ ├── groupversion_info.go │ ├── utils.go │ ├── vaultsecret_types.go │ └── zz_generated.deepcopy.go ├── config ├── certmanager │ ├── certificate.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── crd │ ├── bases │ │ └── maupu.org_vaultsecrets.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_vaultsecrets.yaml │ │ └── webhook_in_vaultsecrets.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ ├── manager_webhook_patch.yaml │ └── webhookcainjection_patch.yaml ├── doc-samples │ ├── maupu.org_v1beta1_vaultsecrets_cr.yaml │ ├── operator.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── service_account.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── auth_proxy_client_clusterrole.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── role.yaml │ ├── role_binding.yaml │ ├── vaultsecret_editor_role.yaml │ └── vaultsecret_viewer_role.yaml ├── samples │ ├── kustomization.yaml │ └── maupu.org_v1beta1_vaultsecret.yaml └── webhook │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── service.yaml ├── controllers ├── suite_test.go ├── vaultsecret_controller.go └── vaultsecret_manager.go ├── go.mod ├── go.sum ├── hack ├── boilerplate.go.txt └── kind │ ├── README.md │ ├── kind.sh │ ├── test_workload │ ├── kustomization.yaml │ ├── namespace.yaml │ └── secret.yaml │ ├── vault │ ├── apps_v1_deployment_vault.yaml │ ├── kustomization.yaml │ ├── rbac.authorization.k8s.io_v1beta1_clusterrolebinding_role-tokenreview-binding.yaml │ ├── v1_namespace_vault.yaml │ ├── v1_service_vault.yaml │ ├── v1_serviceaccount_vault.yaml │ └── vault-init-job.bash │ └── vault_secret │ ├── kustomization.yaml │ └── namespace.yaml ├── main.go ├── pkg ├── k8sutils │ └── resources.go └── vault │ ├── approle.go │ ├── auth_provider.go │ ├── client_cached.go │ ├── client_reader.go │ ├── client_simple.go │ ├── errors.go │ ├── helpers.go │ ├── kubernetes.go │ └── token.go └── version └── version.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | executors: 3 | golang-executor: 4 | environment: 5 | - ORG_NAME: nmaupu 6 | - PROJECT_NAME: vault-secret 7 | docker: 8 | - image: circleci/golang:1.13 9 | working_directory: /go/src/github.com/nmaupu/vault-secret 10 | 11 | # https://circleci.com/docs/2.0/reusing-config/ 12 | commands: 13 | cmd_prepare_release: 14 | description: "Prepare either a test or a milestone release" 15 | parameters: 16 | release_name: 17 | type: string 18 | default: "master" 19 | steps: 20 | - checkout 21 | - run: 22 | name: Preparing for release 23 | command: | 24 | RELEASE_NAME=<< parameters.release_name >> make CI-prepare-release 25 | - save_cache: 26 | name: Saving cache for release version 27 | key: release-{{ .Revision }}-<< parameters.release_name >> 28 | paths: 29 | - release 30 | - version/version.go 31 | cmd_docker_build_push: 32 | description: "Build and push docker images" 33 | parameters: 34 | tag_name: 35 | type: string 36 | default: "test" 37 | cpu_platforms: 38 | type: string 39 | default: "linux/amd64 linux/arm64" 40 | steps: 41 | - attach_workspace: 42 | at: /go 43 | - checkout 44 | - restore_cache: 45 | keys: 46 | - release-{{ .Revision }}-<< parameters.tag_name >> 47 | - setup_remote_docker 48 | - run: 49 | name: Docker login 50 | command: | 51 | echo $DOCKER_PASSWORD | docker login --username $DOCKER_LOGIN --password-stdin 52 | - run: 53 | name: Building and pushing docker image 54 | command: | 55 | cd $GOPATH/src/github.com/$ORG_NAME/$PROJECT_NAME 56 | for platform in << parameters.cpu_platforms >>; do 57 | export GOOS="${platform%/*}" 58 | export GOARCH="${platform#*/}" 59 | RELEASE_NAME="<< parameters.tag_name >>-${GOOS}-${GOARCH}" make CI-docker-build CI-docker-push 60 | done 61 | 62 | jobs: 63 | docker-build-push-branch: 64 | executor: golang-executor 65 | steps: 66 | - cmd_docker_build_push: 67 | tag_name: ${CIRCLE_BRANCH} 68 | docker-build-push-tag: 69 | executor: golang-executor 70 | steps: 71 | - cmd_docker_build_push: 72 | tag_name: ${CIRCLE_TAG} 73 | prepare-release-branch: 74 | executor: golang-executor 75 | steps: 76 | - cmd_prepare_release: 77 | release_name: ${CIRCLE_BRANCH} 78 | prepare-release-tag: 79 | executor: golang-executor 80 | steps: 81 | - cmd_prepare_release: 82 | release_name: ${CIRCLE_TAG} 83 | release-github: 84 | executor: golang-executor 85 | steps: 86 | - checkout 87 | - restore_cache: 88 | keys: 89 | - release-{{ .Revision }}-${CIRCLE_TAG} 90 | - run: 91 | name: Publish release on Github 92 | command: | 93 | go get github.com/tcnksm/ghr 94 | cd $GOPATH/src/github.com/$ORG_NAME/$PROJECT_NAME 95 | make CI-process-release 96 | 97 | workflows: 98 | version: 2.1 99 | branch: 100 | jobs: 101 | - prepare-release-branch 102 | - docker-build-push-branch: 103 | requires: 104 | - prepare-release-branch 105 | release: 106 | jobs: 107 | - prepare-release-tag: 108 | filters: 109 | branches: 110 | ignore: /.*/ 111 | tags: 112 | only: /^\d+\.\d+\.\d+$/ 113 | - docker-build-push-tag: 114 | requires: 115 | - prepare-release-tag 116 | filters: 117 | branches: 118 | ignore: /.*/ 119 | tags: 120 | only: /^\d+\.\d+\.\d+$/ 121 | - release-github: 122 | requires: 123 | - prepare-release-tag 124 | filters: 125 | branches: 126 | ignore: /.*/ 127 | tags: 128 | only: /^\d+\.\d+\.\d+$/ 129 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary Build Files 2 | build/_output 3 | build/_test 4 | bin/ 5 | Dockerfile.temp 6 | manager 7 | # Created by https://www.gitignore.io/api/go,vim,emacs,visualstudiocode 8 | ### Emacs ### 9 | # -*- mode: gitignore; -*- 10 | *~ 11 | \#*\# 12 | /.emacs.desktop 13 | /.emacs.desktop.lock 14 | *.elc 15 | auto-save-list 16 | tramp 17 | .\#* 18 | # Org-mode 19 | .org-id-locations 20 | *_archive 21 | # flymake-mode 22 | *_flymake.* 23 | # eshell files 24 | /eshell/history 25 | /eshell/lastdir 26 | # elpa packages 27 | /elpa/ 28 | # reftex files 29 | *.rel 30 | # AUCTeX auto folder 31 | /auto/ 32 | # cask packages 33 | .cask/ 34 | dist/ 35 | # Flycheck 36 | flycheck_*.el 37 | # server auth directory 38 | /server/ 39 | # projectiles files 40 | .projectile 41 | projectile-bookmarks.eld 42 | # directory configuration 43 | .dir-locals.el 44 | # saveplace 45 | places 46 | # url cache 47 | url/cache/ 48 | # cedet 49 | ede-projects.el 50 | # smex 51 | smex-items 52 | # company-statistics 53 | company-statistics-cache.el 54 | # anaconda-mode 55 | anaconda-mode/ 56 | ### Go ### 57 | # Binaries for programs and plugins 58 | *.exe 59 | *.exe~ 60 | *.dll 61 | *.so 62 | *.dylib 63 | # Test binary, build with 'go test -c' 64 | *.test 65 | # Output of the go coverage tool, specifically when used with LiteIDE 66 | *.out 67 | ### Vim ### 68 | # swap 69 | .sw[a-p] 70 | .*.sw[a-p] 71 | # session 72 | Session.vim 73 | # temporary 74 | .netrwhist 75 | # auto-generated tag files 76 | tags 77 | ### VisualStudioCode ### 78 | .vscode/* 79 | .history 80 | # End of https://www.gitignore.io/api/go,vim,emacs,visualstudiocode 81 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.13 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | # cache deps before building and copying source so that we don't need to re-download as much 9 | # and so that source changes don't invalidate our downloaded layer 10 | RUN go mod download 11 | 12 | # Copy the go source 13 | COPY main.go main.go 14 | COPY api/ api/ 15 | COPY controllers/ controllers/ 16 | COPY pkg/ pkg/ 17 | COPY version/ version/ 18 | 19 | # Build 20 | ARG GOOS 21 | ARG GOARCH 22 | RUN CGO_ENABLED=0 GO111MODULE=on GOOS=${GOOS} GOARCH=${GOARCH} go build -a -o manager main.go 23 | 24 | # Use distroless as minimal base image to package the manager binary 25 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 26 | FROM gcr.io/distroless/static:nonroot 27 | WORKDIR / 28 | COPY --from=builder /workspace/manager . 29 | USER nonroot:nonroot 30 | 31 | ENTRYPOINT ["/manager"] 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export GO111MODULE=on 2 | 3 | # Current Operator version 4 | VERSION ?= 1.0.1 5 | # Default bundle image tag 6 | BUNDLE_IMG ?= controller-bundle:$(VERSION) 7 | # Default version used when preparing for a release, image building and pushing 8 | RELEASE_NAME ?= $(CIRCLE_TAG) 9 | GOOS ?= linux 10 | GOARCH ?= amd64 11 | 12 | # Options for 'bundle-build' 13 | ifneq ($(origin CHANNELS), undefined) 14 | BUNDLE_CHANNELS := --channels=$(CHANNELS) 15 | endif 16 | ifneq ($(origin DEFAULT_CHANNEL), undefined) 17 | BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL) 18 | endif 19 | BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) 20 | 21 | # Image URL to use all building/pushing image targets 22 | IMG ?= vault-secret:test 23 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) 24 | CRD_OPTIONS ?= "crd:trivialVersions=true,crdVersions=v1" 25 | 26 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 27 | ifeq (,$(shell go env GOBIN)) 28 | GOBIN=$(shell go env GOPATH)/bin 29 | else 30 | GOBIN=$(shell go env GOBIN) 31 | endif 32 | 33 | all: manager 34 | 35 | # Run tests 36 | test: generate fmt vet manifests 37 | go test ./... -coverprofile cover.out 38 | 39 | # Build manager binary 40 | manager: generate fmt vet 41 | CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) GO111MODULE=on go build -o bin/manager main.go 42 | 43 | # Run against the configured Kubernetes cluster in ~/.kube/config 44 | run: generate fmt vet manifests 45 | go run ./main.go 46 | 47 | # Install CRDs into a cluster 48 | install: manifests kustomize 49 | $(KUSTOMIZE) build config/crd | kubectl apply -f - 50 | 51 | # Uninstall CRDs from a cluster 52 | uninstall: manifests kustomize 53 | $(KUSTOMIZE) build config/crd | kubectl delete -f - 54 | 55 | # Deploy controller in the configured Kubernetes cluster in ~/.kube/config 56 | deploy: manifests kustomize 57 | cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) 58 | $(KUSTOMIZE) build config/default | kubectl apply -f - 59 | 60 | # Generate manifests e.g. CRD, RBAC etc. 61 | manifests: controller-gen 62 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases 63 | 64 | # Run go fmt against code 65 | fmt: 66 | go fmt ./... 67 | 68 | # Run go vet against code 69 | vet: 70 | go vet ./... 71 | 72 | # Generate code 73 | generate: controller-gen 74 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 75 | 76 | # Build the docker image 77 | docker-build: test 78 | docker build . -t ${IMG} 79 | 80 | # Push the docker image 81 | docker-push: 82 | docker push ${IMG} 83 | 84 | # find or download controller-gen 85 | # download controller-gen if necessary 86 | controller-gen: 87 | ifeq (, $(shell which controller-gen)) 88 | @{ \ 89 | set -e ;\ 90 | CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\ 91 | cd $$CONTROLLER_GEN_TMP_DIR ;\ 92 | go mod init tmp ;\ 93 | go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.3.0 ;\ 94 | rm -rf $$CONTROLLER_GEN_TMP_DIR ;\ 95 | } 96 | CONTROLLER_GEN=$(GOBIN)/controller-gen 97 | else 98 | CONTROLLER_GEN=$(shell which controller-gen) 99 | endif 100 | 101 | kustomize: 102 | ifeq (, $(shell which kustomize)) 103 | @{ \ 104 | set -e ;\ 105 | KUSTOMIZE_GEN_TMP_DIR=$$(mktemp -d) ;\ 106 | cd $$KUSTOMIZE_GEN_TMP_DIR ;\ 107 | go mod init tmp ;\ 108 | go get sigs.k8s.io/kustomize/kustomize/v3@v3.5.4 ;\ 109 | rm -rf $$KUSTOMIZE_GEN_TMP_DIR ;\ 110 | } 111 | KUSTOMIZE=$(GOBIN)/kustomize 112 | else 113 | KUSTOMIZE=$(shell which kustomize) 114 | endif 115 | 116 | # Generate bundle manifests and metadata, then validate generated files. 117 | bundle: manifests 118 | operator-sdk generate kustomize manifests -q 119 | cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) 120 | kustomize build config/manifests | operator-sdk generate bundle -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) 121 | operator-sdk bundle validate ./bundle 122 | 123 | # Build the bundle image. 124 | bundle-build: 125 | docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) . 126 | 127 | ## custom tasks 128 | .PHONY: CI-prepare-release 129 | CI-prepare-release: 130 | mkdir -p release/manifests/crds 131 | cp -a config/crd/bases/maupu.org_vaultsecrets.yaml release/manifests/crds 132 | cp -a config/doc-samples/* release/manifests/ 133 | tar cfz release/vault-secret-manifests-$(RELEASE_NAME).tar.gz -C release manifests 134 | rm -rf release/manifests/ 135 | sed -i -e "s/latest/$(RELEASE_NAME)/g" version/version.go 136 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o release/vault-secret-$(RELEASE_NAME)-linux-amd64 main.go 137 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GO111MODULE=on go build -a -o release/vault-secret-$(RELEASE_NAME)-linux-arm64 main.go 138 | 139 | .PHONY: CI-process-release 140 | CI-process-release: 141 | @echo "Version to be released: $(CIRCLE_TAG)" 142 | ghr -t $(GITHUB_TOKEN) \ 143 | -u $(CIRCLE_PROJECT_USERNAME) \ 144 | -r $(CIRCLE_PROJECT_REPONAME) \ 145 | -c $(CIRCLE_SHA1) \ 146 | -n "Release v$(CIRCLE_TAG)" \ 147 | -b "$(shell git log --format=%B -n1 $(CIRCLE_SHA1))" \ 148 | -delete \ 149 | $(CIRCLE_TAG) release/ 150 | 151 | .PHONY: CI-docker-build 152 | CI-docker-build: 153 | docker build --build-arg=GOOS=$(GOOS) --build-arg=GOARCH=$(GOARCH) --no-cache . -t $(ORG_NAME)/$(PROJECT_NAME):$(RELEASE_NAME) 154 | 155 | .PHONY: CI-docker-push 156 | CI-docker-push: 157 | docker push $(ORG_NAME)/$(PROJECT_NAME):$(RELEASE_NAME) 158 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: 2 | layout: go.kubebuilder.io/v2 3 | repo: github.com/nmaupu/vault-secret 4 | resources: 5 | - group: maupu.org 6 | kind: VaultSecret 7 | version: v1beta1 8 | version: 3-alpha 9 | plugins: 10 | go.sdk.operatorframework.io/v2-alpha: {} 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/nmaupu/vault-secret/tree/master.svg?style=shield)](https://circleci.com/gh/nmaupu/vault-secret/tree/master) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/nmaupu/vault-secret)](https://goreportcard.com/report/github.com/nmaupu/vault-secret) 3 | 4 | # Kubernetes Secrets from Hashicorp Vault 5 | 6 | **Problem:** My secret are stored in Vault, how can I inject them into Kubernetes secret ? 7 | 8 | **Solution:** Use vault-secret custom resource to specify Vault server, path and keys and the operator will retrieve all the needed information from vault and push them into a Kubernetes secret resource ready to be used in the cluster. 9 | 10 | # Note on upgrading to 1.0.1 onward 11 | 12 | From version `1.0.1`, k8s auth method switches from using the local *service account* configured on the operator side to using the one from the client's namespace defined in the *custom resource*. 13 | This is improving security but as a result, you will probably have to check your vault configuration is in adequation with this change. 14 | 15 | # Note for Kubernetes 1.24+ 16 | 17 | From Kubernetes 1.24, secrets are not created along a service account anymore. A secret needs to be manually created to make the controller happy. 18 | See https://github.com/nmaupu/vault-secret/issues/40 for more info. 19 | 20 | # Installation 21 | 22 | ## Kubernetes version requirements 23 | 24 | This operator is supported from **Kubernetes `1.10`**. 25 | 26 | If using *Kubernetes 1.10* version, the feature gate `CustomResourceSubresources` must be enabled for the Custom Resource status field to get updated! 27 | This feature is enabled by default starting from *Kubernetes 1.11*. 28 | 29 | ## Operator 30 | 31 | Get the latest release from https://github.com/nmaupu/vault-secret/releases 32 | 33 | Deploy the Custom Resource Definition and the operator: 34 | ``` 35 | $ kubectl apply -f config/crd/bases/maupu.org_vaultsecrets.yaml 36 | $ kubectl apply -f config/doc-samples/operator.yaml 37 | $ kubectl apply -f config/doc-samples/role.yaml 38 | $ kubectl apply -f config/doc-samples/role_binding.yaml 39 | $ kubectl apply -f config/doc-samples/service_account.yaml 40 | ``` 41 | 42 | ### Configuration 43 | 44 | #### Env vars 45 | 46 | The *vault-secret operator* can be configured to watch a unique namespace, a set of namespaces or can also be cluster wide. In that case, modify RBAC role and role binding to be cluster scoped. 47 | The following environment variables are available to configure the operator: 48 | - `WATCH_NAMESPACE`: namespace to watch for new CR. If not defined, use `WATCH_MULTINAMESPACES` or configure a cluster wide operator. 49 | - `WATCH_MULTINAMESPACES`: comma separated list of namespaces to watch for new CR, if not defined, the operator will be cluster scoped except if `WATCH_NAMESPACE` is set. 50 | - `OPERATOR_NAME`: name of the operator. 51 | 52 | #### Label filtering 53 | 54 | One can use the command line flag `--filter-label` to filter which vaultsecret custom resource to process by the operator. 55 | This flag can be used multiple times. 56 | 57 | Example usage: 58 | 59 | ``` 60 | --filter-label=mylabel=myvalue 61 | ``` 62 | 63 | ## Custom resource 64 | 65 | Here is an example (`config/doc-samples/maupu.org_v1beta1_vaultsecrets_cr.yaml`) : 66 | ``` 67 | apiVersion: maupu.org/v1beta1 68 | kind: VaultSecret 69 | metadata: 70 | name: example-vaultsecret 71 | namespace: nma 72 | spec: 73 | secretName: vault-secret-test 74 | secretLabels: 75 | foo: bar 76 | secretAnnotations: 77 | foo: bar 78 | secrets: 79 | - secretKey: username 80 | kvPath: secrets/kv 81 | path: test 82 | field: username 83 | - secretKey: password 84 | kvPath: secrets/kv 85 | path: test 86 | field: password 87 | syncPeriod: 1h 88 | config: 89 | addr: https://vault.example.com 90 | auth: 91 | kubernetes: 92 | role: myrole 93 | cluster: kubernetes 94 | ``` 95 | 96 | A corresponding secret would be created in the same namespace as the *VaultSecret* custom resource. 97 | This secret would contain two keys filled with vault content: 98 | - `username` 99 | - `password` 100 | 101 | --- 102 | 103 | It's possible to add annotations and labels to the generated secret with `secretAnnotations` and `secretLabels`. 104 | 105 | Here is another example for "dockerconfig" secrets: 106 | ``` 107 | apiVersion: maupu.org/v1beta1 108 | kind: VaultSecret 109 | metadata: 110 | name: dockerconfig-example 111 | namespace: nma 112 | spec: 113 | secretName: dockerconfig-test 114 | secretType: kubernetes.io/dockerconfigjson 115 | secrets: 116 | - secretKey: .dockerconfigjson 117 | kvPath: secrets/dockerconfig 118 | field: dockerconfigjson 119 | path: / 120 | config: 121 | addr: https://vault.example.com 122 | auth: 123 | kubernetes: 124 | role: myrole 125 | cluster: kubernetes 126 | ``` 127 | 128 | It's possible to set the secret type in the spec with `secretType`, if it isn't specified the default value is `Opaque`. 129 | 130 | --- 131 | 132 | Secret are resynced periodically (after a maximum of 10h) but it's possible to reduce this delay with the `syncPeriod` option (`syncPeriod: 1h`). 133 | 134 | --- 135 | 136 | If your Vault is using *TLS* but if its certificates are not signed by a *known authority*, one can use the config option `insecure` to skip tls verification. 137 | 138 | Do not use `TLS_SKIP_VERIFY` env variable when starting the operator, **it's not** being taken into account. 139 | 140 | Here is an example: 141 | ``` 142 | apiVersion: maupu.org/v1beta1 143 | kind: VaultSecret 144 | metadata: 145 | name: example-vaultsecret-insecure 146 | spec: 147 | secretName: vault-secret-test 148 | secrets: 149 | - secretKey: foo 150 | kvPath: secret 151 | path: foo/bar 152 | field: value 153 | config: 154 | insecure: true 155 | addr: https://localhost 156 | auth: 157 | ... 158 | ``` 159 | 160 | ## Vault configuration 161 | 162 | To authenticate, the operator uses the `config` section of the Custom Resource Definition. The following options are supported: 163 | - AppRole Auth Method (https://www.vaultproject.io/docs/auth/approle.html) 164 | - Vault Kubernetes Auth Method (https://www.vaultproject.io/docs/auth/kubernetes.html) 165 | - Directly using a token 166 | 167 | The prefered way is to use *Vault Kubernetes Auth Method* because the other authentication methods require to push a *secret* into the custom resource (e.g. `token` or `role_id/secret_id`). 168 | 169 | ### Kubernetes Auth Method usage 170 | 171 | ``` 172 | config: 173 | addr: https://vault.example.com 174 | auth: 175 | kubernetes: 176 | role: myrole 177 | cluster: kubernetes 178 | ``` 179 | 180 | The section `kubernetes` takes two arguments: 181 | - `role`: role associated with the *service account* configured. 182 | - `cluster`: name used in the url when configuring auth on vault side. 183 | 184 | ### Token 185 | 186 | ``` 187 | config: 188 | addr: https://vault.example.com 189 | auth: 190 | token: 191 | ``` 192 | 193 | ### AppRole 194 | 195 | ``` 196 | config: 197 | addr: https://vault.example.com 198 | auth: 199 | approle: 200 | roleId: 201 | secretId: 202 | ``` 203 | 204 | If several configuration options are specified, there are used in the following order: 205 | - Token 206 | - AppRole 207 | - Kubernetes Auth Method 208 | 209 | # Development 210 | 211 | ## Prerequisites 212 | 213 | - Operator SDK installation (https://github.com/operator-framework/operator-sdk) 214 | - Go Dep (https://golang.github.io/dep/docs/installation.html) 215 | 216 | ## Building 217 | 218 | To build, simply use *make*: 219 | ``` 220 | make docker-build 221 | IMG=local/vault-secret:test make docker-build 222 | ``` 223 | 224 | This task will: 225 | - build the binary (using docker) 226 | - create a docker image 227 | 228 | You can then push it to any docker repository or use it locally. 229 | -------------------------------------------------------------------------------- /api/v1beta1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1beta1 contains API Schema definitions for the maupu.org v1beta1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=maupu.org 20 | package v1beta1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "maupu.org", Version: "v1beta1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /api/v1beta1/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | import ( 20 | "errors" 21 | 22 | "github.com/nmaupu/vault-secret/pkg/k8sutils" 23 | nmvault "github.com/nmaupu/vault-secret/pkg/vault" 24 | "sigs.k8s.io/controller-runtime/pkg/client" 25 | ) 26 | 27 | // BySecretKey allows sorting an array of VaultSecretSpecSecret by SecretKey 28 | type BySecretKey []VaultSecretSpecSecret 29 | 30 | // Len returns the len of a BySecretKey object 31 | func (a BySecretKey) Len() int { return len(a) } 32 | 33 | // Swap swaps two elements of a BySecretKey object 34 | func (a BySecretKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 35 | 36 | // Less checks if a given SecretKey object is lexicographically inferior to another SecretKey object 37 | func (a BySecretKey) Less(i, j int) bool { return a[i].SecretKey < a[j].SecretKey } 38 | 39 | // GetVaultAuthProvider implem from custom resource object 40 | func (cr *VaultSecret) GetVaultAuthProvider(c client.Client) (nmvault.AuthProvider, error) { 41 | // Checking order: 42 | // - Token 43 | // - AppRole 44 | // - Kubernetes Auth Method 45 | if cr.Spec.Config.Auth.Token != "" { 46 | return nmvault.NewTokenProvider(cr.Spec.Config.Auth.Token), nil 47 | } else if cr.Spec.Config.Auth.AppRole.RoleID != "" { 48 | appRoleName := "approle" // Default approle name value 49 | if cr.Spec.Config.Auth.AppRole.Name != "" { 50 | appRoleName = cr.Spec.Config.Auth.AppRole.Name 51 | } 52 | return nmvault.NewAppRoleProvider( 53 | appRoleName, 54 | cr.Spec.Config.Auth.AppRole.RoleID, 55 | cr.Spec.Config.Auth.AppRole.SecretID, 56 | ), nil 57 | } else if cr.Spec.Config.Auth.Kubernetes.Role != "" { 58 | // Retrieving token from the serviceAccount configured 59 | saName := cr.Spec.Config.Auth.Kubernetes.ServiceAccount 60 | if saName == "" { 61 | saName = "default" 62 | } 63 | 64 | tok, err := k8sutils.GetTokenFromSA(c, cr.Namespace, saName) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | return nmvault.NewKubernetesProvider( 70 | cr.Spec.Config.Auth.Kubernetes.Role, 71 | cr.Spec.Config.Auth.Kubernetes.Cluster, 72 | tok, 73 | ), nil 74 | } 75 | 76 | return nil, errors.New("Cannot find a way to authenticate, please choose between Token, AppRole or Kubernetes") 77 | } 78 | -------------------------------------------------------------------------------- /api/v1beta1/vaultsecret_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | import ( 20 | corev1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // VaultSecretSpec defines the desired state of VaultSecret 25 | // +k8s:openapi-gen=true 26 | type VaultSecretSpec struct { 27 | Config VaultSecretSpecConfig `json:"config,required"` 28 | // +listType=set 29 | Secrets []VaultSecretSpecSecret `json:"secrets,required"` 30 | SecretName string `json:"secretName,omitempty"` 31 | SecretType corev1.SecretType `json:"secretType,omitempty"` 32 | SecretLabels map[string]string `json:"secretLabels,omitempty"` 33 | SecretAnnotations map[string]string `json:"secretAnnotations,omitempty"` 34 | SyncPeriod metav1.Duration `json:"syncPeriod,omitempty"` 35 | } 36 | 37 | // VaultSecretSpecConfig Configuration part of a vault-secret object 38 | type VaultSecretSpecConfig struct { 39 | Addr string `json:"addr,required"` 40 | Namespace string `json:"namespace,omitempty"` 41 | Insecure bool `json:"insecure,omitempty"` 42 | Auth VaultSecretSpecConfigAuth `json:"auth,required"` 43 | } 44 | 45 | // VaultSecretSpecConfigAuth Mean of authentication for Vault 46 | type VaultSecretSpecConfigAuth struct { 47 | Token string `json:"token,omitempty"` 48 | Kubernetes KubernetesAuthType `json:"kubernetes,omitempty"` 49 | AppRole AppRoleAuthType `json:"approle,omitempty"` 50 | } 51 | 52 | // KubernetesAuthType Kubernetes authentication type 53 | type KubernetesAuthType struct { 54 | Role string `json:"role,required"` 55 | Cluster string `json:"cluster,required"` 56 | // ServiceAccount to use for authentication, using "default" if not provided 57 | ServiceAccount string `json:"serviceAccount,omitempty"` 58 | } 59 | 60 | // AppRoleAuthType AppRole authentication type 61 | type AppRoleAuthType struct { 62 | Name string `json:"name,omitempty"` 63 | RoleID string `json:"roleId,required"` 64 | SecretID string `json:"secretId,required"` 65 | } 66 | 67 | // VaultSecretSpecSecret Defines secrets to create from Vault 68 | type VaultSecretSpecSecret struct { 69 | // Key name in the secret to create 70 | SecretKey string `json:"secretKey,required"` 71 | // Path of the key-value storage 72 | KvPath string `json:"kvPath,required"` 73 | // Path of the vault secret 74 | Path string `json:"path,required"` 75 | // Field to retrieve from the path 76 | Field string `json:"field,required"` 77 | // KvVersion is the version of the KV backend, if unspecified, try to automatically determine it 78 | KvVersion int `json:"kvVersion,omitempty"` 79 | } 80 | 81 | // VaultSecretStatus Status field regarding last custom resource process 82 | // +k8s:openapi-gen=true 83 | type VaultSecretStatus struct { 84 | // +listType=set 85 | Entries []VaultSecretStatusEntry `json:"entries,omitempty"` 86 | } 87 | 88 | // VaultSecretStatusEntry Entry for the status field 89 | type VaultSecretStatusEntry struct { 90 | Secret VaultSecretSpecSecret `json:"secret,required"` 91 | Status bool `json:"status,required"` 92 | Message string `json:"message,omitempty"` 93 | RootError string `json:"rootError,omitempty"` 94 | } 95 | 96 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 97 | 98 | // VaultSecret is the Schema for the vaultsecrets API 99 | // +k8s:openapi-gen=true 100 | // +kubebuilder:object:root=true 101 | // +kubebuilder:subresource:status 102 | // +kubebuilder:resource:path=vaultsecrets,scope=Namespaced 103 | type VaultSecret struct { 104 | metav1.TypeMeta `json:",inline"` 105 | metav1.ObjectMeta `json:"metadata,omitempty"` 106 | 107 | Spec VaultSecretSpec `json:"spec,omitempty"` 108 | Status VaultSecretStatus `json:"status,omitempty"` 109 | } 110 | 111 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 112 | // +kubebuilder:object:root=true 113 | 114 | // VaultSecretList contains a list of VaultSecret 115 | type VaultSecretList struct { 116 | metav1.TypeMeta `json:",inline"` 117 | metav1.ListMeta `json:"metadata,omitempty"` 118 | Items []VaultSecret `json:"items"` 119 | } 120 | 121 | func init() { 122 | SchemeBuilder.Register(&VaultSecret{}, &VaultSecretList{}) 123 | } 124 | -------------------------------------------------------------------------------- /api/v1beta1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Code generated by controller-gen. DO NOT EDIT. 21 | 22 | package v1beta1 23 | 24 | import ( 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *AppRoleAuthType) DeepCopyInto(out *AppRoleAuthType) { 30 | *out = *in 31 | } 32 | 33 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppRoleAuthType. 34 | func (in *AppRoleAuthType) DeepCopy() *AppRoleAuthType { 35 | if in == nil { 36 | return nil 37 | } 38 | out := new(AppRoleAuthType) 39 | in.DeepCopyInto(out) 40 | return out 41 | } 42 | 43 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 44 | func (in BySecretKey) DeepCopyInto(out *BySecretKey) { 45 | { 46 | in := &in 47 | *out = make(BySecretKey, len(*in)) 48 | copy(*out, *in) 49 | } 50 | } 51 | 52 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BySecretKey. 53 | func (in BySecretKey) DeepCopy() BySecretKey { 54 | if in == nil { 55 | return nil 56 | } 57 | out := new(BySecretKey) 58 | in.DeepCopyInto(out) 59 | return *out 60 | } 61 | 62 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 63 | func (in *KubernetesAuthType) DeepCopyInto(out *KubernetesAuthType) { 64 | *out = *in 65 | } 66 | 67 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesAuthType. 68 | func (in *KubernetesAuthType) DeepCopy() *KubernetesAuthType { 69 | if in == nil { 70 | return nil 71 | } 72 | out := new(KubernetesAuthType) 73 | in.DeepCopyInto(out) 74 | return out 75 | } 76 | 77 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 78 | func (in *VaultSecret) DeepCopyInto(out *VaultSecret) { 79 | *out = *in 80 | out.TypeMeta = in.TypeMeta 81 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 82 | in.Spec.DeepCopyInto(&out.Spec) 83 | in.Status.DeepCopyInto(&out.Status) 84 | } 85 | 86 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultSecret. 87 | func (in *VaultSecret) DeepCopy() *VaultSecret { 88 | if in == nil { 89 | return nil 90 | } 91 | out := new(VaultSecret) 92 | in.DeepCopyInto(out) 93 | return out 94 | } 95 | 96 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 97 | func (in *VaultSecret) DeepCopyObject() runtime.Object { 98 | if c := in.DeepCopy(); c != nil { 99 | return c 100 | } 101 | return nil 102 | } 103 | 104 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 105 | func (in *VaultSecretList) DeepCopyInto(out *VaultSecretList) { 106 | *out = *in 107 | out.TypeMeta = in.TypeMeta 108 | in.ListMeta.DeepCopyInto(&out.ListMeta) 109 | if in.Items != nil { 110 | in, out := &in.Items, &out.Items 111 | *out = make([]VaultSecret, len(*in)) 112 | for i := range *in { 113 | (*in)[i].DeepCopyInto(&(*out)[i]) 114 | } 115 | } 116 | } 117 | 118 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultSecretList. 119 | func (in *VaultSecretList) DeepCopy() *VaultSecretList { 120 | if in == nil { 121 | return nil 122 | } 123 | out := new(VaultSecretList) 124 | in.DeepCopyInto(out) 125 | return out 126 | } 127 | 128 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 129 | func (in *VaultSecretList) DeepCopyObject() runtime.Object { 130 | if c := in.DeepCopy(); c != nil { 131 | return c 132 | } 133 | return nil 134 | } 135 | 136 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 137 | func (in *VaultSecretSpec) DeepCopyInto(out *VaultSecretSpec) { 138 | *out = *in 139 | out.Config = in.Config 140 | if in.Secrets != nil { 141 | in, out := &in.Secrets, &out.Secrets 142 | *out = make([]VaultSecretSpecSecret, len(*in)) 143 | copy(*out, *in) 144 | } 145 | if in.SecretLabels != nil { 146 | in, out := &in.SecretLabels, &out.SecretLabels 147 | *out = make(map[string]string, len(*in)) 148 | for key, val := range *in { 149 | (*out)[key] = val 150 | } 151 | } 152 | if in.SecretAnnotations != nil { 153 | in, out := &in.SecretAnnotations, &out.SecretAnnotations 154 | *out = make(map[string]string, len(*in)) 155 | for key, val := range *in { 156 | (*out)[key] = val 157 | } 158 | } 159 | out.SyncPeriod = in.SyncPeriod 160 | } 161 | 162 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultSecretSpec. 163 | func (in *VaultSecretSpec) DeepCopy() *VaultSecretSpec { 164 | if in == nil { 165 | return nil 166 | } 167 | out := new(VaultSecretSpec) 168 | in.DeepCopyInto(out) 169 | return out 170 | } 171 | 172 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 173 | func (in *VaultSecretSpecConfig) DeepCopyInto(out *VaultSecretSpecConfig) { 174 | *out = *in 175 | out.Auth = in.Auth 176 | } 177 | 178 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultSecretSpecConfig. 179 | func (in *VaultSecretSpecConfig) DeepCopy() *VaultSecretSpecConfig { 180 | if in == nil { 181 | return nil 182 | } 183 | out := new(VaultSecretSpecConfig) 184 | in.DeepCopyInto(out) 185 | return out 186 | } 187 | 188 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 189 | func (in *VaultSecretSpecConfigAuth) DeepCopyInto(out *VaultSecretSpecConfigAuth) { 190 | *out = *in 191 | out.Kubernetes = in.Kubernetes 192 | out.AppRole = in.AppRole 193 | } 194 | 195 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultSecretSpecConfigAuth. 196 | func (in *VaultSecretSpecConfigAuth) DeepCopy() *VaultSecretSpecConfigAuth { 197 | if in == nil { 198 | return nil 199 | } 200 | out := new(VaultSecretSpecConfigAuth) 201 | in.DeepCopyInto(out) 202 | return out 203 | } 204 | 205 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 206 | func (in *VaultSecretSpecSecret) DeepCopyInto(out *VaultSecretSpecSecret) { 207 | *out = *in 208 | } 209 | 210 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultSecretSpecSecret. 211 | func (in *VaultSecretSpecSecret) DeepCopy() *VaultSecretSpecSecret { 212 | if in == nil { 213 | return nil 214 | } 215 | out := new(VaultSecretSpecSecret) 216 | in.DeepCopyInto(out) 217 | return out 218 | } 219 | 220 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 221 | func (in *VaultSecretStatus) DeepCopyInto(out *VaultSecretStatus) { 222 | *out = *in 223 | if in.Entries != nil { 224 | in, out := &in.Entries, &out.Entries 225 | *out = make([]VaultSecretStatusEntry, len(*in)) 226 | copy(*out, *in) 227 | } 228 | } 229 | 230 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultSecretStatus. 231 | func (in *VaultSecretStatus) DeepCopy() *VaultSecretStatus { 232 | if in == nil { 233 | return nil 234 | } 235 | out := new(VaultSecretStatus) 236 | in.DeepCopyInto(out) 237 | return out 238 | } 239 | 240 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 241 | func (in *VaultSecretStatusEntry) DeepCopyInto(out *VaultSecretStatusEntry) { 242 | *out = *in 243 | out.Secret = in.Secret 244 | } 245 | 246 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultSecretStatusEntry. 247 | func (in *VaultSecretStatusEntry) DeepCopy() *VaultSecretStatusEntry { 248 | if in == nil { 249 | return nil 250 | } 251 | out := new(VaultSecretStatusEntry) 252 | in.DeepCopyInto(out) 253 | return out 254 | } 255 | -------------------------------------------------------------------------------- /config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for 4 | # breaking changes 5 | apiVersion: cert-manager.io/v1alpha2 6 | kind: Issuer 7 | metadata: 8 | name: selfsigned-issuer 9 | namespace: system 10 | spec: 11 | selfSigned: {} 12 | --- 13 | apiVersion: cert-manager.io/v1alpha2 14 | kind: Certificate 15 | metadata: 16 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 17 | namespace: system 18 | spec: 19 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 20 | dnsNames: 21 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 22 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 23 | issuerRef: 24 | kind: Issuer 25 | name: selfsigned-issuer 26 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 27 | -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref and var substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert-manager.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert-manager.io 8 | path: spec/issuerRef/name 9 | 10 | varReference: 11 | - kind: Certificate 12 | group: cert-manager.io 13 | path: spec/commonName 14 | - kind: Certificate 15 | group: cert-manager.io 16 | path: spec/dnsNames 17 | -------------------------------------------------------------------------------- /config/crd/bases/maupu.org_vaultsecrets.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: v0.3.0 8 | creationTimestamp: null 9 | name: vaultsecrets.maupu.org 10 | spec: 11 | group: maupu.org 12 | names: 13 | kind: VaultSecret 14 | listKind: VaultSecretList 15 | plural: vaultsecrets 16 | singular: vaultsecret 17 | scope: Namespaced 18 | versions: 19 | - name: v1beta1 20 | schema: 21 | openAPIV3Schema: 22 | description: VaultSecret is the Schema for the vaultsecrets API 23 | properties: 24 | apiVersion: 25 | description: 'APIVersion defines the versioned schema of this representation 26 | of an object. Servers should convert recognized schemas to the latest 27 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 28 | type: string 29 | kind: 30 | description: 'Kind is a string value representing the REST resource this 31 | object represents. Servers may infer this from the endpoint the client 32 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 33 | type: string 34 | metadata: 35 | type: object 36 | spec: 37 | description: VaultSecretSpec defines the desired state of VaultSecret 38 | properties: 39 | config: 40 | description: VaultSecretSpecConfig Configuration part of a vault-secret 41 | object 42 | properties: 43 | addr: 44 | type: string 45 | auth: 46 | description: VaultSecretSpecConfigAuth Mean of authentication 47 | for Vault 48 | properties: 49 | approle: 50 | description: AppRoleAuthType AppRole authentication type 51 | properties: 52 | name: 53 | type: string 54 | roleId: 55 | type: string 56 | secretId: 57 | type: string 58 | required: 59 | - roleId 60 | - secretId 61 | type: object 62 | kubernetes: 63 | description: KubernetesAuthType Kubernetes authentication 64 | type 65 | properties: 66 | cluster: 67 | type: string 68 | role: 69 | type: string 70 | serviceAccount: 71 | description: ServiceAccount to use for authentication, 72 | using "default" if not provided 73 | type: string 74 | required: 75 | - cluster 76 | - role 77 | type: object 78 | token: 79 | type: string 80 | type: object 81 | insecure: 82 | type: boolean 83 | namespace: 84 | type: string 85 | required: 86 | - addr 87 | - auth 88 | type: object 89 | secretAnnotations: 90 | additionalProperties: 91 | type: string 92 | type: object 93 | secretLabels: 94 | additionalProperties: 95 | type: string 96 | type: object 97 | secretName: 98 | type: string 99 | secretType: 100 | type: string 101 | secrets: 102 | items: 103 | description: VaultSecretSpecSecret Defines secrets to create from 104 | Vault 105 | properties: 106 | field: 107 | description: Field to retrieve from the path 108 | type: string 109 | kvPath: 110 | description: Path of the key-value storage 111 | type: string 112 | kvVersion: 113 | description: KvVersion is the version of the KV backend, if 114 | unspecified, try to automatically determine it 115 | type: integer 116 | path: 117 | description: Path of the vault secret 118 | type: string 119 | secretKey: 120 | description: Key name in the secret to create 121 | type: string 122 | required: 123 | - field 124 | - kvPath 125 | - path 126 | - secretKey 127 | type: object 128 | type: array 129 | x-kubernetes-list-type: atomic 130 | syncPeriod: 131 | type: string 132 | required: 133 | - config 134 | - secrets 135 | type: object 136 | status: 137 | description: VaultSecretStatus Status field regarding last custom resource 138 | process 139 | properties: 140 | entries: 141 | items: 142 | description: VaultSecretStatusEntry Entry for the status field 143 | properties: 144 | message: 145 | type: string 146 | rootError: 147 | type: string 148 | secret: 149 | description: VaultSecretSpecSecret Defines secrets to create 150 | from Vault 151 | properties: 152 | field: 153 | description: Field to retrieve from the path 154 | type: string 155 | kvPath: 156 | description: Path of the key-value storage 157 | type: string 158 | kvVersion: 159 | description: KvVersion is the version of the KV backend, 160 | if unspecified, try to automatically determine it 161 | type: integer 162 | path: 163 | description: Path of the vault secret 164 | type: string 165 | secretKey: 166 | description: Key name in the secret to create 167 | type: string 168 | required: 169 | - field 170 | - kvPath 171 | - path 172 | - secretKey 173 | type: object 174 | status: 175 | type: boolean 176 | required: 177 | - secret 178 | - status 179 | type: object 180 | type: array 181 | x-kubernetes-list-type: atomic 182 | type: object 183 | type: object 184 | served: true 185 | storage: true 186 | subresources: 187 | status: {} 188 | status: 189 | acceptedNames: 190 | kind: "" 191 | plural: "" 192 | conditions: [] 193 | storedVersions: [] 194 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/maupu.org_vaultsecrets.yaml 6 | # +kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patchesStrategicMerge: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | #- patches/webhook_in_vaultsecrets.yaml 12 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 13 | 14 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. 15 | # patches here are for enabling the CA injection for each CRD 16 | #- patches/cainjection_in_vaultsecrets.yaml 17 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch 18 | 19 | # the following config is for teaching kustomize how to do kustomization for CRDs. 20 | configurations: 21 | - kustomizeconfig.yaml 22 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | group: apiextensions.k8s.io 8 | path: spec/conversion/webhook/clientConfig/service/name 9 | 10 | namespace: 11 | - kind: CustomResourceDefinition 12 | group: apiextensions.k8s.io 13 | path: spec/conversion/webhook/clientConfig/service/namespace 14 | create: false 15 | 16 | varReference: 17 | - path: metadata/annotations 18 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_vaultsecrets.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: vaultsecrets.maupu.org 9 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_vaultsecrets.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: vaultsecrets.maupu.org 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhook: 11 | clientConfig: 12 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 13 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 14 | caBundle: Cg== 15 | service: 16 | namespace: system 17 | name: webhook-service 18 | path: /convert 19 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: vault-secret-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: vault-secret- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | - ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 20 | # crd/kustomization.yaml 21 | #- ../webhook 22 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 23 | #- ../certmanager 24 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 25 | #- ../prometheus 26 | 27 | patchesStrategicMerge: 28 | # Protect the /metrics endpoint by putting it behind auth. 29 | # If you want your controller-manager to expose the /metrics 30 | # endpoint w/o any authn/z, please comment the following line. 31 | - manager_auth_proxy_patch.yaml 32 | 33 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 34 | # crd/kustomization.yaml 35 | #- manager_webhook_patch.yaml 36 | 37 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 38 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 39 | # 'CERTMANAGER' needs to be enabled to use ca injection 40 | #- webhookcainjection_patch.yaml 41 | 42 | # the following config is for teaching kustomize how to do var substitution 43 | vars: 44 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 45 | #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 46 | # objref: 47 | # kind: Certificate 48 | # group: cert-manager.io 49 | # version: v1alpha2 50 | # name: serving-cert # this name should match the one in certificate.yaml 51 | # fieldref: 52 | # fieldpath: metadata.namespace 53 | #- name: CERTIFICATE_NAME 54 | # objref: 55 | # kind: Certificate 56 | # group: cert-manager.io 57 | # version: v1alpha2 58 | # name: serving-cert # this name should match the one in certificate.yaml 59 | #- name: SERVICE_NAMESPACE # namespace of the service 60 | # objref: 61 | # kind: Service 62 | # version: v1 63 | # name: webhook-service 64 | # fieldref: 65 | # fieldpath: metadata.namespace 66 | #- name: SERVICE_NAME 67 | # objref: 68 | # kind: Service 69 | # version: v1 70 | # name: webhook-service 71 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 14 | args: 15 | - "--secure-listen-address=0.0.0.0:8443" 16 | - "--upstream=http://127.0.0.1:8080/" 17 | - "--logtostderr=true" 18 | - "--v=10" 19 | ports: 20 | - containerPort: 8443 21 | name: https 22 | - name: manager 23 | args: 24 | - "--metrics-addr=127.0.0.1:8080" 25 | - "--enable-leader-election" 26 | -------------------------------------------------------------------------------- /config/default/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | ports: 12 | - containerPort: 9443 13 | name: webhook-server 14 | protocol: TCP 15 | volumeMounts: 16 | - mountPath: /tmp/k8s-webhook-server/serving-certs 17 | name: cert 18 | readOnly: true 19 | volumes: 20 | - name: cert 21 | secret: 22 | defaultMode: 420 23 | secretName: webhook-server-cert 24 | -------------------------------------------------------------------------------- /config/default/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. 3 | apiVersion: admissionregistration.k8s.io/v1beta1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | name: mutating-webhook-configuration 7 | annotations: 8 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 9 | --- 10 | apiVersion: admissionregistration.k8s.io/v1beta1 11 | kind: ValidatingWebhookConfiguration 12 | metadata: 13 | name: validating-webhook-configuration 14 | annotations: 15 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 16 | -------------------------------------------------------------------------------- /config/doc-samples/maupu.org_v1beta1_vaultsecrets_cr.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: maupu.org/v1beta1 2 | kind: VaultSecret 3 | metadata: 4 | name: example-vaultsecret 5 | spec: 6 | secretName: vault-secret-test 7 | secrets: 8 | - secretKey: username 9 | kvPath: secrets/kv 10 | path: test 11 | field: username 12 | - secretKey: password 13 | kvPath: secrets/kv 14 | path: test 15 | field: password 16 | syncPeriod: 1h 17 | config: 18 | addr: https://vault.example.com 19 | # namespace: example-namespace 20 | auth: 21 | kubernetes: 22 | role: myrole 23 | cluster: kubernetes 24 | # auth: 25 | # token: ... 26 | # auth: 27 | # approle: 28 | # role_id: ... 29 | # secret_id: ... 30 | -------------------------------------------------------------------------------- /config/doc-samples/operator.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: vault-secret 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | name: vault-secret 10 | template: 11 | metadata: 12 | labels: 13 | name: vault-secret 14 | spec: 15 | serviceAccountName: default 16 | containers: 17 | - name: vault-secret 18 | # Replace this with the built image name 19 | image: nmaupu/vault-secret:master 20 | imagePullPolicy: Always 21 | command: 22 | - /manager 23 | env: 24 | - name: WATCH_NAMESPACE 25 | valueFrom: 26 | fieldRef: 27 | fieldPath: metadata.namespace 28 | #- name: WATCH_MULTINAMESPACES 29 | # value: "default,test" 30 | - name: POD_NAME 31 | valueFrom: 32 | fieldRef: 33 | fieldPath: metadata.name 34 | - name: OPERATOR_NAME 35 | value: "vault-secret" 36 | -------------------------------------------------------------------------------- /config/doc-samples/role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | creationTimestamp: null 5 | name: vault-secret-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - pods 11 | - services 12 | - services/finalizers 13 | - endpoints 14 | - persistentvolumeclaims 15 | - events 16 | - configmaps 17 | - secrets 18 | verbs: 19 | - '*' 20 | - apiGroups: 21 | - "" 22 | resources: 23 | - namespaces 24 | verbs: 25 | - get 26 | - apiGroups: 27 | - apps 28 | resources: 29 | - deployments 30 | - daemonsets 31 | - replicasets 32 | - statefulsets 33 | verbs: 34 | - '*' 35 | - apiGroups: 36 | - extensions 37 | resources: 38 | - replicasets 39 | - deployments 40 | verbs: 41 | - '*' 42 | - apiGroups: 43 | - monitoring.coreos.com 44 | resources: 45 | - servicemonitors 46 | verbs: 47 | - get 48 | - create 49 | - apiGroups: 50 | - maupu.org 51 | resources: 52 | - '*' 53 | verbs: 54 | - '*' 55 | -------------------------------------------------------------------------------- /config/doc-samples/role_binding.yaml: -------------------------------------------------------------------------------- 1 | kind: RoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: vault-secret 5 | subjects: 6 | - kind: ServiceAccount 7 | name: default 8 | namespace: default 9 | roleRef: 10 | kind: Role 11 | name: vault-secret-role 12 | apiGroup: rbac.authorization.k8s.io 13 | -------------------------------------------------------------------------------- /config/doc-samples/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: vault-secret 5 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: vault-secret 5 | namespace: test 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | name: vault-secret 11 | template: 12 | metadata: 13 | labels: 14 | name: vault-secret 15 | spec: 16 | serviceAccountName: default 17 | containers: 18 | - name: vault-secret 19 | # Replace this with the built image name 20 | image: nmaupu/vault-secret:master 21 | imagePullPolicy: Always 22 | command: 23 | - /manager 24 | env: 25 | - name: WATCH_NAMESPACE 26 | valueFrom: 27 | fieldRef: 28 | fieldPath: metadata.namespace 29 | #- name: WATCH_MULTINAMESPACES 30 | # value: "default,test" 31 | - name: POD_NAME 32 | valueFrom: 33 | fieldRef: 34 | fieldPath: metadata.name 35 | - name: OPERATOR_NAME 36 | value: "vault-secret" 37 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | selector: 15 | matchLabels: 16 | control-plane: controller-manager 17 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: ["/metrics"] 7 | verbs: ["get"] 8 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: ["authentication.k8s.io"] 7 | resources: 8 | - tokenreviews 9 | verbs: ["create"] 10 | - apiGroups: ["authorization.k8s.io"] 11 | resources: 12 | - subjectaccessreviews 13 | verbs: ["create"] 14 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: controller-manager-metrics-service 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: controller-manager 15 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - role.yaml 3 | - role_binding.yaml 4 | - leader_election_role.yaml 5 | - leader_election_role_binding.yaml 6 | # Comment the following 4 lines if you want to disable 7 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 8 | # which protects your /metrics endpoint. 9 | - auth_proxy_service.yaml 10 | - auth_proxy_role.yaml 11 | - auth_proxy_role_binding.yaml 12 | - auth_proxy_client_clusterrole.yaml 13 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - configmaps/status 23 | verbs: 24 | - get 25 | - update 26 | - patch 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - events 31 | verbs: 32 | - create 33 | - patch 34 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | creationTimestamp: null 7 | name: manager-role 8 | rules: 9 | - apiGroups: 10 | - "" 11 | resources: 12 | - secrets 13 | verbs: 14 | - create 15 | - delete 16 | - get 17 | - list 18 | - patch 19 | - update 20 | - watch 21 | - apiGroups: 22 | - "" 23 | resources: 24 | - serviceaccounts 25 | verbs: 26 | - get 27 | - list 28 | - watch 29 | - apiGroups: 30 | - maupu.org 31 | resources: 32 | - vaultsecrets 33 | verbs: 34 | - create 35 | - delete 36 | - get 37 | - list 38 | - patch 39 | - update 40 | - watch 41 | - apiGroups: 42 | - maupu.org 43 | resources: 44 | - vaultsecrets/status 45 | verbs: 46 | - get 47 | - patch 48 | - update 49 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/vaultsecret_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit vaultsecrets. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: vaultsecret-editor-role 6 | rules: 7 | - apiGroups: 8 | - maupu.org 9 | resources: 10 | - vaultsecrets 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - maupu.org 21 | resources: 22 | - vaultsecrets/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/vaultsecret_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view vaultsecrets. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: vaultsecret-viewer-role 6 | rules: 7 | - apiGroups: 8 | - maupu.org 9 | resources: 10 | - vaultsecrets 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - maupu.org 17 | resources: 18 | - vaultsecrets/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## This file is auto-generated, do not modify ## 2 | resources: 3 | - maupu.org_v1beta1_vaultsecret.yaml # +kubebuilder:scaffold:manifestskustomizesamples 4 | -------------------------------------------------------------------------------- /config/samples/maupu.org_v1beta1_vaultsecret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: maupu.org/v1beta1 2 | kind: VaultSecret 3 | metadata: 4 | name: example-vaultsecret 5 | spec: 6 | secretName: vault-secret-test 7 | secrets: 8 | - secretKey: username 9 | kvPath: secrets/kv 10 | path: test 11 | field: username 12 | - secretKey: password 13 | kvPath: secrets/kv 14 | path: test 15 | field: password 16 | syncPeriod: 1h 17 | config: 18 | addr: https://vault.example.com 19 | # namespace: example-namespace 20 | auth: 21 | kubernetes: 22 | role: myrole 23 | cluster: kubernetes 24 | # auth: 25 | # token: ... 26 | # auth: 27 | # approle: 28 | # role_id: ... 29 | # secret_id: ... 30 | -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | -------------------------------------------------------------------------------- /config/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting vars. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | 14 | namespace: 15 | - kind: MutatingWebhookConfiguration 16 | group: admissionregistration.k8s.io 17 | path: webhooks/clientConfig/service/namespace 18 | create: true 19 | - kind: ValidatingWebhookConfiguration 20 | group: admissionregistration.k8s.io 21 | path: webhooks/clientConfig/service/namespace 22 | create: true 23 | 24 | varReference: 25 | - path: metadata/annotations 26 | -------------------------------------------------------------------------------- /config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: webhook-service 6 | namespace: system 7 | spec: 8 | ports: 9 | - port: 443 10 | targetPort: 9443 11 | selector: 12 | control-plane: controller-manager 13 | -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "path/filepath" 21 | "testing" 22 | 23 | . "github.com/onsi/ginkgo" 24 | . "github.com/onsi/gomega" 25 | "k8s.io/client-go/kubernetes/scheme" 26 | "k8s.io/client-go/rest" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | "sigs.k8s.io/controller-runtime/pkg/envtest" 29 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer" 30 | logf "sigs.k8s.io/controller-runtime/pkg/log" 31 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 32 | 33 | maupuorgv1beta1 "github.com/nmaupu/vault-secret/api/v1beta1" 34 | // +kubebuilder:scaffold:imports 35 | ) 36 | 37 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 38 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 39 | 40 | var cfg *rest.Config 41 | var k8sClient client.Client 42 | var testEnv *envtest.Environment 43 | 44 | func TestAPIs(t *testing.T) { 45 | RegisterFailHandler(Fail) 46 | 47 | RunSpecsWithDefaultAndCustomReporters(t, 48 | "Controller Suite", 49 | []Reporter{printer.NewlineReporter{}}) 50 | } 51 | 52 | var _ = BeforeSuite(func(done Done) { 53 | logf.SetLogger(zap.LoggerTo(GinkgoWriter, true)) 54 | 55 | By("bootstrapping test environment") 56 | testEnv = &envtest.Environment{ 57 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 58 | } 59 | 60 | var err error 61 | cfg, err = testEnv.Start() 62 | Expect(err).ToNot(HaveOccurred()) 63 | Expect(cfg).ToNot(BeNil()) 64 | 65 | err = maupuorgv1beta1.AddToScheme(scheme.Scheme) 66 | Expect(err).NotTo(HaveOccurred()) 67 | 68 | // +kubebuilder:scaffold:scheme 69 | 70 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 71 | Expect(err).ToNot(HaveOccurred()) 72 | Expect(k8sClient).ToNot(BeNil()) 73 | 74 | close(done) 75 | }, 60) 76 | 77 | var _ = AfterSuite(func() { 78 | By("tearing down the test environment") 79 | err := testEnv.Stop() 80 | Expect(err).ToNot(HaveOccurred()) 81 | }) 82 | -------------------------------------------------------------------------------- /controllers/vaultsecret_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "fmt" 23 | "os" 24 | "sort" 25 | "sync" 26 | "time" 27 | 28 | "github.com/go-logr/logr" 29 | maupuv1beta1 "github.com/nmaupu/vault-secret/api/v1beta1" 30 | nmvault "github.com/nmaupu/vault-secret/pkg/vault" 31 | appVersion "github.com/nmaupu/vault-secret/version" 32 | corev1 "k8s.io/api/core/v1" 33 | "k8s.io/apimachinery/pkg/api/equality" 34 | "k8s.io/apimachinery/pkg/api/errors" 35 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 36 | "k8s.io/apimachinery/pkg/runtime" 37 | "k8s.io/client-go/util/retry" 38 | ctrl "sigs.k8s.io/controller-runtime" 39 | "sigs.k8s.io/controller-runtime/pkg/client" 40 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 41 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 42 | logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" 43 | ) 44 | 45 | var _ reconcile.Reconciler = (*VaultSecretReconciler)(nil) 46 | 47 | const ( 48 | // OperatorAppName is the name of the operator 49 | OperatorAppName = "vaultsecret-operator" 50 | // TimeFormat is the time format to indicate last updated field 51 | TimeFormat = "2006-01-02_15-04-05" 52 | // MinTimeMsBetweenSecretUpdate avoid a secret to be updated too often 53 | MinTimeMsBetweenSecretUpdate = time.Millisecond * 500 54 | ) 55 | 56 | var ( 57 | log = logf.Log.WithName(OperatorAppName) 58 | 59 | // secretsLastUpdateTime store last updated time of a secret to avoid reconciling too often 60 | // the same secret if it changes very fast (like with database KV backend or OTP) 61 | secretsLastUpdateTime = make(map[string]time.Time) 62 | secretsLastUpdateTimeMutex sync.Mutex 63 | 64 | // LabelsFilter filters events on labels 65 | LabelsFilter map[string]string 66 | ) 67 | 68 | // VaultSecretReconciler reconciles a VaultSecret object 69 | type VaultSecretReconciler struct { 70 | client.Client 71 | Log logr.Logger 72 | Scheme *runtime.Scheme 73 | LabelsFilter map[string]string 74 | } 75 | 76 | // AddLabelFilter adds a label for filtering events 77 | func AddLabelFilter(key, value string) { 78 | if LabelsFilter == nil { 79 | LabelsFilter = make(map[string]string) 80 | } 81 | 82 | LabelsFilter[key] = value 83 | } 84 | 85 | // +kubebuilder:rbac:groups=maupu.org,resources=vaultsecrets,verbs=get;list;watch;create;update;patch;delete 86 | // +kubebuilder:rbac:groups=maupu.org,resources=vaultsecrets/status,verbs=get;update;patch 87 | // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete 88 | // +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch 89 | 90 | // Reconcile reads that state of the cluster for a VaultSecret object and makes changes based on the state read 91 | // and what is in the VaultSecret.Spec 92 | // The Controller will requeue the Request to be processed again if the returned error is non-nil or 93 | // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. 94 | func (r *VaultSecretReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { 95 | reqLogger := log.WithValues("Request.Namespace", req.Namespace, "Request.Name", req.Name) 96 | reqLogger.Info("Reconciling VaultSecret") 97 | ctx := context.Background() 98 | 99 | // Fetch the VaultSecret CRInstance 100 | CRInstance := &maupuv1beta1.VaultSecret{} 101 | err := r.Get(ctx, req.NamespacedName, CRInstance) 102 | if err != nil { 103 | if errors.IsNotFound(err) { 104 | // Request object not found, could have been deleted after reconcile request. 105 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 106 | // Return and don't requeue 107 | log.Info("VaultSecret resource not found. Ignoring since object must be deleted") 108 | return ctrl.Result{}, nil 109 | } 110 | 111 | // Error reading the object - requeue the request. 112 | log.Info(fmt.Sprintf("Error reading the VaultSecret object, requeuing, err=%v", err)) 113 | return ctrl.Result{}, err 114 | } 115 | 116 | // Only updating stuff if two updates are not too close from each other 117 | // See secretsLastUpdateTime and MinTimeMsBetweenSecretUpdate variables 118 | updateTimeKey := fmt.Sprintf("%s/%s", CRInstance.GetNamespace(), CRInstance.Spec.SecretName) 119 | secretsLastUpdateTimeMutex.Lock() 120 | defer secretsLastUpdateTimeMutex.Unlock() 121 | ti := secretsLastUpdateTime[updateTimeKey] // no problem if it does not exist: it returns a default time.Time object (set to zero) 122 | now := time.Now() 123 | if now.Sub(ti) > MinTimeMsBetweenSecretUpdate { 124 | operatorName := os.Getenv("OPERATOR_NAME") 125 | if operatorName == "" { 126 | operatorName = OperatorAppName 127 | } 128 | 129 | labels := map[string]string{ 130 | "app.kubernetes.io/name": OperatorAppName, 131 | "app.kubernetes.io/version": appVersion.Version, 132 | "app.kubernetes.io/managed-by": operatorName, 133 | "crName": CRInstance.Name, 134 | "crNamespace": CRInstance.Namespace, 135 | } 136 | 137 | // Adding filtered labels 138 | for key, val := range LabelsFilter { 139 | labels[key] = val 140 | } 141 | 142 | secretName := CRInstance.Spec.SecretName 143 | if secretName == "" { 144 | secretName = CRInstance.Name 145 | } 146 | 147 | secretType := CRInstance.Spec.SecretType 148 | if secretType == "" { 149 | secretType = "Opaque" 150 | } 151 | 152 | for key, val := range CRInstance.Spec.SecretLabels { 153 | labels[key] = val 154 | } 155 | 156 | var secretData map[string][]byte 157 | var statusEntries []maupuv1beta1.VaultSecretStatusEntry 158 | var operationResult controllerutil.OperationResult 159 | 160 | secret := &corev1.Secret{ 161 | ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: req.Namespace}, 162 | } 163 | 164 | err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { 165 | var err error 166 | operationResult, err = controllerutil.CreateOrUpdate(context.TODO(), r.Client, secret, func() error { 167 | // As type field is immutable we quickly update the resource before reading from vault. 168 | // We expect a genuine error from the api server. 169 | if secret.Type != secretType && secret.Type != "" { 170 | secret.Type = secretType 171 | return nil 172 | } 173 | 174 | // Only read secret data once 175 | if secretData == nil { 176 | secretData, statusEntries, err = r.readSecretData(CRInstance) 177 | if err != nil { 178 | return err 179 | } 180 | } 181 | 182 | // Set labels 183 | if secret.Labels == nil { 184 | secret.Labels = make(map[string]string) 185 | } 186 | for k, v := range labels { 187 | secret.Labels[k] = v 188 | } 189 | 190 | // Set data and update lastUpdate if data changed 191 | var changed bool 192 | if secret.Data == nil { 193 | secret.Data = make(map[string][]byte) 194 | } 195 | for key, data := range secretData { 196 | if changed || !bytes.Equal(secret.Data[key], data) { 197 | secret.Data[key] = data 198 | changed = true 199 | } 200 | } 201 | if changed { 202 | secret.Labels["lastUpdate"] = time.Now().Format(TimeFormat) 203 | } 204 | secret.Type = secretType 205 | secret.Annotations = CRInstance.Spec.SecretAnnotations 206 | 207 | if err = controllerutil.SetControllerReference(CRInstance, secret, r.Scheme); err != nil { 208 | return err 209 | } 210 | 211 | // Here no error occurred, check if some field failed to update (status will be updated later on) 212 | for i := range statusEntries { 213 | if !statusEntries[i].Status { 214 | return fmt.Errorf("Some errors occurred while reading from vault, see VaultSecret status field for details") 215 | } 216 | } 217 | 218 | return nil 219 | }) 220 | return err 221 | }) 222 | 223 | // Update the VaultSecret Status only if it changed 224 | var statusEntriesErr error 225 | if statusEntries != nil && !equality.Semantic.DeepEqual(CRInstance.Status.Entries, statusEntries) { 226 | CRInstance.Status.Entries = statusEntries 227 | if statusEntriesErr = r.Client.Status().Update(context.TODO(), CRInstance); err != nil { 228 | reqLogger.Error(err, "Failed to update VaultSecret status") 229 | return reconcile.Result{}, statusEntriesErr 230 | } 231 | } 232 | 233 | if err != nil || statusEntriesErr != nil { 234 | // If the resource is invalid then next reconcile is unlikely to succeed so we don't requeue 235 | if errors.IsInvalid(err) { 236 | reqLogger.Error(err, "Failed to update VaultSecret") 237 | return reconcile.Result{}, nil 238 | } 239 | 240 | if err == nil { 241 | err = statusEntriesErr 242 | } 243 | return reconcile.Result{}, err 244 | } 245 | 246 | switch operationResult { 247 | case controllerutil.OperationResultCreated: 248 | reqLogger.Info("Secret created", "Secret.Name", secretName) 249 | case controllerutil.OperationResultUpdated: 250 | reqLogger.Info("Secret updated", "Secret.Name", secretName) 251 | } 252 | } 253 | 254 | return reconcile.Result{RequeueAfter: CRInstance.Spec.SyncPeriod.Duration}, err 255 | } 256 | 257 | func (r *VaultSecretReconciler) readSecretData(cr *maupuv1beta1.VaultSecret) (map[string][]byte, []maupuv1beta1.VaultSecretStatusEntry, error) { 258 | reqLogger := log.WithValues("func", "readSecretData") 259 | 260 | // Authentication provider 261 | authProvider, err := cr.GetVaultAuthProvider(r.Client) 262 | if err != nil { 263 | return nil, nil, err 264 | } 265 | 266 | // Processing vault login 267 | vaultConfig := nmvault.NewConfig(cr.Spec.Config.Addr) 268 | vaultConfig.Namespace = cr.Spec.Config.Namespace 269 | vaultConfig.Insecure = cr.Spec.Config.Insecure 270 | vClient, err := authProvider.Login(vaultConfig) 271 | if err != nil { 272 | return nil, nil, err 273 | } 274 | 275 | vaultClient := nmvault.NewCachedClient(vClient) 276 | 277 | // Init 278 | secrets := map[string][]byte{} 279 | 280 | // Sort by secret keys to avoid updating the resource if order changes 281 | specSecrets := append(make([]maupuv1beta1.VaultSecretSpecSecret, 0, len(cr.Spec.Secrets)), cr.Spec.Secrets...) 282 | sort.Sort(maupuv1beta1.BySecretKey(specSecrets)) 283 | 284 | statusEntries := make([]maupuv1beta1.VaultSecretStatusEntry, 0, len(cr.Spec.Secrets)) 285 | 286 | // Creating secret data from CR 287 | for _, s := range specSecrets { 288 | var err error 289 | errMessage := "" 290 | rootErrMessage := "" 291 | var status bool 292 | 293 | // Vault read 294 | reqLogger.Info("Reading vault", "KvPath", s.KvPath, "Path", s.Path, "KvVersion", s.KvVersion) 295 | secret, err := vaultClient.Read(s.KvVersion, s.KvPath, s.Path) 296 | 297 | if err != nil { 298 | rootErrMessage = err.Error() 299 | errMessage = "Problem occurred while reading secret" 300 | status = false 301 | } else if secret == nil || secret[s.Field] == nil || secret[s.Field] == "" { 302 | errMessage = "Field does not exist" 303 | status = false 304 | } else { 305 | status = true 306 | secrets[s.SecretKey] = ([]byte)(secret[s.Field].(string)) 307 | } 308 | 309 | // Updating CR Status field 310 | statusEntries = append(statusEntries, maupuv1beta1.VaultSecretStatusEntry{ 311 | Secret: s, 312 | Status: status, 313 | Message: errMessage, 314 | RootError: rootErrMessage, 315 | }) 316 | } 317 | 318 | // Error is returned along with secret if it occurred at least once during loop 319 | // In case of error, we only return secrets that we could read. The caller has to handle itself. 320 | return secrets, statusEntries, nil 321 | } 322 | -------------------------------------------------------------------------------- /controllers/vaultsecret_manager.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | maupuv1beta1 "github.com/nmaupu/vault-secret/api/v1beta1" 21 | corev1 "k8s.io/api/core/v1" 22 | ctrl "sigs.k8s.io/controller-runtime" 23 | "sigs.k8s.io/controller-runtime/pkg/event" 24 | "sigs.k8s.io/controller-runtime/pkg/predicate" 25 | ) 26 | 27 | // SetupWithManager godoc 28 | func (r *VaultSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { 29 | return ctrl.NewControllerManagedBy(mgr). 30 | For(&maupuv1beta1.VaultSecret{}). 31 | Owns(&corev1.Secret{}). 32 | WithEventFilter(r.filterLabelsPredicate()). 33 | Complete(r) 34 | } 35 | 36 | func (r *VaultSecretReconciler) filterLabelsPredicate() predicate.Predicate { 37 | predFunc := func(e interface{}) bool { 38 | log := r.Log.WithValues("func", "predFunc") 39 | var objectLabels map[string]string 40 | 41 | // Trying to determine what sort of event we have 42 | // https://tour.golang.org/methods/16 43 | switch e.(type) { 44 | case event.CreateEvent: 45 | log.Info("Create event") 46 | objectLabels = e.(event.CreateEvent).Meta.GetLabels() 47 | case event.UpdateEvent: 48 | log.Info("Update event") 49 | objectLabels = e.(event.UpdateEvent).MetaNew.GetLabels() 50 | case event.DeleteEvent: 51 | log.Info("Delete event") 52 | objectLabels = e.(event.DeleteEvent).Meta.GetLabels() 53 | case event.GenericEvent: 54 | log.Info("Generic event") 55 | objectLabels = e.(event.GenericEvent).Meta.GetLabels() 56 | default: // should never happen except if a new Event type is created 57 | return false 58 | } 59 | 60 | // If labels match, we process the event, otherwise, simply ignore it 61 | // Verifying that each labels configured are present in the target object 62 | for lfk, lfv := range r.LabelsFilter { 63 | if val, ok := objectLabels[lfk]; ok { 64 | if val != lfv { 65 | return false 66 | } 67 | } else { 68 | return false 69 | } 70 | } 71 | 72 | return true 73 | } 74 | 75 | return predicate.Funcs{ 76 | CreateFunc: func(e event.CreateEvent) bool { 77 | return predFunc(e) 78 | }, 79 | UpdateFunc: func(e event.UpdateEvent) bool { 80 | return predFunc(e) 81 | }, 82 | DeleteFunc: func(e event.DeleteEvent) bool { 83 | return predFunc(e) 84 | }, 85 | GenericFunc: func(e event.GenericEvent) bool { 86 | return predFunc(e) 87 | }, 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nmaupu/vault-secret 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/go-logr/logr v0.1.0 7 | github.com/hashicorp/vault/api v1.0.4 8 | github.com/onsi/ginkgo v1.12.1 9 | github.com/onsi/gomega v1.10.1 10 | github.com/operator-framework/operator-sdk v1.0.0 11 | k8s.io/api v0.18.6 12 | k8s.io/apimachinery v0.18.6 13 | k8s.io/client-go v12.0.0+incompatible 14 | sigs.k8s.io/controller-runtime v0.6.2 15 | ) 16 | 17 | replace k8s.io/client-go => k8s.io/client-go v0.18.2 18 | 19 | replace github.com/googleapis/gnostic v0.6.6 => github.com/google/gnostic v0.6.6 20 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /hack/kind/README.md: -------------------------------------------------------------------------------- 1 | ## Sping up a test Kind Cluster 2 | 3 | ### Requirements 4 | 5 | * kind 0.10.0+ 6 | * kustomize 7 | 8 | 9 | ### Steps 10 | 11 | * ./kind.sh create 12 | * kubectl get pod --all-namespaces -w 13 | * wait for `secret` named `secret` in `test` namespace 14 | * ./kind.sh delete 15 | -------------------------------------------------------------------------------- /hack/kind/kind.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Helper script to start KinD 4 | # 5 | # Also adds a docker-registry and an ingress to aid local development 6 | # 7 | # See https://kind.sigs.k8s.io/docs/user/quick-start/ 8 | # 9 | set -o errexit 10 | 11 | [ "$TRACE" ] && set -x 12 | 13 | VERBOSE=1 14 | [ "$TRACE" ] && VERBOSE=3 15 | 16 | 17 | KIND_K8S_IMAGE=${KIND_K8S_IMAGE:-"kindest/node:v1.20.2@sha256:8f7ea6e7642c0da54f04a7ee10431549c0257315b3a634f6ef2fecaaedb19bab"} 18 | KIND_CLUSTER_NAME=${KIND_CLUSTER_NAME:-"vs"} 19 | KIND_WAIT=${KIND_WAIT:-"120s"} 20 | KIND_API_SERVER_ADDRESS=${KIND_API_SERVER_ADDRESS:-"0.0.0.0"} 21 | KIND_API_SERVER_PORT=${KIND_API_SERVER_PORT:-6443} 22 | 23 | create() { 24 | cat < 0 { 138 | log.Info(fmt.Sprintf("Using WATCH_MULTINAMESPACES value = %+v", multiNamespaces)) 139 | mgrOptions.NewCache = cache.MultiNamespacedCacheBuilder(multiNamespaces) 140 | mgrOptions.Namespace = "" 141 | } else { 142 | log.Info(fmt.Sprintf("Using WATCH_NAMESPACE value = \"%s\"", namespace)) 143 | } 144 | 145 | // Creating the manager 146 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), mgrOptions) 147 | if err != nil { 148 | setupLog.Error(err, "unable to start manager") 149 | os.Exit(1) 150 | } 151 | 152 | if err = (&vaultsecret.VaultSecretReconciler{ 153 | Client: mgr.GetClient(), 154 | Log: ctrl.Log.WithName("controllers").WithName("VaultSecret"), 155 | Scheme: mgr.GetScheme(), 156 | LabelsFilter: labelsFilter, 157 | }).SetupWithManager(mgr); err != nil { 158 | setupLog.Error(err, "unable to create controller", "controller", "VaultSecret") 159 | os.Exit(1) 160 | } 161 | // +kubebuilder:scaffold:builder 162 | 163 | setupLog.Info("starting manager") 164 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 165 | setupLog.Error(err, "problem running manager") 166 | os.Exit(1) 167 | } 168 | } 169 | 170 | // GetWatchMultiNamespaces returns the namespaces list the operator should be watching for changes 171 | // Very similar to WATCH_NAMESPACE but for multiple namespaces 172 | func getWatchMultiNamespaces() ([]string, error) { 173 | var namespaces []string 174 | ns, found := os.LookupEnv(WatchMultiNamespacesEnvVar) 175 | if !found { 176 | return namespaces, fmt.Errorf("%s env var is not set", WatchMultiNamespacesEnvVar) 177 | } 178 | 179 | namespaces = strings.Split(ns, ",") 180 | return namespaces, nil 181 | } 182 | -------------------------------------------------------------------------------- /pkg/k8sutils/resources.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package k8sutils 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | corev1 "k8s.io/api/core/v1" 24 | "k8s.io/apimachinery/pkg/api/errors" 25 | "k8s.io/apimachinery/pkg/types" 26 | "sigs.k8s.io/controller-runtime/pkg/client" 27 | ) 28 | 29 | // GetTokenFromSA gets the token associated to the first secret located in a k8s' service account 30 | func GetTokenFromSA(cli client.Client, ns, saName string) (string, error) { 31 | if cli == nil { 32 | return "", fmt.Errorf("Cannot get token from service account, k8s client is nil") 33 | } 34 | 35 | // Getting SA 36 | saClient := &corev1.ServiceAccount{} 37 | err := cli.Get(context.TODO(), types.NamespacedName{Name: saName, Namespace: ns}, saClient) 38 | if err != nil && errors.IsNotFound(err) { 39 | return "", fmt.Errorf("Unable to retrieve service account, err=%v", err) 40 | } 41 | 42 | if len(saClient.Secrets) == 0 { 43 | return "", fmt.Errorf("No secret associated with the service account %s/%s", ns, saName) 44 | } 45 | 46 | // TODO See how to handle this slice of Secrets instead of taking the first one 47 | saSecret := saClient.Secrets[0] 48 | secret := &corev1.Secret{} 49 | err = cli.Get(context.TODO(), types.NamespacedName{Name: saSecret.Name, Namespace: ns}, secret) 50 | if err != nil { 51 | return "", fmt.Errorf("Unable to retrieve the secret from the service account, err=%v", err) 52 | } 53 | 54 | // Finally, set the token 55 | return string(secret.Data["token"]), nil 56 | } 57 | -------------------------------------------------------------------------------- /pkg/vault/approle.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package vault 18 | 19 | import ( 20 | "crypto/tls" 21 | "fmt" 22 | "net/http" 23 | 24 | vapi "github.com/hashicorp/vault/api" 25 | ) 26 | 27 | var _ AuthProvider = (*AppRoleProvider)(nil) 28 | 29 | // AppRoleProvider is a provider to connect to vault using AppRole 30 | type AppRoleProvider struct { 31 | AppRoleName, RoleID, SecretID string 32 | } 33 | 34 | // NewAppRoleProvider creates a pointer to a AppRoleProvider struct 35 | func NewAppRoleProvider(appRoleName, roleID, secretID string) *AppRoleProvider { 36 | return &AppRoleProvider{ 37 | AppRoleName: appRoleName, 38 | RoleID: roleID, 39 | SecretID: secretID, 40 | } 41 | } 42 | 43 | // Login authenticates to the configured vault server 44 | func (a AppRoleProvider) Login(c *Config) (*vapi.Client, error) { 45 | log.Info("Authenticating using AppRole auth method") 46 | config := vapi.DefaultConfig() 47 | config.Address = c.Address 48 | config.HttpClient.Transport = &http.Transport{ 49 | TLSClientConfig: &tls.Config{InsecureSkipVerify: c.Insecure}, 50 | } 51 | 52 | vclient, err := vapi.NewClient(config) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | vaultNamespace := c.Namespace 58 | if vaultNamespace != "" { 59 | vclient.SetNamespace(vaultNamespace) 60 | } 61 | 62 | data := map[string]interface{}{ 63 | "role_id": a.RoleID, 64 | "secret_id": a.SecretID, 65 | } 66 | s, err := vclient.Logical().Write(fmt.Sprintf("auth/%s/login", a.AppRoleName), data) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | vclient.SetToken(s.Auth.ClientToken) 72 | return vclient, nil 73 | } 74 | -------------------------------------------------------------------------------- /pkg/vault/auth_provider.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package vault 18 | 19 | import ( 20 | vapi "github.com/hashicorp/vault/api" 21 | logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" 22 | ) 23 | 24 | var ( 25 | log = logf.Log.WithName("vault-auth-provider") 26 | ) 27 | 28 | // Config is a struct to configure a vault connection 29 | type Config struct { 30 | Address string 31 | Namespace string 32 | Insecure bool 33 | } 34 | 35 | // NewConfig creates a pointer to a VaultConfig struct 36 | func NewConfig(address string) *Config { 37 | return &Config{ 38 | Address: address, 39 | Namespace: "", 40 | Insecure: false, 41 | } 42 | } 43 | 44 | // AuthProvider is an interface to abstract vault methods' connection 45 | type AuthProvider interface { 46 | Login(*Config) (*vapi.Client, error) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/vault/client_cached.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "fmt" 5 | 6 | vapi "github.com/hashicorp/vault/api" 7 | ) 8 | 9 | var _ Client = (*CachedClient)(nil) 10 | 11 | // CachedClient represents a vault client which caches results from vault for later use 12 | type CachedClient struct { 13 | SimpleClient 14 | cache map[string](map[string]interface{}) 15 | } 16 | 17 | // NewCachedClient creates a pointer to a CachedClient struct 18 | func NewCachedClient(client *vapi.Client) *CachedClient { 19 | return &CachedClient{ 20 | SimpleClient: SimpleClient{ 21 | client: client, 22 | }, 23 | cache: make(map[string](map[string]interface{})), 24 | } 25 | } 26 | 27 | // Read implem for CachedClient struct 28 | func (c *CachedClient) Read(kvVersion int, kvPath string, secretPath string) (map[string]interface{}, error) { 29 | reqLogger := log.WithValues("func", "CachedClient.Read") 30 | 31 | var err error 32 | var secret map[string]interface{} 33 | 34 | cacheKey := fmt.Sprintf("%s/%s", kvPath, secretPath) 35 | if cachedSecret, found := c.cache[cacheKey]; found { 36 | reqLogger.Info("Retreiving vault value from cache", "kvPath", kvPath, "path", secretPath) 37 | secret = cachedSecret 38 | err = nil 39 | } else { 40 | secret, err = c.SimpleClient.Read(kvVersion, kvPath, secretPath) 41 | if err == nil && secret != nil { // only cache value if there is no error and a sec returned 42 | reqLogger.Info("Caching vault value", "kvPath", kvPath, "path", secretPath) 43 | c.cache[cacheKey] = secret 44 | } 45 | } 46 | return secret, err 47 | } 48 | 49 | // Clear clears the existing cache 50 | func (c *CachedClient) Clear() { 51 | c.cache = make(map[string](map[string]interface{})) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/vault/client_reader.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | const ( 4 | // KvVersionAuto detects vault kv version automatically 5 | KvVersionAuto int = iota 6 | // KvVersion1 sets the vault kv version to 1 7 | KvVersion1 8 | // KvVersion2 sets the vault kv version to 2 9 | KvVersion2 10 | ) 11 | 12 | // Client is an interface to read data from vault 13 | type Client interface { 14 | Read(engine int, kvPath string, secretPath string) (map[string]interface{}, error) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/vault/client_simple.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "strings" 7 | 8 | vapi "github.com/hashicorp/vault/api" 9 | ) 10 | 11 | var _ Client = (*SimpleClient)(nil) 12 | 13 | // SimpleClient is a simplistic client to connect to vault 14 | type SimpleClient struct { 15 | client *vapi.Client 16 | } 17 | 18 | // NewSimpleClient creates a pointer to a SimpleClient struct 19 | func NewSimpleClient(client *vapi.Client) *SimpleClient { 20 | return &SimpleClient{ 21 | client: client, 22 | } 23 | } 24 | 25 | // Read implem for SimpleClient struct 26 | func (c *SimpleClient) Read(kvVersion int, kvPath string, secretPath string) (map[string]interface{}, error) { 27 | switch kvVersion { 28 | case KvVersion1: 29 | sec, err := c.read(path.Join(kvPath, secretPath)) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return sec.Data, nil 34 | case KvVersion2: 35 | sec, err := c.read(path.Join(kvPath, "data", secretPath)) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return sec.Data["data"].(map[string]interface{}), nil 40 | case KvVersionAuto: 41 | _, version, err := kvPreflightVersionRequest(c.client, kvPath) 42 | if err != nil { 43 | return nil, err 44 | } 45 | return c.Read(version, kvPath, secretPath) 46 | default: 47 | return nil, fmt.Errorf("unknown version %d", kvVersion) 48 | } 49 | } 50 | 51 | func (c *SimpleClient) read(path string) (*vapi.Secret, error) { 52 | sec, err := c.client.Logical().Read(path) 53 | 54 | if err != nil { 55 | // An unknown error occurred 56 | return nil, err 57 | } else if err == nil && sec != nil && contains(sec.Warnings, KVWarning) >= 0 { 58 | // Calling with a v1 path but needs v2 path 59 | idx := contains(sec.Warnings, KVWarning) 60 | return nil, &WrongVersionError{sec.Warnings[idx]} 61 | } else if err == nil && sec == nil { 62 | return nil, &PathNotFound{path} 63 | } else { 64 | return sec, nil 65 | } 66 | } 67 | 68 | // Check wether s contains str or not 69 | func contains(s []string, str string) int { 70 | for k, v := range s { 71 | if strings.Contains(v, str) { 72 | return k 73 | } 74 | } 75 | return -1 76 | } 77 | -------------------------------------------------------------------------------- /pkg/vault/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package vault 18 | 19 | import "fmt" 20 | 21 | // KVWarning is the warning returned by the vault API when the K/V path is invalid (wrong version) 22 | const KVWarning = "Invalid path for a versioned K/V secrets engine." 23 | 24 | // WrongVersionError represents an error raised when the KV version is not correct 25 | type WrongVersionError struct { 26 | Message string 27 | } 28 | 29 | // Error 30 | func (e *WrongVersionError) Error() string { 31 | return e.Message 32 | } 33 | 34 | // PathNotFound represents an error when a path is not found in vault 35 | type PathNotFound struct { 36 | Path string 37 | } 38 | 39 | // Error 40 | func (e *PathNotFound) Error() string { 41 | return fmt.Sprintf("Path %s not found", e.Path) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/vault/helpers.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/hashicorp/vault/api" 7 | ) 8 | 9 | // https://github.com/hashicorp/vault/blob/d8995bfe42d50a13e8f31b686010b0990c5c9b10/command/kv_helpers.go#L44 10 | func kvPreflightVersionRequest(client *api.Client, path string) (string, int, error) { 11 | // We don't want to use a wrapping call here so save any custom value and 12 | // restore after 13 | currentWrappingLookupFunc := client.CurrentWrappingLookupFunc() 14 | client.SetWrappingLookupFunc(nil) 15 | defer client.SetWrappingLookupFunc(currentWrappingLookupFunc) 16 | currentOutputCurlString := client.OutputCurlString() 17 | client.SetOutputCurlString(false) 18 | defer client.SetOutputCurlString(currentOutputCurlString) 19 | 20 | r := client.NewRequest("GET", "/v1/sys/internal/ui/mounts/"+path) 21 | resp, err := client.RawRequest(r) 22 | if resp != nil { 23 | defer resp.Body.Close() 24 | } 25 | if err != nil { 26 | // If we get a 404 we are using an older version of vault, default to 27 | // version 1 28 | if resp != nil && resp.StatusCode == 404 { 29 | return "", 1, nil 30 | } 31 | 32 | return "", 0, err 33 | } 34 | 35 | secret, err := api.ParseSecret(resp.Body) 36 | if err != nil { 37 | return "", 0, err 38 | } 39 | if secret == nil { 40 | return "", 0, errors.New("nil response from pre-flight request") 41 | } 42 | var mountPath string 43 | if mountPathRaw, ok := secret.Data["path"]; ok { 44 | mountPath = mountPathRaw.(string) 45 | } 46 | options := secret.Data["options"] 47 | if options == nil { 48 | return mountPath, 1, nil 49 | } 50 | versionRaw := options.(map[string]interface{})["version"] 51 | if versionRaw == nil { 52 | return mountPath, 1, nil 53 | } 54 | version := versionRaw.(string) 55 | switch version { 56 | case "", "1": 57 | return mountPath, 1, nil 58 | case "2": 59 | return mountPath, 2, nil 60 | } 61 | 62 | return mountPath, 1, nil 63 | } 64 | -------------------------------------------------------------------------------- /pkg/vault/kubernetes.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package vault 18 | 19 | import ( 20 | "crypto/tls" 21 | "fmt" 22 | "net/http" 23 | 24 | vapi "github.com/hashicorp/vault/api" 25 | ) 26 | 27 | var ( 28 | _ AuthProvider = KubernetesProvider{} 29 | ) 30 | 31 | // KubernetesProvider is a provider to authenticate using the Vault Kubernetes Auth Method plugin 32 | // https://www.vaultproject.io/docs/auth/kubernetes 33 | type KubernetesProvider struct { 34 | // Role to use for the authentication 35 | Role string 36 | // Cluster is the path to use to call the login URL 37 | Cluster string 38 | // JWT token to use for the authentication 39 | jwt string 40 | } 41 | 42 | // NewKubernetesProvider creates a new KubernetesProvider object 43 | func NewKubernetesProvider(role, cluster, jwt string) *KubernetesProvider { 44 | return &KubernetesProvider{ 45 | Role: role, 46 | Cluster: cluster, 47 | jwt: jwt, 48 | } 49 | } 50 | 51 | // SetJWT set the jwt token to use for authentication 52 | func (k *KubernetesProvider) SetJWT(jwt string) { 53 | k.jwt = jwt 54 | } 55 | 56 | // Login - godoc 57 | func (k KubernetesProvider) Login(c *Config) (*vapi.Client, error) { 58 | reqLogger := log.WithValues("func", "KubernetesProvider.Login") 59 | reqLogger.Info("Authenticating using Kubernetes auth method") 60 | 61 | if k.jwt == "" { 62 | return nil, fmt.Errorf("Token is empty, please provide a valid jwt token") 63 | } 64 | 65 | config := vapi.DefaultConfig() 66 | config.Address = c.Address 67 | config.HttpClient.Transport = &http.Transport{ 68 | TLSClientConfig: &tls.Config{InsecureSkipVerify: c.Insecure}, 69 | } 70 | 71 | vclient, err := vapi.NewClient(config) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | vaultNamespace := c.Namespace 77 | if vaultNamespace != "" { 78 | vclient.SetNamespace(vaultNamespace) 79 | } 80 | 81 | data := map[string]interface{}{ 82 | "role": k.Role, 83 | "jwt": k.jwt, 84 | } 85 | s, err := vclient.Logical().Write(fmt.Sprintf("auth/%s/login", k.Cluster), data) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | vclient.SetToken(s.Auth.ClientToken) 91 | return vclient, nil 92 | } 93 | -------------------------------------------------------------------------------- /pkg/vault/token.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package vault 18 | 19 | import ( 20 | "crypto/tls" 21 | "net/http" 22 | 23 | vapi "github.com/hashicorp/vault/api" 24 | ) 25 | 26 | var _ AuthProvider = (*TokenProvider)(nil) 27 | 28 | // TokenProvider connects to vaut using a bare token 29 | type TokenProvider struct { 30 | Token string 31 | } 32 | 33 | // NewTokenProvider creates a pointer to a TokenProvider 34 | func NewTokenProvider(token string) *TokenProvider { 35 | return &TokenProvider{ 36 | Token: token, 37 | } 38 | } 39 | 40 | // Login - godoc 41 | func (t TokenProvider) Login(c *Config) (*vapi.Client, error) { 42 | log.Info("Authenticating using Token auth method") 43 | config := vapi.DefaultConfig() 44 | config.Address = c.Address 45 | config.HttpClient.Transport = &http.Transport{ 46 | TLSClientConfig: &tls.Config{InsecureSkipVerify: c.Insecure}, 47 | } 48 | 49 | vclient, err := vapi.NewClient(config) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | vaultNamespace := c.Namespace 55 | if vaultNamespace != "" { 56 | vclient.SetNamespace(vaultNamespace) 57 | } 58 | 59 | vclient.SetToken(t.Token) 60 | return vclient, nil 61 | } 62 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package version 18 | 19 | var ( 20 | // Version is the version of the operator, replaced when releasing with the correct tag 21 | // DO NOT change latest to something else, the Makefile replace the pattern "latest" ;) 22 | Version = "latest" 23 | ) 24 | --------------------------------------------------------------------------------