Repository: DaspawnW/vault-crd Branch: master Commit: 9037143a6b0c Files: 110 Total size: 274.3 KB Directory structure: gitextract_p7tbtk3k/ ├── .github/ │ └── workflows/ │ ├── codeql-analysis.yml │ └── maven.yaml ├── .gitignore ├── .mvn/ │ └── wrapper/ │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── Dockerfile ├── LICENSE ├── crd.yml ├── deploy/ │ ├── admission-webhook.yaml │ └── rbac.yaml ├── examples/ │ ├── cert.yml │ ├── certjks-rollout-redo.yml │ ├── certjks.yml │ ├── dockercfg-error.yml │ ├── dockercfg.yml │ ├── keyvalue.yml │ ├── keyvaluev2-version.yml │ ├── keyvaluev2.yml │ ├── kind/ │ │ ├── cluster.yaml │ │ ├── run.sh │ │ └── vault.yaml │ ├── pki.yml │ ├── pki_chain.yml │ ├── pkijks.yml │ └── properties.yml ├── mvnw ├── mvnw.cmd ├── pom.xml ├── readme.md └── src/ ├── main/ │ ├── java/ │ │ └── de/ │ │ └── koudingspawn/ │ │ └── vault/ │ │ ├── Constants.java │ │ ├── VaultApplication.java │ │ ├── admissionreview/ │ │ │ ├── AdmissionReviewRestService.java │ │ │ └── AdmissionReviewService.java │ │ ├── crd/ │ │ │ ├── Vault.java │ │ │ ├── VaultChangeAdjustmentCallback.java │ │ │ ├── VaultDockerCfgConfiguration.java │ │ │ ├── VaultJKSConfiguration.java │ │ │ ├── VaultList.java │ │ │ ├── VaultPkiConfiguration.java │ │ │ ├── VaultPropertiesConfiguration.java │ │ │ ├── VaultSpec.java │ │ │ ├── VaultType.java │ │ │ └── VaultVersionedConfiguration.java │ │ ├── kubernetes/ │ │ │ ├── ChangeAdjustmentService.java │ │ │ ├── EventHandler.java │ │ │ ├── KubernetesConnection.java │ │ │ ├── KubernetesService.java │ │ │ ├── Watcher.java │ │ │ ├── cache/ │ │ │ │ ├── SecretCache.java │ │ │ │ └── SecretCacheConfiguration.java │ │ │ ├── event/ │ │ │ │ ├── EventNotification.java │ │ │ │ └── EventType.java │ │ │ └── scheduler/ │ │ │ ├── RefreshConfiguration.java │ │ │ ├── RequiresRefresh.java │ │ │ ├── ScheduledRefresh.java │ │ │ ├── TypeRefreshFactory.java │ │ │ └── impl/ │ │ │ ├── CertJksRefresh.java │ │ │ ├── CertRefresh.java │ │ │ ├── CompareHash.java │ │ │ ├── DockerCfgRefresh.java │ │ │ ├── KeyValueRefresh.java │ │ │ ├── KeyValueV2Refresh.java │ │ │ ├── PkiJksRefresh.java │ │ │ ├── PkiRefresh.java │ │ │ └── PropertiesRefresh.java │ │ └── vault/ │ │ ├── TypedSecretGenerator.java │ │ ├── TypedSecretGeneratorFactory.java │ │ ├── VaultCommunication.java │ │ ├── VaultConfiguration.java │ │ ├── VaultHealthCheck.java │ │ ├── VaultSecret.java │ │ ├── VaultService.java │ │ ├── communication/ │ │ │ ├── SecretNotAccessibleException.java │ │ │ ├── TokenLookup.java │ │ │ └── TokenLookupData.java │ │ └── impl/ │ │ ├── CertGenerator.java │ │ ├── CertJksGenerator.java │ │ ├── DockerCfgGenerator.java │ │ ├── EncryptionUtils.java │ │ ├── KeyValueGenerator.java │ │ ├── KeyValueV2Generator.java │ │ ├── PkiJksGenerator.java │ │ ├── PkiSecretGenerator.java │ │ ├── PropertiesGenerator.java │ │ ├── Sha256.java │ │ ├── SharedVaultResponseMapper.java │ │ ├── dockercfg/ │ │ │ └── PullSecret.java │ │ ├── pki/ │ │ │ ├── PKIRequest.java │ │ │ ├── PKIResponse.java │ │ │ └── VaultResponseData.java │ │ └── properties/ │ │ └── VaultJinjaLookup.java │ └── resources/ │ └── application.properties └── test/ ├── java/ │ └── de/ │ └── koudingspawn/ │ └── vault/ │ ├── CertChainTest.java │ ├── CertTest.java │ ├── DockerCfgTest.java │ ├── EventNotificationTest.java │ ├── KeyValueTest.java │ ├── KeyValueV2Test.java │ ├── OwnerReferenceBugfixTest.java │ ├── PKIChainTest.java │ ├── PKITest.java │ ├── PropertiesTest.java │ ├── TestHelper.java │ ├── admissionreview/ │ │ └── AdmissionReviewTest.java │ ├── kubernetes/ │ │ ├── EventHandlerTest.java │ │ └── KubernetesServiceTest.java │ └── vault/ │ └── VaultHealthCheckTest.java └── resources/ ├── application.properties ├── test.properties └── vault-crd.yaml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ master ] pull_request: # The branches below must be a subset of the branches above branches: [ master ] schedule: - cron: '26 9 * * 0' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'java' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://git.io/codeql-language-support steps: - name: Checkout repository uses: actions/checkout@v2 - name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: 17 cache: 'maven' # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main - run: mvn -B package --file pom.xml -Dspring.profiles.active=test -DskipTests - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 ================================================ FILE: .github/workflows/maven.yaml ================================================ name: Java CI on: [push] jobs: test: name: "Test" runs-on: ubuntu-latest strategy: matrix: kubernetes_version: - "kindest/node:v1.29.1@sha256:a0cc28af37cf39b019e2b448c54d1a3f789de32536cb5a5db61a49623e527144" - "kindest/node:v1.28.6@sha256:b7e1cf6b2b729f604133c667a6be8aab6f4dde5bb042c1891ae248d9154f665b" - "kindest/node:v1.27.10@sha256:3700c811144e24a6c6181065265f69b9bf0b437c45741017182d7c82b908918f" - "kindest/node:v1.26.13@sha256:15ae92d507b7d4aec6e8920d358fc63d3b980493db191d7327541fbaaed1f789" steps: - uses: actions/checkout@v3 - name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: 17 cache: 'maven' - uses: helm/kind-action@v1.5.0 with: version: "v0.17.0" node_image: "${{ matrix.kubernetes_version }}" - name: "Kubernetes version" run: | kubectl version - name: "Create Custom Resource" run: | kubectl apply -f crd.yml - name: Build with Maven run: mvn -B package --file pom.xml -Dspring.profiles.active=test docker-push: name: Docker Push (GHCR & public ECR) runs-on: ubuntu-latest if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/heads/docker-') permissions: id-token: write contents: read packages: write needs: test steps: - uses: actions/checkout@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - name: Set output id: vars run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} - name: Docker publish uses: daspawnw/docker-multi-build-push-action@master with: platforms: "linux/amd64,linux/arm64" docker-tag: "${{ steps.vars.outputs.tag }}" ghcr-enabled: "true" ghcr-token: "${{ secrets.GITHUB_TOKEN }}" ecr-enabled: ${{ github.repository == 'daspawnw/vault-crd' }} ecr-role-to-assume: "${{ secrets.AWS_PUBLIC_ECR_ARN }}" ecr-repository-url: "public.ecr.aws/l2l6k4u5/vault-crd" ================================================ FILE: .gitignore ================================================ target/* .idea/* *.iml vault/* ================================================ FILE: .mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.3/apache-maven-3.5.3-bin.zip ================================================ FILE: Dockerfile ================================================ FROM gcr.io/distroless/java17-debian11:nonroot AS SECURITY FROM openjdk:17 AS BUILD COPY . /opt WORKDIR /opt RUN ./mvnw clean install -DskipTests ENV JAVA_RANDOM="file:/dev/./urandom" COPY --from=SECURITY /etc/java-17-openjdk/security/java.security /java.security RUN echo "networkaddress.cache.ttl=60" >> /java.security RUN sed -i -e "s@^securerandom.source=.*@securerandom.source=${JAVA_RANDOM}@" /java.security FROM gcr.io/distroless/java17-debian11:nonroot COPY --from=BUILD /opt/target/vault-crd.jar /opt/vault-crd.jar COPY --from=BUILD /java.security /etc/java-17-openjdk/security/java.security ENTRYPOINT ["/usr/bin/java", "-Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts", "-Djavax.net.ssl.trustStorePassword=changeit", "-Djavax.net.ssl.trustStoreType=jks", "-Dkeystore.pkcs12.legacy"] CMD ["-jar", "/opt/vault-crd.jar"] ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" files file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE files file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE files from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The files should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: crd.yml ================================================ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: vault.koudingspawn.de spec: group: koudingspawn.de scope: Namespaced names: plural: vault singular: vault kind: Vault shortNames: - vt versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: path: type: string pattern: '^.*?\/.*?(\/.*?)?$' type: type: string enum: - PKI - PKIJKS - CERT - CERTJKS - DOCKERCFG - KEYVALUE - KEYVALUEV2 - PROPERTIES pkiConfiguration: type: object properties: commonName: type: string altNames: type: string ipSans: type: string ttl: type: string pattern: '^[0-9]{1,}[hm]$' jksConfiguration: type: object properties: password: type: string alias: type: string keyName: type: string caAlias: type: string versionConfiguration: type: object properties: version: type: integer propertiesConfiguration: type: object properties: context: type: object x-kubernetes-preserve-unknown-fields: true files: type: object x-kubernetes-preserve-unknown-fields: true dockerCfgConfiguration: type: object properties: type: type: string enum: - KEYVALUE - KEYVALUEV2 version: type: integer changeAdjustmentCallback: type: object properties: type: type: string name: type: string required: - type ================================================ FILE: deploy/admission-webhook.yaml ================================================ apiVersion: v1 kind: Service metadata: name: vault-crd namespace: vault-crd spec: selector: app: vault-crd ports: - port: 8080 type: ClusterIP --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: labels: app: vault-crd name: vault-crd-admission webhooks: - name: validate.vault.koudingspawn.de admissionReviewVersions: ["v1"] sideEffects: None rules: - apiGroups: - koudingspawn.de apiVersions: - v1 operations: - CREATE - UPDATE resources: - vault failurePolicy: Fail clientConfig: service: namespace: vault-crd name: vault-crd path: /validation/vault-crd port: 8080 caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURRVENDQWltZ0F3SUJBZ0lVUDk4cG9lNXF0TVExWXhrVS85eHc5ZGp4N05Bd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0dqRVlNQllHQTFVRUF4TVBhMjkxWkdsdVozTndZWGR1TG1SbE1CNFhEVEl3TURZeU5ERTVNVEF4TkZvWApEVE13TURZeU1qRTVNVEEwTkZvd0dqRVlNQllHQTFVRUF4TVBhMjkxWkdsdVozTndZWGR1TG1SbE1JSUJJakFOCkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXNWbklIY2JCeHl2NmpOS3V5YkZpMFNkbEtONlAKU1U2RlJOUzJwQ1NNK3R3eWtCblllZUtacTMxQmZYQW5EMlVrR1dod0gwWHF1QmJ4S3Bsa2lYWVVMT25qTFQwLwpWVG5LN1U2NWFWZ0VMZlZJVFNHRnJFcjMwdTdYbWN5NkNWZmkzd25CMjZkZnR1V2NSWlFoaXIxZUE4VUZjejBQCkxlTWhiSTRybENWcnU2bHFFTzl0bGRId21ScGc5dWYyYnJiTi9PNDlaSUtYVGRBSW5jVTVacnV3d21MOVpnbUIKc2tzaDBvZWFkaXpMbzRuSmNjdVZYQjlwMHJjcjBPdG5qSHo1SGdCUElzTDB6UVZMUzVSZ01CTkxDbTVnZjlrcAp4S1UzQ0ZnU0Z0Vng5WlE3aG9hUEl3VnRqWUNrcCtzQVBycjBQNkk0Um95c2U3RDB2UERQUCs5NDF3SURBUUFCCm8zOHdmVEFPQmdOVkhROEJBZjhFQkFNQ0FRWXdEd1lEVlIwVEFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVUKa0hrOXBHMkg0eTJac0ZEUVZlNGJVTGhoMCtrd0h3WURWUjBqQkJnd0ZvQVVrSGs5cEcySDR5MlpzRkRRVmU0YgpVTGhoMCtrd0dnWURWUjBSQkJNd0VZSVBhMjkxWkdsdVozTndZWGR1TG1SbE1BMEdDU3FHU0liM0RRRUJDd1VBCkE0SUJBUUFoMW9pMy9iRW5Lb1Rxdkt2NEZZeVJKVDQzMkIxYkp2MG94ZERLaFJndVowYmQ1WXVOMHBnNTcxL2QKb1UvVUN6ellzaUVYZmw3NHREUndNWVUveXhlQVJKQ3B2RWswcVhOdHJlS1hZL0dDU0wxbjlKU1dTMk1xVDBBeQpuTWxqdEkrd3R5Ujh2MW05SnppNFdFNHdWdVRuclhlb1BSeVpKR0F1Q2xRbnk2VW5Fei9LS3ZvQ0pCQW1UY1NKCk5hZkNwYUh3b25sQVp5bXZRN0JQZHZoMk52ckdQazk2aEVZc1lnUVl6VW5KcURoT0Z2RWF1MjRLeEk2NlpXUnIKMGlSNkZmTTQ2ZjBNRVdqdmRSUGRucHh2dDhPbTBiNjVER0czc2hVTHNLZWJENFI4YjdCeTdrVUV0U1FkNVkzcwpHK1k5RTdpbXdWR1hlUTh1eWw3ZGNSV1AwTkJ1Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K ================================================ FILE: deploy/rbac.yaml ================================================ apiVersion: v1 kind: Namespace metadata: name: vault-crd --- apiVersion: v1 kind: ServiceAccount metadata: name: vault-crd-serviceaccount namespace: vault-crd --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: vault-crd-clusterrole rules: - apiGroups: - apiextensions.k8s.io resources: - customresourcedefinitions verbs: - get - apiGroups: - "koudingspawn.de" resources: - vault verbs: - list - watch - get - apiGroups: - "" resources: - secrets - events verbs: - get - create - patch - update - delete - watch - list - apiGroups: - extensions - apps resources: - deployments verbs: - update - get - patch --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: vault-crd-clusterrole-binding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: vault-crd-clusterrole subjects: - kind: ServiceAccount name: vault-crd-serviceaccount namespace: vault-crd --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: vault.koudingspawn.de spec: group: koudingspawn.de scope: Namespaced names: plural: vault singular: vault kind: Vault shortNames: - vt versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: path: type: string pattern: '^.*?\/.*?(\/.*?)?$' type: type: string enum: - PKI - PKIJKS - CERT - CERTJKS - DOCKERCFG - KEYVALUE - KEYVALUEV2 - PROPERTIES pkiConfiguration: type: object properties: commonName: type: string altNames: type: string ipSans: type: string ttl: type: string pattern: '^[0-9]{1,}[hm]$' jksConfiguration: type: object properties: password: type: string alias: type: string keyName: type: string caAlias: type: string versionConfiguration: type: object properties: version: type: integer propertiesConfiguration: type: object properties: context: type: object x-kubernetes-preserve-unknown-fields: true files: type: object x-kubernetes-preserve-unknown-fields: true dockerCfgConfiguration: type: object properties: type: type: string enum: - KEYVALUE - KEYVALUEV2 version: type: integer changeAdjustmentCallback: type: object properties: type: type: string name: type: string required: - type --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: vault-crd name: vault-crd namespace: vault-crd spec: selector: matchLabels: app: vault-crd replicas: 1 template: metadata: labels: app: vault-crd spec: serviceAccountName: vault-crd-serviceaccount # initContainers: # - name: convert-https # image: shamelesscookie/openssl:1.1.1g # command: # - /bin/bash # args: # - "-c" # - "openssl pkcs12 -export -in /opt/certificate/tls.crt -inkey /opt/certificate/tls.key -out /opt/target/keystore.p12 -passout pass:changeit -name admission-tls" # volumeMounts: # - mountPath: /opt/certificate # name: pem-cert # - mountPath: /opt/target # name: pkcs12-cert containers: - name: vault-crd image: daspawnw/vault-crd:1.11.0 env: - name: KUBERNETES_VAULT_URL value: "http://localhost:8080/v1/" - name: KUBERNETES_VAULT_TOKEN valueFrom: secretKeyRef: name: vault-token key: token # - name: SERVER_SSL_KEY-STORE-TYPE # value: PKCS12 # - name: SERVER_SSL_KEY-STORE # value: "/opt/certificate/keystore.p12" # - name: SERVER_SSL_KEY-STORE-PASSWORD # value: changeit # - name: SERVER_SSL_KEY-ALIAS # value: "admission-tls" ports: - containerPort: 8080 livenessProbe: httpGet: port: 8080 path: "/actuator/health" # scheme: HTTPS initialDelaySeconds: 30 failureThreshold: 3 periodSeconds: 30 successThreshold: 1 timeoutSeconds: 5 # volumeMounts: # - mountPath: /opt/certificate # name: pkcs12-cert # volumes: # - name: pem-cert # secret: # secretName: vault-crd-tls # - name: pkcs12-cert # emptyDir: {} restartPolicy: Always --- apiVersion: v1 kind: Secret metadata: name: vault-token namespace: vault-crd data: token: "cm9vdA==" --- #apiVersion: v1 #kind: Secret #metadata: # name: vault-crd-tls # namespace: vault-crd #data: # tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURjakNDQWxxZ0F3SUJBZ0lVY3d6Z0hrdjRhOXpHOE5hQm1rbVFPdGxZM3hjd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0dqRVlNQllHQTFVRUF4TVBhMjkxWkdsdVozTndZWGR1TG1SbE1CNFhEVEl3TURZeU5ERTVNVEExTUZvWApEVEl3TURjeU5qRTVNVEV5TUZvd0hqRWNNQm9HQTFVRUF4TVRkbUYxYkhRdFkzSmtMblpoZFd4MExXTnlaRENDCkFTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTG84Z2lWMjQ5UTArazMvenFnY2xVN3oKTisyT2N1VDNERG5JbXZaYTNiOXZGYjRXNVhOaXlpT0xtbHhnMDB3RGkyV01DcEI1RldmRUQwQU5KQUpWMEdRdwowVmlFTG5TVDRJYTRUcTlxUWlvc0J0RXd5Vkx1QkZSU1RHTDJiSUV5K3dBaGlrQ3dmbEI2L2trN05VbHlXZG8rCkRtcUorbDQ4RnVkbytpdTJyYkxtR0lnMTFDTy8rUTJlRnZCaTBaTTZEUzliUVNYOUYxTmMxdFZyNndtSWNOTHQKNzRHT2JQaG5rbFBaQjMrUzRFTk8xbHhzcCtkNGw0QkZEYWJWMHdCdWVmaFdqdktMSlZNRzlOWWZkWjdBaXArZwpYWWhpYVpPQS9xTlhSTDQzWlY1OXBBS2NtNWlFdUFLMStrYVBuWUdYQUtRRFE3L29hSlJwbEVqT1VLVHRObDBDCkF3RUFBYU9CcXpDQnFEQU9CZ05WSFE4QkFmOEVCQU1DQTZnd0hRWURWUjBsQkJZd0ZBWUlLd1lCQlFVSEF3RUcKQ0NzR0FRVUZCd01DTUIwR0ExVWREZ1FXQkJRNjFvN2cwVllHc2pZZTJ2bkVKQ29LTElvYnREQWZCZ05WSFNNRQpHREFXZ0JTUWVUMmtiWWZqTFptd1VOQlY3aHRRdUdIVDZUQTNCZ05WSFJFRU1EQXVnaE4yWVhWc2RDMWpjbVF1CmRtRjFiSFF0WTNKa2doZDJZWFZzZEMxamNtUXVkbUYxYkhRdFkzSmtMbk4yWXpBTkJna3Foa2lHOXcwQkFRc0YKQUFPQ0FRRUFSVTVud05rb24xRWdxZWVQNnRHZzZiNmY5TC94cnd5bHFndURyWVJOUmIyeGl1ZTV0UDREZFNVZAo2eXBVNXdsWGdHTmlmcHBpeVhSbFlIZUtIT09jRUtlTlByb25KYnBaa25Wb3ZpaVU2aWhJNFBLM3lRRW51N2twCjhjdldxVDh2WVVJa2VwV0dEd2NYRTRNNWlSelJ5VXVYSkxXZk4yRnJjTlJ1RUdEL3JKRFlwQ00rTDNDd0U5TFgKbmFCaDRJTG1HZjJmMHpZaVZMWTN0bTF4c1E2ZGZCQlZMMHZDZmlBVzlKYkNGbTVSOXRySWttcXdBY09lQTFGUApMdExUNjJ5OHRZbjFFK0h6ZTVneVRBdXpoRHhHbmxXRGtkRDJVSXMrbUxqcWxOVzZrcTI0NHFJOUdZZ3drVFJiCm1DOURyZ2lFRWFIQi9yN2lBZVNaWHZkUzg2ckVpZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K # tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBdWp5Q0pYYmoxRFQ2VGYvT3FCeVZUdk0zN1k1eTVQY01PY2lhOWxyZHYyOFZ2aGJsCmMyTEtJNHVhWEdEVFRBT0xaWXdLa0hrVlo4UVBRQTBrQWxYUVpERFJXSVF1ZEpQZ2hyaE9yMnBDS2l3RzBUREoKVXU0RVZGSk1ZdlpzZ1RMN0FDR0tRTEIrVUhyK1NUczFTWEpaMmo0T2FvbjZYandXNTJqNks3YXRzdVlZaURYVQpJNy81RFo0VzhHTFJrem9OTDF0QkpmMFhVMXpXMVd2ckNZaHcwdTN2Z1k1cytHZVNVOWtIZjVMZ1EwN1dYR3luCjUzaVhnRVVOcHRYVEFHNTUrRmFPOG9zbFV3YjAxaDkxbnNDS242QmRpR0pwazREK28xZEV2amRsWG4ya0FweWIKbUlTNEFyWDZSbytkZ1pjQXBBTkR2K2hvbEdtVVNNNVFwTzAyWFFJREFRQUJBb0lCQUh5TjhXRUxGYTZjUy9lVQpxV3NIeXRnRmxKY2RtVHdHK2pjL01seW5RdjFBVnlOTi91RmY1ZDlHQTlQYXNoWjVuR1lxOWZuUDhYLzN3VmRPCk1wSVpRSWx4bU9HQmJleHI1bE5UdXRSWTFhMk15blpvRVkyVVFITUFvN1BnS1l0elJDbS9STTZrKzZYcHpGMi8KNnBDWG1QNThXSG5xay9jb2F3MFR5WlVvMVJ6NjMvemxSYXJBMXdqQVVGMDNJRElNTFlzYWxCZld5Nm9neEhHZQpuOC95Z1E4cyt5Z29UY3FZb1hHYnZ2a01IOVB6b1oyZko3RWlrTW9Cb29jcjRxbjZZYUFIb3BJR0dMYmM5MFdsCmZVUHBheEtkcDMzUTlXK1MzbU11bm9zUk9MbHdqMS9HTHhaKzJWRkZqUTJseHpBOGdqOW1SV0tabzhQYnVHUVEKU0p4cFhVRUNnWUVBNnZhWDFQSm4yUUNxeCtET3k4SnZFNWp6WVBGcTlFR0FpRFg5VENTb3R4U1d2OTVOUVh0MQpBR1d1RmVETE1YSmZqejhibldwdVVuVzRqK0tNRnRaUDNMZ1Nmd3ZTUnVnNGVsQ1RHVEllWmJmQ0pqTGlrNVNPCjJ6SVBiK2xtL3Z3eXU5V3NkZEh3OVIyUjc3eWlzWkkxZlBDMndTTzZzRjlxT21rL2t1RGZibzBDZ1lFQXl1a1YKcjI3andnM2FpMEJLdTNhcmhSRExqbklzSnF5Qy9yUVJEUzlVQjVuTks1L1l4K1o4S0R5M2toMmV2YXNic1Q0cgozcFUxQXdKOGZDQnRXaC93YXRVYlNRZkNOQ0ppZ2hsbUpYcTJDeEVRUUllN1d5MGpNeWpINzJodVgwMGY0dFJWCkt1clVpK2V0SjA3NHBvbHNUaG9DUnZqbzdyaHREY3R3UmM2bUd4RUNnWUVBdUNoS1hJY1p5Y1Z5RlhNbjRpQWsKdXpGNElCVllCTldLRGpoeXJVbFdTeGlDQnlRUFhUR01SS0Z0VG94LzllTjA3bXRDRTZFbGtzL2R0amlVSUJvZApRaHVyczVQcVhkVUkzeVZrQmExNGtiVHpJTWxsT05LSkhWZ2hMVSs4Z0VIZTZjWFJoQTdtVXRlNFdEUjdONzRtCjJpUTR1U3h0MkdzUWNYT29kbEIyRHNrQ2dZRUF5VDZwZ2toaDNlbnRrZVNlK2hSMWd0RW9na3ZjWERNRzdPVGMKY0k0N01ocXBjWlhrNUVaRlo0Ym9yaU53ZUQ3SGhWL2JGTFE1VXBYWnJ5WmVMbCsxQzgvMmN0VWVHS1R0dklqQwpWWFBDTDNHcUE4WmEzTkFFdEUzREZrQW1ENkVuZWNvTCtqZlR2RHAzOHArUlgySzJwek9HaEt1RUlwZUptWC9uCkIyVXdPM0VDZ1lFQTRYK25CRkRsNEdXbFNQU2VKZzZ4L3JWUXFyZUkwcmIrSUViNGhGOVZBcndEc2Z6L0hnb3cKSGpDTkR0N2t0YlJVTzlsNTJaZ3YrbWlOS3Z5dnNCbUNQalA5d0k4Y2hUZlhOZ1ZIYzE5YzJLTWRxTkdIckw1YwpTdytHMkl3MFRjREpQeFZyNGxkN01NNFZtYmtIKzBUVEtyVThLWkZQc2o2RGYraS9mMSs0NEE0PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= ================================================ FILE: examples/cert.yml ================================================ apiVersion: "koudingspawn.de/v1" kind: Vault metadata: name: test-cert spec: path: "keyvaluev1/vault.koudingspawn.de" type: "CERT" ================================================ FILE: examples/certjks-rollout-redo.yml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: nginx labels: app: nginx spec: replicas: 1 template: metadata: name: nginx labels: app: nginx spec: containers: - name: nginx image: nginx:latest imagePullPolicy: IfNotPresent restartPolicy: Always selector: matchLabels: app: nginx --- apiVersion: "koudingspawn.de/v1" kind: Vault metadata: name: test-rollout-redo-certjks spec: path: "keyvaluev1/vault.koudingspawn.de" type: "CERTJKS" changeAdjustmentCallback: type: deployment name: nginx ================================================ FILE: examples/certjks.yml ================================================ apiVersion: "koudingspawn.de/v1" kind: Vault metadata: name: test-certjks spec: path: "keyvaluev1/vault.koudingspawn.de" type: "CERTJKS" ================================================ FILE: examples/dockercfg-error.yml ================================================ apiVersion: "koudingspawn.de/v1" kind: Vault metadata: name: test-dockercfg-error spec: path: "blub/docker-hub" type: "DOCKERCFG" ================================================ FILE: examples/dockercfg.yml ================================================ apiVersion: "koudingspawn.de/v1" kind: Vault metadata: name: test-dockercfg spec: path: "keyvaluev1/docker-hub" type: "DOCKERCFG" ================================================ FILE: examples/keyvalue.yml ================================================ apiVersion: "koudingspawn.de/v1" kind: Vault metadata: name: test-keyvalue spec: path: "keyvaluev1/docker-hub" type: "KEYVALUE" ================================================ FILE: examples/keyvaluev2-version.yml ================================================ apiVersion: "koudingspawn.de/v1" kind: Vault metadata: name: test-keyvaluev2 spec: path: "keyvaluev2/example" type: "KEYVALUEV2" versionConfiguration: version: 2 ================================================ FILE: examples/keyvaluev2.yml ================================================ apiVersion: "koudingspawn.de/v1" kind: Vault metadata: name: test-keyvaluev2 spec: path: "keyvaluev2/example" type: "KEYVALUEV2" ================================================ FILE: examples/kind/cluster.yaml ================================================ kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane extraPortMappings: - containerPort: 30078 hostPort: 8200 ================================================ FILE: examples/kind/run.sh ================================================ #!/usr/bin/env bash ### setup kind cluster kind create cluster --config $PWD/cluster.yaml ### it exposes at 8200 a port for vault ### install vault with a static token kind get kubeconfig > ~/.kube/kind_config export KUBECONFIG="$HOME/.kube/kind_config" kubectl create namespace vault kubectl apply -f vault.yaml --namespace vault while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:8200/ui/)" != "200" ]]; do sleep 5; done echo "Vault is up and running" export VAULT_ADDR="http://localhost:8200" export VAULT_TOKEN="root" ### end: install vault with a static token ### deploy vault-crd kubectl apply -f ../../deploy/rbac.yaml kubectl apply -f ../../deploy/admission-webhook.yaml ### end: deploy vault-crd ### configure vault vault secrets enable -version=1 --path=keyvaluev1 kv echo "Configure vault with default values" vault write keyvaluev1/docker-hub url=registry.gitlab.com username=username password=VERYSECURE email=john.doe@test.com vault secrets enable -path=testpki -description=testpki pki vault secrets tune -max-lease-ttl=8760h testpki vault write testpki/root/generate/internal \ common_name=koudingspawn.de \ ttl=8500h vault write testpki/roles/testrole \ allowed_domains=koudingspawn.de \ allow_subdomains=true \ max_ttl=200h vault write -format=json testpki/issue/testrole common_name=vault.koudingspawn.de > data.json vault write keyvaluev1/vault.koudingspawn.de @data.json rm data.json vault secrets enable -version=2 --path=keyvaluev2 kv vault kv put keyvaluev2/example key=first-version value=first-version vault kv put keyvaluev2/example key=second-version value=second-version vault kv put keyvaluev2/example key=third-version value=third-version vault kv put keyvaluev2/example key=fourth-version value=fourth-version vault kv put keyvaluev2/database/root username=root password=really vault write keyvaluev1/database/host host=localhost ### end: configure vault ================================================ FILE: examples/kind/vault.yaml ================================================ --- # Source: vault/templates/server-serviceaccount.yaml apiVersion: v1 kind: ServiceAccount metadata: name: vault namespace: vault labels: helm.sh/chart: vault-0.6.0 app.kubernetes.io/name: vault app.kubernetes.io/instance: vault app.kubernetes.io/managed-by: Helm --- # Source: vault/templates/server-clusterrolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: vault-server-binding labels: helm.sh/chart: vault-0.6.0 app.kubernetes.io/name: vault app.kubernetes.io/instance: vault app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: system:auth-delegator subjects: - kind: ServiceAccount name: vault namespace: vault --- # Source: vault/templates/server-headless-service.yaml # Service for Vault cluster apiVersion: v1 kind: Service metadata: name: vault-internal namespace: vault labels: helm.sh/chart: vault-0.6.0 app.kubernetes.io/name: vault app.kubernetes.io/instance: vault app.kubernetes.io/managed-by: Helm annotations: service.alpha.kubernetes.io/tolerate-unready-endpoints: "true" spec: clusterIP: None publishNotReadyAddresses: true ports: - name: "http" port: 8200 targetPort: 8200 - name: https-internal port: 8201 targetPort: 8201 selector: app.kubernetes.io/name: vault app.kubernetes.io/instance: vault component: server --- # Source: vault/templates/server-service.yaml # Service for Vault cluster apiVersion: v1 kind: Service metadata: name: vault namespace: vault labels: helm.sh/chart: vault-0.6.0 app.kubernetes.io/name: vault app.kubernetes.io/instance: vault app.kubernetes.io/managed-by: Helm annotations: # This must be set in addition to publishNotReadyAddresses due # to an open issue where it may not work: # https://github.com/kubernetes/kubernetes/issues/58662 service.alpha.kubernetes.io/tolerate-unready-endpoints: "true" spec: type: NodePort # We want the servers to become available even if they're not ready # since this DNS is also used for join operations. publishNotReadyAddresses: true ports: - name: http port: 8200 targetPort: 8200 nodePort: 30078 - name: https-internal port: 8201 targetPort: 8201 selector: app.kubernetes.io/name: vault app.kubernetes.io/instance: vault component: server --- # Source: vault/templates/server-statefulset.yaml # StatefulSet to run the actual vault server cluster. apiVersion: apps/v1 kind: StatefulSet metadata: name: vault namespace: vault labels: app.kubernetes.io/name: vault app.kubernetes.io/instance: vault app.kubernetes.io/managed-by: Helm spec: serviceName: vault-internal podManagementPolicy: Parallel replicas: 1 updateStrategy: type: OnDelete selector: matchLabels: app.kubernetes.io/name: vault app.kubernetes.io/instance: vault component: server template: metadata: labels: helm.sh/chart: vault-0.6.0 app.kubernetes.io/name: vault app.kubernetes.io/instance: vault component: server spec: terminationGracePeriodSeconds: 10 serviceAccountName: vault securityContext: runAsNonRoot: true runAsGroup: 1000 runAsUser: 100 fsGroup: 1000 volumes: - name: home emptyDir: {} containers: - name: vault image: vault:1.4.2 imagePullPolicy: IfNotPresent command: args: env: - name: HOST_IP valueFrom: fieldRef: fieldPath: status.hostIP - name: POD_IP valueFrom: fieldRef: fieldPath: status.podIP - name: VAULT_K8S_POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: VAULT_K8S_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: VAULT_ADDR value: "http://127.0.0.1:8200" - name: VAULT_API_ADDR value: "http://$(POD_IP):8200" - name: SKIP_CHOWN value: "true" - name: SKIP_SETCAP value: "true" - name: HOSTNAME valueFrom: fieldRef: fieldPath: metadata.name - name: VAULT_CLUSTER_ADDR value: "https://$(HOSTNAME).vault-internal:8201" - name: HOME value: "/home/vault" - name: VAULT_DEV_ROOT_TOKEN_ID value: "root" volumeMounts: - name: home mountPath: /home/vault ports: - containerPort: 8200 name: http - containerPort: 8201 name: https-internal - containerPort: 8202 name: http-rep readinessProbe: # Check status; unsealed vault servers return 0 # The exit code reflects the seal status: # 0 - unsealed # 1 - error # 2 - sealed exec: command: ["/bin/sh", "-ec", "vault status -tls-skip-verify"] failureThreshold: 2 initialDelaySeconds: 5 periodSeconds: 3 successThreshold: 1 timeoutSeconds: 5 lifecycle: # Vault container doesn't receive SIGTERM from Kubernetes # and after the grace period ends, Kube sends SIGKILL. This # causes issues with graceful shutdowns such as deregistering itself # from Consul (zombie services). preStop: exec: command: [ "/bin/sh", "-c", # Adding a sleep here to give the pod eviction a # chance to propagate, so requests will not be made # to this pod while it's terminating "sleep 5 && kill -SIGTERM $(pidof vault)", ] ================================================ FILE: examples/pki.yml ================================================ apiVersion: "koudingspawn.de/v1" kind: Vault metadata: name: test-pki spec: path: "testpki/issue/testrole" type: "PKI" pkiConfiguration: commonName: "vault.koudingspawn.de" ttl: "7m" ================================================ FILE: examples/pki_chain.yml ================================================ apiVersion: "koudingspawn.de/v1" kind: Vault metadata: name: test-pki spec: path: "pki_int/issue/testrole" type: "PKI" pkiConfiguration: commonName: "localhost" ttl: "7m" ================================================ FILE: examples/pkijks.yml ================================================ apiVersion: "koudingspawn.de/v1" kind: Vault metadata: name: test-pkijks spec: path: "testpki/issue/testrole" type: "PKIJKS" jksConfiguration: caAlias: CARoot pkiConfiguration: commonName: "vault.koudingspawn.de" ttl: "7m" ================================================ FILE: examples/properties.yml ================================================ apiVersion: "koudingspawn.de/v1" kind: Vault metadata: name: properties-example spec: type: "PROPERTIES" propertiesConfiguration: context: contextKey: value files: test.properties: | test={{ contextKey }} datasource.username={{ vault.lookupV2('keyvaluev2/database/root').get('username') }} datasource.password={{ vault.lookupV2('keyvaluev2/database/root').get('password') }} datasource.host={{ vault.lookup('keyvaluev1/database/host', 'host') }} ================================================ FILE: mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven2 Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "`uname`" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home PRG="$0" # need this for relative symlinks while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG="`dirname "$PRG"`/$link" fi done saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi # For Migwn, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" # TODO classpath? fi if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=`which readlink` if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then if $darwin ; then javaHome="`dirname \"$javaExecutable\"`" javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" else javaExecutable="`readlink -f \"$javaExecutable\"`" fi javaHome="`dirname \"$javaExecutable\"`" javaHome=`expr "$javaHome" : '\(.*\)/bin'` JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="`which java`" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} echo $MAVEN_PROJECTBASEDIR MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM http://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Maven2 Start Up Batch script @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%" == "on" pause if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% exit /B %ERROR_CODE% ================================================ FILE: pom.xml ================================================ 4.0.0 de.koudingspawn vault 0.0.1-SNAPSHOT jar vault-crd Vault CRD for sharing Vault Secrets to Kubernetes org.springframework.boot spring-boot-starter-parent 3.1.3 UTF-8 UTF-8 17 6.8.1 org.springframework.boot spring-boot-starter org.hibernate.validator hibernate-validator 8.0.1.Final org.springframework.boot spring-boot-starter-web org.springframework.vault spring-vault-core 3.0.4 org.springframework.boot spring-boot-starter-actuator io.fabric8 kubernetes-client ${fabric8.version} com.github.ben-manes.caffeine caffeine 3.1.8 com.hubspot.jinjava jinjava 2.7.1 org.springframework.boot spring-boot-starter-test test com.github.tomakehurst wiremock 3.0.1 test org.bouncycastle bcpkix-jdk15on 1.70 test org.junit.vintage junit-vintage-engine test ${project.name} org.springframework.boot spring-boot-maven-plugin org.apache.maven.plugins maven-compiler-plugin 3.10.1 17 true -XDignore.symbol.file ================================================ FILE: readme.md ================================================ # What is Vault-CRD? Vault-CRD is a custom resource definition for holding secrets that are stored in HashiCorp Vault up to date with Kubernetes secrets. The following Secret engines of Vault are supported: * KV (Version 1) * KV (Version 2) * PKI The following types of secrets can be managed by Vault-CRD: * Docker Pull Secret (DockerCfg) * Ingress Certificates * JKS Key Stores For more details please see: [https://vault.koudingspawn.de/how-does-vault-crd-work](https://vault.koudingspawn.de/how-does-vault-crd-work) ## Note Due to Docker's decision to discontinue its Free Teams, I decided to host my Docker images on GHCR (GitHub Container Registry) and public ECR (Elastic Container Registry) in the future. ================================================ FILE: src/main/java/de/koudingspawn/vault/Constants.java ================================================ package de.koudingspawn.vault; public class Constants { private Constants() { } public static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm'Z'"; public static final String COMPARE_ANNOTATION = "/compare"; public static final String LAST_UPDATE_ANNOTATION = "/lastUpdated"; } ================================================ FILE: src/main/java/de/koudingspawn/vault/VaultApplication.java ================================================ package de.koudingspawn.vault; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableScheduling public class VaultApplication { public static void main(String[] args) { SpringApplication.run(VaultApplication.class, args); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/admissionreview/AdmissionReviewRestService.java ================================================ package de.koudingspawn.vault.admissionreview; import io.fabric8.kubernetes.api.model.admission.v1.AdmissionResponse; import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview; import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReviewBuilder; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/validation/vault-crd") public class AdmissionReviewRestService { private final AdmissionReviewService admissionReviewService; public AdmissionReviewRestService(AdmissionReviewService admissionReviewService) { this.admissionReviewService = admissionReviewService; } @PostMapping public AdmissionReview validate(@RequestBody AdmissionReview admissionRequest) { AdmissionResponse admissionResponse = admissionReviewService.validate(admissionRequest.getRequest()); return new AdmissionReviewBuilder() .withResponse(admissionResponse) .build(); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/admissionreview/AdmissionReviewService.java ================================================ package de.koudingspawn.vault.admissionreview; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.vault.VaultService; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import io.fabric8.kubernetes.api.model.Status; import io.fabric8.kubernetes.api.model.StatusBuilder; import io.fabric8.kubernetes.api.model.admission.v1.AdmissionRequest; import io.fabric8.kubernetes.api.model.admission.v1.AdmissionResponse; import io.fabric8.kubernetes.api.model.admission.v1.AdmissionResponseBuilder; import io.fabric8.kubernetes.client.utils.Serialization; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @Service public class AdmissionReviewService { private static final Logger log = LoggerFactory.getLogger(AdmissionReviewService.class); private final VaultService vaultService; public AdmissionReviewService(VaultService vaultService) { this.vaultService = vaultService; } public AdmissionResponse validate(AdmissionRequest admissionRequest) { try { String s = Serialization.asYaml(admissionRequest.getObject()); Vault vault = Serialization.unmarshal(s, Vault.class); vaultService.generateSecret(vault); } catch (ClassCastException ex) { log.error("Received Admission Request of invalid type!"); return invalidRequest(admissionRequest.getUid(), "Received Admission Request of invalid type!"); } catch (SecretNotAccessibleException e) { log.error("Admission Request failed with Secret not Accessible Exception", e); return invalidRequest(admissionRequest.getUid(), e.getMessage()); } return validRequest(admissionRequest.getUid()); } private AdmissionResponse validRequest(String uuid) { return new AdmissionResponseBuilder() .withAllowed(true) .withUid(uuid) .build(); } private AdmissionResponse invalidRequest(String uid, String message) { Status status = new StatusBuilder() .withCode(400) .withMessage(message) .build(); return new AdmissionResponseBuilder() .withAllowed(false) .withUid(uid) .withStatus(status) .build(); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/crd/Vault.java ================================================ package de.koudingspawn.vault.crd; import io.fabric8.kubernetes.api.model.Namespaced; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.model.annotation.*; import java.util.HashMap; import java.util.Objects; @Version(Vault.VERSION) @Group(Vault.GROUP) @Kind(Vault.KIND) @Singular(Vault.SINGULAR) @Plural(Vault.PLURAL) public class Vault extends CustomResource implements Namespaced { public static final String GROUP = "koudingspawn.de"; public static final String VERSION = "v1"; public static final String KIND = "Vault"; public static final String SINGULAR = "vault"; public static final String PLURAL = "vault"; public boolean modifyHandlerEquals(Object o) { if (o == null || getClass() != o.getClass()) return false; Vault vault = (Vault) o; // spec equals if (vault.getSpec() == null && spec != null) return false; if (vault.getSpec() != null && spec == null) return false; if (vault.getSpec() != null && spec != null) { if (!vault.getSpec().equals(spec)) return false; } // null && null => true for spec // metadata equals if (vault.getMetadata() == null && getMetadata() == null) return true; if (vault.getMetadata() == null) return false; if (getMetadata() == null) return false; // metadata.name, metadata.namespace, metadata.uid equals if (!vault.getMetadata().getName().equals(getMetadata().getName())) return false; if (!vault.getMetadata().getNamespace().equals(getMetadata().getNamespace())) return false; if (!vault.getMetadata().getUid().equals(getMetadata().getUid())) return false; // metadata.labels equals if (vault.getMetadata().getLabels() == null && getMetadata().getLabels() != null) return false; if (vault.getMetadata().getLabels() != null && getMetadata().getLabels() == null) return false; if (!Objects.equals(vault.getMetadata().getLabels(), getMetadata().getLabels())) return false; // metadata.annotations equals if (vault.getMetadata().getAnnotations() == null && getMetadata().getAnnotations() != null) return false; if (vault.getMetadata().getAnnotations() != null && getMetadata().getAnnotations() == null) return false; if (vault.getMetadata().getAnnotations() != null && getMetadata().getAnnotations() != null) { HashMap vaultAnnotations = new HashMap<>(vault.getMetadata().getAnnotations()); vaultAnnotations.remove("kubectl.kubernetes.io/last-applied-configuration"); HashMap annotations = new HashMap<>(getMetadata().getAnnotations()); annotations.remove("kubectl.kubernetes.io/last-applied-configuration"); return Objects.equals(vaultAnnotations, annotations); } return true; } } ================================================ FILE: src/main/java/de/koudingspawn/vault/crd/VaultChangeAdjustmentCallback.java ================================================ package de.koudingspawn.vault.crd; import java.util.Objects; public class VaultChangeAdjustmentCallback { private String type; private String name; public VaultChangeAdjustmentCallback() { } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return type + "/" + name; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; VaultChangeAdjustmentCallback that = (VaultChangeAdjustmentCallback) o; return Objects.equals(type, that.type) && Objects.equals(name, that.name); } @Override public int hashCode() { return Objects.hash(type, name); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/crd/VaultDockerCfgConfiguration.java ================================================ package de.koudingspawn.vault.crd; import java.util.Objects; public class VaultDockerCfgConfiguration { private VaultType type; private Integer version; public VaultDockerCfgConfiguration() { this.type = VaultType.KEYVALUE; } public VaultType getType() { return type; } public void setType(VaultType version) { this.type = version; } public Integer getVersion() { return version; } public void setVersion(Integer version) { this.version = version; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; VaultDockerCfgConfiguration that = (VaultDockerCfgConfiguration) o; return type == that.type && Objects.equals(version, that.version); } @Override public int hashCode() { return Objects.hash(type, version); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/crd/VaultJKSConfiguration.java ================================================ package de.koudingspawn.vault.crd; import java.util.Objects; public class VaultJKSConfiguration { private String password; private String alias; private String keyName; private String caAlias; public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getAlias() { return alias; } public void setAlias(String alias) { this.alias = alias; } public String getKeyName() { return keyName; } public void setKeyName(String keyName) { this.keyName = keyName; } public String getCaAlias() { return caAlias; } public void setCaAlias(String caAlias) { this.caAlias = caAlias; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; VaultJKSConfiguration that = (VaultJKSConfiguration) o; return Objects.equals(password, that.password) && Objects.equals(alias, that.alias) && Objects.equals(keyName, that.keyName) && Objects.equals(caAlias, that.caAlias); } @Override public int hashCode() { return Objects.hash(password, alias, keyName, caAlias); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/crd/VaultList.java ================================================ package de.koudingspawn.vault.crd; import io.fabric8.kubernetes.api.model.DefaultKubernetesResourceList; public class VaultList extends DefaultKubernetesResourceList { } ================================================ FILE: src/main/java/de/koudingspawn/vault/crd/VaultPkiConfiguration.java ================================================ package de.koudingspawn.vault.crd; import java.util.Objects; public class VaultPkiConfiguration { private String commonName; private String altNames; private String ipSans; private String ttl; public String getCommonName() { return commonName; } public void setCommonName(String commonName) { this.commonName = commonName; } public String getAltNames() { return altNames; } public void setAltNames(String altNames) { this.altNames = altNames; } public String getIpSans() { return ipSans; } public void setIpSans(String ipSans) { this.ipSans = ipSans; } public String getTtl() { return ttl; } public void setTtl(String ttl) { this.ttl = ttl; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; VaultPkiConfiguration that = (VaultPkiConfiguration) o; return Objects.equals(commonName, that.commonName) && Objects.equals(altNames, that.altNames) && Objects.equals(ipSans, that.ipSans) && Objects.equals(ttl, that.ttl); } @Override public int hashCode() { return Objects.hash(commonName, altNames, ipSans, ttl); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/crd/VaultPropertiesConfiguration.java ================================================ package de.koudingspawn.vault.crd; import java.util.HashMap; import java.util.Objects; public class VaultPropertiesConfiguration { private HashMap files; private HashMap context; public HashMap getFiles() { return files; } public void setFiles(HashMap files) { this.files = files; } public HashMap getContext() { return context; } public void setContext(HashMap context) { this.context = context; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; VaultPropertiesConfiguration that = (VaultPropertiesConfiguration) o; return Objects.equals(files, that.files) && Objects.equals(context, that.context); } @Override public int hashCode() { return Objects.hash(files, context); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/crd/VaultSpec.java ================================================ package de.koudingspawn.vault.crd; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import io.fabric8.kubernetes.api.model.KubernetesResource; import java.util.Objects; @JsonDeserialize @JsonInclude(JsonInclude.Include.NON_NULL) public class VaultSpec implements KubernetesResource { private String path; private VaultType type; private VaultPkiConfiguration pkiConfiguration; private VaultJKSConfiguration jksConfiguration; private VaultVersionedConfiguration versionConfiguration; private VaultPropertiesConfiguration propertiesConfiguration; private VaultDockerCfgConfiguration dockerCfgConfiguration; private VaultChangeAdjustmentCallback changeAdjustmentCallback; public String getPath() { return path; } public void setPath(String path) { this.path = path; } public VaultType getType() { return type; } public void setType(VaultType type) { this.type = type; } public VaultPkiConfiguration getPkiConfiguration() { return pkiConfiguration; } public void setPkiConfiguration(VaultPkiConfiguration pkiConfiguration) { this.pkiConfiguration = pkiConfiguration; } public VaultJKSConfiguration getJksConfiguration() { return jksConfiguration; } public void setJksConfiguration(VaultJKSConfiguration jksConfiguration) { this.jksConfiguration = jksConfiguration; } public VaultVersionedConfiguration getVersionConfiguration() { return versionConfiguration; } public void setVersionConfiguration(VaultVersionedConfiguration versionConfiguration) { this.versionConfiguration = versionConfiguration; } public VaultPropertiesConfiguration getPropertiesConfiguration() { return propertiesConfiguration; } public void setPropertiesConfiguration(VaultPropertiesConfiguration propertiesConfiguration) { this.propertiesConfiguration = propertiesConfiguration; } public VaultDockerCfgConfiguration getDockerCfgConfiguration() { return dockerCfgConfiguration; } public void setDockerCfgConfiguration(VaultDockerCfgConfiguration dockerCfgConfiguration) { this.dockerCfgConfiguration = dockerCfgConfiguration; } public VaultChangeAdjustmentCallback getChangeAdjustmentCallback() { return changeAdjustmentCallback; } public void setChangeAdjustmentCallback(VaultChangeAdjustmentCallback changeAdjustmentCallback) { this.changeAdjustmentCallback = changeAdjustmentCallback; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; VaultSpec vaultSpec = (VaultSpec) o; return Objects.equals(path, vaultSpec.path) && type == vaultSpec.type && Objects.equals(pkiConfiguration, vaultSpec.pkiConfiguration) && Objects.equals(jksConfiguration, vaultSpec.jksConfiguration) && Objects.equals(versionConfiguration, vaultSpec.versionConfiguration) && Objects.equals(propertiesConfiguration, vaultSpec.propertiesConfiguration) && Objects.equals(dockerCfgConfiguration, vaultSpec.dockerCfgConfiguration) && Objects.equals(changeAdjustmentCallback, vaultSpec.changeAdjustmentCallback); } @Override public int hashCode() { return Objects.hash(path, type, pkiConfiguration, jksConfiguration, versionConfiguration, propertiesConfiguration, dockerCfgConfiguration, changeAdjustmentCallback); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/crd/VaultType.java ================================================ package de.koudingspawn.vault.crd; public enum VaultType { PKI, CERT, DOCKERCFG, KEYVALUE, PKIJKS, CERTJKS, KEYVALUEV2, PROPERTIES } ================================================ FILE: src/main/java/de/koudingspawn/vault/crd/VaultVersionedConfiguration.java ================================================ package de.koudingspawn.vault.crd; import java.util.Objects; public class VaultVersionedConfiguration { private Integer version; public Integer getVersion() { return version; } public void setVersion(Integer version) { this.version = version; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; VaultVersionedConfiguration that = (VaultVersionedConfiguration) o; return Objects.equals(version, that.version); } @Override public int hashCode() { return Objects.hash(version); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/ChangeAdjustmentService.java ================================================ package de.koudingspawn.vault.kubernetes; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultChangeAdjustmentCallback; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @Service public class ChangeAdjustmentService { private static final Logger log = LoggerFactory.getLogger(ChangeAdjustmentService.class); private final KubernetesClient client; public ChangeAdjustmentService(KubernetesClient client) { this.client = client; } public void handle(Vault resource) { VaultChangeAdjustmentCallback changeAdjustmentCallback = resource.getSpec().getChangeAdjustmentCallback(); if (changeAdjustmentCallback != null && changeAdjustmentCallback.getType() != null && changeAdjustmentCallback.getName() != null) { switch (changeAdjustmentCallback.getType().toLowerCase()) { case "deployment" -> rotateDeployment(resource.getMetadata().getNamespace(), changeAdjustmentCallback.getName()); case "statefulset" -> rotateStatefulSet(resource.getMetadata().getNamespace(), changeAdjustmentCallback.getName()); default -> log.info("Currently a change adjustment is only supported for type deployment. Resource {} in namespace {} has type {}", resource.getMetadata().getName(), resource.getMetadata().getNamespace(), changeAdjustmentCallback.getType()); } } else { log.warn("Change adjustment callback for resource {} in namespace {} is invalid!", resource.getMetadata().getName(), resource.getMetadata().getNamespace()); } } private void rotateDeployment(String namespace, String name) { try { log.info("Start rotation of deployment {} in namespace {}", name, namespace); client.apps() .deployments() .inNamespace(namespace) .withName(name) .edit(d -> new DeploymentBuilder(d) .editSpec() .editTemplate() .editMetadata() .addToAnnotations("certificate-change-on", "vault-crd_" + System.currentTimeMillis()) .endMetadata() .endTemplate() .endSpec() .build()); } catch (Exception ex) { log.error("Failed to rotate deployment {} in namespace {} with exception:", name, namespace, ex); } } private void rotateStatefulSet(String namespace, String name) { try { log.info("Start rotation of statefulSet {} in namespace {}", name, namespace); client.apps() .statefulSets() .inNamespace(namespace) .withName(name) .edit(statefulSet -> new StatefulSetBuilder(statefulSet) .editSpec() .editTemplate() .editMetadata() .addToAnnotations("certificate-change-on", "vault-crd_" + System.currentTimeMillis()) .endMetadata() .endTemplate() .endSpec() .build()); } catch (Exception ex) { log.error("Failed to rotate statefulSet {} in namespace {} with exception:", name, namespace, ex); } } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/EventHandler.java ================================================ package de.koudingspawn.vault.kubernetes; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.kubernetes.event.EventNotification; import de.koudingspawn.vault.vault.VaultSecret; import de.koudingspawn.vault.vault.VaultService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import static de.koudingspawn.vault.kubernetes.event.EventType.*; @Component public class EventHandler { private static final Logger log = LoggerFactory.getLogger(EventHandler.class); private final VaultService vaultService; private final KubernetesService kubernetesService; private final ChangeAdjustmentService changeAdjustmentService; private final EventNotification eventNotification; private final boolean fixOwnerReferenceEnabled; public EventHandler(VaultService vaultService, KubernetesService kubernetesService, ChangeAdjustmentService changeAdjustmentService, EventNotification eventNotification, @Value("${kubernetes.ownerreference-fix.enabled:true}") boolean fixOwnerReferenceEnabled) { this.vaultService = vaultService; this.kubernetesService = kubernetesService; this.changeAdjustmentService = changeAdjustmentService; this.eventNotification = eventNotification; this.fixOwnerReferenceEnabled = fixOwnerReferenceEnabled; } public void addHandler(Vault resource) { if (!kubernetesService.exists(resource)) { try { VaultSecret secretContent = vaultService.generateSecret(resource); kubernetesService.createSecret(resource, secretContent); eventNotification.storeNewEvent(CREATION_SUCCESSFUL, "Successfully created secret", resource); } catch (Exception e) { log.error("Failed to generate secret for vault resource {} in namespace {} failed with exception:", resource.getMetadata().getName(), resource.getMetadata().getNamespace(), e); eventNotification.storeNewEvent(CREATION_FAILED, "Failed to generate secret with exception " + e.getMessage(), resource); } } else if (fixOwnerReferenceEnabled && kubernetesService.hasBrokenOwnerReference(resource)) { log.info("Fix owner reference for secret {} in namespace {}", resource.getMetadata().getName(), resource.getMetadata().getNamespace()); modifyHandler(resource); eventNotification.storeNewEvent(FIXED_REFERENCE, "Fixed owner reference", resource); } } void deleteHandler(Vault resource) { kubernetesService.deleteSecret(resource.getMetadata()); eventNotification.storeNewEvent(DELETION, "Deleted secret for resource", resource); } public void modifyHandler(Vault resource) { try { VaultSecret secretContent = vaultService.generateSecret(resource); kubernetesService.modifySecret(resource, secretContent); eventNotification.storeNewEvent(MODIFICATION_SUCCESSFUL, "Successfully modified secret", resource); if (resource.getSpec().getChangeAdjustmentCallback() != null) { changeAdjustmentService.handle(resource); eventNotification.storeNewEvent(ROTATION, "Successfully started rotation of associated resource " + resource.getSpec().getChangeAdjustmentCallback().toString(), resource); } } catch (Exception e) { log.error("Failed to modify secret for vault resource {} in namespace {} failed with exception:", resource.getMetadata().getName(), resource.getMetadata().getNamespace(), e); eventNotification.storeNewEvent(MODIFICATION_FAILED, "Modification failed " + e.getMessage(), resource); } } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/KubernetesConnection.java ================================================ package de.koudingspawn.vault.kubernetes; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultList; import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.ConfigBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @Configuration public class KubernetesConnection { private static final Logger log = LoggerFactory.getLogger(KubernetesConnection.class); @Bean @Profile("development") public KubernetesClient testClient() { Config config = new ConfigBuilder().withMasterUrl("http://localhost:8001").withWatchReconnectLimit(5).build(); return new KubernetesClientBuilder() .withConfig(config) .build(); } @Bean @Profile("!development") public KubernetesClient client() { return new KubernetesClientBuilder().build(); } @Bean public MixedOperation> customResource( KubernetesClient client, @Value("${kubernetes.crd.name}") String crdName) { Resource crdResource = client.apiextensions().v1().customResourceDefinitions().withName(crdName); CustomResourceDefinition customResourceDefinition = crdResource.get(); if (customResourceDefinition == null) { log.error("Please first apply custom resource definition and then restart vault-crd"); System.exit(1); } return client.resources(Vault.class, VaultList.class); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/KubernetesService.java ================================================ package de.koudingspawn.vault.kubernetes; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.kubernetes.cache.SecretCache; import de.koudingspawn.vault.vault.VaultSecret; import io.fabric8.kubernetes.api.model.DeletionPropagation; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.OwnerReference; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import static de.koudingspawn.vault.Constants.COMPARE_ANNOTATION; import static de.koudingspawn.vault.Constants.LAST_UPDATE_ANNOTATION; @Component public class KubernetesService { private static final Logger log = LoggerFactory.getLogger(KubernetesService.class); private final KubernetesClient client; private final String crdName; private final String crdGroup; private final SecretCache secretCache; public KubernetesService(KubernetesClient client, SecretCache secretCache, @Value("${kubernetes.crd.name}") String crdName, @Value("${kubernetes.crd.group}") String crdGroup) { this.client = client; this.crdName = crdName; this.crdGroup = crdGroup; this.secretCache = secretCache; } boolean exists(Vault resource) { return getSecretByVault(resource) != null; } private Secret newSecretInstance(Vault resource, VaultSecret vaultSecret) { Secret secret = new Secret(); secret.setType(vaultSecret.getType()); secret.setMetadata(metaData(resource.getMetadata(), vaultSecret.getCompare())); secret.setData(vaultSecret.getData()); return secret; } void createSecret(Vault resource, VaultSecret vaultSecret) { Secret secret = newSecretInstance(resource, vaultSecret); secretCache.invalidate(secret.getMetadata().getNamespace(), secret.getMetadata().getName()); client.secrets().inNamespace(resource.getMetadata().getNamespace()).resource(secret).create(); log.info("Created secret for vault resource {} in namespace {}", secret.getMetadata().getName(), secret.getMetadata().getNamespace()); } void deleteSecret(ObjectMeta resourceMetadata) { secretCache.invalidate(resourceMetadata.getNamespace(), resourceMetadata.getName()); client.secrets().inNamespace(resourceMetadata.getNamespace()).withName(resourceMetadata.getName()).withPropagationPolicy(DeletionPropagation.BACKGROUND).delete(); log.info("Deleted secret {} in namespace {}", resourceMetadata.getName(), resourceMetadata.getNamespace()); } void modifySecret(Vault resource, VaultSecret vaultSecret) { Resource secretResource = client.secrets().inNamespace(resource.getMetadata().getNamespace()).withName(resource.getMetadata().getName()); Secret secret; if (secretResource.get() != null) { secret = secretResource.get(); } else { secret = newSecretInstance(resource, vaultSecret); } secret.setType(vaultSecret.getType()); secret.setMetadata(metaData(resource.getMetadata(), vaultSecret.getCompare())); secret.setData(vaultSecret.getData()); secretCache.invalidate(resource.getMetadata().getNamespace(), resource.getMetadata().getName()); client.secrets().inNamespace(resource.getMetadata().getNamespace()).resource(secret).createOrReplace(); log.info("Modified secret {} in namespace {}", resource.getMetadata().getName(), resource.getMetadata().getNamespace()); } public Secret getSecretByVault(Vault resource) { return secretCache.get(resource.getMetadata().getNamespace(), resource.getMetadata().getName()); } private ObjectMeta metaData(ObjectMeta resource, String compare) { ObjectMeta meta = new ObjectMeta(); meta.setNamespace(resource.getNamespace()); meta.setName(resource.getName()); if (resource.getLabels() != null) { meta.setLabels(resource.getLabels()); } if (meta.getLabels() == null) { meta.setLabels(new HashMap<>()); } meta.getLabels().put(crdName, "vault"); HashMap annotations = new HashMap<>(); if (resource.getAnnotations() != null) { annotations.putAll(resource.getAnnotations()); } annotations.put(crdName + LAST_UPDATE_ANNOTATION, LocalDateTime.now().toString()); annotations.put(crdName + COMPARE_ANNOTATION, compare); meta.setAnnotations(annotations); meta.setOwnerReferences(getOwnerReference(resource)); return meta; } private List getOwnerReference(ObjectMeta resource) { boolean blockOwnerDeletion = false; boolean controller = true; OwnerReference owner = new OwnerReference( crdGroup + "/v1", blockOwnerDeletion, controller, "Vault", resource.getName(), resource.getUid() ); ArrayList owners = new ArrayList<>(); owners.add(owner); return owners; } public boolean hasBrokenOwnerReference(Vault resource) { Resource secretResource = client.secrets().inNamespace(resource.getMetadata().getNamespace()).withName(resource.getMetadata().getName()); if (secretResource.get() != null) { Secret secret = secretResource.get(); if (secret.getMetadata() != null && secret.getMetadata().getOwnerReferences() != null && secret.getMetadata().getOwnerReferences().size() == 1) { OwnerReference ownerReference = secret.getMetadata().getOwnerReferences().get(0); return ownerReference.getApiVersion().equals(crdName + "/v1"); } } return false; } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/Watcher.java ================================================ package de.koudingspawn.vault.kubernetes; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.kubernetes.scheduler.ScheduledRefresh; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.fabric8.kubernetes.client.informers.SharedInformerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import java.util.concurrent.TimeUnit; @Configuration @Profile("!test") public class Watcher { private static final Logger log = LoggerFactory.getLogger(Watcher.class); private final EventHandler eventHandler; private final KubernetesClient client; private final ScheduledRefresh scheduledRefresh; private final long resyncIntervalSecond; public Watcher(EventHandler eventHandler, KubernetesClient client, ScheduledRefresh scheduledRefresh, @Value("${kubernetes.interval}") long resyncIntervalSecond) { this.eventHandler = eventHandler; this.client = client; this.scheduledRefresh = scheduledRefresh; this.resyncIntervalSecond = resyncIntervalSecond; } @Bean CommandLineRunner watchForResource() { return (args) -> run(); } private void run() { SharedInformerFactory sharedInformerFactory = client.informers(); SharedIndexInformer vaultInformer = sharedInformerFactory.sharedIndexInformerFor(Vault.class, TimeUnit.SECONDS.toMillis(resyncIntervalSecond)); vaultInformer.addEventHandler( new ResourceEventHandler<>() { @Override public void onAdd(Vault resource) { log.info("Received add for {} in namespace {}", resource.getMetadata().getName(), resource.getMetadata().getNamespace()); eventHandler.addHandler(resource); } @Override public void onUpdate(Vault oldObj, Vault resource) { if (oldObj.modifyHandlerEquals(resource)) { log.info("Received scheduled refresh for {} in namespace {}", resource.getMetadata().getName(), resource.getMetadata().getNamespace()); scheduledRefresh.refreshVaultResource(resource); } else { log.info("Received update for {} in namespace {}", resource.getMetadata().getName(), resource.getMetadata().getNamespace()); eventHandler.modifyHandler(resource); } } @Override public void onDelete(Vault resource, boolean deletedFinalStateUnknown) { log.info("Received delete for {} in namespace {}", resource.getMetadata().getName(), resource.getMetadata().getNamespace()); eventHandler.deleteHandler(resource); } } ); sharedInformerFactory.addSharedInformerEventListener(ex -> log.error("Exception occurred in shared informer, but caught: {}", ex.getMessage())); log.info("Starting informer"); sharedInformerFactory.startAllRegisteredInformers(); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/cache/SecretCache.java ================================================ package de.koudingspawn.vault.kubernetes.cache; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.TimeUnit; public class SecretCache { private static final Logger log = LoggerFactory.getLogger(SecretCache.class); private final Cache secretResourceCache = Caffeine.newBuilder().build(); private final KubernetesClient client; public SecretCache(KubernetesClient client, boolean watch) { this.client = client; if (watch) { this.watcher(); } } public void watcher() { client.secrets().inAnyNamespace().withLabel("vault.koudingspawn.de=vault").inform( new ResourceEventHandler<>() { private String cacheKey(String namespace, String name) { return "%s/%s".formatted(namespace, name); } @Override public void onAdd(Secret obj) { String key = cacheKey(obj.getMetadata().getNamespace(), obj.getMetadata().getName()); log.debug("Received create secret for {}", key); secretResourceCache.put(key, obj); } @Override public void onUpdate(Secret oldObj, Secret newObj) { String key = cacheKey(newObj.getMetadata().getNamespace(), newObj.getMetadata().getName()); log.debug("Received update for secret {}", key); secretResourceCache.put(key, newObj); } @Override public void onDelete(Secret obj, boolean deletedFinalStateUnknown) { String key = cacheKey(obj.getMetadata().getNamespace(), obj.getMetadata().getName()); log.debug("Invalidate secret cache for {} after delete", key); secretResourceCache.invalidate(key); } }, TimeUnit.MINUTES.toMillis(60)); } public Secret get(String namespace, String name) { String key = String.format("%s/%s", namespace, name); Secret cacheSecret = secretResourceCache.getIfPresent(key); if (cacheSecret != null) { return cacheSecret; } Secret clusterSecret = client.secrets().inNamespace(namespace).withName(name).get(); if (clusterSecret != null) { secretResourceCache.put(key, clusterSecret); } return clusterSecret; } public void invalidate(String namespace, String name) { String key = String.format("%s/%s", namespace, name); log.debug("Invalidate secret cache for {}", key); secretResourceCache.invalidate(key); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/cache/SecretCacheConfiguration.java ================================================ package de.koudingspawn.vault.kubernetes.cache; import io.fabric8.kubernetes.client.KubernetesClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @Configuration public class SecretCacheConfiguration { @Bean @Profile("!test") public SecretCache secretCache(KubernetesClient client) { return new SecretCache(client, true); } @Bean @Profile("test") public SecretCache testSecretCache(KubernetesClient client) { return new SecretCache(client, false); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/event/EventNotification.java ================================================ package de.koudingspawn.vault.kubernetes.event; import de.koudingspawn.vault.crd.Vault; import io.fabric8.kubernetes.api.model.Event; import io.fabric8.kubernetes.api.model.EventBuilder; import io.fabric8.kubernetes.api.model.ObjectReference; import io.fabric8.kubernetes.api.model.ObjectReferenceBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.TimeZone; @Service public class EventNotification { private static final Logger log = LoggerFactory.getLogger(EventNotification.class); private final String crdGroup; private final KubernetesClient client; public EventNotification(@Value("${kubernetes.crd.group}") String crdGroup, KubernetesClient client) { this.crdGroup = crdGroup; this.client = client; } public void storeNewEvent(EventType type, String message, Vault resource) { // @deprecated in 1.25 event v1beta1 will be deprecated ObjectReference ref = new ObjectReferenceBuilder() .withName(resource.getMetadata().getName()) .withNamespace(resource.getMetadata().getNamespace()) .withApiVersion(crdGroup + "/v1") .withKind("Vault") .withUid(resource.getMetadata().getUid()) .build(); TimeZone tz = TimeZone.getTimeZone("UTC"); DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); // Quoted "Z" to indicate UTC, no timezone offset df.setTimeZone(tz); String nowAsISO = df.format(new Date()); Event evt = new EventBuilder() .withNewMetadata() .withGenerateName(resource.getMetadata().getName()) .withNamespace(resource.getMetadata().getNamespace()) .endMetadata() .withInvolvedObject(ref) .withLastTimestamp(nowAsISO) .withFirstTimestamp(nowAsISO) .withReportingComponent("vault-crd") .withType(type.getEventType()) .withReason(type.getReason()) .withMessage(message) .build(); try { client.v1().events().inNamespace(resource.getMetadata().getNamespace()).resource(evt).create(); } catch (Exception ex) { log.error("Failed to store event for {} in namespace {} next to resource with error", resource.getMetadata().getName(), resource.getMetadata().getNamespace(), ex); } } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/event/EventType.java ================================================ package de.koudingspawn.vault.kubernetes.event; public enum EventType { CREATION_SUCCESSFUL("Normal", "SuccessfulCreated"), CREATION_FAILED("Failure", "FailedCreation"), MODIFICATION_SUCCESSFUL("Normal", "SuccessfulModified"), MODIFICATION_FAILED("Failure", "FailedModification"), ROTATION("Rotation", "RotationTriggered"), FIXED_REFERENCE("Normal", "FixedOwnerReference"), DELETION("Normal", "DeletionOfResource"); private final String type; private final String reason; EventType(String type, String reason) { this.type = type; this.reason = reason; } public String getEventType() { return type; } public String getReason() { return reason; } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/scheduler/RefreshConfiguration.java ================================================ package de.koudingspawn.vault.kubernetes.scheduler; import org.springframework.beans.factory.config.ServiceLocatorFactoryBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RefreshConfiguration { @Bean("typeRefreshFactory") public ServiceLocatorFactoryBean slfbForTypeRefresh() { ServiceLocatorFactoryBean slfb = new ServiceLocatorFactoryBean(); slfb.setServiceLocatorInterface(TypeRefreshFactory.class); return slfb; } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/scheduler/RequiresRefresh.java ================================================ package de.koudingspawn.vault.kubernetes.scheduler; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; public interface RequiresRefresh { boolean refreshIsNeeded(Vault resource) throws SecretNotAccessibleException; } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/scheduler/ScheduledRefresh.java ================================================ package de.koudingspawn.vault.kubernetes.scheduler; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.kubernetes.EventHandler; import de.koudingspawn.vault.kubernetes.event.EventNotification; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import static de.koudingspawn.vault.kubernetes.event.EventType.MODIFICATION_FAILED; @Component public class ScheduledRefresh { private static final Logger log = LoggerFactory.getLogger(ScheduledRefresh.class); private final TypeRefreshFactory typeRefreshFactory; private final EventHandler eventHandler; private final EventNotification eventNotification; public ScheduledRefresh( EventHandler eventHandler, TypeRefreshFactory typeRefreshFactory, EventNotification eventNotification) { this.typeRefreshFactory = typeRefreshFactory; this.eventHandler = eventHandler; this.eventNotification = eventNotification; } public void refreshVaultResource(Vault resource) { RequiresRefresh requiresRefresh = typeRefreshFactory.get(resource.getSpec().getType().toString()); try { if (requiresRefresh.refreshIsNeeded(resource)) { log.info("Executing scheduled refresh for {} in namespace {}", resource.getMetadata().getName(), resource.getMetadata().getNamespace()); eventHandler.modifyHandler(resource); } } catch (SecretNotAccessibleException e) { log.info("Refresh of secret {} in namespace {} failed with exception", resource.getMetadata().getName(), resource.getMetadata().getNamespace(), e); eventNotification.storeNewEvent(MODIFICATION_FAILED, "Modification of secret failed with exception " + e.getMessage(), resource); } } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/scheduler/TypeRefreshFactory.java ================================================ package de.koudingspawn.vault.kubernetes.scheduler; public interface TypeRefreshFactory { RequiresRefresh get(String name); } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/scheduler/impl/CertJksRefresh.java ================================================ package de.koudingspawn.vault.kubernetes.scheduler.impl; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.kubernetes.scheduler.RequiresRefresh; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import org.springframework.stereotype.Component; @Component("CERTJKS") public class CertJksRefresh implements RequiresRefresh { private final CertRefresh certRefresh; public CertJksRefresh(CertRefresh certRefresh) { this.certRefresh = certRefresh; } @Override public boolean refreshIsNeeded(Vault resource) throws SecretNotAccessibleException { return certRefresh.refreshIsNeeded(resource); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/scheduler/impl/CertRefresh.java ================================================ package de.koudingspawn.vault.kubernetes.scheduler.impl; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.kubernetes.KubernetesService; import de.koudingspawn.vault.kubernetes.scheduler.RequiresRefresh; import de.koudingspawn.vault.vault.TypedSecretGenerator; import de.koudingspawn.vault.vault.TypedSecretGeneratorFactory; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import io.fabric8.kubernetes.api.model.Secret; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component("CERT") public class CertRefresh extends CompareHash implements RequiresRefresh { private final String crdName; private final KubernetesService kubernetesService; private final TypedSecretGeneratorFactory typedSecretGeneratorFactory; public CertRefresh(@Value("${kubernetes.crd.name}") String crdName, KubernetesService kubernetesService, TypedSecretGeneratorFactory typedSecretGeneratorFactory) { this.crdName = crdName; this.typedSecretGeneratorFactory = typedSecretGeneratorFactory; this.kubernetesService = kubernetesService; } @Override public boolean refreshIsNeeded(Vault resource) throws SecretNotAccessibleException { return certHashHasChanged(resource); } private boolean certHashHasChanged(Vault resource) throws SecretNotAccessibleException { Secret secretByVault = kubernetesService.getSecretByVault(resource); TypedSecretGenerator certGenerator = typedSecretGeneratorFactory.get("CERTGENERATOR"); String vaultSha256 = certGenerator.getHash(resource.getSpec()); return super.hashHasChanged(secretByVault, vaultSha256, crdName); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/scheduler/impl/CompareHash.java ================================================ package de.koudingspawn.vault.kubernetes.scheduler.impl; import io.fabric8.kubernetes.api.model.Secret; import org.springframework.util.StringUtils; import static de.koudingspawn.vault.Constants.COMPARE_ANNOTATION; abstract public class CompareHash { boolean hashHasChanged(Secret secretByVault, String vaultSha256, String crdName) { if (secretByVault == null) { // secret is not available... return true; } if (secretByVault.getMetadata().getAnnotations() != null) { String kubernetesSha256 = secretByVault.getMetadata().getAnnotations().get(crdName + COMPARE_ANNOTATION); if (!StringUtils.hasText(kubernetesSha256)) { // has no sha256 then calculate it return true; } // check if vault and kubernetes are identical return !vaultSha256.equals(kubernetesSha256); } return true; } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/scheduler/impl/DockerCfgRefresh.java ================================================ package de.koudingspawn.vault.kubernetes.scheduler.impl; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.kubernetes.KubernetesService; import de.koudingspawn.vault.kubernetes.scheduler.RequiresRefresh; import de.koudingspawn.vault.vault.TypedSecretGenerator; import de.koudingspawn.vault.vault.TypedSecretGeneratorFactory; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import io.fabric8.kubernetes.api.model.Secret; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component("DOCKERCFG") public class DockerCfgRefresh extends CompareHash implements RequiresRefresh { private final String crdName; private final KubernetesService kubernetesService; private final TypedSecretGeneratorFactory typedSecretGeneratorFactory; public DockerCfgRefresh(@Value("${kubernetes.crd.name}") String crdName, KubernetesService kubernetesService, TypedSecretGeneratorFactory typedSecretGeneratorFactory) { this.crdName = crdName; this.kubernetesService = kubernetesService; this.typedSecretGeneratorFactory = typedSecretGeneratorFactory; } public boolean refreshIsNeeded(Vault resource) throws SecretNotAccessibleException { return dockerCfgHashHasChanged(resource); } private boolean dockerCfgHashHasChanged(Vault resource) throws SecretNotAccessibleException { Secret secretByVault = kubernetesService.getSecretByVault(resource); TypedSecretGenerator dockercfg = typedSecretGeneratorFactory.get("DOCKERCFGGENERATOR"); String vaultSha256 = dockercfg.getHash(resource.getSpec()); return super.hashHasChanged(secretByVault, vaultSha256, crdName); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/scheduler/impl/KeyValueRefresh.java ================================================ package de.koudingspawn.vault.kubernetes.scheduler.impl; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.kubernetes.KubernetesService; import de.koudingspawn.vault.kubernetes.scheduler.RequiresRefresh; import de.koudingspawn.vault.vault.TypedSecretGeneratorFactory; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import io.fabric8.kubernetes.api.model.Secret; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component("KEYVALUE") public class KeyValueRefresh extends CompareHash implements RequiresRefresh { private final String crdName; private final KubernetesService kubernetesService; private final TypedSecretGeneratorFactory typedSecretGeneratorFactory; public KeyValueRefresh(@Value("${kubernetes.crd.name}") String crdName, KubernetesService kubernetesService, TypedSecretGeneratorFactory typedSecretGeneratorFactory) { this.crdName = crdName; this.kubernetesService = kubernetesService; this.typedSecretGeneratorFactory = typedSecretGeneratorFactory; } public boolean refreshIsNeeded(Vault resource) throws SecretNotAccessibleException { return certHashHasChanged(resource); } private boolean certHashHasChanged(Vault resource) throws SecretNotAccessibleException { Secret secretByVault = kubernetesService.getSecretByVault(resource); String vaultSha256 = typedSecretGeneratorFactory.get("KEYVALUEGENERATOR").getHash(resource.getSpec()); return super.hashHasChanged(secretByVault, vaultSha256, crdName); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/scheduler/impl/KeyValueV2Refresh.java ================================================ package de.koudingspawn.vault.kubernetes.scheduler.impl; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.kubernetes.KubernetesService; import de.koudingspawn.vault.kubernetes.scheduler.RequiresRefresh; import de.koudingspawn.vault.vault.TypedSecretGeneratorFactory; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import io.fabric8.kubernetes.api.model.Secret; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component("KEYVALUEV2") public class KeyValueV2Refresh extends CompareHash implements RequiresRefresh { private final String crdName; private final KubernetesService kubernetesService; private final TypedSecretGeneratorFactory typedSecretGeneratorFactory; public KeyValueV2Refresh(@Value("${kubernetes.crd.name}") String crdName, KubernetesService kubernetesService, TypedSecretGeneratorFactory typedSecretGeneratorFactory) { this.crdName = crdName; this.kubernetesService = kubernetesService; this.typedSecretGeneratorFactory = typedSecretGeneratorFactory; } public boolean refreshIsNeeded(Vault resource) throws SecretNotAccessibleException { return hashHasChanged(resource); } private boolean hashHasChanged(Vault resource) throws SecretNotAccessibleException { Secret secretByVault = kubernetesService.getSecretByVault(resource); String vaultSha256 = typedSecretGeneratorFactory.get("KEYVALUEV2GENERATOR").getHash(resource.getSpec()); return super.hashHasChanged(secretByVault, vaultSha256, crdName); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/scheduler/impl/PkiJksRefresh.java ================================================ package de.koudingspawn.vault.kubernetes.scheduler.impl; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.kubernetes.scheduler.RequiresRefresh; import org.springframework.stereotype.Component; @Component("PKIJKS") public class PkiJksRefresh implements RequiresRefresh { private final PkiRefresh pkiRefresh; public PkiJksRefresh(PkiRefresh pkiRefresh) { this.pkiRefresh = pkiRefresh; } @Override public boolean refreshIsNeeded(Vault resource) { return pkiRefresh.refreshIsNeeded(resource); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/scheduler/impl/PkiRefresh.java ================================================ package de.koudingspawn.vault.kubernetes.scheduler.impl; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.kubernetes.KubernetesService; import de.koudingspawn.vault.kubernetes.scheduler.RequiresRefresh; import io.fabric8.kubernetes.api.model.Secret; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Optional; import java.util.TimeZone; import static de.koudingspawn.vault.Constants.COMPARE_ANNOTATION; import static de.koudingspawn.vault.Constants.DATE_FORMAT; @Component("PKI") public class PkiRefresh implements RequiresRefresh { private static final Logger log = LoggerFactory.getLogger(PkiRefresh.class); private final int interval; private final KubernetesService kubernetesService; private final String crdName; public PkiRefresh(@Value("${kubernetes.interval}") int interval, @Value("${kubernetes.crd.name}") String crdName, KubernetesService kubernetesService) { this.interval = interval; this.kubernetesService = kubernetesService; this.crdName = crdName; } public boolean refreshIsNeeded(Vault resource) { Secret secretByVault = kubernetesService.getSecretByVault(resource); return secretByVault == null || certificateIsNearExpirationDate(secretByVault); } private boolean certificateIsNearExpirationDate(Secret secretByVault) { if (secretByVault.getMetadata().getAnnotations() != null) { String expiration = secretByVault.getMetadata().getAnnotations().get(crdName + COMPARE_ANNOTATION); Optional expirationDate = parseDate(expiration); if (expirationDate.isPresent()) { Date nextIntervals = new Date(); nextIntervals.setTime(nextIntervals.getTime() + (interval * 1000 * 5)); return nextIntervals.after(expirationDate.get()); } else { log.error("Failed to parse date of secret {} in namespace {}", secretByVault.getMetadata().getName(), secretByVault.getMetadata().getNamespace()); } } return true; } private Optional parseDate(String date) { if (date == null) { return Optional.empty(); } try { SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT); TimeZone tz = TimeZone.getTimeZone("UTC"); format.setTimeZone(tz); return Optional.of(format.parse(date)); } catch (ParseException e) { return Optional.empty(); } } } ================================================ FILE: src/main/java/de/koudingspawn/vault/kubernetes/scheduler/impl/PropertiesRefresh.java ================================================ package de.koudingspawn.vault.kubernetes.scheduler.impl; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.kubernetes.scheduler.RequiresRefresh; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import org.springframework.stereotype.Component; @Component("PROPERTIES") public class PropertiesRefresh implements RequiresRefresh { @Override public boolean refreshIsNeeded(Vault resource) throws SecretNotAccessibleException { //TODO: allow properties refresh return false; } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/TypedSecretGenerator.java ================================================ package de.koudingspawn.vault.vault; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultSpec; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; public interface TypedSecretGenerator { VaultSecret generateSecret(Vault resource) throws SecretNotAccessibleException; String getHash(VaultSpec spec) throws SecretNotAccessibleException; } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/TypedSecretGeneratorFactory.java ================================================ package de.koudingspawn.vault.vault; public interface TypedSecretGeneratorFactory { TypedSecretGenerator get(String name); } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/VaultCommunication.java ================================================ package de.koudingspawn.vault.vault; import de.koudingspawn.vault.crd.VaultDockerCfgConfiguration; import de.koudingspawn.vault.crd.VaultPkiConfiguration; import de.koudingspawn.vault.crd.VaultType; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import de.koudingspawn.vault.vault.communication.TokenLookup; import de.koudingspawn.vault.vault.impl.dockercfg.PullSecret; import de.koudingspawn.vault.vault.impl.pki.PKIRequest; import de.koudingspawn.vault.vault.impl.pki.PKIResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpEntity; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.vault.VaultException; import org.springframework.vault.core.VaultTemplate; import org.springframework.vault.core.VaultVersionedKeyValueOperations; import org.springframework.vault.support.VaultResponseSupport; import org.springframework.vault.support.Versioned; import org.springframework.vault.support.Versioned.Version; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestOperations; import java.util.HashMap; import java.util.Optional; import java.util.regex.Pattern; @Component public class VaultCommunication { private static final Logger log = LoggerFactory.getLogger(VaultCommunication.class); private static final Pattern keyValuePattern = Pattern.compile("^.*?\\/.*?$"); private final VaultTemplate vaultTemplate; public VaultCommunication(VaultTemplate vaultTemplate) { this.vaultTemplate = vaultTemplate; } public PKIResponse createPki(String path, VaultPkiConfiguration configuration) throws SecretNotAccessibleException { PKIRequest pkiRequest = generateRequest(configuration); HttpEntity requestEntity = new HttpEntity<>(pkiRequest); try { return vaultTemplate.doWithSession(restOperations -> restOperations.postForObject(path, requestEntity, PKIResponse.class)); } catch (HttpStatusCodeException exception) { int statusCode = exception.getStatusCode().value(); throw new SecretNotAccessibleException( String.format("Couldn't generate pki secret from vault path %s status code %d", path, statusCode)); } catch (RestClientException ex) { throw new SecretNotAccessibleException("Couldn't communicate with vault", ex); } } public PKIResponse getCert(String path) throws SecretNotAccessibleException { return getRequest(path, PKIResponse.class); } public PullSecret getDockerCfg(String path, VaultDockerCfgConfiguration dockerCfgConfiguration) throws SecretNotAccessibleException { if (dockerCfgConfiguration.getType().equals(VaultType.KEYVALUE)) { return getRequest(path, PullSecret.class); } else { return getVersionedSecret(path, Optional.ofNullable(dockerCfgConfiguration.getVersion()), PullSecret.class); } } public HashMap getKeyValue(String path) throws SecretNotAccessibleException { return getRequest(path, HashMap.class); } private T getRequest(String path, Class clazz) throws SecretNotAccessibleException { try { VaultResponseSupport response = vaultTemplate.read(path, clazz); if (response != null) { return response.getData(); } else { throw new SecretNotAccessibleException(String.format("The secret %s is not available or in the wrong format.", path)); } } catch (VaultException exception) { throw new SecretNotAccessibleException( String.format("Couldn't load secret from vault path %s", path), exception); } } private PKIRequest generateRequest(VaultPkiConfiguration configuration) { PKIRequest pkiRequest = new PKIRequest(); if (configuration != null) { if (StringUtils.hasText(configuration.getCommonName())) { pkiRequest.setCommon_name(configuration.getCommonName()); } if (StringUtils.hasText(configuration.getAltNames())) { pkiRequest.setAlt_names(configuration.getAltNames()); } if (StringUtils.hasText(configuration.getIpSans())) { pkiRequest.setIp_sans(configuration.getIpSans()); } if (StringUtils.hasText(configuration.getTtl())) { pkiRequest.setTtl(configuration.getTtl()); } } return pkiRequest; } public HashMap getVersionedSecret(String path, Optional version) throws SecretNotAccessibleException { return getVersionedSecret(path, version, HashMap.class); } private T getVersionedSecret(String path, Optional version, Class clazz) throws SecretNotAccessibleException { String mountPoint = extractMountPoint(path); String extractedKey = extractKey(path); VaultVersionedKeyValueOperations versionedKV = vaultTemplate.opsForVersionedKeyValue(mountPoint); Versioned versionedResponse; try { if (version.isPresent()) { versionedResponse = versionedKV.get(extractedKey, Version.from(version.get()), clazz); } else { versionedResponse = versionedKV.get(extractedKey, clazz); } if (versionedResponse != null) { return versionedResponse.getData(); } throw new SecretNotAccessibleException(String.format("The secret %s is not available or in the wrong format.", path)); } catch (VaultException ex) { throw new SecretNotAccessibleException( String.format("Couldn't load secret from vault path %s", path), ex); } } public boolean isHealthy() { return vaultTemplate.doWithSession(this::doWithRestOperations); } private boolean doWithRestOperations(RestOperations restOperations) { try { ResponseEntity healthEntity = restOperations.getForEntity("/auth/token/lookup-self", TokenLookup.class); return healthEntity.getStatusCode().is2xxSuccessful(); } catch (RestClientException ex) { log.error("Vault health check failed!", ex); return false; } } private String extractMountPoint(String path) throws SecretNotAccessibleException { if (keyValuePattern.matcher(path).matches()) { return path.split("/", 2)[0]; } throw new SecretNotAccessibleException(String.format("Could not extract mountpoint from path: %s. A valid path looks like 'mountpoint/key'", path)); } private String extractKey(String path) throws SecretNotAccessibleException { if (keyValuePattern.matcher(path).matches()) { return path.split("/", 2)[1]; } throw new SecretNotAccessibleException(String.format("Could not extract key from path: %s. A valid path looks like 'mountpoint/key'", path)); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/VaultConfiguration.java ================================================ package de.koudingspawn.vault.vault; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.ServiceLocatorFactoryBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.vault.authentication.ClientAuthentication; import org.springframework.vault.authentication.KubernetesAuthentication; import org.springframework.vault.authentication.KubernetesAuthenticationOptions; import org.springframework.vault.authentication.TokenAuthentication; import org.springframework.vault.client.VaultEndpoint; import org.springframework.vault.config.AbstractVaultConfiguration; import java.net.URI; @Configuration public class VaultConfiguration { @Bean public ServiceLocatorFactoryBean slfbForTypeRefresh() { ServiceLocatorFactoryBean slfb = new ServiceLocatorFactoryBean(); slfb.setServiceLocatorInterface(TypedSecretGeneratorFactory.class); return slfb; } @Configuration @ConditionalOnProperty(name = "kubernetes.vault.auth", havingValue = "token") class VaultTokenConnection extends AbstractVaultConfiguration { private final String vaultToken; private final String vaultUrl; VaultTokenConnection(@Value("${kubernetes.vault.token}") String vaultToken, @Value("${kubernetes.vault.url}") String vaultUrl) { this.vaultToken = vaultToken; this.vaultUrl = vaultUrl; } @Override public VaultEndpoint vaultEndpoint() { return VaultEndpoint.from(getVaultUrlWithoutPath(vaultUrl)); } @Override public ClientAuthentication clientAuthentication() { return new TokenAuthentication(vaultToken); } } @Configuration @ConditionalOnProperty(name = "kubernetes.vault.auth", havingValue = "serviceAccount") class VaultServiceAccountConnection extends AbstractVaultConfiguration { private final String vaultUrl; private final String role; private final String path; VaultServiceAccountConnection(@Value("${kubernetes.vault.url}") String vaultUrl, @Value("${kubernetes.vault.role}") String role, @Value("${kubernetes.vault.path:kubernetes}") String path) { this.vaultUrl = vaultUrl; this.role = role; this.path = path; } @Override public VaultEndpoint vaultEndpoint() { return VaultEndpoint.from(getVaultUrlWithoutPath(vaultUrl)); } @Override public ClientAuthentication clientAuthentication() { KubernetesAuthenticationOptions options = KubernetesAuthenticationOptions.builder().path(path).role(role).build(); return new KubernetesAuthentication(options, restOperations()); } } private URI getVaultUrlWithoutPath(String vaultUrl) { return URI.create(vaultUrl.replace("/v1/", "")); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/VaultHealthCheck.java ================================================ package de.koudingspawn.vault.vault; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.stereotype.Component; @Component public class VaultHealthCheck implements HealthIndicator { private final VaultCommunication vaultCommunication; public VaultHealthCheck(VaultCommunication vaultCommunication) { this.vaultCommunication = vaultCommunication; } @Override public Health health() { Health.Builder healthBuilder = Health.down(); if (this.vaultCommunication.isHealthy()) { healthBuilder.up(); } return healthBuilder.build(); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/VaultSecret.java ================================================ package de.koudingspawn.vault.vault; import java.util.Map; public class VaultSecret { private Map data; private String compare; private String type; public VaultSecret(Map data, String compare) { this.data = data; this.compare = compare; this.type = "Opaque"; } public VaultSecret(Map data, String compare, String type) { this.data = data; this.compare = compare; this.type = type; } public Map getData() { return data; } public void setData(Map data) { this.data = data; } public String getCompare() { return compare; } public void setCompare(String compare) { this.compare = compare; } public String getType() { return type; } public void setType(String type) { this.type = type; } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/VaultService.java ================================================ package de.koudingspawn.vault.vault; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import org.springframework.stereotype.Component; @Component public class VaultService { private final TypedSecretGeneratorFactory typedSecretGeneratorFactory; public VaultService(TypedSecretGeneratorFactory typedSecretGeneratorFactory) { this.typedSecretGeneratorFactory = typedSecretGeneratorFactory; } public VaultSecret generateSecret(Vault resource) throws SecretNotAccessibleException { TypedSecretGenerator typedSecretGenerator = typedSecretGeneratorFactory.get(resource.getSpec().getType().toString() + "GENERATOR"); return typedSecretGenerator.generateSecret(resource); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/communication/SecretNotAccessibleException.java ================================================ package de.koudingspawn.vault.vault.communication; public class SecretNotAccessibleException extends Exception { public SecretNotAccessibleException(String message) { super(message); } public SecretNotAccessibleException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/communication/TokenLookup.java ================================================ package de.koudingspawn.vault.vault.communication; public class TokenLookup { private String request_id; private boolean renewable; private TokenLookupData data; public String getRequest_id() { return request_id; } public void setRequest_id(String request_id) { this.request_id = request_id; } public boolean isRenewable() { return renewable; } public void setRenewable(boolean renewable) { this.renewable = renewable; } public TokenLookupData getData() { return data; } public void setData(TokenLookupData data) { this.data = data; } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/communication/TokenLookupData.java ================================================ package de.koudingspawn.vault.vault.communication; public class TokenLookupData { private String accessor; private String display_name; private String id; private String path; public String getAccessor() { return accessor; } public void setAccessor(String accessor) { this.accessor = accessor; } public String getDisplay_name() { return display_name; } public void setDisplay_name(String display_name) { this.display_name = display_name; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/impl/CertGenerator.java ================================================ package de.koudingspawn.vault.vault.impl; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultSpec; import de.koudingspawn.vault.vault.TypedSecretGenerator; import de.koudingspawn.vault.vault.VaultCommunication; import de.koudingspawn.vault.vault.VaultSecret; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import de.koudingspawn.vault.vault.impl.pki.PKIResponse; import de.koudingspawn.vault.vault.impl.pki.VaultResponseData; import org.springframework.stereotype.Component; @Component("CERTGENERATOR") public class CertGenerator implements TypedSecretGenerator { private final VaultCommunication vaultCommunication; private final SharedVaultResponseMapper sharedVaultResponseMapper; public CertGenerator(VaultCommunication vaultCommunication, SharedVaultResponseMapper sharedVaultResponseMapper) { this.vaultCommunication = vaultCommunication; this.sharedVaultResponseMapper = sharedVaultResponseMapper; } @Override public VaultSecret generateSecret(Vault resource) throws SecretNotAccessibleException { PKIResponse pkiResponse = vaultCommunication.getCert(resource.getSpec().getPath()); return sharedVaultResponseMapper.mapCert(pkiResponse.getData()); } @Override public String getHash(VaultSpec spec) throws SecretNotAccessibleException { PKIResponse cert = vaultCommunication.getCert(spec.getPath()); if (cert != null && cert.getData() != null) { VaultResponseData pkiResponse = cert.getData(); return sharedVaultResponseMapper.mapCert(pkiResponse).getCompare(); } throw new SecretNotAccessibleException("Secret has no data field"); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/impl/CertJksGenerator.java ================================================ package de.koudingspawn.vault.vault.impl; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultSpec; import de.koudingspawn.vault.vault.TypedSecretGenerator; import de.koudingspawn.vault.vault.VaultCommunication; import de.koudingspawn.vault.vault.VaultSecret; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import de.koudingspawn.vault.vault.impl.pki.PKIResponse; import org.springframework.stereotype.Component; @Component("CERTJKSGENERATOR") public class CertJksGenerator implements TypedSecretGenerator { private final VaultCommunication vaultCommunication; private final SharedVaultResponseMapper sharedVaultResponseMapper; public CertJksGenerator(VaultCommunication vaultCommunication, SharedVaultResponseMapper sharedVaultResponseMapper) { this.vaultCommunication = vaultCommunication; this.sharedVaultResponseMapper = sharedVaultResponseMapper; } @Override public VaultSecret generateSecret(Vault resource) throws SecretNotAccessibleException { PKIResponse jksCert = vaultCommunication.getCert(resource.getSpec().getPath()); return sharedVaultResponseMapper.mapJks(jksCert.getData(), resource.getSpec().getJksConfiguration(), resource.getSpec().getType()); } @Override public String getHash(VaultSpec spec) { return null; } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/impl/DockerCfgGenerator.java ================================================ package de.koudingspawn.vault.vault.impl; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultDockerCfgConfiguration; import de.koudingspawn.vault.crd.VaultSpec; import de.koudingspawn.vault.vault.TypedSecretGenerator; import de.koudingspawn.vault.vault.VaultCommunication; import de.koudingspawn.vault.vault.VaultSecret; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import de.koudingspawn.vault.vault.impl.dockercfg.PullSecret; import org.springframework.stereotype.Component; import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.Optional; @Component("DOCKERCFGGENERATOR") public class DockerCfgGenerator implements TypedSecretGenerator { private final VaultCommunication vaultCommunication; public DockerCfgGenerator(VaultCommunication vaultCommunication) { this.vaultCommunication = vaultCommunication; } @Override public VaultSecret generateSecret(Vault resource) throws SecretNotAccessibleException { PullSecret dockerCfg = vaultCommunication.getDockerCfg(resource.getSpec().getPath(), getConfiguration(resource.getSpec())); return mapDockerCfg(dockerCfg); } @Override public String getHash(VaultSpec spec) throws SecretNotAccessibleException { PullSecret dockerCfg = vaultCommunication.getDockerCfg(spec.getPath(), getConfiguration(spec)); if (dockerCfg != null) { return mapDockerCfg(dockerCfg).getCompare(); } throw new SecretNotAccessibleException("Secret has no data field"); } private VaultDockerCfgConfiguration getConfiguration(VaultSpec spec) { return Optional.ofNullable(spec.getDockerCfgConfiguration()).orElse(new VaultDockerCfgConfiguration()); } private VaultSecret mapDockerCfg(PullSecret pullSecret) { String dockerCfg = String.format("{\"%s\": {\"username\": \"%s\", \"password\": \"%s\", \"email\": \"%s\", \"auth\": \"%s\"}}", pullSecret.getUrl(), pullSecret.getUsername(), pullSecret.getPassword(), pullSecret.getEmail(), pullSecret.getAuth()); Map data = new HashMap<>(); data.put(".dockercfg", Base64.getEncoder().encodeToString(dockerCfg.getBytes())); return new VaultSecret(data, Sha256.generateSha256(dockerCfg), "kubernetes.io/dockercfg"); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/impl/EncryptionUtils.java ================================================ package de.koudingspawn.vault.vault.impl; import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Base64; // https://github.com/Mastercard/client-encryption-java/blob/44b38fbc8e9fd64d252cbbf47a4bc5208a8ae741/src/main/java/com/mastercard/developer/utils/EncryptionUtils.java#L95 public class EncryptionUtils { private static final String PKCS_1_PEM_HEADER = "-----BEGIN RSA PRIVATE KEY-----"; private static final String PKCS_1_PEM_FOOTER = "-----END RSA PRIVATE KEY-----"; private static final String PKCS_8_PEM_HEADER = "-----BEGIN PRIVATE KEY-----"; private static final String PKCS_8_PEM_FOOTER = "-----END PRIVATE KEY-----"; private EncryptionUtils() { } /** * Load a RSA decryption key from a file (PEM or DER). */ public static PrivateKey loadPrivateKey(String keyDataString) throws GeneralSecurityException { if (keyDataString.contains(PKCS_1_PEM_HEADER)) { // OpenSSL / PKCS#1 Base64 PEM encoded file keyDataString = keyDataString.replace(PKCS_1_PEM_HEADER, ""); keyDataString = keyDataString.replace(PKCS_1_PEM_FOOTER, ""); keyDataString = keyDataString.replace("\n", ""); keyDataString = keyDataString.replace("\r\n", ""); return readPkcs1PrivateKey(Base64.getDecoder().decode(keyDataString)); } if (keyDataString.contains(PKCS_8_PEM_HEADER)) { // PKCS#8 Base64 PEM encoded file keyDataString = keyDataString.replace(PKCS_8_PEM_HEADER, ""); keyDataString = keyDataString.replace(PKCS_8_PEM_FOOTER, ""); keyDataString = keyDataString.replace("\n", ""); keyDataString = keyDataString.replace("\r\n", ""); return readPkcs8PrivateKey(Base64.getDecoder().decode(keyDataString)); } throw new GeneralSecurityException("No parser for secret found"); } /** * Create a PrivateKey instance from raw PKCS#8 bytes. */ private static PrivateKey readPkcs8PrivateKey(byte[] pkcs8Bytes) throws GeneralSecurityException { KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pkcs8Bytes); try { return keyFactory.generatePrivate(keySpec); } catch (InvalidKeySpecException e) { throw new IllegalArgumentException("Unexpected key format!", e); } } /** * Create a PrivateKey instance from raw PKCS#1 bytes. */ private static PrivateKey readPkcs1PrivateKey(byte[] pkcs1Bytes) throws GeneralSecurityException { // We can't use Java internal APIs to parse ASN.1 structures, so we build a PKCS#8 key Java can understand int pkcs1Length = pkcs1Bytes.length; int totalLength = pkcs1Length + 22; byte[] pkcs8Header = new byte[]{ 0x30, (byte) 0x82, (byte) ((totalLength >> 8) & 0xff), (byte) (totalLength & 0xff), // Sequence + total length 0x2, 0x1, 0x0, // Integer (0) 0x30, 0xD, 0x6, 0x9, 0x2A, (byte) 0x86, 0x48, (byte) 0x86, (byte) 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0, // Sequence: 1.2.840.113549.1.1.1, NULL 0x4, (byte) 0x82, (byte) ((pkcs1Length >> 8) & 0xff), (byte) (pkcs1Length & 0xff) // Octet string + length }; byte[] pkcs8bytes = join(pkcs8Header, pkcs1Bytes); return readPkcs8PrivateKey(pkcs8bytes); } private static byte[] join(byte[] byteArray1, byte[] byteArray2) { byte[] bytes = new byte[byteArray1.length + byteArray2.length]; System.arraycopy(byteArray1, 0, bytes, 0, byteArray1.length); System.arraycopy(byteArray2, 0, bytes, byteArray1.length, byteArray2.length); return bytes; } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/impl/KeyValueGenerator.java ================================================ package de.koudingspawn.vault.vault.impl; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultSpec; import de.koudingspawn.vault.vault.TypedSecretGenerator; import de.koudingspawn.vault.vault.VaultCommunication; import de.koudingspawn.vault.vault.VaultSecret; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import org.springframework.stereotype.Component; import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; import java.util.stream.Collectors; @Component("KEYVALUEGENERATOR") public class KeyValueGenerator implements TypedSecretGenerator { private final VaultCommunication vaultCommunication; public KeyValueGenerator(VaultCommunication vaultCommunication) { this.vaultCommunication = vaultCommunication; } @Override public VaultSecret generateSecret(Vault resource) throws SecretNotAccessibleException { HashMap keyValueResponse = vaultCommunication.getKeyValue(resource.getSpec().getPath()); return mapKeyValueResponse(keyValueResponse); } @Override public String getHash(VaultSpec resource) throws SecretNotAccessibleException { HashMap keyValue = vaultCommunication.getKeyValue(resource.getPath()); if (keyValue != null) { return mapKeyValueResponse(keyValue).getCompare(); } throw new SecretNotAccessibleException("Secret has no data field"); } private VaultSecret mapKeyValueResponse(HashMap keyValue) { TreeMap sortedMap = new TreeMap<>(keyValue); Map base64Encoded = sortedMap.entrySet() .stream() .collect(Collectors .toMap(Map.Entry::getKey, e -> Base64.getEncoder().encodeToString(e.getValue().getBytes()))); String sha256 = Sha256.generateSha256(base64Encoded.values().toArray(new String[0])); return new VaultSecret(base64Encoded, sha256); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/impl/KeyValueV2Generator.java ================================================ package de.koudingspawn.vault.vault.impl; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultSpec; import de.koudingspawn.vault.vault.TypedSecretGenerator; import de.koudingspawn.vault.vault.VaultCommunication; import de.koudingspawn.vault.vault.VaultSecret; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import org.springframework.stereotype.Component; import java.util.*; import java.util.stream.Collectors; @Component("KEYVALUEV2GENERATOR") public class KeyValueV2Generator implements TypedSecretGenerator { private final VaultCommunication vaultCommunication; public KeyValueV2Generator(VaultCommunication vaultCommunication) { this.vaultCommunication = vaultCommunication; } @Override public VaultSecret generateSecret(Vault resource) throws SecretNotAccessibleException { Optional version = getVersion(resource.getSpec()); HashMap versionedKVResponse = vaultCommunication.getVersionedSecret(resource.getSpec().getPath(), version); return mapKeyValueResponse(versionedKVResponse); } @Override public String getHash(VaultSpec resource) throws SecretNotAccessibleException { Optional version = getVersion(resource); HashMap keyValue = vaultCommunication.getVersionedSecret(resource.getPath(), version); if (keyValue != null) { return mapKeyValueResponse(keyValue).getCompare(); } throw new SecretNotAccessibleException("Secret has no data field"); } private VaultSecret mapKeyValueResponse(HashMap keyValue) { TreeMap sortedMap = new TreeMap<>(keyValue); Map base64Encoded = sortedMap.entrySet() .stream() .collect(Collectors .toMap(Map.Entry::getKey, e -> Base64.getEncoder().encodeToString(e.getValue().getBytes()))); String sha256 = Sha256.generateSha256(base64Encoded.values().toArray(new String[0])); return new VaultSecret(base64Encoded, sha256); } private Optional getVersion(VaultSpec resource) { if (resource.getVersionConfiguration() != null ) { return Optional.ofNullable(resource.getVersionConfiguration().getVersion()); } return Optional.empty(); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/impl/PkiJksGenerator.java ================================================ package de.koudingspawn.vault.vault.impl; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultSpec; import de.koudingspawn.vault.vault.TypedSecretGenerator; import de.koudingspawn.vault.vault.VaultCommunication; import de.koudingspawn.vault.vault.VaultSecret; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import de.koudingspawn.vault.vault.impl.pki.PKIResponse; import org.springframework.stereotype.Component; @Component("PKIJKSGENERATOR") public class PkiJksGenerator implements TypedSecretGenerator { private final VaultCommunication vaultCommunication; private final SharedVaultResponseMapper sharedVaultResponseMapper; public PkiJksGenerator(VaultCommunication vaultCommunication, SharedVaultResponseMapper sharedVaultResponseMapper) { this.vaultCommunication = vaultCommunication; this.sharedVaultResponseMapper = sharedVaultResponseMapper; } @Override public VaultSecret generateSecret(Vault resource) throws SecretNotAccessibleException { PKIResponse jksPki = vaultCommunication.createPki(resource.getSpec().getPath(), resource.getSpec().getPkiConfiguration()); return sharedVaultResponseMapper.mapJks(jksPki.getData(), resource.getSpec().getJksConfiguration(), resource.getSpec().getType()); } @Override public String getHash(VaultSpec spec) { return null; } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/impl/PkiSecretGenerator.java ================================================ package de.koudingspawn.vault.vault.impl; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultSpec; import de.koudingspawn.vault.vault.TypedSecretGenerator; import de.koudingspawn.vault.vault.VaultCommunication; import de.koudingspawn.vault.vault.VaultSecret; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import de.koudingspawn.vault.vault.impl.pki.PKIResponse; import org.springframework.stereotype.Component; @Component("PKIGENERATOR") public class PkiSecretGenerator implements TypedSecretGenerator { private final VaultCommunication vaultCommunication; private final SharedVaultResponseMapper sharedVaultResponseMapper; public PkiSecretGenerator(VaultCommunication vaultCommunication, SharedVaultResponseMapper sharedVaultResponseMapper) { this.vaultCommunication = vaultCommunication; this.sharedVaultResponseMapper = sharedVaultResponseMapper; } @Override public VaultSecret generateSecret(Vault resource) throws SecretNotAccessibleException { PKIResponse pki = vaultCommunication.createPki(resource.getSpec().getPath(), resource.getSpec().getPkiConfiguration()); return sharedVaultResponseMapper.mapPki(pki.getData()); } @Override public String getHash(VaultSpec spec) { return null; } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/impl/PropertiesGenerator.java ================================================ package de.koudingspawn.vault.vault.impl; import com.google.common.collect.Maps; import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.interpret.FatalTemplateErrorsException; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultPropertiesConfiguration; import de.koudingspawn.vault.crd.VaultSpec; import de.koudingspawn.vault.vault.TypedSecretGenerator; import de.koudingspawn.vault.vault.VaultCommunication; import de.koudingspawn.vault.vault.VaultSecret; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import de.koudingspawn.vault.vault.impl.properties.VaultJinjaLookup; import org.springframework.stereotype.Component; import java.util.Base64; import java.util.HashMap; import java.util.Map; @Component("PROPERTIESGENERATOR") public class PropertiesGenerator implements TypedSecretGenerator { private final VaultCommunication vaultCommunication; public PropertiesGenerator(VaultCommunication vaultCommunication) { this.vaultCommunication = vaultCommunication; } @Override public VaultSecret generateSecret(Vault resource) throws SecretNotAccessibleException { VaultPropertiesConfiguration propertiesConfiguration = resource.getSpec().getPropertiesConfiguration(); if (propertiesConfiguration != null && propertiesConfiguration.getFiles() != null) { Map context = Maps.newHashMap(); context.put("vault", new VaultJinjaLookup(vaultCommunication)); if (propertiesConfiguration.getContext() != null) { context.putAll(propertiesConfiguration.getContext()); } try { Map renderedFiles = renderFiles(context, propertiesConfiguration.getFiles()); // TODO: support change in properties return new VaultSecret(renderedFiles, "COMPARE"); } catch (FatalTemplateErrorsException ex) { throw new SecretNotAccessibleException(ex.getMessage(), ex); } } throw new SecretNotAccessibleException("Does not contain the required Files to render"); } @Override public String getHash(VaultSpec spec) throws SecretNotAccessibleException { return "COMPARE"; } private Map renderFiles(Map context, Map files) throws FatalTemplateErrorsException { Jinjava jinjava = new Jinjava(); Map targetFiles = new HashMap<>(); files.forEach((key, value) -> { String renderedContent = jinjava.render(value, context); targetFiles.put(key, Base64.getEncoder().encodeToString(renderedContent.getBytes())); }); return targetFiles; } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/impl/Sha256.java ================================================ package de.koudingspawn.vault.vault.impl; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Base64; public class Sha256 { public static String generateSha256(String ... args) { try { StringBuilder sb = new StringBuilder(); for (String arg : args) { sb.append(arg).append(";"); } MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] hash = digest.digest(sb.toString().getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(hash); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return null; } } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/impl/SharedVaultResponseMapper.java ================================================ package de.koudingspawn.vault.vault.impl; import de.koudingspawn.vault.Constants; import de.koudingspawn.vault.crd.VaultJKSConfiguration; import de.koudingspawn.vault.crd.VaultType; import de.koudingspawn.vault.vault.VaultSecret; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import de.koudingspawn.vault.vault.impl.pki.VaultResponseData; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.text.SimpleDateFormat; import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.TimeZone; @Component public class SharedVaultResponseMapper { @Value("${kubernetes.jks.default-alias}") private String defaultAlias; @Value("${kubernetes.jks.default-password}") private String defaultPassword; @Value("${kubernetes.jks.default-secret-key-name}") private String defaultKeyName; VaultSecret mapPki(VaultResponseData responseData) throws SecretNotAccessibleException { try { Certificate[] publicKeyList = getPublicKey(responseData.getCertificate()); X509Certificate compareCert = getCertificateWithShortestLivetime(publicKeyList); SimpleDateFormat dateFormat = new SimpleDateFormat(Constants.DATE_FORMAT); TimeZone tz = TimeZone.getTimeZone("UTC"); dateFormat.setTimeZone(tz); String compare = dateFormat.format(compareCert.getNotAfter()); Map mappedPki = mappedCertValues(responseData); return new VaultSecret(mappedPki, compare); } catch (CertificateException e) { throw new SecretNotAccessibleException("Couldn't get Expiration date of pki", e); } } VaultSecret mapCert(VaultResponseData vaultResponseData) { Map mappedPki = mappedCertValues(vaultResponseData); String compareSha = Sha256.generateSha256( mappedPki.get("tls.crt"), mappedPki.get("tls.key") ); return new VaultSecret(mappedPki, compareSha); } private Map mappedCertValues(VaultResponseData vaultResponseData) { String crt = getCrt(vaultResponseData); String key = getKey(vaultResponseData); Map mappedPki = new HashMap<>(); mappedPki.put("tls.crt", crt); mappedPki.put("tls.key", key); return mappedPki; } private String getCrt(VaultResponseData responseData) { return Base64.getEncoder().encodeToString(responseData.getChainedCertificate().getBytes()); } private String getKey(VaultResponseData responseData) { return Base64.getEncoder().encodeToString(responseData.getPrivate_key().getBytes()); } VaultSecret mapJks(VaultResponseData data, VaultJKSConfiguration jksConfiguration, VaultType type) throws SecretNotAccessibleException { try { KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(null, null); Certificate[] publicKeyList = getPublicKey(data.getCertificate()); keyStore.setKeyEntry( getAlias(jksConfiguration), EncryptionUtils.loadPrivateKey(data.getPrivate_key()), getPassword(jksConfiguration).toCharArray(), publicKeyList); if (jksConfiguration != null && StringUtils.hasText(jksConfiguration.getCaAlias())) { keyStore.setCertificateEntry( jksConfiguration.getCaAlias(), getPublicKey(data.getIssuing_ca())[0] ); } ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); keyStore.store(outputStream, getPassword(jksConfiguration).toCharArray()); String b64KeyStore = Base64.getEncoder().encodeToString(outputStream.toByteArray()); HashMap secretData = new HashMap<>() {{ put(getKey(jksConfiguration), b64KeyStore); }}; String compare; if (type.equals(VaultType.CERTJKS)) { String base64Cert = Base64.getEncoder().encodeToString(data.getCertificate().getBytes()); String base64Key = Base64.getEncoder().encodeToString(data.getPrivate_key().getBytes()); compare = Sha256.generateSha256(base64Cert, base64Key); } else { // VaultType.PKIJKS X509Certificate compareCert = getCertificateWithShortestLivetime(publicKeyList); SimpleDateFormat dateFormat = new SimpleDateFormat(Constants.DATE_FORMAT); TimeZone tz = TimeZone.getTimeZone("UTC"); dateFormat.setTimeZone(tz); compare = dateFormat.format(compareCert.getNotAfter()); } return new VaultSecret(secretData, compare); } catch (IOException | GeneralSecurityException e) { throw new SecretNotAccessibleException("Couldn't generate keystore", e); } } private String getAlias(VaultJKSConfiguration jksConfiguration) { if (jksConfiguration == null || !StringUtils.hasText(jksConfiguration.getAlias())) { return defaultAlias; } return jksConfiguration.getAlias(); } private String getPassword(VaultJKSConfiguration jksConfiguration) { if (jksConfiguration == null || !StringUtils.hasText(jksConfiguration.getPassword())) { return defaultPassword; } return jksConfiguration.getPassword(); } private String getKey(VaultJKSConfiguration jksConfiguration) { if (jksConfiguration == null || !StringUtils.hasText(jksConfiguration.getKeyName())) { return defaultKeyName; } return jksConfiguration.getKeyName(); } private Certificate[] getPublicKey(String pem) throws CertificateException { return CertificateFactory.getInstance("X509") .generateCertificates(new ByteArrayInputStream(pem.getBytes())).toArray(new Certificate[0]); } private X509Certificate getCertificateWithShortestLivetime(Certificate[] certificates) { if (certificates.length == 1) { return (X509Certificate) certificates[0]; } else { X509Certificate shortestLiveTime = (X509Certificate) certificates[0]; for (Certificate certificate : certificates) { if (((X509Certificate) certificate).getNotAfter().before(shortestLiveTime.getNotAfter())) { shortestLiveTime = (X509Certificate) certificate; } } return shortestLiveTime; } } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/impl/dockercfg/PullSecret.java ================================================ package de.koudingspawn.vault.vault.impl.dockercfg; import java.util.Base64; public class PullSecret { private String username; private String password; private String email; private String url; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getAuth() { String concatedAuth = getUsername() + ":" + getPassword(); return Base64.getEncoder().encodeToString(concatedAuth.getBytes()); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/impl/pki/PKIRequest.java ================================================ package de.koudingspawn.vault.vault.impl.pki; public class PKIRequest { private String common_name; private String alt_names; private String ip_sans; private String format = "pem"; private String ttl; public String getCommon_name() { return common_name; } public void setCommon_name(String common_name) { this.common_name = common_name; } public String getAlt_names() { return alt_names; } public void setAlt_names(String alt_names) { this.alt_names = alt_names; } public String getIp_sans() { return ip_sans; } public void setIp_sans(String ip_sans) { this.ip_sans = ip_sans; } public String getFormat() { return format; } public void setFormat(String format) { this.format = format; } public String getTtl() { return ttl; } public void setTtl(String ttl) { this.ttl = ttl; } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/impl/pki/PKIResponse.java ================================================ package de.koudingspawn.vault.vault.impl.pki; public class PKIResponse { private String lease_id; private boolean renewable; private VaultResponseData data; public String getLease_id() { return lease_id; } public void setLease_id(String lease_id) { this.lease_id = lease_id; } public boolean isRenewable() { return renewable; } public void setRenewable(boolean renewable) { this.renewable = renewable; } public VaultResponseData getData() { return data; } public void setData(VaultResponseData data) { this.data = data; } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/impl/pki/VaultResponseData.java ================================================ package de.koudingspawn.vault.vault.impl.pki; import org.springframework.util.CollectionUtils; import java.util.List; public class VaultResponseData { private String certificate; private String issuing_ca; private List ca_chain; private String private_key; private String private_key_type; private String serial_number; public String getCertificate() { return certificate; } public void setCertificate(String certificate) { this.certificate = certificate; } public String getIssuing_ca() { return issuing_ca; } public void setIssuing_ca(String issuing_ca) { this.issuing_ca = issuing_ca; } public List getCa_chain() { return ca_chain; } public void setCa_chain(List ca_chain) { this.ca_chain = ca_chain; } public String getPrivate_key() { return private_key; } public void setPrivate_key(String private_key) { this.private_key = private_key; } public String getPrivate_key_type() { return private_key_type; } public void setPrivate_key_type(String private_key_type) { this.private_key_type = private_key_type; } public String getSerial_number() { return serial_number; } public void setSerial_number(String serial_number) { this.serial_number = serial_number; } public String getChainedCertificate() { StringBuilder sb = new StringBuilder(certificate); if (!CollectionUtils.isEmpty(ca_chain)) { ca_chain.forEach(cert -> sb.append("\n").append(cert)); } return sb.toString(); } } ================================================ FILE: src/main/java/de/koudingspawn/vault/vault/impl/properties/VaultJinjaLookup.java ================================================ package de.koudingspawn.vault.vault.impl.properties; import de.koudingspawn.vault.vault.VaultCommunication; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import java.util.HashMap; import java.util.Optional; public class VaultJinjaLookup { private final VaultCommunication vaultCommunication; public VaultJinjaLookup(VaultCommunication vaultCommunication) { this.vaultCommunication = vaultCommunication; } public String lookup(String path, String key) throws SecretNotAccessibleException { return vaultCommunication.getKeyValue(path).get(key).toString(); } public HashMap lookup(String path) throws SecretNotAccessibleException { return vaultCommunication.getKeyValue(path); } public HashMap lookupV2(String path) throws SecretNotAccessibleException { return vaultCommunication.getVersionedSecret(path, Optional.empty()); } public String lookupV2(String path, String key) throws SecretNotAccessibleException { HashMap versionedSecret = vaultCommunication.getVersionedSecret(path, Optional.empty()); if (versionedSecret.containsKey(key)) { return versionedSecret.get(key).toString(); } throw new SecretNotAccessibleException(String.format("Secret at path %s with key %s not available", path, key)); } public String lookupV2(String path, int version, String key) throws SecretNotAccessibleException { HashMap versionedSecret = vaultCommunication.getVersionedSecret(path, Optional.of(version)); if (versionedSecret.containsKey(key)) { return versionedSecret.get(key).toString(); } throw new SecretNotAccessibleException(String.format("Secret at path %s in version %d with key %s not available", path, version, key)); } } ================================================ FILE: src/main/resources/application.properties ================================================ kubernetes.crd.group=koudingspawn.de kubernetes.crd.name=vault.koudingspawn.de kubernetes.vault.auth=token kubernetes.vault.token=root kubernetes.vault.role=admin kubernetes.vault.url=http://localhost:8200/v1/ kubernetes.interval=60 kubernetes.jks.default-alias=main kubernetes.jks.default-password=changeit kubernetes.jks.default-secret-key-name=key.jks kubernetes.ownerreference-fix.enabled=false management.endpoints.enabled-by-default=false management.endpoint.health.enabled=true ================================================ FILE: src/test/java/de/koudingspawn/vault/CertChainTest.java ================================================ package de.koudingspawn.vault; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultSpec; import de.koudingspawn.vault.crd.VaultType; import de.koudingspawn.vault.kubernetes.EventHandler; import de.koudingspawn.vault.kubernetes.scheduler.impl.CertRefresh; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.junit4.SpringRunner; import java.util.UUID; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; import static de.koudingspawn.vault.Constants.COMPARE_ANNOTATION; import static de.koudingspawn.vault.Constants.LAST_UPDATE_ANNOTATION; import static org.junit.Assert.*; @RunWith(SpringRunner.class) @SpringBootTest( properties = { "kubernetes.vault.url=http://localhost:8206/v1/", "kubernetes.vault.token=c73ab0cb-41e6-b89c-7af6-96b36f1ac87b" } ) public class CertChainTest { @ClassRule public static final WireMockClassRule wireMockClassRule = new WireMockClassRule(wireMockConfig().port(8206)); @Rule public WireMockClassRule instanceRule = wireMockClassRule; @Autowired public EventHandler handler; @Autowired public KubernetesClient client; @org.springframework.boot.test.context.TestConfiguration static class KindConfig { @Bean @Primary public KubernetesClient client() { return new KubernetesClientBuilder().build(); } } @Autowired CertRefresh certRefresh; @Before public void before() { WireMock.resetAllScenarios(); client.secrets().inAnyNamespace().delete(); TestHelper.generateLookupSelfStub(); } @Test public void shouldGenerateCertFromVaultResource() { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("certificate-1").withNamespace("default").withUid(UUID.randomUUID().toString()).build() ); VaultSpec spec = new VaultSpec(); spec.setType(VaultType.CERT); spec.setPath("secret/certificate"); vault.setSpec(spec); stubFor(get(urlEqualTo("/v1/secret/certificate")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(""" { "request_id": "6cc090a8-3821-8244-73e4-5ab62b605587", "lease_id": "", "renewable": false, "lease_duration": 2764800, "data": { "data": { "certificate": "CERTIFICATE", "issuing_ca": "ISSUINGCA", "ca_chain": ["ISSUINGCA"], "private_key": "PRIVATEKEY" } }, "wrap_info": null, "warnings": null, "auth": null }"""))); handler.addHandler(vault); Secret secret = client.secrets().inNamespace("default").withName("certificate-1").get(); assertEquals("certificate-1", secret.getMetadata().getName()); assertEquals("default", secret.getMetadata().getNamespace()); assertEquals("Opaque", secret.getType()); assertNotNull(secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + LAST_UPDATE_ANNOTATION)); assertEquals("GwzyEg3PQ2uSYFL2U6i0X2RibVs9p5gvOoTdZVQdT6s=", secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + COMPARE_ANNOTATION)); String crtB64 = secret.getData().get("tls.crt"); String crt = new String(java.util.Base64.getDecoder().decode(crtB64)); String keyB64 = secret.getData().get("tls.key"); String key = new String(java.util.Base64.getDecoder().decode(keyB64)); assertEquals("CERTIFICATE\nISSUINGCA", crt); assertEquals("PRIVATEKEY", key); } @Test public void shouldCheckIfCertificateHasChangedAndReturnFalse() throws SecretNotAccessibleException { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("certificate-2").withNamespace("default").withUid(UUID.randomUUID().toString()).build() ); VaultSpec spec = new VaultSpec(); spec.setType(VaultType.CERT); spec.setPath("secret/certificate"); vault.setSpec(spec); stubFor(get(urlEqualTo("/v1/secret/certificate")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(""" { "request_id": "6cc090a8-3821-8244-73e4-5ab62b605587", "lease_id": "", "renewable": false, "lease_duration": 2764800, "data": { "data": { "certificate": "CERTIFICATE", "issuing_ca": "ISSUINGCA", "ca_chain": ["ISSUINGCA"], "private_key": "PRIVATEKEY" } }, "wrap_info": null, "warnings": null, "auth": null }"""))); handler.addHandler(vault); assertFalse(certRefresh.refreshIsNeeded(vault)); } @Test public void shouldCheckIfCertificateHasChangedAndReturnTrue() throws SecretNotAccessibleException { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("certificate-3").withNamespace("default").withUid(UUID.randomUUID().toString()).build() ); VaultSpec spec = new VaultSpec(); spec.setType(VaultType.CERT); spec.setPath("secret/certificate"); vault.setSpec(spec); stubFor(get(urlEqualTo("/v1/secret/certificate")) .inScenario("Cert secret change") .whenScenarioStateIs(STARTED) .willSetStateTo("Cert first request done") .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(""" { "request_id": "6cc090a8-3821-8244-73e4-5ab62b605587", "lease_id": "", "renewable": false, "lease_duration": 2764800, "data": { "data": { "certificate": "CERTIFICATE", "issuing_ca": "ISSUINGCA", "ca_chain": ["ISSUINGCA"], "private_key": "PRIVATEKEY" } }, "wrap_info": null, "warnings": null, "auth": null }"""))); stubFor(get(urlEqualTo("/v1/secret/certificate")) .inScenario("Cert secret change") .whenScenarioStateIs("Cert first request done") .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(""" { "request_id": "6cc090a8-3821-8244-73e4-5ab62b605587", "lease_id": "", "renewable": false, "lease_duration": 2764800, "data": { "data": { "certificate": "CERTIFICATECHANGE", "issuing_ca": "ISSUINGCA", "ca_chain": ["ISSUINGCA"], "private_key": "PRIVATEKEY" } }, "wrap_info": null, "warnings": null, "auth": null }"""))); handler.addHandler(vault); assertTrue(certRefresh.refreshIsNeeded(vault)); } } ================================================ FILE: src/test/java/de/koudingspawn/vault/CertTest.java ================================================ package de.koudingspawn.vault; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultSpec; import de.koudingspawn.vault.crd.VaultType; import de.koudingspawn.vault.kubernetes.EventHandler; import de.koudingspawn.vault.kubernetes.scheduler.impl.CertRefresh; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.junit4.SpringRunner; import java.util.UUID; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; import static de.koudingspawn.vault.Constants.COMPARE_ANNOTATION; import static de.koudingspawn.vault.Constants.LAST_UPDATE_ANNOTATION; import static org.junit.Assert.*; @RunWith(SpringRunner.class) @SpringBootTest( properties = { "kubernetes.vault.url=http://localhost:8201/v1/" } ) public class CertTest { @ClassRule public static final WireMockClassRule wireMockClassRule = new WireMockClassRule(wireMockConfig().port(8201)); @Rule public WireMockClassRule instanceRule = wireMockClassRule; @Autowired public EventHandler handler; @Autowired public KubernetesClient client; @org.springframework.boot.test.context.TestConfiguration static class KindConfig { @Bean @Primary public KubernetesClient client() { return new KubernetesClientBuilder().build(); } } @Autowired CertRefresh certRefresh; @Before public void before() { WireMock.resetAllScenarios(); client.secrets().inAnyNamespace().delete(); TestHelper.generateLookupSelfStub(); } @Test public void shouldGenerateCertFromVaultResource() { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("certificate-1").withNamespace("default").withUid(UUID.randomUUID().toString()).build() ); VaultSpec spec = new VaultSpec(); spec.setType(VaultType.CERT); spec.setPath("secret/certificate"); vault.setSpec(spec); stubFor(get(urlEqualTo("/v1/secret/certificate")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(""" { "request_id": "6cc090a8-3821-8244-73e4-5ab62b605587", "lease_id": "", "renewable": false, "lease_duration": 2764800, "data": { "data": { "certificate": "CERTIFICATE", "issuing_ca": "ISSUINGCA", "private_key": "PRIVATEKEY" } }, "wrap_info": null, "warnings": null, "auth": null }"""))); handler.addHandler(vault); Secret secret = client.secrets().inNamespace("default").withName("certificate-1").get(); assertEquals("certificate-1", secret.getMetadata().getName()); assertEquals("default", secret.getMetadata().getNamespace()); assertEquals("Opaque", secret.getType()); assertNotNull(secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + LAST_UPDATE_ANNOTATION)); assertEquals("NNreOhDpdqcmcxEvF/KGNSQBZpAjszzrhjQVT4X8EXE=", secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + COMPARE_ANNOTATION)); String crtB64 = secret.getData().get("tls.crt"); String crt = new String(java.util.Base64.getDecoder().decode(crtB64)); String keyB64 = secret.getData().get("tls.key"); String key = new String(java.util.Base64.getDecoder().decode(keyB64)); assertEquals("CERTIFICATE", crt); assertEquals("PRIVATEKEY", key); } @Test public void shouldCheckIfCertificateHasChangedAndReturnFalse() throws SecretNotAccessibleException { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("certificate-2").withNamespace("default").withUid(UUID.randomUUID().toString()).build() ); VaultSpec spec = new VaultSpec(); spec.setType(VaultType.CERT); spec.setPath("secret/certificate"); vault.setSpec(spec); stubFor(get(urlEqualTo("/v1/secret/certificate")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(""" { "request_id": "6cc090a8-3821-8244-73e4-5ab62b605587", "lease_id": "", "renewable": false, "lease_duration": 2764800, "data": { "data": { "certificate": "CERTIFICATE", "issuing_ca": "ISSUINGCA", "private_key": "PRIVATEKEY" } }, "wrap_info": null, "warnings": null, "auth": null }"""))); handler.addHandler(vault); assertFalse(certRefresh.refreshIsNeeded(vault)); } @Test public void shouldCheckIfCertificateHasChangedAndReturnTrue() throws SecretNotAccessibleException { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("certificate-3").withNamespace("default").withUid(UUID.randomUUID().toString()).build() ); VaultSpec spec = new VaultSpec(); spec.setType(VaultType.CERT); spec.setPath("secret/certificate"); vault.setSpec(spec); stubFor(get(urlEqualTo("/v1/secret/certificate")) .inScenario("Cert secret change") .whenScenarioStateIs(STARTED) .willSetStateTo("Cert first request done") .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(""" { "request_id": "6cc090a8-3821-8244-73e4-5ab62b605587", "lease_id": "", "renewable": false, "lease_duration": 2764800, "data": { "data": { "certificate": "CERTIFICATE", "issuing_ca": "ISSUINGCA", "private_key": "PRIVATEKEY" } }, "wrap_info": null, "warnings": null, "auth": null }"""))); stubFor(get(urlEqualTo("/v1/secret/certificate")) .inScenario("Cert secret change") .whenScenarioStateIs("Cert first request done") .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(""" { "request_id": "6cc090a8-3821-8244-73e4-5ab62b605587", "lease_id": "", "renewable": false, "lease_duration": 2764800, "data": { "data": { "certificate": "CERTIFICATECHANGE", "issuing_ca": "ISSUINGCA", "private_key": "PRIVATEKEY" } }, "wrap_info": null, "warnings": null, "auth": null }"""))); handler.addHandler(vault); assertTrue(certRefresh.refreshIsNeeded(vault)); } } ================================================ FILE: src/test/java/de/koudingspawn/vault/DockerCfgTest.java ================================================ package de.koudingspawn.vault; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultDockerCfgConfiguration; import de.koudingspawn.vault.crd.VaultSpec; import de.koudingspawn.vault.crd.VaultType; import de.koudingspawn.vault.kubernetes.EventHandler; import de.koudingspawn.vault.kubernetes.scheduler.impl.DockerCfgRefresh; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import io.fabric8.kubernetes.api.model.DeletionPropagation; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import org.junit.*; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; import java.util.Base64; import java.util.UUID; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; import static de.koudingspawn.vault.Constants.COMPARE_ANNOTATION; import static de.koudingspawn.vault.Constants.LAST_UPDATE_ANNOTATION; import static org.junit.Assert.*; @RunWith(SpringRunner.class) @SpringBootTest( properties = { "kubernetes.vault.url=http://localhost:8202/v1/" } ) public class DockerCfgTest { @ClassRule public static final WireMockClassRule wireMockClassRule = new WireMockClassRule(wireMockConfig().port(8202)); @Rule public WireMockClassRule instanceRule = wireMockClassRule; @Autowired public EventHandler handler; @Autowired public DockerCfgRefresh dockerCfgRefresh; @Autowired public KubernetesClient client; @org.springframework.boot.test.context.TestConfiguration static class KindConfig { @Bean @Primary public KubernetesClient client() { return new KubernetesClientBuilder().build(); } } @Before public void before() { WireMock.resetAllScenarios(); client.secrets().inAnyNamespace().delete(); TestHelper.generateLookupSelfStub(); } @Test public void shouldGenerateDockerCfgFromVaultResource() throws IOException { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("dockercfg").withNamespace("default").withUid(UUID.randomUUID().toString()).build() ); VaultSpec spec = new VaultSpec(); spec.setType(VaultType.DOCKERCFG); spec.setPath("secret/docker"); vault.setSpec(spec); stubFor(get(urlEqualTo("/v1/secret/docker")) .inScenario("Simple Vault request") .whenScenarioStateIs(STARTED) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(""" { "request_id": "6cc090a8-3821-8244-73e4-5ab62b605587", "lease_id": "", "renewable": false, "lease_duration": 2764800, "data": { "username": "username", "password": "password", "url": "hub.docker.com", "email": "test-user@test.com" }, "wrap_info": null, "warnings": null, "auth": null }"""))); handler.addHandler(vault); Secret secret = client.secrets().inNamespace("default").withName("dockercfg").get(); assertEquals("dockercfg", secret.getMetadata().getName()); assertEquals("default", secret.getMetadata().getNamespace()); assertEquals("kubernetes.io/dockercfg", secret.getType()); assertNotNull(secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + LAST_UPDATE_ANNOTATION)); assertEquals("+gE+L0DNsGWDlNz5T3jLp1/U08KbD4OF+ez2lXQlTPM=", secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + COMPARE_ANNOTATION)); String dockerCfgBase64 = secret.getData().get(".dockercfg"); String dockerCfg = new String(Base64.getDecoder().decode(dockerCfgBase64)); ObjectMapper mapper = new ObjectMapper(); JsonNode dockerCfgNode = mapper.readTree(dockerCfg); assertTrue(dockerCfgNode.has("hub.docker.com")); JsonNode credentials = dockerCfgNode.get("hub.docker.com"); assertEquals("username", credentials.get("username").asText()); assertEquals("password", credentials.get("password").asText()); assertEquals("test-user@test.com", credentials.get("email").asText()); assertEquals("username:password", new String(Base64.getDecoder().decode(credentials.get("auth").asText()))); } @Test public void shouldCheckIfDockerCfgHasChangedAndReturnTrue() throws SecretNotAccessibleException { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("dockercfg").withNamespace("default").withUid(UUID.randomUUID().toString()).build() ); VaultSpec spec = new VaultSpec(); spec.setType(VaultType.DOCKERCFG); spec.setPath("secret/docker"); vault.setSpec(spec); stubFor(get(urlEqualTo("/v1/secret/docker")) .inScenario("Docker secret change") .whenScenarioStateIs(STARTED) .willSetStateTo("Docker first request done") .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{\"request_id\":\"6cc090a8-3821-8244-73e4-5ab62b605587\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":2764800,\"data\":{\"username\":\"username\", \"password\": \"password\", \"url\": \"hub.docker.com\", \"email\": \"test-user@test.com\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}"))); stubFor(get(urlEqualTo("/v1/secret/docker")) .inScenario("Docker secret change") .whenScenarioStateIs("Docker first request done") .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{\"request_id\":\"6cc090a8-3821-8244-73e4-5ab62b605587\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":2764800,\"data\":{\"username\":\"usernamehaschanged\", \"password\": \"password\", \"url\": \"hub.docker.com\", \"email\": \"test-user@test.com\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}"))); handler.addHandler(vault); assertTrue(dockerCfgRefresh.refreshIsNeeded(vault)); } @Test public void shouldCheckIfDockerCfgHasChangedAndReturnFalse() throws SecretNotAccessibleException { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("dockercfg").withNamespace("default").withUid(UUID.randomUUID().toString()).build() ); VaultSpec spec = new VaultSpec(); spec.setType(VaultType.DOCKERCFG); spec.setPath("secret/docker"); vault.setSpec(spec); stubFor(get(urlEqualTo("/v1/secret/docker")) .inScenario("Simple Vault request") .whenScenarioStateIs(STARTED) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{\"request_id\":\"6cc090a8-3821-8244-73e4-5ab62b605587\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":2764800,\"data\":{\"username\":\"username\", \"password\": \"password\", \"url\": \"hub.docker.com\", \"email\": \"test-user@test.com\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}"))); handler.addHandler(vault); assertFalse(dockerCfgRefresh.refreshIsNeeded(vault)); } @Test public void shouldGenerateDockerCfgV2() throws JsonProcessingException { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("dockercfg").withNamespace("default").withUid(UUID.randomUUID().toString()).build() ); VaultSpec spec = new VaultSpec(); spec.setType(VaultType.DOCKERCFG); spec.setPath("secret/docker"); VaultDockerCfgConfiguration dockerConfig = new VaultDockerCfgConfiguration(); dockerConfig.setType(VaultType.KEYVALUEV2); spec.setDockerCfgConfiguration(dockerConfig); vault.setSpec(spec); stubFor(get(urlPathMatching("/v1/secret/data/docker")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(""" { "request_id": "1cfee2a6-318a-ea12-f5b5-6fd52d74d2c6", "lease_id": "", "renewable": false, "lease_duration": 0, "data": { "data": { "username": "username", "password": "password", "url": "hub.docker.com", "email": "test-user@test.com" }, "metadata": { "created_time": "2018-12-10T18:59:53.337997525Z", "deletion_time": "", "destroyed": false, "version": 1 } }, "wrap_info": null, "warnings": null, "auth": null }"""))); handler.addHandler(vault); Secret secret = client.secrets().inNamespace("default").withName("dockercfg").get(); assertEquals("dockercfg", secret.getMetadata().getName()); assertEquals("default", secret.getMetadata().getNamespace()); assertEquals("kubernetes.io/dockercfg", secret.getType()); assertNotNull(secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + LAST_UPDATE_ANNOTATION)); assertEquals("+gE+L0DNsGWDlNz5T3jLp1/U08KbD4OF+ez2lXQlTPM=", secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + COMPARE_ANNOTATION)); String dockerCfgBase64 = secret.getData().get(".dockercfg"); String dockerCfg = new String(Base64.getDecoder().decode(dockerCfgBase64)); ObjectMapper mapper = new ObjectMapper(); JsonNode dockerCfgNode = mapper.readTree(dockerCfg); assertTrue(dockerCfgNode.has("hub.docker.com")); JsonNode credentials = dockerCfgNode.get("hub.docker.com"); assertEquals("username", credentials.get("username").asText()); assertEquals("password", credentials.get("password").asText()); assertEquals("test-user@test.com", credentials.get("email").asText()); assertEquals("username:password", new String(Base64.getDecoder().decode(credentials.get("auth").asText()))); } @After @Before public void cleanup() { Secret secret = client.secrets().inNamespace("default").withName("dockercfg").get(); if (secret != null) { client.secrets().inNamespace("default").withName("dockercfg").withPropagationPolicy(DeletionPropagation.BACKGROUND).delete(); } } } ================================================ FILE: src/test/java/de/koudingspawn/vault/EventNotificationTest.java ================================================ package de.koudingspawn.vault; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.kubernetes.event.EventNotification; import de.koudingspawn.vault.kubernetes.event.EventType; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; @RunWith(SpringRunner.class) @SpringBootTest(properties = { "kubernetes.vault.url=http://localhost:8202/v1/" }) @AutoConfigureMockMvc @ActiveProfiles("test") public class EventNotificationTest { @Autowired public KubernetesClient client; @Autowired public EventNotification evtNotification; @org.springframework.boot.test.context.TestConfiguration static class KindConfig { @Bean @Primary public KubernetesClient client() { return new KubernetesClientBuilder().build(); } } @Test public void shouldBeAbleToCreateEvent() { String uuid = UUID.randomUUID().toString(); Vault vault = new Vault(); vault.setMetadata(new ObjectMetaBuilder() .withName("test") .withNamespace("default") .withUid(uuid) .build()); evtNotification.storeNewEvent(EventType.CREATION_SUCCESSFUL, "Successfully created secret", vault); assertEquals(1, client.v1().events().inNamespace("default").list().getItems() .stream().filter(event -> event.getInvolvedObject().getName().equals("test") && event.getInvolvedObject().getUid().equals(uuid)).count()); } } ================================================ FILE: src/test/java/de/koudingspawn/vault/KeyValueTest.java ================================================ package de.koudingspawn.vault; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultSpec; import de.koudingspawn.vault.crd.VaultType; import de.koudingspawn.vault.kubernetes.EventHandler; import de.koudingspawn.vault.kubernetes.scheduler.impl.KeyValueRefresh; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.junit4.SpringRunner; import java.util.UUID; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; import static de.koudingspawn.vault.Constants.COMPARE_ANNOTATION; import static de.koudingspawn.vault.Constants.LAST_UPDATE_ANNOTATION; import static org.junit.Assert.*; @RunWith(SpringRunner.class) @SpringBootTest( properties = { "kubernetes.vault.url=http://localhost:8209/v1/" } ) public class KeyValueTest { @ClassRule public static final WireMockClassRule wireMockClassRule = new WireMockClassRule(wireMockConfig().port(8209)); @Rule public WireMockClassRule instanceRule = wireMockClassRule; @Autowired public EventHandler handler; @Autowired public KeyValueRefresh keyValueRefresh; @Autowired public KubernetesClient client; @org.springframework.boot.test.context.TestConfiguration static class KindConfig { @Bean @Primary public KubernetesClient client() { return new KubernetesClientBuilder().build(); } } @Before public void before() { WireMock.resetAllScenarios(); client.secrets().inAnyNamespace().delete(); TestHelper.generateLookupSelfStub(); } @Test public void shouldGenerateSimpleSecretFromVaultCustomResource() { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("simple-kv1-1").withNamespace("default").withUid(UUID.randomUUID().toString()).build()); VaultSpec vaultSpec = new VaultSpec(); vaultSpec.setType(VaultType.KEYVALUE); vaultSpec.setPath("secret/simple"); vault.setSpec(vaultSpec); stubFor(get(urlPathMatching("/v1/secret/simple")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{\"request_id\":\"6cc090a8-3821-8244-73e4-5ab62b605587\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":2764800,\"data\":{\"key\":\"value\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}"))); handler.addHandler(vault); Secret secret = client.secrets().inNamespace("default").withName("simple-kv1-1").get(); assertEquals("simple-kv1-1", secret.getMetadata().getName()); assertEquals("default", secret.getMetadata().getNamespace()); assertEquals("Opaque", secret.getType()); assertEquals("dmFsdWU=", secret.getData().get("key")); assertNotNull(secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + LAST_UPDATE_ANNOTATION)); assertEquals("dYxf3NXqZ1l2d1YL1htbVBs6EUot33VjoBUUrBJg1eY=", secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + COMPARE_ANNOTATION)); } @Test public void shouldCheckIfSimpleSecretHasChangedAndReturnTrue() throws SecretNotAccessibleException { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("simple-kv1-2").withNamespace("default").withUid(UUID.randomUUID().toString()).build()); VaultSpec vaultSpec = new VaultSpec(); vaultSpec.setType(VaultType.KEYVALUE); vaultSpec.setPath("secret/simple"); vault.setSpec(vaultSpec); stubFor(get(urlPathMatching("/v1/secret/simple")) .inScenario("Vault secret change") .whenScenarioStateIs(STARTED) .willSetStateTo("First request done") .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{\"request_id\":\"6cc090a8-3821-8244-73e4-5ab62b605587\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":2764800,\"data\":{\"key\":\"value\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}"))); stubFor(get(urlPathMatching("/v1/secret/simple")) .inScenario("Vault secret change") .whenScenarioStateIs("First request done") .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{\"request_id\":\"6cc090a8-3821-8244-73e4-5ab62b605587\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":2764800,\"data\":{\"key\":\"value1\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}"))); handler.addHandler(vault); assertTrue(keyValueRefresh.refreshIsNeeded(vault)); } @Test public void shouldCheckIfSimpleSecretHasChangedAndReturnFalse() throws SecretNotAccessibleException { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("simple-kv1-3").withNamespace("default").withUid(UUID.randomUUID().toString()).build()); VaultSpec vaultSpec = new VaultSpec(); vaultSpec.setType(VaultType.KEYVALUE); vaultSpec.setPath("secret/simple"); vault.setSpec(vaultSpec); stubFor(get(urlPathMatching("/v1/secret/simple")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{\"request_id\":\"6cc090a8-3821-8244-73e4-5ab62b605587\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":2764800,\"data\":{\"key\":\"value\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}"))); handler.addHandler(vault); assertFalse(keyValueRefresh.refreshIsNeeded(vault)); } @Test public void preventNullPointerExceptionWhenSecretDoesNotExist() { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("simple-kv1-4").withNamespace("default").withUid(UUID.randomUUID().toString()).build()); VaultSpec vaultSpec = new VaultSpec(); vaultSpec.setType(VaultType.KEYVALUE); vaultSpec.setPath("secret/simple"); vault.setSpec(vaultSpec); stubFor(get(urlPathMatching("/v1/secret/simple")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{\"request_id\":\"6cc090a8-3821-8244-73e4-5ab62b605587\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":2764800,\"data\":{\"key\":\"value\"},\"wrap_info\":null,\"warnings\":null,\"auth\":null}"))); handler.modifyHandler(vault); Secret secret = client.secrets().inNamespace("default").withName("simple-kv1-4").get(); assertEquals("simple-kv1-4", secret.getMetadata().getName()); assertEquals("default", secret.getMetadata().getNamespace()); assertEquals("Opaque", secret.getType()); assertEquals("dmFsdWU=", secret.getData().get("key")); assertNotNull(secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + LAST_UPDATE_ANNOTATION)); assertEquals("dYxf3NXqZ1l2d1YL1htbVBs6EUot33VjoBUUrBJg1eY=", secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + COMPARE_ANNOTATION)); } } ================================================ FILE: src/test/java/de/koudingspawn/vault/KeyValueV2Test.java ================================================ package de.koudingspawn.vault; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultSpec; import de.koudingspawn.vault.crd.VaultType; import de.koudingspawn.vault.kubernetes.EventHandler; import de.koudingspawn.vault.kubernetes.scheduler.impl.KeyValueV2Refresh; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import java.util.UUID; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; import static de.koudingspawn.vault.Constants.COMPARE_ANNOTATION; import static de.koudingspawn.vault.Constants.LAST_UPDATE_ANNOTATION; import static org.junit.Assert.*; @RunWith(SpringRunner.class) @SpringBootTest( properties = { "kubernetes.vault.url=http://localhost:8207/v1/" } ) @ActiveProfiles("test") public class KeyValueV2Test { @ClassRule public static final WireMockClassRule wireMockClassRule = new WireMockClassRule(wireMockConfig().port(8207)); @Rule public WireMockClassRule instanceRule = wireMockClassRule; @Autowired public EventHandler handler; @Autowired public KeyValueV2Refresh keyValueV2Refresh; @Autowired public KubernetesClient client; @org.springframework.boot.test.context.TestConfiguration static class KindConfig { @Bean @Primary public KubernetesClient client() { return new KubernetesClientBuilder().build(); } } @Before public void before() { WireMock.resetAllScenarios(); client.secrets().inAnyNamespace().delete(); TestHelper.generateLookupSelfStub(); } @Test public void shouldGenerateSimpleSecretFromVaultCustomResource() { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("simple-kv2-1").withNamespace("default").withUid(UUID.randomUUID().toString()).build()); VaultSpec vaultSpec = new VaultSpec(); vaultSpec.setType(VaultType.KEYVALUEV2); vaultSpec.setPath("secret/simple"); vault.setSpec(vaultSpec); stubFor(get(urlPathMatching("/v1/secret/data/simple")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(""" { "request_id": "1cfee2a6-318a-ea12-f5b5-6fd52d74d2c6", "lease_id": "", "renewable": false, "lease_duration": 0, "data": { "data": { "key": "value" }, "metadata": { "created_time": "2018-12-10T18:59:53.337997525Z", "deletion_time": "", "destroyed": false, "version": 1 } }, "wrap_info": null, "warnings": null, "auth": null }"""))); handler.addHandler(vault); Secret secret = client.secrets().inNamespace("default").withName("simple-kv2-1").get(); assertEquals("simple-kv2-1", secret.getMetadata().getName()); assertEquals("default", secret.getMetadata().getNamespace()); assertEquals("Opaque", secret.getType()); assertEquals("dmFsdWU=", secret.getData().get("key")); assertNotNull(secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + LAST_UPDATE_ANNOTATION)); assertEquals("dYxf3NXqZ1l2d1YL1htbVBs6EUot33VjoBUUrBJg1eY=", secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + COMPARE_ANNOTATION)); } @Test public void shouldCheckIfSimpleSecretHasChangedAndReturnTrue() throws SecretNotAccessibleException { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("simple-kv2-2").withNamespace("default").withUid(UUID.randomUUID().toString()).build()); VaultSpec vaultSpec = new VaultSpec(); vaultSpec.setType(VaultType.KEYVALUEV2); vaultSpec.setPath("secret/simple"); vault.setSpec(vaultSpec); stubFor(get(urlPathMatching("/v1/secret/data/simple")) .inScenario("Vault secret change") .whenScenarioStateIs(STARTED) .willSetStateTo("First request done") .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(""" { "request_id": "1cfee2a6-318a-ea12-f5b5-6fd52d74d2c6", "lease_id": "", "renewable": false, "lease_duration": 0, "data": { "data": { "key": "value" }, "metadata": { "created_time": "2018-12-10T18:59:53.337997525Z", "deletion_time": "", "destroyed": false, "version": 1 } }, "wrap_info": null, "warnings": null, "auth": null }"""))); stubFor(get(urlPathMatching("/v1/secret/data/simple")) .inScenario("Vault secret change") .whenScenarioStateIs("First request done") .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(""" { "request_id": "1cfee2a6-318a-ea12-f5b5-6fd52d74d2c6", "lease_id": "", "renewable": false, "lease_duration": 0, "data": { "data": { "key": "value1" }, "metadata": { "created_time": "2018-12-10T18:59:53.337997525Z", "deletion_time": "", "destroyed": false, "version": 1 } }, "wrap_info": null, "warnings": null, "auth": null }"""))); handler.addHandler(vault); assertTrue(keyValueV2Refresh.refreshIsNeeded(vault)); } @Test public void shouldCheckIfSimpleSecretHasChangedAndReturnFalse() throws SecretNotAccessibleException { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("simple-kv2-3").withNamespace("default").withUid(UUID.randomUUID().toString()).build()); VaultSpec vaultSpec = new VaultSpec(); vaultSpec.setType(VaultType.KEYVALUEV2); vaultSpec.setPath("secret/simple"); vault.setSpec(vaultSpec); stubFor(get(urlPathMatching("/v1/secret/data/simple")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(""" { "request_id": "1cfee2a6-318a-ea12-f5b5-6fd52d74d2c6", "lease_id": "", "renewable": false, "lease_duration": 0, "data": { "data": { "key": "value" }, "metadata": { "created_time": "2018-12-10T18:59:53.337997525Z", "deletion_time": "", "destroyed": false, "version": 1 } }, "wrap_info": null, "warnings": null, "auth": null }"""))); handler.addHandler(vault); assertFalse(keyValueV2Refresh.refreshIsNeeded(vault)); } @Test public void shouldSupportNestedPath() { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("simple-kv2-4").withNamespace("default").withUid(UUID.randomUUID().toString()).build()); VaultSpec vaultSpec = new VaultSpec(); vaultSpec.setType(VaultType.KEYVALUEV2); vaultSpec.setPath("secret/simple/nested"); vault.setSpec(vaultSpec); stubFor(get(urlPathMatching("/v1/secret/data/simple/nested")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(""" { "request_id": "1cfee2a6-318a-ea12-f5b5-6fd52d74d2c6", "lease_id": "", "renewable": false, "lease_duration": 0, "data": { "data": { "key": "value", "nested": "value2" }, "metadata": { "created_time": "2018-12-10T18:59:53.337997525Z", "deletion_time": "", "destroyed": false, "version": 1 } }, "wrap_info": null, "warnings": null, "auth": null }"""))); handler.addHandler(vault); Secret secret = client.secrets().inNamespace("default").withName("simple-kv2-4").get(); assertEquals("simple-kv2-4", secret.getMetadata().getName()); assertEquals("default", secret.getMetadata().getNamespace()); assertEquals("Opaque", secret.getType()); assertEquals("dmFsdWU=", secret.getData().get("key")); assertEquals("dmFsdWUy", secret.getData().get("nested")); assertNotNull(secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + LAST_UPDATE_ANNOTATION)); assertEquals("z/SCo8oELBDAF2DQvX2H3yLs6vvn55Z6c8fdS3Y7l64=", secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + COMPARE_ANNOTATION)); } } ================================================ FILE: src/test/java/de/koudingspawn/vault/OwnerReferenceBugfixTest.java ================================================ package de.koudingspawn.vault; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import com.google.common.collect.ImmutableMap; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultList; import de.koudingspawn.vault.kubernetes.EventHandler; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.OwnerReference; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; import java.util.Collections; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static de.koudingspawn.vault.PropertiesTest.generatePropertiesManifest; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @RunWith(SpringRunner.class) @SpringBootTest( properties = { "kubernetes.vault.url=http://localhost:8210/v1/", "kubernetes.vault.token=c73ab0cb-41e6-b89c-7af6-96b36f1ac87b" } ) public class OwnerReferenceBugfixTest { @ClassRule public static final WireMockClassRule wireMockClassRule = new WireMockClassRule(wireMockConfig().port(8210)); @Rule public WireMockClassRule instanceRule = wireMockClassRule; @Autowired public EventHandler handler; @Autowired public KubernetesClient client; @Autowired public MixedOperation> customResource; @org.springframework.boot.test.context.TestConfiguration static class KindConfig { @Bean @Primary public KubernetesClient client() { return new KubernetesClientBuilder().build(); } } @Before public void before() { WireMock.resetAllScenarios(); client.secrets().inAnyNamespace().delete(); TestHelper.generateLookupSelfStub(); } @Test public void hasCorrectOwnerReference() throws IOException { TestHelper.generateKVStup("kv/key", ImmutableMap.of("value", "kv1content")); TestHelper.generateKV2Stup("kv2/key", ImmutableMap.of("value", "kv2content", "value2", "kv3content")); Vault vault = generatePropertiesManifest("properties-correct-owner-1"); handler.addHandler(vault); Secret secret = client.secrets().inNamespace("default").withName("properties-correct-owner-1").get(); assertEquals(1, secret.getMetadata().getOwnerReferences().size()); assertEquals("something-not-garbage-collected.de/v1", secret.getMetadata().getOwnerReferences().get(0).getApiVersion()); } @Test public void fixOwnerReference() throws IOException { TestHelper.generateKVStup("kv/key", ImmutableMap.of("value", "kv1content")); TestHelper.generateKV2Stup("kv2/key", ImmutableMap.of("value", "kv2content", "value2", "kv3content")); Vault vault = generatePropertiesManifest("properties-correct-owner-2"); Secret secret = new SecretBuilder() .withMetadata( new ObjectMetaBuilder().withName("properties-correct-owner-2").withNamespace("default") .addToOwnerReferences( new OwnerReference( "vault.koudingspawn.de/v1", false, true, "Vault", "properties-correct-owner-2", vault.getMetadata().getUid() ) ).build() ) .withData(Collections.singletonMap("key", "dmFsdWU=")) .build(); client.secrets().inNamespace("default").resource(secret).create(); handler.addHandler(vault); Secret foundSecret = client.secrets().inNamespace("default").withName("properties-correct-owner-2").get(); assertNotNull(foundSecret); assertEquals(1, foundSecret.getMetadata().getOwnerReferences().size()); assertEquals("something-not-garbage-collected.de/v1", foundSecret.getMetadata().getOwnerReferences().get(0).getApiVersion()); } } ================================================ FILE: src/test/java/de/koudingspawn/vault/PKIChainTest.java ================================================ package de.koudingspawn.vault; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultPkiConfiguration; import de.koudingspawn.vault.crd.VaultSpec; import de.koudingspawn.vault.crd.VaultType; import de.koudingspawn.vault.kubernetes.EventHandler; import de.koudingspawn.vault.kubernetes.scheduler.impl.CertRefresh; import de.koudingspawn.vault.vault.impl.pki.VaultResponseData; import io.fabric8.kubernetes.api.model.DeletionPropagation; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import org.junit.*; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.junit4.SpringRunner; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.*; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static de.koudingspawn.vault.Constants.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @RunWith(SpringRunner.class) @SpringBootTest( properties = { "kubernetes.vault.url=http://localhost:8205/v1/" } ) public class PKIChainTest { @ClassRule public static final WireMockClassRule wireMockClassRule = new WireMockClassRule(wireMockConfig().port(8205)); @Rule public WireMockClassRule instanceRule = wireMockClassRule; @Autowired public EventHandler handler; @Autowired public KubernetesClient client; @org.springframework.boot.test.context.TestConfiguration static class KindConfig { @Bean @Primary public KubernetesClient client() { return new KubernetesClientBuilder().build(); } } @Autowired CertRefresh certRefresh; @Before public void before() { WireMock.resetAllScenarios(); client.secrets().inAnyNamespace().delete(); TestHelper.generateLookupSelfStub(); } @Test public void shouldGeneratePkiFromVaultChainResource() throws Exception { VaultResponseData keyPair = generateKeyPair(); Vault vaultResource = generateVaultResource(); stubFor(post(urlEqualTo("/v1/testpki/issue/testrole")) .withRequestBody(matchingJsonPath("$.common_name", containing("test.url.de"))) .withRequestBody(matchingJsonPath("$.ttl", containing("10m"))) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody( String.format(""" { "request_id": "6cc090a8-3821-8244-73e4-5ab62b605587", "lease_id": "", "renewable": false, "lease_duration": 2764800, "data": { "certificate": "%s", "ca_chain": ["%s"], "issuing_ca": "%s", "private_key": "%s" }, "wrap_info": null, "warnings": null, "auth": null }""", keyPair.getCertificate(), keyPair.getCa_chain().get(0), keyPair.getIssuing_ca(), keyPair.getPrivate_key()) ))); handler.addHandler(vaultResource); Secret secret = client.secrets().inNamespace("default").withName("pki").get(); // metadata assertEquals("pki", secret.getMetadata().getName()); assertEquals("default", secret.getMetadata().getNamespace()); assertEquals("Opaque", secret.getType()); assertNotNull(secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + LAST_UPDATE_ANNOTATION)); // compare date String formatedExpirationDate = secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + COMPARE_ANNOTATION); LocalDateTime parsedCompareDate = parseDate(formatedExpirationDate); LocalDateTime expirationDate = parseDate("2018-06-25T16:02Z"); assertEquals(expirationDate, parsedCompareDate); // body String crtB64 = secret.getData().get("tls.crt"); String crt = new String(Base64.getDecoder().decode(crtB64)); String keyB64 = secret.getData().get("tls.key"); String key = new String(Base64.getDecoder().decode(keyB64)); // not so nice, but wiremock expects double escaping assertEquals(keyPair.getChainedCertificate().replaceAll("\\\\n", "").replaceAll("\\n", ""), crt.replaceAll("\\n", "")); assertEquals(keyPair.getPrivate_key().replaceAll("\\\\n", "").replaceAll("\\n", ""), key.replaceAll("\\n", "")); } @After @Before public void cleanup() { Secret secret = client.secrets().inNamespace("default").withName("pki").get(); if (secret != null) { client.secrets().inNamespace("default").withName("pki").withPropagationPolicy(DeletionPropagation.BACKGROUND).delete(); } } private VaultResponseData generateKeyPair() { String certificate = "-----BEGIN CERTIFICATE-----\\n" + "MIIDZjCCAk6gAwIBAgIUc8PIl50sEQM28x6CV7iK6fae4t4wDQYJKoZIhvcNAQEL\\n" + "BQAwLTErMCkGA1UEAxMibXl2YXVsdC5jb20gSW50ZXJtZWRpYXRlIEF1dGhvcml0\\n" + "eTAeFw0xODA2MjIxNjAyMDVaFw0xODA2MjUxNjAyMzRaMBsxGTAXBgNVBAMTEGJs\\n" + "YWguZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCu\\n" + "lPq1WzzgImZFQ3NNlu9i7Xi6/U1a40csFz9Gvho3lIMgY2IgtxwaZTTO5vplKr+s\\n" + "VfM2f6Enxv89i5If6J1gE1R/X728XYqNeXAP/5jgRwaq9S7Eg1len5OgXdkjO3RV\\n" + "WkY8zMmG8N6e0viNgs9cYm9bJV9u9bKDeXYRaDeiSVIh77dL6Vaws06ViJeDzQxp\\n" + "kDiaeSY9jyjhwBor+nqw7Vrvqc8LjaKy5JzD9rUPcv7O0hy3HF0/D3s2ailNdLar\\n" + "4U9qEViI/5BzsykcJnvaLFW3RqZ1DmlUUoompOMURFMwbrEI3Gu4rKXmlu5zc9dx\\n" + "UiDjnTup7e0hK4mNhqZ/AgMBAAGjgY8wgYwwDgYDVR0PAQH/BAQDAgOoMB0GA1Ud\\n" + "JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUEsdbwRqYE9nRbcJE\\n" + "XnAZHjxnnxEwHwYDVR0jBBgwFoAUTu0jLnLe15sBQhrYSEbXAILoLkMwGwYDVR0R\\n" + "BBQwEoIQYmxhaC5leGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAjyqdfK81\\n" + "NjkwS4heUg3DQtSILurLyzt+x+yafPnJTByoX2UE+xAUBeKwyxUmcRO6/IEUdZmM\\n" + "qmGE44x2gyP3+YfjBSkfjKRQz8IslZWW4DPn11O+icpQieNAhFt6goD8rReNeH+i\\n" + "OWVr+vBwi71C7uR9W4NkBtdCqXBfXrSkwtb9aIFZxr+bfYTIFCfsFnv8OAYCbzhk\\n" + "6QtrWjQKduyuxisuvVAztJhk0JMg09xYgTsCJ8oQBNAwYR5UOl55TADgj19R1Xpq\\n" + "8qT7r56++C5I0BMCMk63Q1ofgeYyTGJYsxjjNa+rLLYlK9ysOofrrLYdyp03xniK\\n" + "IX4NZ1EHqWONxQ==\\n" + "-----END CERTIFICATE-----"; String caChain = "-----BEGIN CERTIFICATE-----\\n" + "MIIDNDCCAhygAwIBAgIUOM/FWyCOxZuYgAanfIk+11NQHLswDQYJKoZIhvcNAQEL\\n" + "BQAwFjEUMBIGA1UEAxMLbXl2YXVsdC5jb20wHhcNMTgwNjIyMTU1OTI3WhcNMTgw\\n" + "NzI0MTU1OTU3WjAtMSswKQYDVQQDEyJteXZhdWx0LmNvbSBJbnRlcm1lZGlhdGUg\\n" + "QXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt6EKCNbb\\n" + "+02nY/jM8EIwJ8moLUo6hCqOsEb4jFWYLjbInKICqdO36KsUEQL9W9Kq6LGqdZV4\\n" + "cOYbpWYXr20Ni3p7hSgIttX+LJVPnm9g4Yc/71Wtzv9YsFXsudwQXE+iG+eBH2V2\\n" + "kHbqANh/8ZXDzhZUlNecgR44YOOmS8z0nh3fOYwBu4eTazBvRk9PaUqS6VPtgqNF\\n" + "sUAa7rszmOTRxVVsAN+O/HS08/+vwkIgvgTV849Pvb6diBlBWBc1LOOVuV+UWEsl\\n" + "jfmhCM/nHqtGxg/cnPEV35WjAZH+ND+nkC2wRKaxfxf3B03MUm6WwIrRZrhMsBt2\\n" + "OeEabv+NxKydDwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw\\n" + "AwEB/zAdBgNVHQ4EFgQUTu0jLnLe15sBQhrYSEbXAILoLkMwHwYDVR0jBBgwFoAU\\n" + "LzwRuDNnbxWfzW/iiM+Ek963I28wDQYJKoZIhvcNAQELBQADggEBADgwe8v8YPkJ\\n" + "8Rsx9VkACu6IZ8hDkhDEe82wtU9BdzyIahgPgSwbjoLSIxX9nN3b8ifX1ZNgeio7\\n" + "hkCQ8q0s3Eor479IqXv2i7yBMDcQ7o5DSh/g21/1IQ7cJ5rJVnpCpw6pb5Td2ww9\\n" + "6L90xrHSX13n90xIctglEKiMvAoB0UBQRlFG2qL1IgmhpVYBuiqLPIsaRbj2Bthd\\n" + "nmsvDBflruBcjuimmRyozOVT1Cgw+xxw7nMMYDDs9iDqSgYnuLJZRYiHDVTna/Vx\\n" + "UB6pS3TuoOKzJKuYL3lu2Yvjp0wTOXmaEg9wW9BqpIxu3U0Hd2ScEEOGQ+b3VEyp\\n" + "IwwSk9KcPFs=\\n" + "-----END CERTIFICATE-----"; String issuingCa = "-----BEGIN CERTIFICATE-----\\n" + "MIIDNDCCAhygAwIBAgIUOM/FWyCOxZuYgAanfIk+11NQHLswDQYJKoZIhvcNAQEL\\n" + "BQAwFjEUMBIGA1UEAxMLbXl2YXVsdC5jb20wHhcNMTgwNjIyMTU1OTI3WhcNMTgw\\n" + "NzI0MTU1OTU3WjAtMSswKQYDVQQDEyJteXZhdWx0LmNvbSBJbnRlcm1lZGlhdGUg\\n" + "QXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt6EKCNbb\\n" + "+02nY/jM8EIwJ8moLUo6hCqOsEb4jFWYLjbInKICqdO36KsUEQL9W9Kq6LGqdZV4\\n" + "cOYbpWYXr20Ni3p7hSgIttX+LJVPnm9g4Yc/71Wtzv9YsFXsudwQXE+iG+eBH2V2\\n" + "kHbqANh/8ZXDzhZUlNecgR44YOOmS8z0nh3fOYwBu4eTazBvRk9PaUqS6VPtgqNF\\n" + "sUAa7rszmOTRxVVsAN+O/HS08/+vwkIgvgTV849Pvb6diBlBWBc1LOOVuV+UWEsl\\n" + "jfmhCM/nHqtGxg/cnPEV35WjAZH+ND+nkC2wRKaxfxf3B03MUm6WwIrRZrhMsBt2\\n" + "OeEabv+NxKydDwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw\\n" + "AwEB/zAdBgNVHQ4EFgQUTu0jLnLe15sBQhrYSEbXAILoLkMwHwYDVR0jBBgwFoAU\\n" + "LzwRuDNnbxWfzW/iiM+Ek963I28wDQYJKoZIhvcNAQELBQADggEBADgwe8v8YPkJ\\n" + "8Rsx9VkACu6IZ8hDkhDEe82wtU9BdzyIahgPgSwbjoLSIxX9nN3b8ifX1ZNgeio7\\n" + "hkCQ8q0s3Eor479IqXv2i7yBMDcQ7o5DSh/g21/1IQ7cJ5rJVnpCpw6pb5Td2ww9\\n" + "6L90xrHSX13n90xIctglEKiMvAoB0UBQRlFG2qL1IgmhpVYBuiqLPIsaRbj2Bthd\\n" + "nmsvDBflruBcjuimmRyozOVT1Cgw+xxw7nMMYDDs9iDqSgYnuLJZRYiHDVTna/Vx\\n" + "UB6pS3TuoOKzJKuYL3lu2Yvjp0wTOXmaEg9wW9BqpIxu3U0Hd2ScEEOGQ+b3VEyp\\n" + "IwwSk9KcPFs=\\n" + "-----END CERTIFICATE-----"; String privateKey = "-----BEGIN RSA PRIVATE KEY-----\\n" + "MIIEowIBAAKCAQEArpT6tVs84CJmRUNzTZbvYu14uv1NWuNHLBc/Rr4aN5SDIGNi\\n" + "ILccGmU0zub6ZSq/rFXzNn+hJ8b/PYuSH+idYBNUf1+9vF2KjXlwD/+Y4EcGqvUu\\n" + "xINZXp+ToF3ZIzt0VVpGPMzJhvDentL4jYLPXGJvWyVfbvWyg3l2EWg3oklSIe+3\\n" + "S+lWsLNOlYiXg80MaZA4mnkmPY8o4cAaK/p6sO1a76nPC42isuScw/a1D3L+ztIc\\n" + "txxdPw97NmopTXS2q+FPahFYiP+Qc7MpHCZ72ixVt0amdQ5pVFKKJqTjFERTMG6x\\n" + "CNxruKyl5pbuc3PXcVIg4507qe3tISuJjYamfwIDAQABAoIBABTuFXSCoLS6SwqI\\n" + "wJ0PuFli4POCBLEdyF2X1+UyS1BYhLPwVkZXzY24jnEzrddNHbeaglMJUBfFurn1\\n" + "LqqWp69qAdpXbxbTHBZD9dRlLz3MJhd+14GFwcQfW4KBXdPkf9jvvrXxU0PTQs1F\\n" + "u7izcwq/XlxOCbfyytkKScZieTECaGmy7l6kJphaFP7m8eQ6vwI9LZXeFvA4URLJ\\n" + "IzxM36Y/DkY+ME5AWxc9L+bYZjGj4QjRtfe27Dpy6FyrZg99pFfhoU/mWop3dh9z\\n" + "rHBIvBppYUlp9BBBnOBxDTcmTh+dYGvApIud2gkpy3Om1BxCPXf7/TQgz3GQEnJW\\n" + "JVaE94ECgYEAwSl2a7NPOqP4pYeKjRcdgwaI+lmIRjT6oNgHH4VM1aGolb8vYvdP\\n" + "1VMwmMsDpxygX2p7gp9tbt2ZIcvwKBrIw6QTtS6kqxSqOSMlleeaHdd/WHYUmV9+\\n" + "xiU5uHEWwjYqYivVXo1br06eXTE7zD2bDg9hZHDgCRRGXc6IvniQpr8CgYEA52As\\n" + "I047YoJUX3OWE7Rxp6gIIO2St+HEKEZiV38m+7mWJOghUvGVssy/WYAPEvcGBAQp\\n" + "zty2daaZBevFeW499N7haAFfoVbxpvtCR9fxheL1EkbsFMRPpCjgRGzhI7Rx2QAi\\n" + "D5URL2WppkVDa+BW0wUIczL5jd5L57z3MT0XsEECgYEAviHO89JLEYCnVmAlbB2t\\n" + "qfQ7zplkfx7U+I/L6yXt7Ha0l7nZrgObrHK3ah6jGNIftev9aST+tdsgSVkRqpg6\\n" + "uACAeZ5Q7iloKNfEvlp7pBYjvnJ0ckfCZM3tk/SVH1Prwjg9TVW9QsETNs4oezDE\\n" + "uEFBb3l/vNAdN2b9yOaqE8cCgYAMZ/G16unwPEC95Xq0j8ZQUQgui86EIYzdA/kd\\n" + "6+lxMeBFFlVDF0UJk0TnTaCBSdF+waJkPx1hbY9i6+NowWp9CL5ZT0mLYxgN9gb1\\n" + "xzRiE2tEkZzy+Bu1F6P+xz/DJFe+ZO1unHWRbwgLrEcTL7I4Glr7ok4TN0omoNE4\\n" + "SKhOgQKBgDOZMbY2zzwozBQG5vxdxndIGmG874CRJ5CiS/hfTE0Gxdt54B9VadNU\\n" + "ouZSwidB4YQH2aYHH1aUGhPemExztAcDJz2UholDUoj3v+ft6jCuMjb1loofqJeW\\n" + "+hgtD7tH5sihc8tKYHXg6IfrZLdmUbWWj0qK6ow0hzSFNuJgZB+5\\n" + "-----END RSA PRIVATE KEY-----"; VaultResponseData vaultResponseData = new VaultResponseData(); vaultResponseData.setPrivate_key(privateKey); vaultResponseData.setCertificate(certificate); vaultResponseData.setCa_chain(Collections.singletonList(caChain)); vaultResponseData.setIssuing_ca(issuingCa); return vaultResponseData; } private Vault generateVaultResource() { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("pki").withNamespace("default").withUid(UUID.randomUUID().toString()).build() ); VaultSpec spec = new VaultSpec(); spec.setType(VaultType.PKI); spec.setPath("testpki/issue/testrole"); VaultPkiConfiguration vaultPkiConfiguration = new VaultPkiConfiguration(); vaultPkiConfiguration.setCommonName("test.url.de"); vaultPkiConfiguration.setTtl("10m"); spec.setPkiConfiguration(vaultPkiConfiguration); vault.setSpec(spec); return vault; } private LocalDateTime convertDate(Date date) { return date.toInstant() .atZone(ZoneId.of("UTC")) .toLocalDateTime().truncatedTo(ChronoUnit.MINUTES); } private LocalDateTime parseDate(String date) throws ParseException { SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT); TimeZone tz = TimeZone.getTimeZone("UTC"); format.setTimeZone(tz); return convertDate(format.parse(date)); } } ================================================ FILE: src/test/java/de/koudingspawn/vault/PKITest.java ================================================ package de.koudingspawn.vault; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultPkiConfiguration; import de.koudingspawn.vault.crd.VaultSpec; import de.koudingspawn.vault.crd.VaultType; import de.koudingspawn.vault.kubernetes.EventHandler; import de.koudingspawn.vault.kubernetes.scheduler.impl.CertRefresh; import de.koudingspawn.vault.vault.impl.pki.VaultResponseData; import io.fabric8.kubernetes.api.model.DeletionPropagation; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.junit.*; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.junit4.SpringRunner; import java.math.BigInteger; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.Security; import java.security.cert.Certificate; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.Base64; import java.util.Date; import java.util.TimeZone; import java.util.UUID; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static de.koudingspawn.vault.Constants.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @RunWith(SpringRunner.class) @SpringBootTest( properties = { "kubernetes.vault.url=http://localhost:8204/v1/" } ) public class PKITest { static { Security.addProvider(new BouncyCastleProvider()); } @ClassRule public static final WireMockClassRule wireMockClassRule = new WireMockClassRule(wireMockConfig().port(8204)); @Rule public WireMockClassRule instanceRule = wireMockClassRule; @Autowired public EventHandler handler; @Autowired public KubernetesClient client; @org.springframework.boot.test.context.TestConfiguration static class KindConfig { @Bean @Primary public KubernetesClient client() { return new KubernetesClientBuilder().build(); } } @Autowired CertRefresh certRefresh; @Before public void before() { WireMock.resetAllScenarios(); client.secrets().inAnyNamespace().delete(); TestHelper.generateLookupSelfStub(); } @Test public void shouldGeneratePkiFromVaultResource() throws Exception { Date startDate = new Date(); VaultResponseData keyPair = generateKeyPair(startDate, 60L); Vault vaultResource = generateVaultResource(); stubFor(post(urlEqualTo("/v1/testpki/issue/testrole")) .withRequestBody(matchingJsonPath("$.common_name", containing("test.url.de"))) .withRequestBody(matchingJsonPath("$.ttl", containing("10m"))) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody( String.format(""" { "request_id": "6cc090a8-3821-8244-73e4-5ab62b605587", "lease_id": "", "renewable": false, "lease_duration": 2764800, "data": { "certificate": "%s", "private_key": "%s" }, "wrap_info": null, "warnings": null, "auth": null }""", keyPair.getCertificate(), keyPair.getPrivate_key()) ))); handler.addHandler(vaultResource); Secret secret = client.secrets().inNamespace("default").withName("pki").get(); // metadata assertEquals("pki", secret.getMetadata().getName()); assertEquals("default", secret.getMetadata().getNamespace()); assertEquals("Opaque", secret.getType()); assertNotNull(secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + LAST_UPDATE_ANNOTATION)); // compare date String formatedExpirationDate = secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + COMPARE_ANNOTATION); LocalDateTime parsedCompareDate = parseDate(formatedExpirationDate); LocalDateTime expirationDate = convertDate(startDate).plusMinutes(1); assertEquals(expirationDate, parsedCompareDate); // body String crtB64 = secret.getData().get("tls.crt"); String crt = new String(java.util.Base64.getDecoder().decode(crtB64)); String keyB64 = secret.getData().get("tls.key"); String key = new String(java.util.Base64.getDecoder().decode(keyB64)); // not so nice, but wiremock expects double escaping assertEquals(keyPair.getCertificate().replaceAll("\\\\n", ""), crt.replaceAll("\\n", "")); assertEquals(keyPair.getPrivate_key().replaceAll("\\\\n", ""), key.replaceAll("\\n", "")); } @After @Before public void cleanup() { Secret secret = client.secrets().inNamespace("default").withName("pki").get(); if (secret != null) { client.secrets().inNamespace("default").withName("pki").withPropagationPolicy(DeletionPropagation.BACKGROUND).delete(); } } private VaultResponseData generateKeyPair(Date startDate, long valid) throws Exception { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME); keyPairGenerator.initialize(2048); KeyPair keyPair = keyPairGenerator.generateKeyPair(); X500Name x500Name = new X500Name("CN=Test"); BigInteger certSerialNumber = BigInteger.valueOf(System.currentTimeMillis()); String signatureAlgorithm = "SHA256WithRSA"; ContentSigner contentSigner = new JcaContentSignerBuilder(signatureAlgorithm) .build(keyPair.getPrivate()); Instant endDate = startDate.toInstant().plus(valid, ChronoUnit.SECONDS); JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( x500Name, certSerialNumber, startDate, Date.from(endDate), x500Name, keyPair.getPublic()); Certificate certificate = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME) .getCertificate(certBuilder.build(contentSigner)); byte[] encodedPrivateKey = keyPair.getPrivate().getEncoded(); byte[] encodedPublicKey = certificate.getEncoded(); String privateKeySb = "-----BEGIN PRIVATE KEY-----\n" + Base64.getMimeEncoder().encodeToString(encodedPrivateKey) + "\n-----END PRIVATE KEY-----"; String publicKey = "-----BEGIN PUBLIC KEY-----\n" + Base64.getMimeEncoder().encodeToString(encodedPublicKey) + "\n-----END PUBLIC KEY-----"; privateKeySb = privateKeySb.replaceAll("\\n", "\\\\n"); privateKeySb = privateKeySb.replaceAll("\\r", ""); publicKey = publicKey.replaceAll("\\n", "\\\\n"); publicKey = publicKey.replaceAll("\\r", ""); VaultResponseData vaultResponseData = new VaultResponseData(); vaultResponseData.setPrivate_key(privateKeySb); vaultResponseData.setCertificate(publicKey); return vaultResponseData; } private Vault generateVaultResource() { Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName("pki").withNamespace("default").withUid(UUID.randomUUID().toString()).build() ); VaultSpec spec = new VaultSpec(); spec.setType(VaultType.PKI); spec.setPath("testpki/issue/testrole"); VaultPkiConfiguration vaultPkiConfiguration = new VaultPkiConfiguration(); vaultPkiConfiguration.setCommonName("test.url.de"); vaultPkiConfiguration.setTtl("10m"); spec.setPkiConfiguration(vaultPkiConfiguration); vault.setSpec(spec); return vault; } private LocalDateTime convertDate(Date date) { return date.toInstant() .atZone(ZoneId.of("UTC")) .toLocalDateTime().truncatedTo(ChronoUnit.MINUTES); } private LocalDateTime parseDate(String date) throws ParseException { SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT); TimeZone tz = TimeZone.getTimeZone("UTC"); format.setTimeZone(tz); return convertDate(format.parse(date)); } } ================================================ FILE: src/test/java/de/koudingspawn/vault/PropertiesTest.java ================================================ package de.koudingspawn.vault; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit.WireMockClassRule; import com.google.common.collect.ImmutableMap; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultPropertiesConfiguration; import de.koudingspawn.vault.crd.VaultSpec; import de.koudingspawn.vault.crd.VaultType; import de.koudingspawn.vault.kubernetes.EventHandler; import de.koudingspawn.vault.kubernetes.scheduler.impl.CertRefresh; import de.koudingspawn.vault.vault.VaultService; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import io.fabric8.kubernetes.api.model.DeletionPropagation; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import org.junit.*; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.core.io.ClassPathResource; import org.springframework.test.context.junit4.SpringRunner; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.Base64; import java.util.HashMap; import java.util.UUID; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static de.koudingspawn.vault.Constants.LAST_UPDATE_ANNOTATION; import static org.junit.Assert.*; @RunWith(SpringRunner.class) @SpringBootTest( properties = { "kubernetes.vault.url=http://localhost:8208/v1/", "kubernetes.vault.token=c73ab0cb-41e6-b89c-7af6-96b36f1ac87b" } ) public class PropertiesTest { @ClassRule public static final WireMockClassRule wireMockClassRule = new WireMockClassRule(wireMockConfig().port(8208)); @Rule public WireMockClassRule instanceRule = wireMockClassRule; @Autowired public EventHandler handler; @Autowired public VaultService vaultService; @Autowired public KubernetesClient client; @org.springframework.boot.test.context.TestConfiguration static class KindConfig { @Bean @Primary public KubernetesClient client() { return new KubernetesClientBuilder().build(); } } @Autowired CertRefresh certRefresh; @Before public void before() { WireMock.resetAllScenarios(); client.secrets().inAnyNamespace().delete(); TestHelper.generateLookupSelfStub(); } @Test public void shouldRenderPropertiesFile() throws IOException { TestHelper.generateKVStup("kv/key", ImmutableMap.of("value", "kv1content")); TestHelper.generateKV2Stup("kv2/key", ImmutableMap.of("value", "kv2content", "value2", "kv3content")); Vault vault = generatePropertiesManifest("properties"); handler.addHandler(vault); Secret secret = client.secrets().inNamespace("default").withName("properties").get(); assertEquals("properties", secret.getMetadata().getName()); assertEquals("default", secret.getMetadata().getNamespace()); assertEquals("Opaque", secret.getType()); assertNotNull(secret.getMetadata().getAnnotations().get("vault.koudingspawn.de" + LAST_UPDATE_ANNOTATION)); String renderedB64Properties = secret.getData().get("test.properties"); String renderedProperties = new String(Base64.getDecoder().decode(renderedB64Properties)); assertTrue(renderedProperties.contains("test=kv1content")); assertTrue(renderedProperties.contains("test2=kv2content")); assertTrue(renderedProperties.contains("test3=contextvalue")); assertTrue(renderedProperties.contains("spring.jpa.properties.hibernate.dialect=class.module.classLoader.resources.context.parent.pipeline.first")); } @Test(expected = SecretNotAccessibleException.class) public void shouldFailRenderSecret() throws SecretNotAccessibleException, IOException { TestHelper.generateKVStup("kv/key", ImmutableMap.of("value", "kv1content")); TestHelper.generateKV2Stup("kv2/key", ImmutableMap.of("value", "kv2content")); Vault vault = generatePropertiesManifest("properties-1"); vaultService.generateSecret(vault); } @After @Before public void cleanup() { Secret secret = client.secrets().inNamespace("default").withName("properties").get(); if (secret != null) { client.secrets().inNamespace("default").withName("properties").withPropagationPolicy(DeletionPropagation.BACKGROUND).delete(); } } static Vault generatePropertiesManifest(String name) throws IOException { HashMap properties = new HashMap<>(); File file = new ClassPathResource("test.properties").getFile(); String content = new String(Files.readAllBytes(file.toPath())); properties.put("test.properties", content); HashMap context = new HashMap<>(); context.put("contextkey", "contextvalue"); VaultSpec vaultSpec = new VaultSpec(); vaultSpec.setType(VaultType.PROPERTIES); VaultPropertiesConfiguration vaultPropertiesConfiguration = new VaultPropertiesConfiguration(); vaultPropertiesConfiguration.setFiles(properties); vaultPropertiesConfiguration.setContext(context); vaultSpec.setPropertiesConfiguration(vaultPropertiesConfiguration); Vault vault = new Vault(); vault.setMetadata( new ObjectMetaBuilder().withName(name).withNamespace("default").withUid(UUID.randomUUID().toString()).build() ); vault.setSpec(vaultSpec); return vault; } } ================================================ FILE: src/test/java/de/koudingspawn/vault/TestHelper.java ================================================ package de.koudingspawn.vault; import org.json.JSONObject; import java.util.Map; import static com.github.tomakehurst.wiremock.client.WireMock.*; public class TestHelper { public static void generateLookupSelfStub() { stubFor(get(urlEqualTo("/v1/auth/token/lookup-self")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(""" { "request_id": "200ef4ee-7ca7-9d38-2e63-6002454e00d7", "lease_id": "", "renewable": false, "lease_duration": 0, "data": { "accessor": "c69c3bd7-c142-c655-2757-77bfdc86b04a", "creation_time": 1536033750, "creation_ttl": 0, "display_name": "root", "entity_id": "", "expire_time": null, "explicit_max_ttl": 0, "id": "c73ab0cb-41e6-b89c-7af6-96b36f1ac87b", "meta": null, "num_uses": 0, "orphan": true, "path": "auth/token/root", "policies": [ "root" ], "ttl": 0 }, "wrap_info": null, "warnings": null, "auth": null }"""))); } public static void generateKVStup(String path, Map value) { JSONObject jsonObject = new JSONObject(value); stubFor(get(urlPathMatching("/v1/" + path)) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{\"request_id\":\"6cc090a8-3821-8244-73e4-5ab62b605587\",\"lease_id\":\"\",\"renewable\":false,\"lease_duration\":2764800,\"data\":" + jsonObject + ",\"wrap_info\":null,\"warnings\":null,\"auth\":null}"))); } public static void generateKV2Stup(String path, Map value) { JSONObject jsonObject = new JSONObject(value); String[] splittedPath = path.split("/"); stubFor(get(urlPathMatching("/v1/" + splittedPath[0] + "/data/" + splittedPath[1])) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{\n" + " \"request_id\": \"1cfee2a6-318a-ea12-f5b5-6fd52d74d2c6\",\n" + " \"lease_id\": \"\",\n" + " \"renewable\": false,\n" + " \"lease_duration\": 0,\n" + " \"data\": {\n" + " \"data\": " + jsonObject + ",\n" + " \"metadata\": {\n" + " \"created_time\": \"2018-12-10T18:59:53.337997525Z\",\n" + " \"deletion_time\": \"\",\n" + " \"destroyed\": false,\n" + " \"version\": 1\n" + " }\n" + " },\n" + " \"wrap_info\": null,\n" + " \"warnings\": null,\n" + " \"auth\": null\n" + "}"))); } } ================================================ FILE: src/test/java/de/koudingspawn/vault/admissionreview/AdmissionReviewTest.java ================================================ package de.koudingspawn.vault.admissionreview; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultList; import de.koudingspawn.vault.vault.VaultSecret; import de.koudingspawn.vault.vault.VaultService; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import java.util.HashMap; import static org.mockito.ArgumentMatchers.any; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(SpringRunner.class) @SpringBootTest( properties = { "kubernetes.vault.url=http://localhost:8206/v1/", "kubernetes.vault.token=c73ab0cb-41e6-b89c-7af6-96b36f1ac87b" } ) @AutoConfigureMockMvc @ActiveProfiles("test") public class AdmissionReviewTest { @MockBean VaultService vaultService; @MockBean public MixedOperation> customResource; @Autowired private MockMvc mvc; @Test public void shouldFailWithInvalidRequest() throws Exception { SecretNotAccessibleException secretException = new SecretNotAccessibleException("Secret is not accessible"); Mockito.when(vaultService.generateSecret(any())).thenThrow(secretException); mvc.perform(post("/validation/vault-crd").content(""" { "apiVersion": "admission.k8s.io/v1", "kind": "AdmissionReview", "request": { "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", "object": { "apiVersion": "koudingspawn.de/v1", "kind": "Vault", "metadata": { "name": "test-vault", "namespace": "default" }, "spec": { "type": "KEYVALUE", "path": "secret/qweasd" } } } } """).contentType("application/json")) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.response.uid").value("705ab4f5-6393-11e8-b7cc-42010a800002")) .andExpect(jsonPath("$.response.allowed").value("false")) .andExpect(jsonPath("$.response.status.code").value("400")) .andExpect(jsonPath("$.response.status.message").value("Secret is not accessible")); } @Test public void shouldReturnValidValue() throws Exception { VaultSecret vaultSecret = new VaultSecret(new HashMap<>(), "qweasd"); Mockito.when(vaultService.generateSecret(any())).thenReturn(vaultSecret); mvc.perform(post("/validation/vault-crd").content(""" { "apiVersion": "admission.k8s.io/v1", "kind": "AdmissionReview", "request": { "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", "object": { "apiVersion": "koudingspawn.de/v1", "kind": "Vault", "metadata": { "name": "test-vault", "namespace": "default" }, "spec": { "type": "KEYVALUE", "path": "secret/qweasd" } } } }""").contentType("application/json")) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.response.uid").value("705ab4f5-6393-11e8-b7cc-42010a800002")) .andExpect(jsonPath("$.response.allowed").value("true")); } } ================================================ FILE: src/test/java/de/koudingspawn/vault/kubernetes/EventHandlerTest.java ================================================ package de.koudingspawn.vault.kubernetes; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.crd.VaultSpec; import de.koudingspawn.vault.kubernetes.event.EventNotification; import de.koudingspawn.vault.vault.VaultSecret; import de.koudingspawn.vault.vault.VaultService; import de.koudingspawn.vault.vault.communication.SecretNotAccessibleException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.springframework.test.context.junit4.SpringRunner; import java.util.HashMap; import static org.mockito.Mockito.*; @RunWith(SpringRunner.class) public class EventHandlerTest { @Mock private VaultService vaultService; @Mock private KubernetesService kubernetesService; @Mock private ChangeAdjustmentService changeAdjustmentService; @Mock private EventNotification eventNotification; private EventHandler eventHandler; @Before public void setup() { eventHandler = new EventHandler(vaultService, kubernetesService, changeAdjustmentService, eventNotification, true); } @Test public void shouldGenerateKubernetesSecret() throws SecretNotAccessibleException { Vault vault = new Vault(); VaultSecret vaultSecret = new VaultSecret(new HashMap<>(), "COMPARE"); when(vaultService.generateSecret(vault)).thenReturn(vaultSecret); eventHandler.addHandler(vault); verify(kubernetesService, times(1)).createSecret(vault, vaultSecret); } @Test public void shouldDoNotingIfSecretForVaultAlreadyExists() { Vault vault = new Vault(); when(kubernetesService.exists(vault)).thenReturn(true); eventHandler.addHandler(vault); verify(kubernetesService, never()).createSecret(any(), any()); } @Test public void shouldDoNothingIfGenerateSecretFails() throws SecretNotAccessibleException { Vault vault = new Vault(); when(vaultService.generateSecret(vault)).thenThrow(SecretNotAccessibleException.class); eventHandler.addHandler(vault); verify(kubernetesService, never()).createSecret(any(), any()); } @Test public void shouldModifySecret() throws SecretNotAccessibleException { Vault vault = new Vault(); vault.setSpec(new VaultSpec()); VaultSecret vaultSecret = new VaultSecret(new HashMap<>(), "COMPARE"); when(vaultService.generateSecret(vault)).thenReturn(vaultSecret); eventHandler.modifyHandler(vault); verify(kubernetesService, times(1)).modifySecret(vault, vaultSecret); } @Test public void shouldDoNothingIfCreateSecretForModificationFails() throws SecretNotAccessibleException { Vault vault = new Vault(); vault.setSpec(new VaultSpec()); when(vaultService.generateSecret(vault)).thenThrow(SecretNotAccessibleException.class); eventHandler.modifyHandler(vault); verify(kubernetesService, never()).modifySecret(any(), any()); } } ================================================ FILE: src/test/java/de/koudingspawn/vault/kubernetes/KubernetesServiceTest.java ================================================ package de.koudingspawn.vault.kubernetes; import de.koudingspawn.vault.crd.Vault; import de.koudingspawn.vault.kubernetes.cache.SecretCache; import de.koudingspawn.vault.vault.VaultSecret; import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.junit4.SpringRunner; import java.util.HashMap; import java.util.UUID; import static de.koudingspawn.vault.Constants.COMPARE_ANNOTATION; import static org.junit.Assert.*; @RunWith(SpringRunner.class) public class KubernetesServiceTest { private static final String COMPARE = "COMPARE"; private static final String CRDNAME = "CRDNAME"; private static final String CRDGROUP = "CRDGROUP"; private static final String NAMESPACE = "test"; private static final String SECRETNAME = "testsecret"; @Autowired public KubernetesClient client; private KubernetesService kubernetesService; @org.springframework.boot.test.context.TestConfiguration static class KindConfig { @Bean @Primary public KubernetesClient client() { return new KubernetesClientBuilder().build(); } } @Before public void setUp() { SecretCache secretCache = new SecretCache(client, false); kubernetesService = new KubernetesService(client, secretCache, CRDNAME, CRDGROUP); Namespace ns = new NamespaceBuilder().withMetadata(new ObjectMetaBuilder().withName(NAMESPACE).build()).build(); client.namespaces().resource(ns).createOrReplace(); } @Test public void shouldCheckIfResourceExists() { Vault vault = generateVault(); Secret testsecret = generateSecret(); client.secrets().inNamespace(NAMESPACE).resource(testsecret).create(); boolean exists = kubernetesService.exists(vault); assertTrue(exists); } @Test public void shouldFindNoResource() { Vault vault = generateVault(); boolean exists = kubernetesService.exists(vault); assertFalse(exists); } @Test public void shouldCreateSecret() { Vault vault = generateVault(); VaultSecret vaultSecret = generateVaultSecret(); kubernetesService.createSecret(vault, vaultSecret); Secret secret = client.secrets().inNamespace(NAMESPACE).withName(SECRETNAME).get(); assertEquals("dmFsdWU=", secret.getData().get("key")); // value assertEquals("Opaque", secret.getType()); assertEquals(COMPARE, secret.getMetadata().getAnnotations().get(CRDNAME + COMPARE_ANNOTATION)); assertNotNull(secret.getMetadata().getAnnotations().get(CRDNAME + COMPARE_ANNOTATION)); } @Test public void shouldDeleteSecret() { Secret secret = generateSecret(); client.secrets().inNamespace(NAMESPACE).resource(secret).create(); assertNotNull(client.secrets().inNamespace(NAMESPACE).withName(SECRETNAME).get()); kubernetesService.deleteSecret(generateVault().getMetadata()); assertNull(client.secrets().inNamespace(NAMESPACE).withName(SECRETNAME).get()); } @Test public void shouldModifySecret() { Secret secret = generateSecret(); client.secrets().inNamespace(NAMESPACE).resource(secret).create(); Vault vault = generateVault(); HashMap data = new HashMap<>(); data.put("key1", "dmFsdWUx"); // value1 VaultSecret modifiedVaultSecret = new VaultSecret(data, COMPARE + "NEW"); kubernetesService.modifySecret(vault, modifiedVaultSecret); Secret foundSecret = client.secrets().inNamespace(NAMESPACE).withName(SECRETNAME).get(); assertEquals(COMPARE + "NEW", foundSecret.getMetadata().getAnnotations().get(CRDNAME + COMPARE_ANNOTATION)); assertEquals("Opaque", foundSecret.getType()); assertEquals("dmFsdWUx", foundSecret.getData().get("key1")); assertNull(foundSecret.getData().get("key")); } @After @Before public void cleanup() { Secret secret = client.secrets().inNamespace(NAMESPACE).withName(SECRETNAME).get(); if (secret != null) { client.secrets().inNamespace(NAMESPACE).withName(SECRETNAME).withPropagationPolicy(DeletionPropagation.BACKGROUND).delete(); } } private Secret generateSecret() { HashMap data = new HashMap<>(); data.put("key", "dmFsdWU="); // value return new SecretBuilder() .withNewMetadata() .withName(SECRETNAME) .endMetadata() .addToData(data) .build(); } private VaultSecret generateVaultSecret() { HashMap data = new HashMap<>(); data.put("key", "dmFsdWU="); return new VaultSecret(data, COMPARE); } private Vault generateVault() { Vault vault = new Vault(); ObjectMeta meta = new ObjectMeta(); meta.setNamespace(NAMESPACE); meta.setName(SECRETNAME); meta.setUid(UUID.randomUUID().toString()); vault.setMetadata(meta); return vault; } } ================================================ FILE: src/test/java/de/koudingspawn/vault/vault/VaultHealthCheckTest.java ================================================ package de.koudingspawn.vault.vault; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.boot.actuate.health.Status; import org.springframework.test.context.junit4.SpringRunner; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.when; @RunWith(SpringRunner.class) public class VaultHealthCheckTest { @Mock VaultCommunication vaultCommunication; @InjectMocks VaultHealthCheck vaultHealthCheck; @Test public void shouldReturnUnhealthyIfVaultCommunicationFails() { when(vaultCommunication.isHealthy()).thenReturn(false); assertEquals(Status.DOWN, vaultHealthCheck.health().getStatus()); } @Test public void shouldReturnHealthyResultIfVaultCommunicationWorks() { when(vaultCommunication.isHealthy()).thenReturn(true); assertEquals(Status.UP, vaultHealthCheck.health().getStatus()); } } ================================================ FILE: src/test/resources/application.properties ================================================ kubernetes.crd.group=something-not-garbage-collected.de kubernetes.crd.name=vault.koudingspawn.de kubernetes.vault.auth=token kubernetes.vault.token=50745c04-e6ef-dc59-0bae-7b4af8470fa6 kubernetes.vault.role=admin kubernetes.interval=60 kubernetes.jks.default-alias=main kubernetes.jks.default-password=changeit kubernetes.jks.default-secret-key-name=key.jks kubernetes.ownerreference-fix.enabled=true management.endpoints.enabled-by-default=false management.endpoint.health.enabled=true spring.main.allow-bean-definition-overriding=true ================================================ FILE: src/test/resources/test.properties ================================================ test={{ vault.lookup('kv/key', 'value') }} test2={{ vault.lookupV2('kv2/key').get('value') }} test3={{ contextkey }} test4={{ vault.lookupV2('kv2/key', 'value2') }} # remidiation test spring4shell spring.jpa.properties.hibernate.dialect=class.module.classLoader.resources.context.parent.pipeline.first ================================================ FILE: src/test/resources/vault-crd.yaml ================================================ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: vault.koudingspawn.de spec: group: koudingspawn.de scope: Namespaced names: plural: vault singular: vault kind: Vault shortNames: - vt versions: - name: v1 served: true storage: true validation: openAPIV3Schema: properties: spec: properties: path: type: string pattern: '^.*?\/.*?(\/.*?)?$' type: type: string enum: - PKI - PKIJKS - CERT - CERTJKS - DOCKERCFG - KEYVALUE - KEYVALUEV2 - PROPERTIES pkiConfiguration: type: object properties: commonName: type: string altNames: type: string ipSans: type: string ttl: type: string pattern: '^[0-9]{1,}[hm]$' jksConfiguration: type: object properties: password: type: string alias: type: string keyName: type: string caAlias: type: string versionConfiguration: type: object properties: version: type: integer propertiesConfiguration: type: object properties: context: type: object files: type: object dockerCfgConfiguration: type: object properties: type: type: string enum: - KEYVALUE - KEYVALUEV2 version: type: integer required: - type