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