├── .github ├── actions │ ├── docker-login │ │ └── action.yml │ ├── imgpkg-push │ │ └── action.yml │ └── pack-build │ │ ├── action.yml │ │ └── report.go ├── dependabot.yml └── workflows │ ├── ci.yaml │ └── unit-test.yml ├── .gitignore ├── .markdownlintrc ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cmd ├── setup-ca-certs │ └── main.go └── webhook │ └── main.go ├── config ├── _namespace.yaml ├── configmaps.yaml ├── deployment.yaml ├── images.yaml ├── overlay.yaml ├── rbac.yaml └── schema.yaml ├── e2e ├── config.go ├── e2e_test.go └── testhelpers.go ├── go.mod ├── go.sum ├── hack └── mdlint-readme.sh ├── packaging ├── README.md ├── metadata.yaml └── package.yaml └── pkg ├── certinjectionwebhook ├── admission_controller.go ├── admission_controller_test.go ├── reconciler.go ├── reconciler_test.go └── webhook.go └── certs ├── cert.go └── cert_test.go /.github/actions/docker-login/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Imgpkg Push' 2 | description: 'Create and push imgpkg bundle' 3 | 4 | inputs: 5 | registry: 6 | description: 'regustry host or full tag to log in to' 7 | required: true 8 | username: 9 | description: 'registry user' 10 | required: true 11 | password: 12 | description: 'registry user' 13 | required: true 14 | 15 | 16 | runs: 17 | using: "composite" 18 | steps: 19 | - name: extract host 20 | id: extract 21 | shell: bash 22 | run: | 23 | host=$(echo "${{ inputs.registry }}" | awk -F "/" '{print $1}') 24 | echo "host=$host" > $GITHUB_OUTPUT 25 | - name: Docker Login 26 | uses: docker/login-action@v2.1.0 27 | with: 28 | registry: ${{ steps.extract.outputs.host }} 29 | username: ${{ inputs.username }} 30 | password: ${{ inputs.password }} 31 | -------------------------------------------------------------------------------- /.github/actions/imgpkg-push/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Imgpkg Push' 2 | description: 'Create and push imgpkg bundle' 3 | 4 | inputs: 5 | webhook_image: 6 | description: 'webhook image' 7 | required: true 8 | setup_ca_certs_image: 9 | description: 'setup-ca-certs image' 10 | required: true 11 | tag: 12 | description: 'location to write image' 13 | required: true 14 | bundle_output: 15 | description: 'name of bundle output' 16 | required: true 17 | 18 | runs: 19 | using: "composite" 20 | steps: 21 | - name: Copy config 22 | shell: bash 23 | run: | 24 | mkdir -p imgpkg-bundle 25 | cp -r config imgpkg-bundle 26 | 27 | - name: Extract version 28 | shell: bash 29 | run: | 30 | [[ $GITHUB_REF =~ ^refs\/tags\/v(.*)$ ]] && version=${BASH_REMATCH[1]} || version=${{ github.sha }} 31 | echo "VERSION=${version}" >> $GITHUB_ENV 32 | 33 | - name: Create version overlay 34 | shell: bash 35 | run: | 36 | cat << EOF > imgpkg-bundle/config/version.yml 37 | #@ load("@ytt:data", "data") 38 | #@ load("@ytt:overlay", "overlay") 39 | 40 | #@overlay/match by=overlay.subset({"metadata":{"name":"cert-injection-webhook"}, "kind": "Deployment"}) 41 | --- 42 | metadata: 43 | labels: 44 | #@overlay/match missing_ok=True 45 | version: ${{ env.VERSION }} 46 | EOF 47 | 48 | cat imgpkg-bundle/config/version.yml 49 | 50 | - name: Create imagevalues.yaml 51 | shell: bash 52 | run: | 53 | cat < imgpkg-bundle/config/imagevalues.yml 54 | #@data/values 55 | --- 56 | setup_ca_certs: 57 | image: ${{ inputs.setup_ca_certs_image }} 58 | cert_injection_webhook: 59 | image: ${{ inputs.webhook_image }} 60 | EOF 61 | 62 | cat imgpkg-bundle/config/imagevalues.yml 63 | 64 | - name: Generate imgpkg lock 65 | shell: bash 66 | run: | 67 | mkdir -p imgpkg-bundle/.imgpkg 68 | ytt -f imgpkg-bundle/config | kbld -f- --imgpkg-lock-output imgpkg-bundle/.imgpkg/images.yml 69 | 70 | cat imgpkg-bundle/.imgpkg/images.yml 71 | 72 | - name: imgpkg push 73 | shell: bash 74 | run: | 75 | imgpkg push -f imgpkg-bundle/ -b "${{inputs.tag}}:${{ env.VERSION }}" --lock-output ${{ inputs.bundle_output }} 76 | cat ${{ inputs.bundle_output }} 77 | 78 | - name: Upload Bundle lock 79 | uses: actions/upload-artifact@v4 80 | with: 81 | name: ${{ inputs.bundle_output }} 82 | path: ${{ inputs.bundle_output }} 83 | -------------------------------------------------------------------------------- /.github/actions/pack-build/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Pack Build' 2 | description: 'Pack build images' 3 | 4 | inputs: 5 | artifact_name: 6 | description: 'name of artifact to upload' 7 | required: true 8 | tag: 9 | description: 'location to write image' 10 | required: true 11 | bp_go_targets: 12 | description: 'value of BP_GO_TARGETS env' 13 | builder: 14 | description: 'builder image' 15 | required: true 16 | default: 'paketobuildpacks/builder-jammy-tiny' 17 | pack_version: 18 | description: 'version of pack to use' 19 | required: true 20 | additional_pack_args: 21 | description: 'additional args for pack' 22 | 23 | outputs: 24 | image: 25 | description: "Built image" 26 | value: ${{ steps.build.outputs.image }} 27 | digest: 28 | description: "Built image digest" 29 | value: ${{ steps.build.outputs.digest }} 30 | 31 | runs: 32 | using: "composite" 33 | steps: 34 | - name: setup-pack 35 | uses: buildpacks/github-actions/setup-pack@v5.0.0 36 | with: 37 | pack-version: ${{ inputs.pack_version }} 38 | - name: build 39 | id: build 40 | shell: bash 41 | run: | 42 | mkdir report 43 | 44 | export PATH="$PATH:$(pwd)" 45 | pack build ${{ inputs.tag }} \ 46 | --builder ${{ inputs.builder }} \ 47 | --env BP_GO_TARGETS="${{ inputs.bp_go_targets }}" \ 48 | --report-output-dir . \ 49 | --cache-image ${{ inputs.tag }}-cache \ 50 | --publish ${{ inputs.additional_pack_args }} 51 | 52 | mkdir images 53 | digest=$(go run .github/actions/pack-build/report.go -path ./report.toml) 54 | name=$(basename ${{ inputs.tag }}) 55 | echo "${{ inputs.tag }}@${digest}" > images/$name 56 | 57 | echo "digest=$digest" >> $GITHUB_OUTPUT 58 | echo "image=$(cat images/$name)" >> $GITHUB_OUTPUT 59 | cat images/$name 60 | - name: Upload Image Artifacts 61 | uses: actions/upload-artifact@v4 62 | with: 63 | name: ${{ inputs.artifact_name }} 64 | path: images/ 65 | -------------------------------------------------------------------------------- /.github/actions/pack-build/report.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/BurntSushi/toml" 9 | ) 10 | 11 | var reportFilePath = flag.String("path", "report/report.toml", "path to report.toml") 12 | 13 | func main() { 14 | flag.Parse() 15 | 16 | report := struct { 17 | Image struct { 18 | Digest string `toml:"digest,omitempty"` 19 | } `toml:"image"` 20 | }{} 21 | _, err := toml.DecodeFile(*reportFilePath, &report) 22 | if err != nil { 23 | log.Fatal(err, "error decoding report toml file") 24 | } 25 | 26 | fmt.Println(report.Image.Digest) 27 | } 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Dependencies listed in go.mod 4 | - package-ecosystem: "gomod" 5 | directory: "/" # Location of package manifests 6 | schedule: 7 | interval: "weekly" 8 | ignore: 9 | - dependency-name: "k8s.io/api" 10 | - dependency-name: "k8s.io/apimachinery" 11 | - dependency-name: "k8s.io/client-go" 12 | 13 | # Dependencies listed in .github/workflows/*.yml 14 | - package-ecosystem: "github-actions" 15 | directory: "/" 16 | schedule: 17 | interval: "weekly" 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - release/** 8 | tags: 9 | - v[0-9]+.[0-9]+.[0-9]+-?** 10 | pull_request: 11 | branches: 12 | - release/** 13 | 14 | defaults: 15 | run: 16 | shell: bash 17 | 18 | env: 19 | PUBLIC_IMAGE_DEV_REPO: ${{ vars.PUBLIC_IMAGE_DEV_REPO }} 20 | PUBLIC_IMAGE_REPO: ${{ vars.PUBLIC_IMAGE_REPO }} 21 | PACK_VERSION: ${{ vars.PACK_VERSION }} 22 | 23 | jobs: 24 | unit: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 31 | 32 | - name: Set up Go 33 | uses: actions/setup-go@v5 34 | with: 35 | go-version-file: 'go.mod' 36 | 37 | - name: Run tests 38 | run: go test -v ./pkg/... 39 | 40 | - name: Report coverage 41 | uses: codecov/codecov-action@v5.4.0 42 | 43 | webhook-image: 44 | runs-on: ubuntu-latest 45 | outputs: 46 | digest: ${{ steps.build.outputs.digest }} 47 | image: ${{ steps.build.outputs.image }} 48 | steps: 49 | - name: Checkout 50 | uses: actions/checkout@v4 51 | - name: Docker Login 52 | uses: ./.github/actions/docker-login 53 | with: 54 | registry: ghcr.io 55 | username: ${{ github.actor }} 56 | password: ${{ secrets.GITHUB_TOKEN }} 57 | - name: Build 58 | id: build 59 | uses: ./.github/actions/pack-build 60 | with: 61 | artifact_name: webhook-image 62 | pack_version: ${{ env.PACK_VERSION }} 63 | tag: ${{ env.PUBLIC_IMAGE_DEV_REPO }}/webhook 64 | bp_go_targets: "./cmd/webhook" 65 | 66 | setup-ca-certs-image: 67 | runs-on: ubuntu-latest 68 | outputs: 69 | digest: ${{ steps.build.outputs.digest }} 70 | image: ${{ steps.build.outputs.image }} 71 | steps: 72 | - name: Checkout 73 | uses: actions/checkout@v4 74 | - name: Docker Login 75 | uses: ./.github/actions/docker-login 76 | with: 77 | registry: ghcr.io 78 | username: ${{ github.actor }} 79 | password: ${{ secrets.GITHUB_TOKEN }} 80 | - name: Build 81 | id: build 82 | uses: ./.github/actions/pack-build 83 | with: 84 | artifact_name: setup-ca-certs-image 85 | pack_version: ${{ env.PACK_VERSION }} 86 | tag: ${{ env.PUBLIC_IMAGE_DEV_REPO }}/setup-ca-certs 87 | bp_go_targets: "./cmd/setup-ca-certs" 88 | builder: "paketobuildpacks/builder-jammy-base" 89 | 90 | bundle: 91 | runs-on: ubuntu-latest 92 | needs: 93 | - webhook-image 94 | - setup-ca-certs-image 95 | steps: 96 | - name: Checkout 97 | uses: actions/checkout@v4 98 | - name: Set up Go 99 | uses: actions/setup-go@v5 100 | with: 101 | go-version-file: 'go.mod' 102 | 103 | - name: Docker Login 104 | uses: ./.github/actions/docker-login 105 | with: 106 | registry: ghcr.io 107 | username: ${{ github.actor }} 108 | password: ${{ secrets.GITHUB_TOKEN }} 109 | 110 | - name: Setup carvel 111 | uses: carvel-dev/setup-action@v2 112 | with: 113 | token: ${{ secrets.RELEASE_TOKEN }} 114 | only: ytt, kbld, imgpkg 115 | 116 | - name: imgpkg push 117 | uses: ./.github/actions/imgpkg-push 118 | with: 119 | webhook_image: ${{ needs.webhook-image.outputs.image }} 120 | setup_ca_certs_image: ${{ needs.setup-ca-certs-image.outputs.image }} 121 | bundle_output: pre-release-bundle.lock 122 | tag: ${{ env.PUBLIC_IMAGE_DEV_REPO }}/bundle 123 | 124 | e2e: 125 | needs: 126 | - bundle 127 | - webhook-image 128 | - setup-ca-certs-image 129 | runs-on: ubuntu-latest 130 | steps: 131 | - name: Checkout 132 | uses: actions/checkout@v4 133 | 134 | - name: Set up Go 135 | uses: actions/setup-go@v5 136 | with: 137 | go-version-file: 'go.mod' 138 | 139 | - name: Setup crane 140 | uses: imjasonh/setup-crane@v0.3 141 | 142 | - name: Setup carvel 143 | uses: carvel-dev/setup-action@v2 144 | with: 145 | token: ${{ secrets.RELEASE_TOKEN }} 146 | only: ytt, kapp, imgpkg, kbld 147 | 148 | - name: Download bundle lock 149 | uses: actions/download-artifact@v4 150 | with: 151 | name: pre-release-bundle.lock 152 | 153 | - name: Create Kind Cluster 154 | uses: helm/kind-action@v1.12.0 155 | with: 156 | cluster_name: e2e 157 | 158 | - name: Deploy 159 | run: | 160 | cat < test-values.yaml 161 | --- 162 | labels: 163 | - some-label-1 164 | - some-label-2 165 | annotations: 166 | - some-annotation-1 167 | - some-annotation-2 168 | EOT 169 | 170 | imgpkg pull --lock pre-release-bundle.lock --output pulled-bundle 171 | ytt -f pulled-bundle/config --data-values-file test-values.yaml \ 172 | | kbld -f- -f pulled-bundle/.imgpkg/images.yml \ 173 | | kapp deploy -a cert-injection-webhook -f- -y 174 | 175 | - name: Run Tests 176 | run: go test --timeout=30m -v ./e2e/... 177 | 178 | release: 179 | needs: 180 | - unit 181 | - e2e 182 | - bundle 183 | - webhook-image 184 | - setup-ca-certs-image 185 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 186 | runs-on: ubuntu-latest 187 | steps: 188 | - name: Checkout 189 | uses: actions/checkout@v4 190 | 191 | - name: Set up Go 192 | uses: actions/setup-go@v5 193 | with: 194 | go-version-file: 'go.mod' 195 | 196 | - name: Setup crane 197 | uses: imjasonh/setup-crane@v0.3 198 | 199 | - name: Setup carvel 200 | uses: carvel-dev/setup-action@v2 201 | with: 202 | token: ${{ secrets.RELEASE_TOKEN }} 203 | only: ytt, kbld, imgpkg 204 | 205 | - name: Download artifacts 206 | uses: actions/download-artifact@v4 207 | with: 208 | pattern: '*-image' 209 | path: images 210 | merge-multiple: true 211 | 212 | - name: Docker Login 213 | uses: ./.github/actions/docker-login 214 | with: 215 | registry: ghcr.io 216 | username: ${{ github.actor }} 217 | password: ${{ secrets.GITHUB_TOKEN }} 218 | 219 | - name: Validate release version 220 | run: | 221 | echo "GITHUB_REF=${GITHUB_REF}" 222 | [[ $GITHUB_REF =~ ^refs\/tags\/v(.*)$ ]] && version=${BASH_REMATCH[1]} 223 | if [[ -z "${version}" ]]; then 224 | echo "ERROR: version not detected." 225 | exit 1 226 | fi 227 | 228 | - name: Promote images 229 | id: promote 230 | run: | 231 | mkdir -p final-image-refs 232 | for image in images/*; do 233 | dev_image=$(cat $image) 234 | digest=$(echo $dev_image| cut -d "@" -f 2) 235 | 236 | name=$(basename $image) 237 | final_repo="${{ env.PUBLIC_IMAGE_REPO }}/${name}" 238 | 239 | crane copy "$dev_image" "$final_repo" 240 | 241 | echo "${final_repo}@${digest}" > final-image-refs/$name 242 | echo "$name=$(cat final-image-refs/$name)" >> $GITHUB_OUTPUT 243 | 244 | done 245 | 246 | - name: Upload image refs 247 | uses: actions/upload-artifact@v4 248 | with: 249 | name: final-image-refs 250 | path: final-image-refs/* 251 | 252 | - name: imgpkg push 253 | uses: ./.github/actions/imgpkg-push 254 | with: 255 | webhook_image: ${{ steps.promote.outputs.webhook }} 256 | setup_ca_certs_image: ${{ steps.promote.outputs.setup-ca-certs }} 257 | bundle_output: release-bundle.lock 258 | tag: ${{ env.PUBLIC_IMAGE_REPO }}/bundle 259 | 260 | - name: tar config 261 | run: | 262 | tar -C imgpkg-bundle -cvzf config.tar.gz config 263 | 264 | - name: Upload bundle 265 | uses: actions/upload-artifact@v4 266 | with: 267 | name: config 268 | path: config.tar.gz 269 | 270 | - name: Create Draft Release 271 | uses: softprops/action-gh-release@v2 272 | with: 273 | name: cert-injection-webhook v${{ env.VERSION }} 274 | tag_name: v${{ env.VERSION }} 275 | target_commitish: ${{ github.sha }} 276 | token: ${{ secrets.RELEASE_TOKEN }} 277 | draft: true 278 | prerelease: true 279 | generate_release_notes: true 280 | files: | 281 | release-bundle.lock 282 | config.tar.gz 283 | final-image-refs/* 284 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: unit test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version-file: 'go.mod' 20 | 21 | - name: Build 22 | run: go build -v ./cmd/... 23 | 24 | - name: Test 25 | run: go test -v ./pkg/... 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | plugin-bin 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # IDE 16 | .idea 17 | -------------------------------------------------------------------------------- /.markdownlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "line_length": false, 4 | "MD024": { "allow_different_nesting": true }, 5 | "MD026": { "punctuation": ".,;:!" }, 6 | "MD046": { "style": "fenced" } 7 | } 8 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in the Cert Injection Webhook for Kubernetes project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at oss-coc@vmware.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | The Cert Injection Webhook for Kubernetes team welcomes contributions from the community. 4 | Before you start working with Cert Injection Webhook for Kubernetes, please read our [Developer Certificate of Origin]( https://cla.vmware.com/dco). 5 | All contributions to this repository must be signed as described on that page. 6 | Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. 7 | 8 | When contributing to this repository, please first discuss the change you wish to make via [GitHub issue](https://github.com/pivotal/projects-operator/issues) before making a pull request. 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Cert Injection Webhook for Kubernetes 2 | 3 | Copyright 2020 VMware, Inc. 4 | 5 | The Apache 2.0 license (the "License") set forth below applies to all parts of the Cert Injection Webhook for Kubernetes project. You may not use this file except in compliance with the License. 6 | 7 | Apache License 8 | 9 | Version 2.0, January 2004 10 | http://www.apache.org/licenses/ 11 | 12 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 13 | 14 | 1. Definitions. 15 | 16 | "License" shall mean the terms and conditions for use, reproduction, 17 | and distribution as defined by Sections 1 through 9 of this document. 18 | 19 | "Licensor" shall mean the copyright owner or entity authorized by the 20 | copyright owner that is granting the License. 21 | 22 | "Legal Entity" shall mean the union of the acting entity and all other 23 | entities that control, are controlled by, or are under common control 24 | with that entity. For the purposes of this definition, "control" means 25 | (i) the power, direct or indirect, to cause the direction or management 26 | of such entity, whether by contract or otherwise, or (ii) ownership 27 | of fifty percent (50%) or more of the outstanding shares, or (iii) 28 | beneficial ownership of such entity. 29 | 30 | "You" (or "Your") shall mean an individual or Legal Entity exercising 31 | permissions granted by this License. 32 | 33 | "Source" form shall mean the preferred form for making modifications, 34 | including but not limited to software source code, documentation source, 35 | and configuration files. 36 | 37 | "Object" form shall mean any form resulting from mechanical transformation 38 | or translation of a Source form, including but not limited to compiled 39 | object code, generated documentation, and conversions to other media 40 | types. 41 | 42 | "Work" shall mean the work of authorship, whether in Source or 43 | Object form, made available under the License, as indicated by a copyright 44 | notice that is included in or attached to the work (an example is provided 45 | in the Appendix below). 46 | 47 | "Derivative Works" shall mean any work, whether in Source or Object form, 48 | that is based on (or derived from) the Work and for which the editorial 49 | revisions, annotations, elaborations, or other modifications represent, 50 | as a whole, an original work of authorship. For the purposes of this 51 | License, Derivative Works shall not include works that remain separable 52 | from, or merely link (or bind by name) to the interfaces of, the Work 53 | and Derivative Works thereof. 54 | 55 | "Contribution" shall mean any work of authorship, including the 56 | original version of the Work and any modifications or additions to 57 | that Work or Derivative Works thereof, that is intentionally submitted 58 | to Licensor for inclusion in the Work by the copyright owner or by an 59 | individual or Legal Entity authorized to submit on behalf of the copyright 60 | owner. For the purposes of this definition, "submitted" means any form of 61 | electronic, verbal, or written communication sent to the Licensor or its 62 | representatives, including but not limited to communication on electronic 63 | mailing lists, source code control systems, and issue tracking systems 64 | that are managed by, or on behalf of, the Licensor for the purpose of 65 | discussing and improving the Work, but excluding communication that is 66 | conspicuously marked or otherwise designated in writing by the copyright 67 | owner as "Not a Contribution." 68 | 69 | "Contributor" shall mean Licensor and any individual or Legal Entity 70 | on behalf of whom a Contribution has been received by Licensor and 71 | subsequently incorporated within the Work. 72 | 73 | 2. Grant of Copyright License. 74 | Subject to the terms and conditions of this License, each Contributor 75 | hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, 76 | royalty-free, irrevocable copyright license to reproduce, prepare 77 | Derivative Works of, publicly display, publicly perform, sublicense, and 78 | distribute the Work and such Derivative Works in Source or Object form. 79 | 80 | 3. Grant of Patent License. 81 | Subject to the terms and conditions of this License, each Contributor 82 | hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, 83 | royalty- free, irrevocable (except as stated in this section) patent 84 | license to make, have made, use, offer to sell, sell, import, and 85 | otherwise transfer the Work, where such license applies only to those 86 | patent claims licensable by such Contributor that are necessarily 87 | infringed by their Contribution(s) alone or by combination of 88 | their Contribution(s) with the Work to which such Contribution(s) 89 | was submitted. If You institute patent litigation against any entity 90 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 91 | Work or a Contribution incorporated within the Work constitutes direct 92 | or contributory patent infringement, then any patent licenses granted 93 | to You under this License for that Work shall terminate as of the date 94 | such litigation is filed. 95 | 96 | 4. Redistribution. 97 | You may reproduce and distribute copies of the Work or Derivative Works 98 | thereof in any medium, with or without modifications, and in Source or 99 | Object form, provided that You meet the following conditions: 100 | 101 | a. You must give any other recipients of the Work or Derivative Works 102 | a copy of this License; and 103 | 104 | b. You must cause any modified files to carry prominent notices stating 105 | that You changed the files; and 106 | 107 | c. You must retain, in the Source form of any Derivative Works that 108 | You distribute, all copyright, patent, trademark, and attribution 109 | notices from the Source form of the Work, excluding those notices 110 | that do not pertain to any part of the Derivative Works; and 111 | 112 | d. If the Work includes a "NOTICE" text file as part of its 113 | distribution, then any Derivative Works that You distribute must 114 | include a readable copy of the attribution notices contained 115 | within such NOTICE file, excluding those notices that do not 116 | pertain to any part of the Derivative Works, in at least one of 117 | the following places: within a NOTICE text file distributed as part 118 | of the Derivative Works; within the Source form or documentation, 119 | if provided along with the Derivative Works; or, within a display 120 | generated by the Derivative Works, if and wherever such third-party 121 | notices normally appear. The contents of the NOTICE file are for 122 | informational purposes only and do not modify the License. You 123 | may add Your own attribution notices within Derivative Works that 124 | You distribute, alongside or as an addendum to the NOTICE text 125 | from the Work, provided that such additional attribution notices 126 | cannot be construed as modifying the License. You may add Your own 127 | copyright statement to Your modifications and may provide additional 128 | or different license terms and conditions for use, reproduction, or 129 | distribution of Your modifications, or for any such Derivative Works 130 | as a whole, provided Your use, reproduction, and distribution of the 131 | Work otherwise complies with the conditions stated in this License. 132 | 133 | 5. Submission of Contributions. 134 | Unless You explicitly state otherwise, any Contribution intentionally 135 | submitted for inclusion in the Work by You to the Licensor shall be 136 | under the terms and conditions of this License, without any additional 137 | terms or conditions. Notwithstanding the above, nothing herein shall 138 | supersede or modify the terms of any separate license agreement you may 139 | have executed with Licensor regarding such Contributions. 140 | 141 | 6. Trademarks. 142 | This License does not grant permission to use the trade names, trademarks, 143 | service marks, or product names of the Licensor, except as required for 144 | reasonable and customary use in describing the origin of the Work and 145 | reproducing the content of the NOTICE file. 146 | 147 | 7. Disclaimer of Warranty. 148 | Unless required by applicable law or agreed to in writing, Licensor 149 | provides the Work (and each Contributor provides its Contributions) on 150 | an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 151 | express or implied, including, without limitation, any warranties or 152 | conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR 153 | A PARTICULAR PURPOSE. You are solely responsible for determining the 154 | appropriateness of using or redistributing the Work and assume any risks 155 | associated with Your exercise of permissions under this License. 156 | 157 | 8. Limitation of Liability. 158 | In no event and under no legal theory, whether in tort (including 159 | negligence), contract, or otherwise, unless required by applicable law 160 | (such as deliberate and grossly negligent acts) or agreed to in writing, 161 | shall any Contributor be liable to You for damages, including any direct, 162 | indirect, special, incidental, or consequential damages of any character 163 | arising as a result of this License or out of the use or inability to 164 | use the Work (including but not limited to damages for loss of goodwill, 165 | work stoppage, computer failure or malfunction, or any and all other 166 | commercial damages or losses), even if such Contributor has been advised 167 | of the possibility of such damages. 168 | 169 | 9. Accepting Warranty or Additional Liability. 170 | While redistributing the Work or Derivative Works thereof, You may 171 | choose to offer, and charge a fee for, acceptance of support, warranty, 172 | indemnity, or other liability obligations and/or rights consistent with 173 | this License. However, in accepting such obligations, You may act only 174 | on Your own behalf and on Your sole responsibility, not on behalf of 175 | any other Contributor, and only if You agree to indemnify, defend, and 176 | hold each Contributor harmless for any liability incurred by, or claims 177 | asserted against, such Contributor by reason of your accepting any such 178 | warranty or additional liability. 179 | 180 | END OF TERMS AND CONDITIONS 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cert Injection Webhook for Kubernetes 2 | 3 | ## About 4 | 5 | The Cert Injection Webhook for Kubernetes extends kubernetes with a webhook that injects 6 | CA certificates and proxy environment variables into pods. The webhook uses certificates and 7 | environment variables defined in configmaps and injects them into pods with the desired labels or annotations. 8 | 9 | ## Contributing 10 | 11 | To begin contributing, please read the [contributing](CONTRIBUTING.md) doc. 12 | 13 | ## Installation and Usage 14 | 15 | The Cert Injection Webhook for Kubernetes is deployed using the [Carvel](https://carvel.dev/) tool suite. 16 | 17 | ### Install using kapp controller 18 | If you would like to install with [Tanzu Community Edition](https://tanzucommunityedition.io/). See [this guide](packaging/README.md) 19 | 1. Create an install namespace 20 | ```bash 21 | kubectl create namespace cert-injection-webhook-install 22 | ``` 23 | 24 | 2. Create a service account and role binding for your installation 25 | 26 | ```yaml 27 | --- 28 | apiVersion: v1 29 | kind: ServiceAccount 30 | metadata: 31 | name: cert-injection-webhook-install-sa 32 | namespace: cert-injection-webhook-install 33 | --- 34 | apiVersion: rbac.authorization.k8s.io/v1 35 | kind: ClusterRoleBinding 36 | metadata: 37 | name: cert-injection-webhook-install-admin 38 | roleRef: 39 | apiGroup: rbac.authorization.k8s.io 40 | kind: ClusterRole 41 | name: cluster-admin 42 | subjects: 43 | - kind: ServiceAccount 44 | name: cert-injection-webhook-install-sa 45 | namespace: cert-injection-webhook-install 46 | ``` 47 | 48 | Apply with: 49 | ```bash 50 | kapp deploy -a cert-injection-webhook-sa -n cert-injection-webhook-install -f 51 | ``` 52 | 53 | 3. Create a `cert-injection-webhook-config-values` Secret yaml with the labels or annotations (or both) that you would like to use. 54 | Any pod that matches one of these labels or annotations will have the provided cert injected. 55 | 56 | ```yaml 57 | --- 58 | apiVersion: v1 59 | kind: Secret 60 | metadata: 61 | name: cert-injection-webhook-install-values 62 | namespace: cert-injection-webhook-install 63 | stringData: 64 | values.yml: | 65 | --- 66 | labels: 67 | - kpack.io/build 68 | annotations: 69 | - some-annotation 70 | ca_cert_data: | 71 | -----BEGIN CERTIFICATE----- 72 | MIICrDCCAZQCCQDcakcvwbW4UTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1t 73 | eXdlYnNpdGUuY29tMB4XDTIyMDIxNDE2MjM1OVoXDTMyMDIxMjE2MjM1OVowGDEW 74 | MBQGA1UEAwwNbXl3ZWJzaXRlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 75 | AQoCggEBAMgWkhYr7OPSTuDwGSM5jMQtO5vnqfESPPh829IMTBNXkS0KV6Hi90ka 76 | T/gIbq0H+QO5Abzh8QDIOWqaTLLp5FedsU1xsGTiKQ+YVKfoQ7T7R/K+adWuJL6H 77 | i8kgb4ErzhYhDQqsPU6ZglKkTZTL+7fhpsc7ZewASa7TRJiSo51Qye9K1qsjj3Wd 78 | MB+0qH1vxvN2zs/117qowW/2YH2H++lJSfnEMH4Z67RQ5o56DpeHvE7mLz0LNVu/ 79 | gyM8JXClgsPdr11Iiv17TevWoXSeoWa0ts6MGd/r376dtEZ60wGG+geXcf9szAx1 80 | GZLEQamRHnVyrGvb7U/AvLaJMnNY8PcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA 81 | bc4XeX7sKvtEHK5tYKJDarP6suArgs7/IpfT2DiRB8JSBYX7rHD6NIB3433JxQfc 82 | SHD9FBpH9E8aSMDsCWKcuRRI7GeRarqwfblAqflCv85NJaiC9zu+haue7aNMNnwA 83 | uB+q0urjiKlEOM2OsLqgjXXmx5+nSrdwUhFXmyMsJC2eP4Dm1gJp5tQG2hSONC7w 84 | dX2wAQp7PYaq+h1ASkDNaKy3ZoeD7yEp3Mhbnh+fu0O06NpnJhUZPhdTtMD3LYPJ 85 | +iwL43iSAQt05ZK39u23zsdMc+RLFbqQYsULYZS2g/SmcSnw8CC3aer8X6x4lEw7 86 | FpCpA2Wta8mXHGKqmq0+og== 87 | -----END CERTIFICATE----- 88 | ``` 89 | 90 | Apply with: 91 | ```bash 92 | kapp deploy -a cert-injection-webhook-values -n cert-injection-webhook-install -f 93 | ``` 94 | 95 | 4. Download the [latest release of the cert-injection-webhook](https://github.com/vmware-tanzu/cert-injection-webhook/releases). 96 | 97 | 5. Apply the `package.yaml` and `metadata.yaml` from from the release 98 | ```bash 99 | ytt -f package.yaml -f metadata.yaml | kapp deploy -a cert-injection-webhook-package -n cert-injection-webhook-install 100 | ``` 101 | 102 | 6. Create a package install 103 | 104 | ```yaml 105 | --- 106 | apiVersion: packaging.carvel.dev/v1alpha1 107 | kind: PackageInstall 108 | metadata: 109 | name: cert-injection-webhook-package-install 110 | namespace: cert-injection-webhook-install 111 | spec: 112 | serviceAccountName: cert-injection-webhook-install-sa 113 | packageRef: 114 | refName: cert-injection-webhook.community.tanzu.vmware.com 115 | versionSelection: 116 | constraints: 117 | values: 118 | - secretRef: 119 | name: cert-injection-webhook-install-values 120 | ``` 121 | 122 | Apply with: 123 | ```bash 124 | kapp deploy -a cert-injection-webhook-package-install -n cert-injection-webhook-install -f 125 | ``` 126 | 127 | ### Install using kapp 128 | Download the latest release of the cert-injection-webhook and get the imagevalues.yaml. 129 | Use the Carvel tools to install to your cluster. 130 | 131 | ```bash 132 | $ ytt -f ./config \ 133 | -f \ 134 | -v ca_cert_data="some cert" \ 135 | --data-value-yaml labels="[label-1, label-2]" \ 136 | --data-value-yaml annotations="[annotation-1, annotation-2]" \ 137 | | kapp deploy -a cert-injection-webhook -f- 138 | ``` 139 | **Note**: You may provide labels, annotations, or both. 140 | 141 | If you would like to build the webhook and setup-ca-certs image yourself, 142 | use the [pack](https://github.com/buildpacks/pack) CLI. 143 | 144 | ```bash 145 | $ pack build -e BP_GO_TARGETS="./cmd/webhook" --builder paketobuildpacks/builder:base --publish 146 | $ pack build -e BP_GO_TARGETS="./cmd/setup-ca-certs" --builder paketobuildpacks/builder:base --publish 147 | ``` 148 | 149 | Then, use the Carvel tools to install to your cluster. 150 | 151 | ```bash 152 | $ ytt -f ./config \ 153 | -v webhook_image= \ 154 | -v setup_ca_certs_image= \ 155 | -v ca_cert_data="some cert" \ 156 | --data-value-yaml labels="[label-1, label-2]" \ 157 | --data-value-yaml annotations="[annotation-1, annotation-2]" \ 158 | | kapp deploy -a cert-injection-webhook -f- 159 | ``` 160 | 161 | ### Usage 162 | 163 | To have the webhook operate on a Pod, label or annotate the Pod with the labels and annotations you provided during install. 164 | 165 | #### Injecting certificates into kpack builds 166 | 167 | When providing ca_cert_data directly to kpack, that CA Certificate be injected into builds themselves. 168 | If you want kpack builds to have CA Certificates for communicating with a self-signed registry, 169 | make sure the values yaml has a label with `kpack.io/build`. This will match on any build pod that kpack creates. 170 | 171 | ### Running e2e tests 172 | 173 | 1. Deploy the cert injection webhook using the following values: 174 | 175 | ```yaml 176 | --- 177 | http_proxy: some-http-proxy 178 | https_proxy: some-https-proxy 179 | no_proxy: some-no-proxy 180 | ca_cert_data: some-cert 181 | labels: 182 | - some-label-1 183 | - some-label-2 184 | annotations: 185 | - some-annotation-1 186 | - some-annotation-2 187 | ``` 188 | 189 | 2. Run the e2e tests 190 | 191 | ```bash 192 | go test -v ./e2e/... 193 | ``` 194 | 195 | ### Uninstall 196 | If installed using kapp controller: 197 | ```bash 198 | kapp delete -a cert-injection-webhook-package-install -n cert-injection-webhook-install 199 | kapp delete -a cert-injection-webhook-package -n cert-injection-webhook-install 200 | kapp delete -a cert-injection-webhook-values -n cert-injection-webhook-install 201 | ```` 202 | 203 | You can also delete the namespace 204 | 205 | ```bash 206 | kubectl delete namespace cert-injection-webhook-install 207 | ``` 208 | 209 | If installed using kapp: 210 | ```bash 211 | kapp delete -a cert-injection-webhook 212 | ``` 213 | -------------------------------------------------------------------------------- /cmd/setup-ca-certs/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-Present VMware, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "os" 12 | "os/exec" 13 | "path" 14 | "path/filepath" 15 | 16 | "github.com/vmware-tanzu/cert-injection-webhook/pkg/certs" 17 | ) 18 | 19 | func main() { 20 | logger := log.New(os.Stdout, "", 0) 21 | 22 | tempLocal, err := ioutil.TempDir("", "local") 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | defer os.RemoveAll(tempLocal) 27 | 28 | tempCerts, err := ioutil.TempDir("", "certs") 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | defer os.RemoveAll(tempCerts) 33 | 34 | logger.Println("Parsing certificate(s)...") 35 | caCerts, err := certs.Parse("CA_CERTS_DATA", os.Environ()) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | 40 | logger.Printf("Populate %d certificate(s)...\n", len(caCerts)) 41 | for i, cert := range caCerts { 42 | writeCert(tempLocal, i, cert) 43 | } 44 | 45 | logger.Println("Update CA certificates...") 46 | cmd := exec.Command("update-ca-certificates", "--etccertsdir", tempCerts, "--localcertsdir", tempLocal) 47 | out, err := cmd.CombinedOutput() 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | logger.Println(string(out)) 52 | 53 | logger.Println("Forcing the generation of hashed symlinks...") 54 | cmd = exec.Command("c_rehash", tempCerts) 55 | err = cmd.Run() 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | 60 | logger.Println("Copying CA certificates...") 61 | err = CopyDir(tempCerts, "/workspace") 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | 66 | logger.Println("Finished setting up CA certificates") 67 | } 68 | 69 | func writeCert(dir string, i int, cert string) { 70 | file, err := os.Create(filepath.Join(dir, fmt.Sprintf("cert_injection_webhook_%d.crt", i))) 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | defer file.Close() 75 | 76 | _, err = file.WriteString(cert) 77 | if err != nil { 78 | log.Fatal(err) 79 | } 80 | } 81 | 82 | func CopyDir(src string, dest string) error { 83 | var ( 84 | err error 85 | fds []os.FileInfo 86 | info os.FileInfo 87 | ) 88 | 89 | if info, err = os.Stat(src); err != nil { 90 | return err 91 | } 92 | 93 | if err = os.MkdirAll(dest, info.Mode()); err != nil { 94 | return err 95 | } 96 | 97 | if fds, err = ioutil.ReadDir(src); err != nil { 98 | return err 99 | } 100 | 101 | for _, fd := range fds { 102 | srcPath := path.Join(src, fd.Name()) 103 | destPath := path.Join(dest, fd.Name()) 104 | 105 | if fd.IsDir() { 106 | if err = CopyDir(srcPath, destPath); err != nil { 107 | return err 108 | } 109 | } else { 110 | if err = CopyFile(srcPath, destPath); err != nil { 111 | return err 112 | } 113 | } 114 | } 115 | 116 | return nil 117 | } 118 | 119 | func CopyFile(src, dest string) error { 120 | var ( 121 | err error 122 | srcFile *os.File 123 | destFile *os.File 124 | info os.FileInfo 125 | ) 126 | 127 | if srcFile, err = os.Open(src); err != nil { 128 | return err 129 | } 130 | defer srcFile.Close() 131 | 132 | if destFile, err = os.Create(dest); err != nil { 133 | return err 134 | } 135 | defer destFile.Close() 136 | 137 | if _, err = io.Copy(destFile, srcFile); err != nil { 138 | return err 139 | } 140 | 141 | if info, err = os.Stat(src); err != nil { 142 | return err 143 | } 144 | 145 | return os.Chmod(dest, info.Mode()) 146 | } 147 | -------------------------------------------------------------------------------- /cmd/webhook/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-Present VMware, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "encoding/base64" 9 | "flag" 10 | "io" 11 | "io/ioutil" 12 | "log" 13 | "os" 14 | "strings" 15 | "strconv" 16 | 17 | corev1 "k8s.io/api/core/v1" 18 | "knative.dev/pkg/configmap" 19 | "knative.dev/pkg/controller" 20 | "knative.dev/pkg/injection" 21 | "knative.dev/pkg/injection/sharedmain" 22 | "knative.dev/pkg/signals" 23 | "knative.dev/pkg/webhook" 24 | "knative.dev/pkg/webhook/certificates" 25 | 26 | "github.com/vmware-tanzu/cert-injection-webhook/pkg/certinjectionwebhook" 27 | ) 28 | 29 | const ( 30 | defaultWebhookName = "defaults.webhook.cert-injection.tanzu.vmware.com" 31 | webhookPath = "/certinjectionwebhook" 32 | defaultWebhookSecretName = "cert-injection-webhook-tls" 33 | defaultWebhookPort = 8443 34 | caCertsFile = "/run/config_maps/ca_cert/ca.crt" 35 | httpProxyFile = "/run/config_maps/http_proxy/value" 36 | httpsProxyFile = "/run/config_maps/https_proxy/value" 37 | noProxyFile = "/run/config_maps/no_proxy/value" 38 | ) 39 | 40 | type labelAnnotationFlags []string 41 | 42 | func (l *labelAnnotationFlags) Set(value string) error { 43 | *l = append(*l, value) 44 | return nil 45 | } 46 | 47 | func (l *labelAnnotationFlags) String() string { 48 | return strings.Join(*l, ", ") 49 | } 50 | 51 | var labels, annotations labelAnnotationFlags 52 | 53 | func main() { 54 | flag.Var(&labels, "label", "-label: label to monitor (can be specified multiple times)") 55 | flag.Var(&annotations, "annotation", "-annotation: annotation to monitor (can be specified multiple times)") 56 | flag.Parse() 57 | 58 | webhookSecretName := os.Getenv("WEBHOOK_SECRET_NAME") 59 | 60 | if webhookSecretName == "" { 61 | webhookSecretName = defaultWebhookSecretName 62 | } 63 | 64 | webhookPort := defaultWebhookPort 65 | webhookPortEnv := os.Getenv("WEBHOOK_PORT") 66 | if parsedWebhookPort, err := strconv.Atoi(webhookPortEnv); err == nil { 67 | webhookPort = parsedWebhookPort 68 | } 69 | 70 | ctx := sharedmain.WithHADisabled(webhook.WithOptions(signals.NewContext(), webhook.Options{ 71 | ServiceName: "cert-injection-webhook", 72 | Port: webhookPort, 73 | SecretName: webhookSecretName, 74 | })) 75 | 76 | sharedmain.WebhookMainWithConfig(ctx, "webhook", 77 | injection.ParseAndGetRESTConfigOrDie(), 78 | certificates.NewController, 79 | PodAdmissionController, 80 | ) 81 | } 82 | 83 | func PodAdmissionController(ctx context.Context, cmw configmap.Watcher) *controller.Impl { 84 | envVars, err := loadEnvVars() 85 | if err != nil { 86 | log.Fatal(err) 87 | } 88 | 89 | caCertsData, err := readFile(caCertsFile, readBase64) 90 | if err != nil { 91 | log.Fatal(err) 92 | } 93 | 94 | webhookName := os.Getenv("WEBHOOK_NAME") 95 | if webhookName == "" { 96 | webhookName = defaultWebhookName 97 | } 98 | 99 | var imagePullSecrets corev1.LocalObjectReference 100 | if systemRegistrySecret := os.Getenv("SYSTEM_REGISTRY_SECRET"); systemRegistrySecret != "" { 101 | imagePullSecrets = corev1.LocalObjectReference{Name: systemRegistrySecret} 102 | } 103 | 104 | c, err := certinjectionwebhook.NewController( 105 | ctx, 106 | webhookName, 107 | webhookPath, 108 | func(ctx context.Context) context.Context { 109 | return ctx 110 | }, 111 | labels, 112 | annotations, 113 | envVars, 114 | caCertsData, 115 | os.Getenv("SETUP_CA_CERTS_IMAGE"), 116 | imagePullSecrets, 117 | ) 118 | if err != nil { 119 | log.Fatal(err) 120 | } 121 | return c 122 | } 123 | 124 | func loadEnvVars() ([]corev1.EnvVar, error) { 125 | var envVars []corev1.EnvVar 126 | 127 | httpProxy, err := readFile(httpProxyFile, read) 128 | if err != nil { 129 | return nil, err 130 | } 131 | if httpProxy != "" { 132 | envVars = append(envVars, corev1.EnvVar{Name: "HTTP_PROXY", Value: httpProxy}) 133 | envVars = append(envVars, corev1.EnvVar{Name: "http_proxy", Value: httpProxy}) 134 | } 135 | 136 | httpsProxy, err := readFile(httpsProxyFile, read) 137 | if err != nil { 138 | return nil, err 139 | } 140 | if httpsProxy != "" { 141 | envVars = append(envVars, corev1.EnvVar{Name: "HTTPS_PROXY", Value: httpsProxy}) 142 | envVars = append(envVars, corev1.EnvVar{Name: "https_proxy", Value: httpsProxy}) 143 | } 144 | 145 | noProxy, err := readFile(noProxyFile, read) 146 | if err != nil { 147 | return nil, err 148 | } 149 | if noProxy != "" { 150 | envVars = append(envVars, corev1.EnvVar{Name: "NO_PROXY", Value: noProxy}) 151 | envVars = append(envVars, corev1.EnvVar{Name: "no_proxy", Value: noProxy}) 152 | } 153 | 154 | return envVars, nil 155 | } 156 | 157 | func readFile(filepath string, read func(reader io.Reader) (string, error)) (string, error) { 158 | info, err := os.Stat(filepath) 159 | if err != nil { 160 | log.Fatal(err) 161 | } 162 | 163 | if info.Size() == 0 { 164 | return "", nil 165 | } 166 | 167 | file, err := os.Open(filepath) 168 | if err != nil { 169 | return "", err 170 | } 171 | defer file.Close() 172 | 173 | return read(file) 174 | } 175 | 176 | func readBase64(reader io.Reader) (string, error) { 177 | buf, err := ioutil.ReadAll(base64.NewDecoder(base64.StdEncoding, reader)) 178 | if err != nil { 179 | return "", err 180 | } 181 | 182 | return string(buf), nil 183 | } 184 | 185 | func read(reader io.Reader) (string, error) { 186 | buf, err := ioutil.ReadAll(reader) 187 | if err != nil { 188 | return "", err 189 | } 190 | 191 | return string(buf), nil 192 | } 193 | -------------------------------------------------------------------------------- /config/_namespace.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: cert-injection-webhook 6 | labels: 7 | pod-security.kubernetes.io/enforce: restricted 8 | pod-security.kubernetes.io/enforce-version: v1.25 9 | -------------------------------------------------------------------------------- /config/configmaps.yaml: -------------------------------------------------------------------------------- 1 | #@ load("@ytt:data", "data") 2 | #@ load("@ytt:base64", "base64") 3 | --- 4 | apiVersion: v1 5 | kind: ConfigMap 6 | metadata: 7 | name: ca-cert 8 | namespace: cert-injection-webhook 9 | annotations: 10 | kapp.k14s.io/versioned: "" 11 | data: 12 | ca.crt: #@ base64.encode(data.values.ca_cert_data) if data.values.ca_cert_data else "" 13 | --- 14 | apiVersion: v1 15 | kind: ConfigMap 16 | metadata: 17 | name: http-proxy 18 | namespace: cert-injection-webhook 19 | annotations: 20 | kapp.k14s.io/versioned: "" 21 | data: 22 | value: #@ data.values.http_proxy if data.values.http_proxy else "" 23 | --- 24 | apiVersion: v1 25 | kind: ConfigMap 26 | metadata: 27 | name: https-proxy 28 | namespace: cert-injection-webhook 29 | annotations: 30 | kapp.k14s.io/versioned: "" 31 | data: 32 | value: #@ data.values.https_proxy if data.values.https_proxy else "" 33 | --- 34 | apiVersion: v1 35 | kind: ConfigMap 36 | metadata: 37 | name: no-proxy 38 | namespace: cert-injection-webhook 39 | annotations: 40 | kapp.k14s.io/versioned: "" 41 | data: 42 | value: #@ data.values.no_proxy if data.values.no_proxy else "" 43 | -------------------------------------------------------------------------------- /config/deployment.yaml: -------------------------------------------------------------------------------- 1 | #@ load("@ytt:data", "data") 2 | --- 3 | apiVersion: v1 4 | kind: ConfigMap 5 | metadata: 6 | name: setup-ca-certs-image 7 | namespace: cert-injection-webhook 8 | data: 9 | image: #@ data.values.setup_ca_certs.image 10 | --- 11 | apiVersion: v1 12 | kind: Secret 13 | metadata: 14 | name: cert-injection-webhook-tls 15 | namespace: cert-injection-webhook 16 | --- 17 | apiVersion: apps/v1 18 | kind: Deployment 19 | metadata: 20 | name: cert-injection-webhook 21 | namespace: cert-injection-webhook 22 | labels: 23 | app: cert-injection-webhook 24 | spec: 25 | replicas: 1 26 | selector: 27 | matchLabels: 28 | app: cert-injection-webhook 29 | template: 30 | metadata: 31 | labels: 32 | app: cert-injection-webhook 33 | spec: 34 | serviceAccountName: cert-injection-webhook-sa 35 | securityContext: 36 | runAsNonRoot: true 37 | seccompProfile: 38 | type: "RuntimeDefault" 39 | containers: 40 | - name: server 41 | image: #@ data.values.cert_injection_webhook.image 42 | securityContext: 43 | runAsNonRoot: true 44 | allowPrivilegeEscalation: false 45 | privileged: false 46 | seccompProfile: 47 | type: "RuntimeDefault" 48 | capabilities: 49 | drop: 50 | - ALL 51 | imagePullPolicy: Always 52 | volumeMounts: 53 | - name: webhook-ca-cert 54 | mountPath: /run/config_maps/ca_cert 55 | readOnly: true 56 | - name: http-proxy 57 | mountPath: /run/config_maps/http_proxy 58 | readOnly: true 59 | - name: https-proxy 60 | mountPath: /run/config_maps/https_proxy 61 | readOnly: true 62 | - name: no-proxy 63 | mountPath: /run/config_maps/no_proxy 64 | readOnly: true 65 | ports: 66 | - containerPort: 8443 67 | name: webhook-port 68 | env: 69 | - name: SETUP_CA_CERTS_IMAGE 70 | valueFrom: 71 | configMapKeyRef: 72 | name: setup-ca-certs-image 73 | key: image 74 | - name: SYSTEM_NAMESPACE 75 | valueFrom: 76 | fieldRef: 77 | fieldPath: metadata.namespace 78 | volumes: 79 | - name: webhook-ca-cert 80 | configMap: 81 | name: ca-cert 82 | - name: http-proxy 83 | configMap: 84 | name: http-proxy 85 | - name: https-proxy 86 | configMap: 87 | name: https-proxy 88 | - name: no-proxy 89 | configMap: 90 | name: no-proxy 91 | --- 92 | apiVersion: v1 93 | kind: Service 94 | metadata: 95 | name: cert-injection-webhook 96 | namespace: cert-injection-webhook 97 | spec: 98 | selector: 99 | app: cert-injection-webhook 100 | ports: 101 | - port: 443 102 | targetPort: webhook-port 103 | --- 104 | apiVersion: admissionregistration.k8s.io/v1 105 | kind: MutatingWebhookConfiguration 106 | metadata: 107 | name: defaults.webhook.cert-injection.tanzu.vmware.com 108 | webhooks: 109 | - name: defaults.webhook.cert-injection.tanzu.vmware.com 110 | admissionReviewVersions: 111 | - v1 112 | clientConfig: 113 | service: 114 | name: cert-injection-webhook 115 | namespace: cert-injection-webhook 116 | path: /certinjectionwebhook 117 | port: 443 118 | failurePolicy: Ignore 119 | matchPolicy: Exact 120 | rules: 121 | - operations: ["CREATE", "UPDATE"] 122 | apiGroups: [""] 123 | apiVersions: ["v1"] 124 | resources: ["pods"] 125 | sideEffects: None 126 | 127 | --- 128 | apiVersion: v1 129 | kind: ServiceAccount 130 | metadata: 131 | name: cert-injection-webhook-sa 132 | namespace: cert-injection-webhook 133 | -------------------------------------------------------------------------------- /config/images.yaml: -------------------------------------------------------------------------------- 1 | #@data/values-schema 2 | #@overlay/match-child-defaults missing_ok=True 3 | --- 4 | setup_ca_certs: 5 | image: setup-ca-certs 6 | cert_injection_webhook: 7 | image: cert-injection-webhook 8 | -------------------------------------------------------------------------------- /config/overlay.yaml: -------------------------------------------------------------------------------- 1 | #@ load("@ytt:data", "data") 2 | #@ load("@ytt:overlay", "overlay") 3 | 4 | #@overlay/match by=overlay.subset({"metadata":{"name":"cert-injection-webhook"}, "kind": "Deployment"}) 5 | --- 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | #@overlay/match by="name" 11 | - name: server 12 | #@overlay/match missing_ok=True 13 | args: 14 | #@ for label in data.values.labels: 15 | - #@ "-label={}".format(label) 16 | #@ end 17 | #@ for annotation in data.values.annotations: 18 | - #@ "-annotation={}".format(annotation) 19 | #@ end -------------------------------------------------------------------------------- /config/rbac.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: cert-injection-webhook-role 6 | namespace: cert-injection-webhook 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - secrets 12 | verbs: 13 | - get 14 | - list 15 | - watch 16 | - apiGroups: 17 | - "" 18 | resources: 19 | - secrets 20 | resourceNames: 21 | - cert-injection-webhook-tls 22 | verbs: 23 | - update 24 | - apiGroups: 25 | - "" 26 | resources: 27 | - configmaps 28 | verbs: 29 | - get 30 | - list 31 | - watch 32 | --- 33 | apiVersion: rbac.authorization.k8s.io/v1 34 | kind: RoleBinding 35 | metadata: 36 | name: cert-injection-webhook-role-binding 37 | namespace: cert-injection-webhook 38 | annotations: 39 | kapp.k14s.io/update-strategy: fallback-on-replace 40 | subjects: 41 | - kind: ServiceAccount 42 | name: cert-injection-webhook-sa 43 | namespace: cert-injection-webhook 44 | roleRef: 45 | kind: Role 46 | name: cert-injection-webhook-role 47 | apiGroup: rbac.authorization.k8s.io 48 | --- 49 | apiVersion: rbac.authorization.k8s.io/v1 50 | kind: ClusterRole 51 | metadata: 52 | name: cert-injection-webhook-cluster-role 53 | rules: 54 | - apiGroups: 55 | - admissionregistration.k8s.io 56 | resources: 57 | - mutatingwebhookconfigurations 58 | resourceNames: 59 | - defaults.webhook.cert-injection.tanzu.vmware.com 60 | verbs: 61 | - update 62 | - delete 63 | - apiGroups: 64 | - admissionregistration.k8s.io 65 | resources: 66 | - mutatingwebhookconfigurations 67 | verbs: 68 | - get 69 | - list 70 | - watch 71 | --- 72 | apiVersion: rbac.authorization.k8s.io/v1 73 | kind: ClusterRoleBinding 74 | metadata: 75 | name: cert-injection-webhook-cluster-role-binding 76 | annotations: 77 | kapp.k14s.io/update-strategy: fallback-on-replace 78 | subjects: 79 | - kind: ServiceAccount 80 | name: cert-injection-webhook-sa 81 | namespace: cert-injection-webhook 82 | roleRef: 83 | kind: ClusterRole 84 | name: cert-injection-webhook-cluster-role 85 | apiGroup: rbac.authorization.k8s.io 86 | -------------------------------------------------------------------------------- /config/schema.yaml: -------------------------------------------------------------------------------- 1 | #@data/values-schema 2 | #@overlay/match-child-defaults missing_ok=True 3 | --- 4 | labels: 5 | - "" 6 | annotations: 7 | - "" 8 | 9 | ca_cert_data: "" 10 | http_proxy: "" 11 | https_proxy: "" 12 | no_proxy: "" 13 | 14 | -------------------------------------------------------------------------------- /e2e/config.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "context" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "crypto/x509/pkix" 8 | "encoding/pem" 9 | "flag" 10 | "fmt" 11 | "io" 12 | "math/big" 13 | "math/rand" 14 | "os" 15 | "os/user" 16 | "path" 17 | "sync" 18 | "testing" 19 | "time" 20 | 21 | "github.com/stretchr/testify/require" 22 | "k8s.io/client-go/kubernetes" 23 | "k8s.io/client-go/rest" 24 | "k8s.io/client-go/tools/clientcmd" 25 | ) 26 | 27 | const ( 28 | controllerNamespace = "cert-injection-webhook" 29 | controllerName = "cert-injection-webhook" 30 | ) 31 | 32 | var ( 33 | clientSetup sync.Once 34 | k8sClient *kubernetes.Clientset 35 | clusterConfig *rest.Config 36 | oldConfigs map[string]string 37 | rng io.Reader 38 | ) 39 | 40 | func getClient(t *testing.T) (kubernetes.Interface, error) { 41 | clientSetup.Do(func() { 42 | kubeconfig := flag.String("kubeconfig", getKubeConfig(), "Path to a kubeconfig. Only required if out-of-cluster.") 43 | masterURL := flag.String("master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") 44 | 45 | flag.Parse() 46 | 47 | var err error 48 | clusterConfig, err = clientcmd.BuildConfigFromFlags(*masterURL, *kubeconfig) 49 | require.NoError(t, err) 50 | 51 | k8sClient, err = kubernetes.NewForConfig(clusterConfig) 52 | require.NoError(t, err) 53 | }) 54 | 55 | rng = rand.New(rand.NewSource(time.Now().UnixMilli())) 56 | oldConfigs = make(map[string]string) 57 | 58 | return k8sClient, nil 59 | } 60 | 61 | func getKubeConfig() string { 62 | if config, found := os.LookupEnv("KUBECONFIG"); found { 63 | return config 64 | } 65 | if usr, err := user.Current(); err == nil { 66 | return path.Join(usr.HomeDir, ".kube/config") 67 | } 68 | return "" 69 | } 70 | 71 | func generateCA() (*rsa.PrivateKey, *x509.Certificate, error) { 72 | caKey, err := rsa.GenerateKey(rng, 2048) 73 | if err != nil { 74 | return nil, nil, err 75 | } 76 | 77 | // openssl only uses the first cert found for each common name, so we unique ones 78 | // https://github.com/openssl/openssl/issues/16304 79 | id := rand.Int() 80 | caTemplate := &x509.Certificate{ 81 | SerialNumber: big.NewInt(int64(id)), 82 | Subject: pkix.Name{ 83 | CommonName: fmt.Sprintf("%d.sign", id), 84 | Organization: []string{"signing"}, 85 | }, 86 | NotBefore: time.Now(), 87 | NotAfter: time.Now().AddDate(10, 0, 0), 88 | IsCA: true, 89 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 90 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 91 | BasicConstraintsValid: true, 92 | } 93 | return caKey, caTemplate, nil 94 | } 95 | 96 | func encodeCert(pKey *rsa.PrivateKey, caCert *x509.Certificate) (string, error) { 97 | bytes, err := x509.CreateCertificate(rng, caCert, caCert, &pKey.PublicKey, pKey) 98 | if err != nil { 99 | return "", err 100 | } 101 | 102 | encoded := pem.EncodeToMemory(&pem.Block{ 103 | Type: "CERTIFICATE", 104 | Bytes: bytes, 105 | }) 106 | return string(encoded), nil 107 | } 108 | 109 | func generateAndUpdateCerts(ctx context.Context, client kubernetes.Interface) (*rsa.PrivateKey, *x509.Certificate, error) { 110 | pKey, caCert, err := generateCA() 111 | if err != nil { 112 | return nil, nil, err 113 | } 114 | 115 | caEncoded, err := encodeCert(pKey, caCert) 116 | if err != nil { 117 | return nil, nil, err 118 | } 119 | 120 | err = setCaCerts(ctx, client, caEncoded) 121 | if err != nil { 122 | return nil, nil, err 123 | } 124 | 125 | return pKey, caCert, nil 126 | } 127 | 128 | func generateAndUpdateProxies(ctx context.Context, client kubernetes.Interface) (httpProxy string, httpsProxy string, noProxy string, err error) { 129 | httpProxy = "some-http-proxy" 130 | err = updateConfigmap(ctx, client, "http-proxy", "value", httpProxy) 131 | if err != nil { 132 | return 133 | } 134 | 135 | httpsProxy = "some-https-proxy" 136 | err = updateConfigmap(ctx, client, "https-proxy", "value", httpsProxy) 137 | if err != nil { 138 | return 139 | } 140 | 141 | noProxy = "some-no-proxy" 142 | err = updateConfigmap(ctx, client, "no-proxy", "value", noProxy) 143 | if err != nil { 144 | return 145 | } 146 | return 147 | } 148 | -------------------------------------------------------------------------------- /e2e/e2e_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "testing" 8 | "time" 9 | 10 | "github.com/sclevine/spec" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | corev1 "k8s.io/api/core/v1" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/client-go/kubernetes" 16 | _ "k8s.io/client-go/plugin/pkg/client/auth" 17 | ) 18 | 19 | func TestCertInjectionWebhook(t *testing.T) { 20 | rand.Seed(time.Now().Unix()) 21 | 22 | spec.Run(t, "TestCertInjectionWebhook", testCertInjectionWebhook) 23 | } 24 | 25 | func testCertInjectionWebhook(t *testing.T, when spec.G, it spec.S) { 26 | var ( 27 | client kubernetes.Interface 28 | ctx = context.Background() 29 | 30 | testNamespace = "test" 31 | podName string 32 | 33 | waitForPodTermination = func() { 34 | eventually(t, func() bool { 35 | pod := getPod(t, ctx, client, testNamespace, podName) 36 | return pod.Status.ContainerStatuses[0].State.Terminated != nil 37 | }, 5*time.Second, 2*time.Minute) 38 | } 39 | ) 40 | 41 | it.Before(func() { 42 | var err error 43 | client, err = getClient(t) 44 | require.NoError(t, err) 45 | 46 | deleteNamespace(t, ctx, client, testNamespace) 47 | 48 | _, err = client.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ 49 | ObjectMeta: metav1.ObjectMeta{ 50 | Name: testNamespace, 51 | }, 52 | }, metav1.CreateOptions{}) 53 | require.NoError(t, err) 54 | }) 55 | 56 | when("ensuring containers ar injected", func() { 57 | it.Before(func() { 58 | _, _, err := generateAndUpdateCerts(ctx, client) 59 | require.NoError(t, err) 60 | require.NoError(t, restartController(t, ctx, client)) 61 | }) 62 | 63 | it.After(func() { 64 | deletePod(t, ctx, client, testNamespace, podName) 65 | require.NoError(t, restoreProxies(ctx, client)) 66 | require.NoError(t, restartController(t, ctx, client)) 67 | }) 68 | 69 | it("will match pods that have any label the webhook is matching on", func() { 70 | for i, label := range []string{"some-label-1", "some-label-2"} { 71 | podName = fmt.Sprintf("testpod-label-%d", i) 72 | labels := map[string]string{label: ""} 73 | 74 | createNoopPod(t, ctx, client, testNamespace, podName, labels, map[string]string{}) 75 | pod := getPod(t, ctx, client, testNamespace, podName) 76 | require.True(t, hasInjectedContainer(t, pod), "should have cert injection container") 77 | } 78 | }) 79 | 80 | it("will match pods that have any annotation the webhook is matching on", func() { 81 | for i, annotation := range []string{"some-annotation-1", "some-annotation-2"} { 82 | podName = fmt.Sprintf("testpod-annotation-%d", i) 83 | annotations := map[string]string{annotation: podName} 84 | 85 | createNoopPod(t, ctx, client, testNamespace, podName, map[string]string{}, annotations) 86 | pod := getPod(t, ctx, client, testNamespace, podName) 87 | require.True(t, hasInjectedContainer(t, pod), "should have cert injection container") 88 | } 89 | }) 90 | 91 | it("doesn't match pods that don't have any annotation or label the webhook is matching on", func() { 92 | podName = fmt.Sprintf("testpod-no-match") 93 | labels := map[string]string{"some-label-3": ""} 94 | annotations := map[string]string{"some-annotation-3": podName} 95 | 96 | createNoopPod(t, ctx, client, testNamespace, podName, labels, annotations) 97 | pod := getPod(t, ctx, client, testNamespace, podName) 98 | require.False(t, hasInjectedContainer(t, pod), "should not have cert injection container") 99 | }) 100 | }) 101 | 102 | when("ensuring injected containers are correct", func() { 103 | it.After(func() { 104 | deletePod(t, ctx, client, testNamespace, podName) 105 | require.NoError(t, restoreProxies(ctx, client)) 106 | require.NoError(t, restoreCaCerts(ctx, client)) 107 | require.NoError(t, restartController(t, ctx, client)) 108 | }) 109 | 110 | podLogFormat := `setup-ca-cert container logs: 111 | %s 112 | test container logs: 113 | %s` 114 | 115 | it("injects proxy envs", func() { 116 | http, https, no, err := generateAndUpdateProxies(ctx, client) 117 | require.NoError(t, err) 118 | 119 | require.NoError(t, restartController(t, ctx, client)) 120 | 121 | podName = "testpod-proxy-envs" 122 | createNoopPod(t, ctx, client, testNamespace, podName, map[string]string{"some-label-1": ""}, map[string]string{}) 123 | pod := getPod(t, ctx, client, testNamespace, podName) 124 | expectedEnv := []corev1.EnvVar{ 125 | {Name: "HTTP_PROXY", Value: http}, 126 | {Name: "http_proxy", Value: http}, 127 | {Name: "HTTPS_PROXY", Value: https}, 128 | {Name: "https_proxy", Value: https}, 129 | {Name: "NO_PROXY", Value: no}, 130 | {Name: "no_proxy", Value: no}, 131 | } 132 | actualEnv := pod.Spec.Containers[0].Env 133 | assert.Equal(t, expectedEnv, actualEnv) 134 | }) 135 | 136 | it("injects certs", func() { 137 | caKey, caCert, err := generateAndUpdateCerts(ctx, client) 138 | require.NoError(t, err) 139 | 140 | require.NoError(t, restartController(t, ctx, client)) 141 | 142 | testingCert, err := generateCert(caKey, caCert) 143 | require.NoError(t, err) 144 | 145 | podName = "testpod-verify-cert" 146 | createCertTestPod(t, ctx, client, 147 | testNamespace, podName, 148 | testingCert, 149 | ) 150 | 151 | waitForPodTermination() 152 | pod := getPod(t, ctx, client, testNamespace, podName) 153 | require.Len(t, pod.Status.ContainerStatuses, 1) 154 | 155 | setupLogs := getLogs(t, ctx, client, testNamespace, pod.Name, "setup-ca-certs") 156 | podLogs := getLogs(t, ctx, client, testNamespace, pod.Name, "test") 157 | require.Equal(t, int32(0), pod.Status.ContainerStatuses[0].State.Terminated.ExitCode, 158 | podLogFormat, setupLogs, podLogs, 159 | ) 160 | }) 161 | 162 | it("can handle super long certs", func() { 163 | // k8s configmaps have a size limit of 1mb, 550 certs should be just shy of that. 164 | certChain := "" 165 | for i := 0; i < 549; i++ { 166 | // because openssl rehash skips duplicates, we're currently generating 167 | // unique keys+certs at the cost of increased time (even when using prng 168 | // instead of cryptographically secure rng) 169 | k, c, err := generateCA() 170 | require.NoError(t, err) 171 | p, err := encodeCert(k, c) 172 | require.NoError(t, err) 173 | 174 | certChain += fmt.Sprintln(p) 175 | } 176 | 177 | // actual key/cert that will be used in test 178 | key, cert, err := generateCA() 179 | require.NoError(t, err) 180 | pem, err := encodeCert(key, cert) 181 | require.NoError(t, err) 182 | 183 | certChain += fmt.Sprintln(pem) 184 | 185 | require.NoError(t, setCaCerts(ctx, client, certChain)) 186 | require.NoError(t, restartController(t, ctx, client)) 187 | 188 | testingCert, err := generateCert(key, cert) 189 | require.NoError(t, err) 190 | 191 | podName = "testpod-many-certs" 192 | createCertTestPod(t, ctx, client, 193 | testNamespace, podName, 194 | testingCert, 195 | ) 196 | 197 | waitForPodTermination() 198 | pod := getPod(t, ctx, client, testNamespace, podName) 199 | require.Len(t, pod.Status.ContainerStatuses, 1) 200 | 201 | setupLogs := getLogs(t, ctx, client, testNamespace, pod.Name, "setup-ca-certs") 202 | podLogs := getLogs(t, ctx, client, testNamespace, pod.Name, "test") 203 | require.Equal(t, int32(0), pod.Status.ContainerStatuses[0].State.Terminated.ExitCode, 204 | podLogFormat, setupLogs, podLogs, 205 | ) 206 | }) 207 | }) 208 | } 209 | 210 | func hasInjectedContainer(t *testing.T, pod *corev1.Pod) bool { 211 | var ( 212 | initContainerPresent bool 213 | volumePresent bool 214 | ) 215 | 216 | for _, container := range pod.Spec.InitContainers { 217 | if container.Name == "setup-ca-certs" && container.VolumeMounts[0].Name == "ca-certs" { 218 | initContainerPresent = true 219 | break 220 | } 221 | } 222 | 223 | for _, volume := range pod.Spec.Volumes { 224 | if volume.Name == "ca-certs" { 225 | volumePresent = true 226 | break 227 | } 228 | } 229 | 230 | return initContainerPresent && volumePresent 231 | } 232 | -------------------------------------------------------------------------------- /e2e/testhelpers.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "context" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "crypto/x509/pkix" 8 | "encoding/base64" 9 | "encoding/pem" 10 | "fmt" 11 | "io" 12 | "math/big" 13 | "strings" 14 | "testing" 15 | "time" 16 | 17 | "github.com/stretchr/testify/require" 18 | corev1 "k8s.io/api/core/v1" 19 | k8serrors "k8s.io/apimachinery/pkg/api/errors" 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | "k8s.io/apimachinery/pkg/watch" 22 | "k8s.io/client-go/kubernetes" 23 | ) 24 | 25 | func eventually(t *testing.T, fun func() bool, interval time.Duration, duration time.Duration) { 26 | t.Helper() 27 | endTime := time.Now().Add(duration) 28 | ticker := time.NewTicker(interval) 29 | defer ticker.Stop() 30 | for currentTime := range ticker.C { 31 | if endTime.Before(currentTime) { 32 | t.Fatal("time is up") 33 | } 34 | if fun() { 35 | return 36 | } 37 | } 38 | } 39 | func opensslTestScriptFor(cert string) string { 40 | return fmt.Sprintf(` 41 | cd $HOME 42 | 43 | cat > test.crt < OPENSSLDIR), 48 | # this is usually updated by 'update-ca-certificates', but since we only save 49 | # the /etc/ssl/certs dir, we need to explicitly pass it in here 50 | openssl verify -CApath /etc/ssl/certs/ test.crt 51 | `, cert) 52 | } 53 | 54 | func deletePod(t *testing.T, ctx context.Context, client kubernetes.Interface, namespace, name string) { 55 | err := client.CoreV1().Pods(namespace).Delete(ctx, name, metav1.DeleteOptions{}) 56 | if err != nil { 57 | t.Log(err) 58 | } 59 | } 60 | 61 | func createCertTestPod(t *testing.T, ctx context.Context, client kubernetes.Interface, namespace, name string, verificationCert string) { 62 | t.Helper() 63 | 64 | pod := &corev1.Pod{ 65 | ObjectMeta: metav1.ObjectMeta{ 66 | Name: name, 67 | Namespace: namespace, 68 | Labels: map[string]string{"some-label-1": ""}, 69 | }, 70 | Spec: corev1.PodSpec{ 71 | RestartPolicy: corev1.RestartPolicyNever, 72 | Containers: []corev1.Container{ 73 | { 74 | Name: "test", 75 | Image: "paketobuildpacks/build-jammy-base", 76 | Command: []string{"bash"}, 77 | Args: []string{"-c", opensslTestScriptFor(verificationCert)}, 78 | WorkingDir: "", 79 | }, 80 | }, 81 | }, 82 | } 83 | 84 | _, err := client.CoreV1().Pods(namespace).Create(ctx, pod, metav1.CreateOptions{}) 85 | require.NoError(t, err) 86 | } 87 | 88 | func createNoopPod(t *testing.T, ctx context.Context, client kubernetes.Interface, namespace, name string, labels map[string]string, annotations map[string]string) { 89 | t.Helper() 90 | 91 | pod := &corev1.Pod{ 92 | ObjectMeta: metav1.ObjectMeta{ 93 | Name: name, 94 | Namespace: namespace, 95 | Labels: labels, 96 | Annotations: annotations, 97 | }, 98 | Spec: corev1.PodSpec{ 99 | Containers: []corev1.Container{ 100 | { 101 | Name: "test", 102 | Image: "nginx:latest", 103 | Command: nil, 104 | Args: nil, 105 | WorkingDir: "", 106 | Ports: []corev1.ContainerPort{ 107 | { 108 | ContainerPort: 80, 109 | }, 110 | }, 111 | }, 112 | }, 113 | }, 114 | } 115 | 116 | _, err := client.CoreV1().Pods(namespace).Create(ctx, pod, metav1.CreateOptions{}) 117 | require.NoError(t, err) 118 | } 119 | 120 | func getLogs(t *testing.T, ctx context.Context, client kubernetes.Interface, namespace, name, container string) string { 121 | t.Helper() 122 | req := client.CoreV1().Pods(namespace).GetLogs(name, &corev1.PodLogOptions{Container: container}) 123 | logReader, err := req.Stream(ctx) 124 | require.NoError(t, err) 125 | defer logReader.Close() 126 | 127 | b, err := io.ReadAll(logReader) 128 | require.NoError(t, err) 129 | return string(b) 130 | } 131 | 132 | func deleteNamespace(t *testing.T, ctx context.Context, client kubernetes.Interface, namespace string) { 133 | t.Helper() 134 | 135 | err := client.CoreV1().Namespaces().Delete(ctx, namespace, metav1.DeleteOptions{}) 136 | require.True(t, err == nil || k8serrors.IsNotFound(err)) 137 | if k8serrors.IsNotFound(err) { 138 | return 139 | } 140 | 141 | var ( 142 | timeout int64 = 120 143 | closed = false 144 | ) 145 | 146 | watcher, err := client.CoreV1().Namespaces().Watch(ctx, metav1.ListOptions{ 147 | TimeoutSeconds: &timeout, 148 | }) 149 | require.NoError(t, err) 150 | 151 | for evt := range watcher.ResultChan() { 152 | if evt.Type != watch.Deleted { 153 | continue 154 | } 155 | if ns, ok := evt.Object.(*corev1.Namespace); ok { 156 | if ns.Name == namespace { 157 | closed = true 158 | break 159 | } 160 | } 161 | } 162 | require.True(t, closed) 163 | } 164 | 165 | func getPod(t *testing.T, ctx context.Context, client kubernetes.Interface, namespace, name string) *corev1.Pod { 166 | t.Helper() 167 | 168 | var ( 169 | pod *corev1.Pod 170 | err error 171 | ) 172 | eventually(t, func() bool { 173 | pod, err = client.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{}) 174 | if k8serrors.IsNotFound(err) { 175 | return false 176 | } else if err != nil { 177 | t.Error(err) 178 | return false 179 | } 180 | 181 | return true 182 | }, 5*time.Second, 2*time.Minute) 183 | 184 | return pod 185 | } 186 | 187 | func parseConfigmapName(ctx context.Context, client kubernetes.Interface, name string) (string, error) { 188 | deployment, err := client.AppsV1().Deployments(controllerNamespace).Get(ctx, controllerName, metav1.GetOptions{}) 189 | if err != nil { 190 | return "", err 191 | } 192 | 193 | configmapName := "" 194 | for _, v := range deployment.Spec.Template.Spec.Volumes { 195 | if v.Name == name { 196 | configmapName = v.ConfigMap.Name 197 | break 198 | } 199 | } 200 | 201 | if configmapName == "" { 202 | return "", fmt.Errorf("no configmap found") 203 | } 204 | return configmapName, nil 205 | } 206 | 207 | func updateConfigmap(ctx context.Context, client kubernetes.Interface, name, key, value string) error { 208 | configmapName, err := parseConfigmapName(ctx, client, name) 209 | if err != nil { 210 | return err 211 | } 212 | 213 | config, err := client.CoreV1().ConfigMaps(controllerNamespace).Get(ctx, configmapName, metav1.GetOptions{}) 214 | if err != nil { 215 | return err 216 | } 217 | 218 | oldConfigs[name] = config.Data[key] 219 | 220 | newConfig := config.DeepCopy() 221 | newConfig.Data[key] = value 222 | 223 | _, err = client.CoreV1().ConfigMaps(controllerNamespace).Update(ctx, newConfig, metav1.UpdateOptions{}) 224 | return err 225 | } 226 | 227 | func setCaCerts(ctx context.Context, client kubernetes.Interface, certs string) error { 228 | encoded := &strings.Builder{} 229 | _, err := io.WriteString(base64.NewEncoder(base64.StdEncoding, encoded), certs) 230 | if err != nil { 231 | return err 232 | } 233 | 234 | return updateConfigmap(ctx, client, "webhook-ca-cert", "ca.crt", encoded.String()) 235 | } 236 | 237 | func restoreCaCerts(ctx context.Context, client kubernetes.Interface) error { 238 | return updateConfigmap(ctx, client, "webhook-ca-cert", "ca.crt", oldConfigs["ca-cert"]) 239 | } 240 | 241 | func restoreProxies(ctx context.Context, client kubernetes.Interface) error { 242 | err := updateConfigmap(ctx, client, "http-proxy", "value", oldConfigs["http-proxy"]) 243 | if err != nil { 244 | return err 245 | } 246 | err = updateConfigmap(ctx, client, "https-proxy", "value", oldConfigs["https-proxy"]) 247 | if err != nil { 248 | return err 249 | } 250 | 251 | err = updateConfigmap(ctx, client, "no-proxy", "value", oldConfigs["no-proxy"]) 252 | if err != nil { 253 | return err 254 | } 255 | return nil 256 | } 257 | 258 | func restartController(t *testing.T, ctx context.Context, client kubernetes.Interface) error { 259 | err := client.CoreV1().Pods(controllerNamespace).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{ 260 | LabelSelector: "app=cert-injection-webhook", 261 | }) 262 | require.NoError(t, err) 263 | 264 | eventually(t, func() bool { 265 | list, err := client.CoreV1().Pods(controllerNamespace).List(ctx, metav1.ListOptions{ 266 | LabelSelector: "app=cert-injection-webhook", 267 | }) 268 | require.NoError(t, err) 269 | 270 | return len(list.Items) == 1 271 | }, 5*time.Second, 2*time.Minute) 272 | 273 | return nil 274 | } 275 | 276 | func generateCert(caPrivateKey *rsa.PrivateKey, caCert *x509.Certificate) (string, error) { 277 | privateKey, err := rsa.GenerateKey(rng, 2048) 278 | if err != nil { 279 | return "", err 280 | } 281 | 282 | cert := &x509.Certificate{ 283 | SerialNumber: big.NewInt(2), 284 | Subject: pkix.Name{ 285 | CommonName: "test", 286 | Organization: []string{"testing"}, 287 | }, 288 | NotBefore: time.Now(), 289 | NotAfter: time.Now().AddDate(10, 0, 0), 290 | KeyUsage: x509.KeyUsageDigitalSignature, 291 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 292 | } 293 | certBytes, err := x509.CreateCertificate(rng, cert, caCert, &privateKey.PublicKey, caPrivateKey) 294 | if err != nil { 295 | return "", err 296 | } 297 | 298 | encoded := pem.EncodeToMemory(&pem.Block{ 299 | Type: "CERTIFICATE", 300 | Bytes: certBytes, 301 | }) 302 | return string(encoded), nil 303 | } 304 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vmware-tanzu/cert-injection-webhook 2 | 3 | go 1.24 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/evanphx/json-patch/v5 v5.9.11 9 | github.com/pivotal/kpack v0.16.1-0.20250326154701-a947fa9c800d 10 | github.com/pkg/errors v0.9.1 11 | github.com/sclevine/spec v1.4.0 12 | github.com/stretchr/testify v1.10.0 13 | gomodules.xyz/jsonpatch/v3 v3.0.1 14 | k8s.io/api v0.30.11 15 | k8s.io/apimachinery v0.30.11 16 | k8s.io/client-go v0.30.11 17 | knative.dev/pkg v0.0.0-20250109131202-4ba3f1b39dbf 18 | ) 19 | 20 | require ( 21 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 22 | cloud.google.com/go/kms v1.17.1 // indirect 23 | contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d // indirect 24 | contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect 25 | github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo/helper v0.2.0 // indirect 26 | github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect 27 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 28 | github.com/Azure/go-autorest/autorest v0.11.30 // indirect 29 | github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect 30 | github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect 31 | github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect 32 | github.com/Azure/go-autorest/autorest/date v0.3.1 // indirect 33 | github.com/Azure/go-autorest/logger v0.2.2 // indirect 34 | github.com/Azure/go-autorest/tracing v0.6.1 // indirect 35 | github.com/BurntSushi/toml v1.5.0 // indirect 36 | github.com/Masterminds/semver/v3 v3.3.1 // indirect 37 | github.com/ProtonMail/go-crypto v1.1.4 // indirect 38 | github.com/ThalesIgnite/crypto11 v1.2.5 // indirect 39 | github.com/agext/levenshtein v1.2.3 // indirect 40 | github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect 41 | github.com/alibabacloud-go/cr-20160607 v1.0.1 // indirect 42 | github.com/alibabacloud-go/cr-20181201 v1.0.10 // indirect 43 | github.com/alibabacloud-go/darabonba-openapi v0.2.1 // indirect 44 | github.com/alibabacloud-go/debug v1.0.0 // indirect 45 | github.com/alibabacloud-go/endpoint-util v1.1.1 // indirect 46 | github.com/alibabacloud-go/openapi-util v0.1.0 // indirect 47 | github.com/alibabacloud-go/tea v1.2.1 // indirect 48 | github.com/alibabacloud-go/tea-utils v1.4.5 // indirect 49 | github.com/alibabacloud-go/tea-xml v1.1.3 // indirect 50 | github.com/aliyun/credentials-go v1.3.1 // indirect 51 | github.com/apex/log v1.9.0 // indirect 52 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 53 | github.com/aws/aws-sdk-go-v2 v1.36.1 // indirect 54 | github.com/aws/aws-sdk-go-v2/config v1.29.6 // indirect 55 | github.com/aws/aws-sdk-go-v2/credentials v1.17.59 // indirect 56 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28 // indirect 57 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32 // indirect 58 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32 // indirect 59 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect 60 | github.com/aws/aws-sdk-go-v2/service/ecr v1.40.3 // indirect 61 | github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.31.2 // indirect 62 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect 63 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13 // indirect 64 | github.com/aws/aws-sdk-go-v2/service/sso v1.24.15 // indirect 65 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.14 // indirect 66 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.14 // indirect 67 | github.com/aws/smithy-go v1.22.2 // indirect 68 | github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.9.1 // indirect 69 | github.com/beorn7/perks v1.0.1 // indirect 70 | github.com/blang/semver v3.5.1+incompatible // indirect 71 | github.com/blang/semver/v4 v4.0.0 // indirect 72 | github.com/blendle/zapdriver v1.3.1 // indirect 73 | github.com/buildpacks/imgutil v0.0.0-20240605145725-186f89b2d168 // indirect 74 | github.com/buildpacks/lifecycle v0.20.3 // indirect 75 | github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect 76 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 77 | github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect 78 | github.com/clbanning/mxj/v2 v2.7.0 // indirect 79 | github.com/cloudflare/circl v1.5.0 // indirect 80 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect 81 | github.com/containerd/log v0.1.0 // indirect 82 | github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect 83 | github.com/containerd/typeurl/v2 v2.1.1 // indirect 84 | github.com/cyberphone/json-canonicalization v0.0.0-20231011164504-785e29786b46 // indirect 85 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 86 | github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect 87 | github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect 88 | github.com/dimchansky/utfbom v1.1.1 // indirect 89 | github.com/distribution/reference v0.6.0 // indirect 90 | github.com/docker/cli v28.0.4+incompatible // indirect 91 | github.com/docker/distribution v2.8.3+incompatible // indirect 92 | github.com/docker/docker v26.1.5+incompatible // indirect 93 | github.com/docker/docker-credential-helpers v0.9.3 // indirect 94 | github.com/docker/go-connections v0.5.0 // indirect 95 | github.com/docker/go-metrics v0.0.1 // indirect 96 | github.com/docker/go-units v0.5.0 // indirect 97 | github.com/dustin/go-humanize v1.0.1 // indirect 98 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 99 | github.com/evanphx/json-patch v5.9.0+incompatible // indirect 100 | github.com/fsnotify/fsnotify v1.7.0 // indirect 101 | github.com/go-chi/chi v4.1.2+incompatible // indirect 102 | github.com/go-kit/log v0.2.1 // indirect 103 | github.com/go-logfmt/logfmt v0.5.1 // indirect 104 | github.com/go-logr/logr v1.4.2 // indirect 105 | github.com/go-logr/stdr v1.2.2 // indirect 106 | github.com/go-openapi/analysis v0.23.0 // indirect 107 | github.com/go-openapi/errors v0.22.0 // indirect 108 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 109 | github.com/go-openapi/jsonreference v0.21.0 // indirect 110 | github.com/go-openapi/loads v0.22.0 // indirect 111 | github.com/go-openapi/runtime v0.28.0 // indirect 112 | github.com/go-openapi/spec v0.21.0 // indirect 113 | github.com/go-openapi/strfmt v0.23.0 // indirect 114 | github.com/go-openapi/swag v0.23.1 // indirect 115 | github.com/go-openapi/validate v0.24.0 // indirect 116 | github.com/gogo/protobuf v1.3.2 // indirect 117 | github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 118 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 119 | github.com/golang/protobuf v1.5.4 // indirect 120 | github.com/golang/snappy v0.0.4 // indirect 121 | github.com/google/certificate-transparency-go v1.1.8 // indirect 122 | github.com/google/gnostic-models v0.6.9 // indirect 123 | github.com/google/go-cmp v0.7.0 // indirect 124 | github.com/google/go-containerregistry v0.20.2 // indirect 125 | github.com/google/go-github/v55 v55.0.0 // indirect 126 | github.com/google/go-querystring v1.1.0 // indirect 127 | github.com/google/gofuzz v1.2.0 // indirect 128 | github.com/google/uuid v1.6.0 // indirect 129 | github.com/gorilla/mux v1.8.1 // indirect 130 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect 131 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 132 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 133 | github.com/hashicorp/golang-lru v1.0.2 // indirect 134 | github.com/hashicorp/hcl v1.0.1-vault-5 // indirect 135 | github.com/heroku/color v0.0.6 // indirect 136 | github.com/imdario/mergo v0.3.16 // indirect 137 | github.com/in-toto/in-toto-golang v0.9.0 // indirect 138 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 139 | github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 // indirect 140 | github.com/josharian/intern v1.0.0 // indirect 141 | github.com/json-iterator/go v1.1.12 // indirect 142 | github.com/kelseyhightower/envconfig v1.4.0 // indirect 143 | github.com/klauspost/compress v1.18.0 // indirect 144 | github.com/letsencrypt/boulder v0.0.0-20231026200631-000cd05d5491 // indirect 145 | github.com/magiconair/properties v1.8.7 // indirect 146 | github.com/mailru/easyjson v0.9.0 // indirect 147 | github.com/mattn/go-colorable v0.1.13 // indirect 148 | github.com/mattn/go-isatty v0.0.20 // indirect 149 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect 150 | github.com/miekg/pkcs11 v1.1.1 // indirect 151 | github.com/mitchellh/go-homedir v1.1.0 // indirect 152 | github.com/mitchellh/mapstructure v1.5.0 // indirect 153 | github.com/moby/buildkit v0.13.2 // indirect 154 | github.com/moby/docker-image-spec v1.3.1 // indirect 155 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 156 | github.com/modern-go/reflect2 v1.0.2 // indirect 157 | github.com/mozillazg/docker-credential-acr-helper v0.3.0 // indirect 158 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 159 | github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect 160 | github.com/oklog/ulid v1.3.1 // indirect 161 | github.com/opencontainers/go-digest v1.0.0 // indirect 162 | github.com/opencontainers/image-spec v1.1.1 // indirect 163 | github.com/opentracing/opentracing-go v1.2.0 // indirect 164 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 165 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 166 | github.com/prometheus/client_golang v1.20.5 // indirect 167 | github.com/prometheus/client_model v0.6.1 // indirect 168 | github.com/prometheus/common v0.62.0 // indirect 169 | github.com/prometheus/procfs v0.15.1 // indirect 170 | github.com/prometheus/statsd_exporter v0.22.7 // indirect 171 | github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect 172 | github.com/sagikazarmark/locafero v0.4.0 // indirect 173 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 174 | github.com/sassoftware/relic v7.2.1+incompatible // indirect 175 | github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect 176 | github.com/shibumi/go-pathspec v1.3.0 // indirect 177 | github.com/sigstore/cosign/v2 v2.2.4 // indirect 178 | github.com/sigstore/rekor v1.3.6 // indirect 179 | github.com/sigstore/sigstore v1.8.3 // indirect 180 | github.com/sigstore/timestamp-authority v1.2.2 // indirect 181 | github.com/sirupsen/logrus v1.9.3 // indirect 182 | github.com/sourcegraph/conc v0.3.0 // indirect 183 | github.com/spf13/afero v1.11.0 // indirect 184 | github.com/spf13/cast v1.6.0 // indirect 185 | github.com/spf13/cobra v1.8.1 // indirect 186 | github.com/spf13/pflag v1.0.6 // indirect 187 | github.com/spf13/viper v1.18.2 // indirect 188 | github.com/subosito/gotenv v1.6.0 // indirect 189 | github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect 190 | github.com/thales-e-security/pool v0.0.2 // indirect 191 | github.com/theupdateframework/go-tuf v0.7.0 // indirect 192 | github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect 193 | github.com/tjfoc/gmsm v1.4.1 // indirect 194 | github.com/transparency-dev/merkle v0.0.2 // indirect 195 | github.com/vbatts/tar-split v0.12.1 // indirect 196 | github.com/xanzy/go-gitlab v0.102.0 // indirect 197 | go.mongodb.org/mongo-driver v1.14.0 // indirect 198 | go.opencensus.io v0.24.0 // indirect 199 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 200 | go.opentelemetry.io/otel v1.34.0 // indirect 201 | go.opentelemetry.io/otel/metric v1.34.0 // indirect 202 | go.opentelemetry.io/otel/trace v1.34.0 // indirect 203 | go.uber.org/automaxprocs v1.6.0 // indirect 204 | go.uber.org/multierr v1.11.0 // indirect 205 | go.uber.org/zap v1.27.0 // indirect 206 | golang.org/x/crypto v0.36.0 // indirect 207 | golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect 208 | golang.org/x/mod v0.24.0 // indirect 209 | golang.org/x/net v0.37.0 // indirect 210 | golang.org/x/oauth2 v0.28.0 // indirect 211 | golang.org/x/sync v0.12.0 // indirect 212 | golang.org/x/sys v0.31.0 // indirect 213 | golang.org/x/term v0.30.0 // indirect 214 | golang.org/x/text v0.23.0 // indirect 215 | golang.org/x/time v0.11.0 // indirect 216 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 217 | gomodules.xyz/orderedmap v0.1.0 // indirect 218 | google.golang.org/api v0.183.0 // indirect 219 | google.golang.org/genproto/googleapis/api v0.0.0-20250207221924-e9438ea467c6 // indirect 220 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect 221 | google.golang.org/grpc v1.71.0 // indirect 222 | google.golang.org/protobuf v1.36.6 // indirect 223 | gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect 224 | gopkg.in/inf.v0 v0.9.1 // indirect 225 | gopkg.in/ini.v1 v1.67.0 // indirect 226 | gopkg.in/yaml.v2 v2.4.0 // indirect 227 | gopkg.in/yaml.v3 v3.0.1 // indirect 228 | k8s.io/apiextensions-apiserver v0.30.3 // indirect 229 | k8s.io/klog/v2 v2.130.1 // indirect 230 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 231 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect 232 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 233 | sigs.k8s.io/randfill v1.0.0 // indirect 234 | sigs.k8s.io/release-utils v0.7.7 // indirect 235 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 236 | sigs.k8s.io/yaml v1.4.0 // indirect 237 | ) 238 | -------------------------------------------------------------------------------- /hack/mdlint-readme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #! Copyright 2021 VMware, Inc. 4 | #! SPDX-License-Identifier: Apache-2.0 5 | 6 | set -euo pipefail 7 | 8 | cd "$(dirname "${BASH_SOURCE[0]}")/.." 9 | 10 | # mdlint rules: 11 | # https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md 12 | docker run --rm -v "$(pwd)":/build gcr.io/cluster-api-provider-vsphere/extra/mdlint:0.23.2 -- /md/lint packaging/README.md 13 | -------------------------------------------------------------------------------- /packaging/README.md: -------------------------------------------------------------------------------- 1 | # Cert Injection Webhook 2 | 3 | The Cert Injection Webhook for Kubernetes extends kubernetes with a webhook that injects 4 | CA certificates and proxy environment variables into pods. The webhook uses certificates and 5 | environment variables defined in configmaps and injects them into pods with the desired labels or annotations. 6 | 7 | ## Components 8 | 9 | * cert-injection-webhook 10 | 11 | ## Supported Providers 12 | 13 | The following table shows the providers this package can work with. 14 | 15 | | AWS | Azure | vSphere | Docker | 16 | |-----|-------|---------|--------| 17 | | ✅ | ✅ | ✅ | ✅ | 18 | 19 | ## Configuration 20 | 21 | The following configuration values can be set to customize the cert-injection-webhook 22 | installation. 23 | 24 | | Value | Required/Optional | Description | 25 | |----------------|------------------------------------------|---------------------------------------------------------------------------------------------------------------| 26 | | `ca_cert_data` | Optional | CA cert data to inject into pod trust store | 27 | | `labels` | Required if annotations are not provided | Array of labels that will be used to match on pods that will have certs and proxy environment injected | 28 | | `annotations` | Required if labels are not provided | Array of annotations that will be used to match on pods that will have certs and proxy environment injected | 29 | | `http_proxy` | Optional | The HTTP proxy to inject into pod environment | 30 | | `https_proxy` | Optional | The HTTPS proxy to inject into pod environment | 31 | | `no_proxy` | Optional | A comma-separated list of hostnames, IP addresses, or IP ranges in CIDR format to inject into pod environment | 32 | 33 | ## Installation 34 | 35 | ### Package Installation steps 36 | 37 | 1. Create a `cert-injection-webhook-config-values.yaml` with the labels or annotations (or both) that you would like to use. 38 | Any pod that matches one of these labels or annotations will have the provided cert injected. For example: 39 | 40 | ```yaml 41 | --- 42 | labels: 43 | - kpack.io/build 44 | annotations: 45 | - some-annotation 46 | ca_cert_data: | 47 | -----BEGIN CERTIFICATE----- 48 | MIICrDCCAZQCCQDcakcvwbW4UTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1t 49 | eXdlYnNpdGUuY29tMB4XDTIyMDIxNDE2MjM1OVoXDTMyMDIxMjE2MjM1OVowGDEW 50 | MBQGA1UEAwwNbXl3ZWJzaXRlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 51 | AQoCggEBAMgWkhYr7OPSTuDwGSM5jMQtO5vnqfESPPh829IMTBNXkS0KV6Hi90ka 52 | T/gIbq0H+QO5Abzh8QDIOWqaTLLp5FedsU1xsGTiKQ+YVKfoQ7T7R/K+adWuJL6H 53 | i8kgb4ErzhYhDQqsPU6ZglKkTZTL+7fhpsc7ZewASa7TRJiSo51Qye9K1qsjj3Wd 54 | MB+0qH1vxvN2zs/117qowW/2YH2H++lJSfnEMH4Z67RQ5o56DpeHvE7mLz0LNVu/ 55 | gyM8JXClgsPdr11Iiv17TevWoXSeoWa0ts6MGd/r376dtEZ60wGG+geXcf9szAx1 56 | GZLEQamRHnVyrGvb7U/AvLaJMnNY8PcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA 57 | bc4XeX7sKvtEHK5tYKJDarP6suArgs7/IpfT2DiRB8JSBYX7rHD6NIB3433JxQfc 58 | SHD9FBpH9E8aSMDsCWKcuRRI7GeRarqwfblAqflCv85NJaiC9zu+haue7aNMNnwA 59 | uB+q0urjiKlEOM2OsLqgjXXmx5+nSrdwUhFXmyMsJC2eP4Dm1gJp5tQG2hSONC7w 60 | dX2wAQp7PYaq+h1ASkDNaKy3ZoeD7yEp3Mhbnh+fu0O06NpnJhUZPhdTtMD3LYPJ 61 | +iwL43iSAQt05ZK39u23zsdMc+RLFbqQYsULYZS2g/SmcSnw8CC3aer8X6x4lEw7 62 | FpCpA2Wta8mXHGKqmq0+og== 63 | -----END CERTIFICATE----- 64 | ``` 65 | 66 | You can install the cert-injection-webhook package using the command below - 67 | 68 | `tanzu package install cert-injection-webhook --package-name cert-injection-webhook.community.tanzu.vmware.com --version -f cert-injection-webhook-config-values.yaml` 69 | 70 | ### Injecting certificates into kpack builds 71 | 72 | When providing ca_cert_data directly to kpack, that CA Certificate be injected into builds themselves. 73 | If you want kpack builds to have CA Certificates for communicating with a self-signed registry, 74 | make sure the values yaml has a label with `kpack.io/build`. This will match on any build pod that kpack creates. 75 | -------------------------------------------------------------------------------- /packaging/metadata.yaml: -------------------------------------------------------------------------------- 1 | #! Copyright 2021 VMware, Inc. 2 | #! SPDX-License-Identifier: Apache-2.0 3 | --- 4 | apiVersion: data.packaging.carvel.dev/v1alpha1 5 | kind: PackageMetadata 6 | metadata: 7 | name: cert-injection-webhook.community.tanzu.vmware.com 8 | spec: 9 | displayName: "cert-injection-webhook" 10 | longDescription: "The Cert Injection Webhook for Kubernetes extends kubernetes with a webhook that injects CA certificates and proxy environment variables into pods. The webhook uses certificates and environment variables defined in configmaps and injects them into pods with the desired labels or annotations." 11 | shortDescription: "The Cert Injection Webhook injects CA certificates and proxy environment variables into pods" 12 | supportDescription: "Go to https://github.com/vmware-tanzu/cert-injection-webhook for documentation or the #kpack channel on Kubernetes slack" 13 | providerName: "VMware" 14 | maintainers: 15 | - name: "Matthew McNew" 16 | - name: "Tom Kennedy" 17 | - name: "Viraj Patel" 18 | - name: "Matt Gibson" 19 | - name: "Daniel Chen" 20 | -------------------------------------------------------------------------------- /packaging/package.yaml: -------------------------------------------------------------------------------- 1 | #! Copyright 2021 VMware, Inc. 2 | #! SPDX-License-Identifier: Apache-2.0 3 | 4 | #@ load("@ytt:data", "data") 5 | --- 6 | apiVersion: data.packaging.carvel.dev/v1alpha1 7 | kind: Package 8 | metadata: 9 | name: #@ "cert-injection-webhook.community.tanzu.vmware.com." + data.values.version 10 | spec: 11 | refName: cert-injection-webhook.community.tanzu.vmware.com 12 | version: #@ data.values.version 13 | releaseNotes: #@ "https://github.com/vmware-tanzu/cert-injection-webhook/releases/tag/v" + data.values.version 14 | valuesSchema: 15 | openAPIv3: 16 | title: cert-injection-webhook.tanzu.vmware.com values schema 17 | properties: 18 | ca_cert_data: 19 | type: string 20 | description: contents of CA certificate to be injected into pod trust store 21 | annotations: 22 | type: array 23 | items: 24 | type: string 25 | description: pod annotations to match on for ca cert injection 26 | labels: 27 | type: array 28 | items: 29 | type: string 30 | description: pod labels to match on for ca cert injection 31 | http_proxy: 32 | type: string 33 | description: the HTTP proxy to use for network traffic 34 | https_proxy: 35 | type: string 36 | description: the HTTPS proxy to use for network traffic. 37 | no_proxy: 38 | type: string 39 | description: a comma-separated list of hostnames, IP addresses, or IP ranges in CIDR format that should not use a proxy 40 | template: 41 | spec: 42 | fetch: 43 | - imgpkgBundle: 44 | image: #@ data.values.bundle_image 45 | template: 46 | - ytt: 47 | paths: 48 | - "config/" 49 | - kbld: 50 | paths: 51 | - "-" 52 | - ".imgpkg/images.yml" 53 | deploy: 54 | - kapp: {} 55 | -------------------------------------------------------------------------------- /pkg/certinjectionwebhook/admission_controller.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-Present VMware, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package certinjectionwebhook 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | 12 | "github.com/pkg/errors" 13 | admissionv1 "k8s.io/api/admission/v1" 14 | corev1 "k8s.io/api/core/v1" 15 | apierrors "k8s.io/apimachinery/pkg/api/errors" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | "k8s.io/apimachinery/pkg/runtime" 18 | "k8s.io/apimachinery/pkg/runtime/serializer" 19 | "knative.dev/pkg/apis" 20 | "knative.dev/pkg/apis/duck" 21 | "knative.dev/pkg/logging" 22 | "knative.dev/pkg/webhook" 23 | 24 | "github.com/vmware-tanzu/cert-injection-webhook/pkg/certs" 25 | ) 26 | 27 | const ( 28 | caCertsVolumeName = "ca-certs" 29 | caCertsMountPath = "/etc/ssl/certs" 30 | ) 31 | 32 | var ( 33 | errMissingNewObject = errors.New("the new object may not be nil") 34 | podResource = metav1.GroupVersionResource{Version: "v1", Resource: "pods"} 35 | ) 36 | 37 | // Implements webhook.AdmissionController 38 | type admissionController struct { 39 | name string 40 | path string 41 | 42 | withContext func(context.Context) context.Context 43 | 44 | labels []string 45 | annotations []string 46 | envVars []corev1.EnvVar 47 | setupCACertsImage string 48 | caCertsData string 49 | imagePullSecrets corev1.LocalObjectReference 50 | } 51 | 52 | func NewAdmissionController( 53 | name string, 54 | path string, 55 | wc func(context.Context) context.Context, 56 | 57 | labels []string, 58 | annotations []string, 59 | envVars []corev1.EnvVar, 60 | setupCACertsImage string, 61 | caCertsData string, 62 | imagePullSecrets corev1.LocalObjectReference, 63 | ) (*admissionController, error) { 64 | 65 | if len(labels) == 0 && len(annotations) == 0 { 66 | return nil, errors.New("at least one label or annotation required") 67 | } 68 | 69 | return &admissionController{ 70 | name: name, 71 | path: path, 72 | withContext: wc, 73 | labels: labels, 74 | annotations: annotations, 75 | envVars: envVars, 76 | setupCACertsImage: setupCACertsImage, 77 | caCertsData: caCertsData, 78 | imagePullSecrets: imagePullSecrets, 79 | }, nil 80 | } 81 | 82 | func (ac *admissionController) Path() string { 83 | return ac.path 84 | } 85 | 86 | func (ac *admissionController) Admit(ctx context.Context, request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse { 87 | if ac.withContext != nil { 88 | ctx = ac.withContext(ctx) 89 | } 90 | 91 | logger := logging.FromContext(ctx) 92 | 93 | if request.Resource != podResource { 94 | logger.Infof("expected resource to be %v", podResource) 95 | return &admissionv1.AdmissionResponse{Allowed: true} 96 | } 97 | 98 | switch request.Operation { 99 | case admissionv1.Create: 100 | default: 101 | logger.Infof("Unhandled webhook operation, letting it through %v", request.Operation) 102 | return &admissionv1.AdmissionResponse{Allowed: true} 103 | } 104 | 105 | raw := request.Object.Raw 106 | pod := corev1.Pod{} 107 | if _, _, err := universalDeserializer.Decode(raw, nil, &pod); err != nil { 108 | reason := fmt.Sprintf("could not deserialize pod object: %v", err) 109 | logger.Error(reason) 110 | result := apierrors.NewBadRequest(reason).Status() 111 | return &admissionv1.AdmissionResponse{ 112 | Result: &result, 113 | Allowed: true, 114 | } 115 | } 116 | 117 | if pod.Spec.NodeSelector["kubernetes.io/os"] == "windows" { 118 | return &admissionv1.AdmissionResponse{Allowed: true} 119 | } 120 | 121 | if !(intersect(ac.labels, pod.Labels) || intersect(ac.annotations, pod.Annotations)) { 122 | logger.Info("does not contain matching labels or annotations, letting it through") 123 | return &admissionv1.AdmissionResponse{Allowed: true} 124 | } 125 | 126 | patchBytes, err := ac.mutate(ctx, request) 127 | if err != nil { 128 | logger.Error(fmt.Sprintf("mutation failed: %v", err)) 129 | status := webhook.MakeErrorStatus("mutation failed: %v", err) 130 | return status 131 | } 132 | logger.Infof("Kind: %q PatchBytes: %v", request.Kind, string(patchBytes)) 133 | 134 | return &admissionv1.AdmissionResponse{ 135 | Patch: patchBytes, 136 | Allowed: true, 137 | PatchType: func() *admissionv1.PatchType { 138 | pt := admissionv1.PatchTypeJSONPatch 139 | return &pt 140 | }(), 141 | } 142 | } 143 | 144 | func (ac *admissionController) mutate(ctx context.Context, req *admissionv1.AdmissionRequest) ([]byte, error) { 145 | newBytes := req.Object.Raw 146 | 147 | var newObj corev1.Pod 148 | if len(newBytes) != 0 { 149 | newDecoder := json.NewDecoder(bytes.NewBuffer(newBytes)) 150 | if err := newDecoder.Decode(&newObj); err != nil { 151 | return nil, fmt.Errorf("cannot decode incoming new object: %v", err) 152 | } 153 | } 154 | 155 | var patches duck.JSONPatch 156 | var err error 157 | 158 | ctx = apis.WithinCreate(ctx) 159 | ctx = apis.WithUserInfo(ctx, &req.UserInfo) 160 | 161 | if patches, err = ac.setBuildServicePodDefaults(ctx, patches, newObj); err != nil { 162 | return nil, errors.Wrap(err, "Failed to set default env vars and ca cert on pod") 163 | } 164 | 165 | if &newObj == nil { 166 | return nil, errMissingNewObject 167 | } 168 | return json.Marshal(patches) 169 | } 170 | 171 | func (ac *admissionController) SetEnvVars(ctx context.Context, obj *corev1.Pod) { 172 | if len(ac.envVars) == 0 { 173 | return 174 | } 175 | 176 | for i := range obj.Spec.Containers { 177 | for _, envVar := range ac.envVars { 178 | obj.Spec.Containers[i].Env = append(obj.Spec.Containers[i].Env, envVar) 179 | } 180 | } 181 | 182 | for i := range obj.Spec.InitContainers { 183 | for _, envVar := range ac.envVars { 184 | obj.Spec.InitContainers[i].Env = append(obj.Spec.InitContainers[i].Env, envVar) 185 | } 186 | } 187 | } 188 | 189 | func (ac *admissionController) SetCaCerts(ctx context.Context, obj *corev1.Pod) { 190 | if ac.caCertsData == "" { 191 | return 192 | } 193 | 194 | volume := corev1.Volume{ 195 | Name: caCertsVolumeName, 196 | VolumeSource: corev1.VolumeSource{ 197 | EmptyDir: &corev1.EmptyDirVolumeSource{}, 198 | }, 199 | } 200 | obj.Spec.Volumes = append(obj.Spec.Volumes, volume) 201 | 202 | mount := corev1.VolumeMount{ 203 | Name: caCertsVolumeName, 204 | MountPath: caCertsMountPath, 205 | ReadOnly: true, 206 | } 207 | for i := range obj.Spec.InitContainers { 208 | obj.Spec.InitContainers[i].VolumeMounts = append(obj.Spec.InitContainers[i].VolumeMounts, mount) 209 | } 210 | for i := range obj.Spec.Containers { 211 | obj.Spec.Containers[i].VolumeMounts = append(obj.Spec.Containers[i].VolumeMounts, mount) 212 | } 213 | 214 | if ac.imagePullSecrets != (corev1.LocalObjectReference{}) { 215 | obj.Spec.ImagePullSecrets = append(obj.Spec.ImagePullSecrets, ac.imagePullSecrets) 216 | } 217 | 218 | var envVars []corev1.EnvVar 219 | for i, cert := range certs.Split(ac.caCertsData) { 220 | envVars = append(envVars, corev1.EnvVar{ 221 | Name: fmt.Sprintf("CA_CERTS_DATA_%d", i), 222 | Value: cert, 223 | }) 224 | } 225 | 226 | container := corev1.Container{ 227 | Name: "setup-ca-certs", 228 | Image: ac.setupCACertsImage, 229 | Env: envVars, 230 | ImagePullPolicy: corev1.PullIfNotPresent, 231 | WorkingDir: "/workspace", 232 | VolumeMounts: []corev1.VolumeMount{ 233 | { 234 | Name: caCertsVolumeName, 235 | MountPath: "/workspace", 236 | }, 237 | }, 238 | SecurityContext: &corev1.SecurityContext{ 239 | RunAsNonRoot: boolPointer(true), 240 | AllowPrivilegeEscalation: boolPointer(false), 241 | Privileged: boolPointer(false), 242 | SeccompProfile: &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault}, 243 | Capabilities: &corev1.Capabilities{Drop: []corev1.Capability{"ALL"}}, 244 | }, 245 | } 246 | obj.Spec.InitContainers = append([]corev1.Container{container}, obj.Spec.InitContainers...) 247 | } 248 | 249 | func (ac *admissionController) setBuildServicePodDefaults(ctx context.Context, patches duck.JSONPatch, pod corev1.Pod) (duck.JSONPatch, error) { 250 | before, after := pod.DeepCopyObject(), pod 251 | ac.SetEnvVars(ctx, &after) 252 | ac.SetCaCerts(ctx, &after) 253 | 254 | patch, err := duck.CreatePatch(before, after) 255 | if err != nil { 256 | return nil, err 257 | } 258 | 259 | return append(patches, patch...), nil 260 | } 261 | 262 | var universalDeserializer = serializer.NewCodecFactory(runtime.NewScheme()).UniversalDeserializer() 263 | 264 | func intersect(a []string, b map[string]string) bool { 265 | for _, k := range a { 266 | if _, ok := b[k]; ok { 267 | return true 268 | } 269 | } 270 | return false 271 | } 272 | 273 | func boolPointer(b bool) *bool { 274 | return &b 275 | } 276 | -------------------------------------------------------------------------------- /pkg/certinjectionwebhook/admission_controller_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-Present VMware, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package certinjectionwebhook_test 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "testing" 10 | 11 | jp "github.com/evanphx/json-patch/v5" 12 | "github.com/sclevine/spec" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | "gomodules.xyz/jsonpatch/v3" 16 | admissionv1 "k8s.io/api/admission/v1" 17 | corev1 "k8s.io/api/core/v1" 18 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 | "k8s.io/apimachinery/pkg/runtime" 20 | wtesting "knative.dev/pkg/webhook/testing" 21 | 22 | "github.com/vmware-tanzu/cert-injection-webhook/pkg/certinjectionwebhook" 23 | ) 24 | 25 | func TestPodAdmissionController(t *testing.T) { 26 | spec.Run(t, "Pod Admission Controller", testPodAdmissionController) 27 | } 28 | 29 | func testPodAdmissionController(t *testing.T, when spec.G, it spec.S) { 30 | const ( 31 | name = "some-webhook" 32 | path = "/some-path" 33 | ) 34 | 35 | when("#NewAdmissionController", func() { 36 | it("returns an error if there is not at least one label or annotation", func() { 37 | _, err := certinjectionwebhook.NewAdmissionController( 38 | "", 39 | "", 40 | nil, 41 | []string{}, 42 | []string{}, 43 | nil, 44 | "", 45 | "", 46 | corev1.LocalObjectReference{}, 47 | ) 48 | require.Errorf(t, err, "at least one label or annotation required") 49 | 50 | _, err = certinjectionwebhook.NewAdmissionController( 51 | "", 52 | "", 53 | nil, 54 | []string{"label"}, 55 | []string{}, 56 | nil, 57 | "", 58 | "", 59 | corev1.LocalObjectReference{}, 60 | ) 61 | require.NoError(t, err) 62 | 63 | _, err = certinjectionwebhook.NewAdmissionController( 64 | "", 65 | "", 66 | nil, 67 | []string{}, 68 | []string{"annotation"}, 69 | nil, 70 | "", 71 | "", 72 | corev1.LocalObjectReference{}, 73 | ) 74 | require.NoError(t, err) 75 | }) 76 | }) 77 | 78 | when("#Admit", func() { 79 | const ( 80 | label = "some/label" 81 | annotation = "some.annotation" 82 | 83 | setupCACertsImage = "some-ca-certs-image" 84 | caCertsData = "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----" 85 | ) 86 | 87 | envVars := []corev1.EnvVar{ 88 | { 89 | Name: "HTTP_PROXY", 90 | Value: "http://my.proxy.com", 91 | }, 92 | { 93 | Name: "NO_PROXY", 94 | Value: "http://my.local.com", 95 | }, 96 | } 97 | 98 | testPod := &corev1.Pod{ 99 | ObjectMeta: metav1.ObjectMeta{ 100 | Name: "object-meta", 101 | }, 102 | Spec: corev1.PodSpec{ 103 | InitContainers: []corev1.Container{ 104 | { 105 | Name: "init-container-without-env", 106 | Image: "image", 107 | Env: nil, 108 | }, 109 | { 110 | Name: "init-container-with-env", 111 | Image: "image", 112 | Env: []corev1.EnvVar{ 113 | { 114 | Name: "EXISTING", 115 | Value: "VALUE", 116 | }, 117 | }, 118 | }, 119 | }, 120 | Containers: []corev1.Container{ 121 | { 122 | Name: "container-without-env", 123 | Image: "image", 124 | Env: nil, 125 | }, 126 | { 127 | Name: "container-with-env", 128 | Image: "image", 129 | Env: []corev1.EnvVar{ 130 | { 131 | Name: "EXISTING", 132 | Value: "VALUE", 133 | }, 134 | }, 135 | }, 136 | }, 137 | }, 138 | } 139 | 140 | ctx := context.TODO() 141 | 142 | it("sets the env vars on all containers on the pods that are labelled", func() { 143 | ac, err := certinjectionwebhook.NewAdmissionController( 144 | name, 145 | path, 146 | func(ctx context.Context) context.Context { return ctx }, 147 | []string{label}, 148 | []string{}, 149 | envVars, 150 | "", 151 | "", 152 | corev1.LocalObjectReference{}, 153 | ) 154 | require.NoError(t, err) 155 | 156 | testPod.Labels = map[string]string{ 157 | label: "some value", 158 | } 159 | 160 | bytes, err := json.Marshal(testPod) 161 | require.NoError(t, err) 162 | 163 | admissionRequest := &admissionv1.AdmissionRequest{ 164 | Name: "testAdmissionRequest", 165 | Object: runtime.RawExtension{ 166 | Raw: bytes, 167 | }, 168 | Operation: admissionv1.Create, 169 | Resource: metav1.GroupVersionResource{Version: "v1", Resource: "pods"}, 170 | } 171 | 172 | response := ac.Admit(ctx, admissionRequest) 173 | wtesting.ExpectAllowed(t, response) 174 | 175 | patch, err := jp.DecodePatch(response.Patch) 176 | require.NoError(t, err) 177 | 178 | buf, err := patch.Apply(bytes) 179 | require.NoError(t, err) 180 | 181 | var actualPod corev1.Pod 182 | err = json.Unmarshal(buf, &actualPod) 183 | require.NoError(t, err) 184 | 185 | expectedPod := corev1.Pod{ 186 | ObjectMeta: metav1.ObjectMeta{ 187 | Name: "object-meta", 188 | Labels: map[string]string{ 189 | label: "some value", 190 | }, 191 | }, 192 | Spec: corev1.PodSpec{ 193 | InitContainers: []corev1.Container{ 194 | { 195 | Name: "init-container-without-env", 196 | Image: "image", 197 | Env: []corev1.EnvVar{ 198 | { 199 | Name: "HTTP_PROXY", 200 | Value: "http://my.proxy.com", 201 | }, 202 | { 203 | Name: "NO_PROXY", 204 | Value: "http://my.local.com", 205 | }, 206 | }, 207 | }, 208 | { 209 | Name: "init-container-with-env", 210 | Image: "image", 211 | Env: []corev1.EnvVar{ 212 | { 213 | Name: "EXISTING", 214 | Value: "VALUE", 215 | }, 216 | { 217 | Name: "HTTP_PROXY", 218 | Value: "http://my.proxy.com", 219 | }, 220 | { 221 | Name: "NO_PROXY", 222 | Value: "http://my.local.com", 223 | }, 224 | }, 225 | }, 226 | }, 227 | Containers: []corev1.Container{ 228 | { 229 | Name: "container-without-env", 230 | Image: "image", 231 | Env: []corev1.EnvVar{ 232 | { 233 | Name: "HTTP_PROXY", 234 | Value: "http://my.proxy.com", 235 | }, 236 | { 237 | Name: "NO_PROXY", 238 | Value: "http://my.local.com", 239 | }, 240 | }, 241 | }, 242 | { 243 | Name: "container-with-env", 244 | Image: "image", 245 | Env: []corev1.EnvVar{ 246 | { 247 | Name: "EXISTING", 248 | Value: "VALUE", 249 | }, 250 | { 251 | Name: "HTTP_PROXY", 252 | Value: "http://my.proxy.com", 253 | }, 254 | { 255 | Name: "NO_PROXY", 256 | Value: "http://my.local.com", 257 | }, 258 | }, 259 | }, 260 | }, 261 | }, 262 | } 263 | require.Equal(t, expectedPod, actualPod) 264 | }) 265 | 266 | it("sets the env vars on all containers on the pods that are annotated", func() { 267 | testPod.Annotations = map[string]string{ 268 | annotation: "some value", 269 | } 270 | 271 | bytes, err := json.Marshal(testPod) 272 | require.NoError(t, err) 273 | 274 | admissionRequest := &admissionv1.AdmissionRequest{ 275 | Name: "testAdmissionRequest", 276 | Object: runtime.RawExtension{ 277 | Raw: bytes, 278 | }, 279 | Operation: admissionv1.Create, 280 | Resource: metav1.GroupVersionResource{Version: "v1", Resource: "pods"}, 281 | } 282 | 283 | ac, err := certinjectionwebhook.NewAdmissionController( 284 | name, 285 | path, 286 | func(ctx context.Context) context.Context { return ctx }, 287 | []string{}, 288 | []string{annotation}, 289 | envVars, 290 | "", 291 | "", 292 | corev1.LocalObjectReference{}, 293 | ) 294 | require.NoError(t, err) 295 | 296 | response := ac.Admit(ctx, admissionRequest) 297 | wtesting.ExpectAllowed(t, response) 298 | 299 | patch, err := jp.DecodePatch(response.Patch) 300 | require.NoError(t, err) 301 | 302 | buf, err := patch.Apply(bytes) 303 | require.NoError(t, err) 304 | 305 | var actualPod corev1.Pod 306 | err = json.Unmarshal(buf, &actualPod) 307 | require.NoError(t, err) 308 | 309 | expectedPod := corev1.Pod{ 310 | ObjectMeta: metav1.ObjectMeta{ 311 | Name: "object-meta", 312 | Annotations: map[string]string{ 313 | annotation: "some value", 314 | }, 315 | }, 316 | Spec: corev1.PodSpec{ 317 | InitContainers: []corev1.Container{ 318 | { 319 | Name: "init-container-without-env", 320 | Image: "image", 321 | Env: []corev1.EnvVar{ 322 | { 323 | Name: "HTTP_PROXY", 324 | Value: "http://my.proxy.com", 325 | }, 326 | { 327 | Name: "NO_PROXY", 328 | Value: "http://my.local.com", 329 | }, 330 | }, 331 | }, 332 | { 333 | Name: "init-container-with-env", 334 | Image: "image", 335 | Env: []corev1.EnvVar{ 336 | { 337 | Name: "EXISTING", 338 | Value: "VALUE", 339 | }, 340 | { 341 | Name: "HTTP_PROXY", 342 | Value: "http://my.proxy.com", 343 | }, 344 | { 345 | Name: "NO_PROXY", 346 | Value: "http://my.local.com", 347 | }, 348 | }, 349 | }, 350 | }, 351 | Containers: []corev1.Container{ 352 | { 353 | Name: "container-without-env", 354 | Image: "image", 355 | Env: []corev1.EnvVar{ 356 | { 357 | Name: "HTTP_PROXY", 358 | Value: "http://my.proxy.com", 359 | }, 360 | { 361 | Name: "NO_PROXY", 362 | Value: "http://my.local.com", 363 | }, 364 | }, 365 | }, 366 | { 367 | Name: "container-with-env", 368 | Image: "image", 369 | Env: []corev1.EnvVar{ 370 | { 371 | Name: "EXISTING", 372 | Value: "VALUE", 373 | }, 374 | { 375 | Name: "HTTP_PROXY", 376 | Value: "http://my.proxy.com", 377 | }, 378 | { 379 | Name: "NO_PROXY", 380 | Value: "http://my.local.com", 381 | }, 382 | }, 383 | }, 384 | }, 385 | }, 386 | } 387 | require.Equal(t, expectedPod, actualPod) 388 | }) 389 | 390 | it("sets the ca certs on all containers on the pods that are labelled", func() { 391 | testPod.Labels = map[string]string{ 392 | label: "some value", 393 | } 394 | 395 | bytes, err := json.Marshal(testPod) 396 | require.NoError(t, err) 397 | 398 | admissionRequest := &admissionv1.AdmissionRequest{ 399 | Name: "testAdmissionRequest", 400 | Object: runtime.RawExtension{ 401 | Raw: bytes, 402 | }, 403 | Operation: admissionv1.Create, 404 | Resource: metav1.GroupVersionResource{Version: "v1", Resource: "pods"}, 405 | } 406 | 407 | ac, err := certinjectionwebhook.NewAdmissionController( 408 | name, 409 | path, 410 | func(ctx context.Context) context.Context { return ctx }, 411 | []string{label}, 412 | []string{}, 413 | []corev1.EnvVar{}, 414 | setupCACertsImage, 415 | caCertsData, 416 | corev1.LocalObjectReference{}, 417 | ) 418 | require.NoError(t, err) 419 | 420 | response := ac.Admit(ctx, admissionRequest) 421 | wtesting.ExpectAllowed(t, response) 422 | 423 | var actualPatch []jsonpatch.JsonPatchOperation 424 | err = json.Unmarshal(response.Patch, &actualPatch) 425 | require.NoError(t, err) 426 | 427 | expectedJSON := `[ 428 | { 429 | "op": "add", 430 | "path": "/spec/volumes", 431 | "value": [ 432 | { 433 | "emptyDir": {}, 434 | "name": "ca-certs" 435 | } 436 | ] 437 | }, 438 | { 439 | "op": "add", 440 | "path": "/spec/initContainers/2", 441 | "value": { 442 | "env": [ 443 | { 444 | "name": "EXISTING", 445 | "value": "VALUE" 446 | } 447 | ], 448 | "image": "image", 449 | "name": "init-container-with-env", 450 | "resources": {}, 451 | "volumeMounts": [ 452 | { 453 | "mountPath": "/etc/ssl/certs", 454 | "name": "ca-certs", 455 | "readOnly": true 456 | } 457 | ] 458 | } 459 | }, 460 | { 461 | "op": "add", 462 | "path": "/spec/initContainers/0/env", 463 | "value": [ 464 | { 465 | "name": "CA_CERTS_DATA_0", 466 | "value": "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----\n" 467 | } 468 | ] 469 | }, 470 | { 471 | "op": "add", 472 | "path": "/spec/initContainers/0/volumeMounts", 473 | "value": [ 474 | { 475 | "mountPath": "/workspace", 476 | "name": "ca-certs" 477 | } 478 | ] 479 | }, 480 | { 481 | "op": "add", 482 | "path": "/spec/initContainers/0/imagePullPolicy", 483 | "value": "IfNotPresent" 484 | }, 485 | { 486 | "op": "add", 487 | "path": "/spec/initContainers/0/securityContext", 488 | "value": { 489 | "allowPrivilegeEscalation": false, 490 | "capabilities": {"drop": ["ALL"]}, 491 | "privileged": false, 492 | "runAsNonRoot": true, 493 | "seccompProfile": {"type": "RuntimeDefault"} 494 | } 495 | }, 496 | { 497 | "op": "replace", 498 | "path": "/spec/initContainers/0/name", 499 | "value": "setup-ca-certs" 500 | }, 501 | { 502 | "op": "replace", 503 | "path": "/spec/initContainers/0/image", 504 | "value": "some-ca-certs-image" 505 | }, 506 | { 507 | "op": "add", 508 | "path": "/spec/initContainers/0/workingDir", 509 | "value": "/workspace" 510 | }, 511 | { 512 | "op": "replace", 513 | "path": "/spec/initContainers/1/name", 514 | "value": "init-container-without-env" 515 | }, 516 | { 517 | "op": "add", 518 | "path": "/spec/initContainers/1/volumeMounts", 519 | "value": [ 520 | { 521 | "mountPath": "/etc/ssl/certs", 522 | "name": "ca-certs", 523 | "readOnly": true 524 | } 525 | ] 526 | }, 527 | { 528 | "op": "remove", 529 | "path": "/spec/initContainers/1/env" 530 | }, 531 | { 532 | "op": "add", 533 | "path": "/spec/containers/0/volumeMounts", 534 | "value": [ 535 | { 536 | "mountPath": "/etc/ssl/certs", 537 | "name": "ca-certs", 538 | "readOnly": true 539 | } 540 | ] 541 | }, 542 | { 543 | "op": "add", 544 | "path": "/spec/containers/1/volumeMounts", 545 | "value": [ 546 | { 547 | "mountPath": "/etc/ssl/certs", 548 | "name": "ca-certs", 549 | "readOnly": true 550 | } 551 | ] 552 | } 553 | ]` 554 | var expectedPatch []jsonpatch.JsonPatchOperation 555 | err = json.Unmarshal([]byte(expectedJSON), &expectedPatch) 556 | require.NoError(t, err) 557 | 558 | assert.ElementsMatch(t, expectedPatch, actualPatch) 559 | }) 560 | 561 | it("does not inject ca certs on windows pods", func() { 562 | testPod.Labels = map[string]string{ 563 | label: "some value", 564 | } 565 | selectorMap := map[string]string{"kubernetes.io/os": "windows"} 566 | 567 | testPod.Spec.NodeSelector = selectorMap 568 | 569 | bytes, err := json.Marshal(testPod) 570 | require.NoError(t, err) 571 | 572 | admissionRequest := &admissionv1.AdmissionRequest{ 573 | Name: "testAdmissionRequest", 574 | Object: runtime.RawExtension{ 575 | Raw: bytes, 576 | }, 577 | Operation: admissionv1.Create, 578 | Resource: metav1.GroupVersionResource{Version: "v1", Resource: "pods"}, 579 | } 580 | 581 | ac, err := certinjectionwebhook.NewAdmissionController( 582 | name, 583 | path, 584 | func(ctx context.Context) context.Context { return ctx }, 585 | []string{label}, 586 | []string{}, 587 | []corev1.EnvVar{}, 588 | setupCACertsImage, 589 | caCertsData, 590 | corev1.LocalObjectReference{}, 591 | ) 592 | require.NoError(t, err) 593 | 594 | response := ac.Admit(ctx, admissionRequest) 595 | wtesting.ExpectAllowed(t, response) 596 | 597 | require.Nil(t, response.Patch) 598 | require.NoError(t, err) 599 | 600 | require.Equal(t, true, response.Allowed) 601 | 602 | }) 603 | 604 | it("sets the ca certs on all containers on the pods that are annotated", func() { 605 | testPod.Annotations = map[string]string{ 606 | annotation: "some value", 607 | } 608 | 609 | bytes, err := json.Marshal(testPod) 610 | require.NoError(t, err) 611 | 612 | admissionRequest := &admissionv1.AdmissionRequest{ 613 | Name: "testAdmissionRequest", 614 | Object: runtime.RawExtension{ 615 | Raw: bytes, 616 | }, 617 | Operation: admissionv1.Create, 618 | Resource: metav1.GroupVersionResource{Version: "v1", Resource: "pods"}, 619 | } 620 | 621 | ac, err := certinjectionwebhook.NewAdmissionController( 622 | name, 623 | path, 624 | func(ctx context.Context) context.Context { return ctx }, 625 | []string{}, 626 | []string{annotation}, 627 | []corev1.EnvVar{}, 628 | setupCACertsImage, 629 | caCertsData, 630 | corev1.LocalObjectReference{}, 631 | ) 632 | require.NoError(t, err) 633 | 634 | response := ac.Admit(ctx, admissionRequest) 635 | wtesting.ExpectAllowed(t, response) 636 | 637 | var actualPatch []jsonpatch.JsonPatchOperation 638 | err = json.Unmarshal(response.Patch, &actualPatch) 639 | require.NoError(t, err) 640 | 641 | expectedJSON := `[ 642 | { 643 | "op": "add", 644 | "path": "/spec/volumes", 645 | "value": [ 646 | { 647 | "emptyDir": {}, 648 | "name": "ca-certs" 649 | } 650 | ] 651 | }, 652 | { 653 | "op": "add", 654 | "path": "/spec/initContainers/2", 655 | "value": { 656 | "env": [ 657 | { 658 | "name": "EXISTING", 659 | "value": "VALUE" 660 | } 661 | ], 662 | "image": "image", 663 | "name": "init-container-with-env", 664 | "resources": {}, 665 | "volumeMounts": [ 666 | { 667 | "mountPath": "/etc/ssl/certs", 668 | "name": "ca-certs", 669 | "readOnly": true 670 | } 671 | ] 672 | } 673 | }, 674 | { 675 | "op": "add", 676 | "path": "/spec/initContainers/0/env", 677 | "value": [ 678 | { 679 | "name": "CA_CERTS_DATA_0", 680 | "value": "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----\n" 681 | } 682 | ] 683 | }, 684 | { 685 | "op": "add", 686 | "path": "/spec/initContainers/0/volumeMounts", 687 | "value": [ 688 | { 689 | "mountPath": "/workspace", 690 | "name": "ca-certs" 691 | } 692 | ] 693 | }, 694 | { 695 | "op": "add", 696 | "path": "/spec/initContainers/0/imagePullPolicy", 697 | "value": "IfNotPresent" 698 | }, 699 | { 700 | "op": "add", 701 | "path": "/spec/initContainers/0/securityContext", 702 | "value": { 703 | "allowPrivilegeEscalation": false, 704 | "capabilities": {"drop": ["ALL"]}, 705 | "privileged": false, 706 | "runAsNonRoot": true, 707 | "seccompProfile": {"type": "RuntimeDefault"} 708 | } 709 | }, 710 | { 711 | "op": "replace", 712 | "path": "/spec/initContainers/0/name", 713 | "value": "setup-ca-certs" 714 | }, 715 | { 716 | "op": "replace", 717 | "path": "/spec/initContainers/0/image", 718 | "value": "some-ca-certs-image" 719 | }, 720 | { 721 | "op": "add", 722 | "path": "/spec/initContainers/0/workingDir", 723 | "value": "/workspace" 724 | }, 725 | { 726 | "op": "add", 727 | "path": "/spec/initContainers/1/volumeMounts", 728 | "value": [ 729 | { 730 | "mountPath": "/etc/ssl/certs", 731 | "name": "ca-certs", 732 | "readOnly": true 733 | } 734 | ] 735 | }, 736 | { 737 | "op": "replace", 738 | "path": "/spec/initContainers/1/name", 739 | "value": "init-container-without-env" 740 | }, 741 | { 742 | "op": "remove", 743 | "path": "/spec/initContainers/1/env" 744 | }, 745 | { 746 | "op": "add", 747 | "path": "/spec/containers/0/volumeMounts", 748 | "value": [ 749 | { 750 | "mountPath": "/etc/ssl/certs", 751 | "name": "ca-certs", 752 | "readOnly": true 753 | } 754 | ] 755 | }, 756 | { 757 | "op": "add", 758 | "path": "/spec/containers/1/volumeMounts", 759 | "value": [ 760 | { 761 | "mountPath": "/etc/ssl/certs", 762 | "name": "ca-certs", 763 | "readOnly": true 764 | } 765 | ] 766 | } 767 | ] 768 | ` 769 | 770 | var expectedPatch []jsonpatch.JsonPatchOperation 771 | err = json.Unmarshal([]byte(expectedJSON), &expectedPatch) 772 | require.NoError(t, err) 773 | 774 | assert.ElementsMatch(t, expectedPatch, actualPatch) 775 | }) 776 | 777 | it("applies both env and certs changes for custom labels", func() { 778 | testPod.Labels = map[string]string{ 779 | label: "some value", 780 | } 781 | 782 | bytes, err := json.Marshal(testPod) 783 | require.NoError(t, err) 784 | 785 | admissionRequest := &admissionv1.AdmissionRequest{ 786 | Name: "testAdmissionRequest", 787 | Object: runtime.RawExtension{ 788 | Raw: bytes, 789 | }, 790 | Operation: admissionv1.Create, 791 | Resource: metav1.GroupVersionResource{Version: "v1", Resource: "pods"}, 792 | } 793 | 794 | ac, err := certinjectionwebhook.NewAdmissionController( 795 | name, 796 | path, 797 | func(ctx context.Context) context.Context { return ctx }, 798 | []string{label}, 799 | []string{}, 800 | envVars, 801 | setupCACertsImage, 802 | caCertsData, 803 | corev1.LocalObjectReference{}, 804 | ) 805 | require.NoError(t, err) 806 | 807 | response := ac.Admit(ctx, admissionRequest) 808 | wtesting.ExpectAllowed(t, response) 809 | 810 | var actualPatch []jsonpatch.JsonPatchOperation 811 | err = json.Unmarshal(response.Patch, &actualPatch) 812 | require.NoError(t, err) 813 | 814 | expectedJSON := `[ 815 | { 816 | "op": "add", 817 | "path": "/spec/volumes", 818 | "value": [ 819 | { 820 | "emptyDir": {}, 821 | "name": "ca-certs" 822 | } 823 | ] 824 | }, 825 | { 826 | "op": "add", 827 | "path": "/spec/initContainers/2", 828 | "value": { 829 | "env": [ 830 | { 831 | "name": "EXISTING", 832 | "value": "VALUE" 833 | }, 834 | { 835 | "name": "HTTP_PROXY", 836 | "value": "http://my.proxy.com" 837 | }, 838 | { 839 | "name": "NO_PROXY", 840 | "value": "http://my.local.com" 841 | } 842 | ], 843 | "image": "image", 844 | "name": "init-container-with-env", 845 | "resources": {}, 846 | "volumeMounts": [ 847 | { 848 | "mountPath": "/etc/ssl/certs", 849 | "name": "ca-certs", 850 | "readOnly": true 851 | } 852 | ] 853 | } 854 | }, 855 | { 856 | "op": "add", 857 | "path": "/spec/initContainers/0/env", 858 | "value": [ 859 | { 860 | "name": "CA_CERTS_DATA_0", 861 | "value": "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----\n" 862 | } 863 | ] 864 | }, 865 | { 866 | "op": "add", 867 | "path": "/spec/initContainers/0/volumeMounts", 868 | "value": [ 869 | { 870 | "mountPath": "/workspace", 871 | "name": "ca-certs" 872 | } 873 | ] 874 | }, 875 | { 876 | "op": "add", 877 | "path": "/spec/initContainers/0/imagePullPolicy", 878 | "value": "IfNotPresent" 879 | }, 880 | { 881 | "op": "add", 882 | "path": "/spec/initContainers/0/securityContext", 883 | "value": { 884 | "allowPrivilegeEscalation": false, 885 | "capabilities": {"drop": ["ALL"]}, 886 | "privileged": false, 887 | "runAsNonRoot": true, 888 | "seccompProfile": {"type": "RuntimeDefault"} 889 | } 890 | }, 891 | { 892 | "op": "replace", 893 | "path": "/spec/initContainers/0/name", 894 | "value": "setup-ca-certs" 895 | }, 896 | { 897 | "op": "replace", 898 | "path": "/spec/initContainers/0/image", 899 | "value": "some-ca-certs-image" 900 | }, 901 | { 902 | "op": "add", 903 | "path": "/spec/initContainers/0/workingDir", 904 | "value": "/workspace" 905 | }, 906 | { 907 | "op": "replace", 908 | "path": "/spec/initContainers/1/name", 909 | "value": "init-container-without-env" 910 | }, 911 | { 912 | "op": "add", 913 | "path": "/spec/initContainers/1/env/1", 914 | "value": { 915 | "name": "NO_PROXY", 916 | "value": "http://my.local.com" 917 | } 918 | }, 919 | { 920 | "op": "replace", 921 | "path": "/spec/initContainers/1/env/0/name", 922 | "value": "HTTP_PROXY" 923 | }, 924 | { 925 | "op": "replace", 926 | "path": "/spec/initContainers/1/env/0/value", 927 | "value": "http://my.proxy.com" 928 | }, 929 | { 930 | "op": "add", 931 | "path": "/spec/initContainers/1/volumeMounts", 932 | "value": [ 933 | { 934 | "mountPath": "/etc/ssl/certs", 935 | "name": "ca-certs", 936 | "readOnly": true 937 | } 938 | ] 939 | }, 940 | { 941 | "op": "add", 942 | "path": "/spec/containers/0/env", 943 | "value": [ 944 | { 945 | "name": "HTTP_PROXY", 946 | "value": "http://my.proxy.com" 947 | }, 948 | { 949 | "name": "NO_PROXY", 950 | "value": "http://my.local.com" 951 | } 952 | ] 953 | }, 954 | { 955 | "op": "add", 956 | "path": "/spec/containers/0/volumeMounts", 957 | "value": [ 958 | { 959 | "mountPath": "/etc/ssl/certs", 960 | "name": "ca-certs", 961 | "readOnly": true 962 | } 963 | ] 964 | }, 965 | { 966 | "op": "add", 967 | "path": "/spec/containers/1/env/2", 968 | "value": { 969 | "name": "NO_PROXY", 970 | "value": "http://my.local.com" 971 | } 972 | }, 973 | { 974 | "op": "add", 975 | "path": "/spec/containers/1/env/1", 976 | "value": { 977 | "name": "HTTP_PROXY", 978 | "value": "http://my.proxy.com" 979 | } 980 | }, 981 | { 982 | "op": "add", 983 | "path": "/spec/containers/1/volumeMounts", 984 | "value": [ 985 | { 986 | "mountPath": "/etc/ssl/certs", 987 | "name": "ca-certs", 988 | "readOnly": true 989 | } 990 | ] 991 | } 992 | ]` 993 | var expectedPatch []jsonpatch.JsonPatchOperation 994 | err = json.Unmarshal([]byte(expectedJSON), &expectedPatch) 995 | require.NoError(t, err) 996 | 997 | assert.ElementsMatch(t, expectedPatch, actualPatch) 998 | }) 999 | 1000 | it("applies both env and certs changes for custom annotations", func() { 1001 | testPod.Annotations = map[string]string{ 1002 | annotation: "some value", 1003 | } 1004 | 1005 | bytes, err := json.Marshal(testPod) 1006 | require.NoError(t, err) 1007 | 1008 | admissionRequest := &admissionv1.AdmissionRequest{ 1009 | Name: "testAdmissionRequest", 1010 | Object: runtime.RawExtension{ 1011 | Raw: bytes, 1012 | }, 1013 | Operation: admissionv1.Create, 1014 | Resource: metav1.GroupVersionResource{Version: "v1", Resource: "pods"}, 1015 | } 1016 | 1017 | ac, err := certinjectionwebhook.NewAdmissionController( 1018 | name, 1019 | path, 1020 | func(ctx context.Context) context.Context { return ctx }, 1021 | []string{}, 1022 | []string{annotation}, 1023 | envVars, 1024 | setupCACertsImage, 1025 | caCertsData, 1026 | corev1.LocalObjectReference{}, 1027 | ) 1028 | require.NoError(t, err) 1029 | 1030 | response := ac.Admit(ctx, admissionRequest) 1031 | wtesting.ExpectAllowed(t, response) 1032 | 1033 | var actualPatch []jsonpatch.JsonPatchOperation 1034 | err = json.Unmarshal(response.Patch, &actualPatch) 1035 | require.NoError(t, err) 1036 | 1037 | expectedJSON := `[ 1038 | { 1039 | "op": "add", 1040 | "path": "/spec/volumes", 1041 | "value": [ 1042 | { 1043 | "emptyDir": {}, 1044 | "name": "ca-certs" 1045 | } 1046 | ] 1047 | }, 1048 | { 1049 | "op": "add", 1050 | "path": "/spec/initContainers/2", 1051 | "value": { 1052 | "env": [ 1053 | { 1054 | "name": "EXISTING", 1055 | "value": "VALUE" 1056 | }, 1057 | { 1058 | "name": "HTTP_PROXY", 1059 | "value": "http://my.proxy.com" 1060 | }, 1061 | { 1062 | "name": "NO_PROXY", 1063 | "value": "http://my.local.com" 1064 | } 1065 | ], 1066 | "image": "image", 1067 | "name": "init-container-with-env", 1068 | "resources": {}, 1069 | "volumeMounts": [ 1070 | { 1071 | "mountPath": "/etc/ssl/certs", 1072 | "name": "ca-certs", 1073 | "readOnly": true 1074 | } 1075 | ] 1076 | } 1077 | }, 1078 | { 1079 | "op": "add", 1080 | "path": "/spec/initContainers/0/imagePullPolicy", 1081 | "value": "IfNotPresent" 1082 | }, 1083 | { 1084 | "op": "add", 1085 | "path": "/spec/initContainers/0/securityContext", 1086 | "value": { 1087 | "allowPrivilegeEscalation": false, 1088 | "capabilities": {"drop": ["ALL"]}, 1089 | "privileged": false, 1090 | "runAsNonRoot": true, 1091 | "seccompProfile": {"type": "RuntimeDefault"} 1092 | } 1093 | }, 1094 | { 1095 | "op": "replace", 1096 | "path": "/spec/initContainers/0/name", 1097 | "value": "setup-ca-certs" 1098 | }, 1099 | { 1100 | "op": "replace", 1101 | "path": "/spec/initContainers/0/image", 1102 | "value": "some-ca-certs-image" 1103 | }, 1104 | { 1105 | "op": "add", 1106 | "path": "/spec/initContainers/0/workingDir", 1107 | "value": "/workspace" 1108 | }, 1109 | { 1110 | "op": "add", 1111 | "path": "/spec/initContainers/0/env", 1112 | "value": [ 1113 | { 1114 | "name": "CA_CERTS_DATA_0", 1115 | "value": "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----\n" 1116 | } 1117 | ] 1118 | }, 1119 | { 1120 | "op": "add", 1121 | "path": "/spec/initContainers/0/volumeMounts", 1122 | "value": [ 1123 | { 1124 | "mountPath": "/workspace", 1125 | "name": "ca-certs" 1126 | } 1127 | ] 1128 | }, 1129 | { 1130 | "op": "replace", 1131 | "path": "/spec/initContainers/1/name", 1132 | "value": "init-container-without-env" 1133 | }, 1134 | { 1135 | "op": "add", 1136 | "path": "/spec/initContainers/1/env/1", 1137 | "value": { 1138 | "name": "NO_PROXY", 1139 | "value": "http://my.local.com" 1140 | } 1141 | }, 1142 | { 1143 | "op": "replace", 1144 | "path": "/spec/initContainers/1/env/0/name", 1145 | "value": "HTTP_PROXY" 1146 | }, 1147 | { 1148 | "op": "replace", 1149 | "path": "/spec/initContainers/1/env/0/value", 1150 | "value": "http://my.proxy.com" 1151 | }, 1152 | { 1153 | "op": "add", 1154 | "path": "/spec/initContainers/1/volumeMounts", 1155 | "value": [ 1156 | { 1157 | "mountPath": "/etc/ssl/certs", 1158 | "name": "ca-certs", 1159 | "readOnly": true 1160 | } 1161 | ] 1162 | }, 1163 | { 1164 | "op": "add", 1165 | "path": "/spec/containers/0/env", 1166 | "value": [ 1167 | { 1168 | "name": "HTTP_PROXY", 1169 | "value": "http://my.proxy.com" 1170 | }, 1171 | { 1172 | "name": "NO_PROXY", 1173 | "value": "http://my.local.com" 1174 | } 1175 | ] 1176 | }, 1177 | { 1178 | "op": "add", 1179 | "path": "/spec/containers/0/volumeMounts", 1180 | "value": [ 1181 | { 1182 | "mountPath": "/etc/ssl/certs", 1183 | "name": "ca-certs", 1184 | "readOnly": true 1185 | } 1186 | ] 1187 | }, 1188 | { 1189 | "op": "add", 1190 | "path": "/spec/containers/1/env/2", 1191 | "value": { 1192 | "name": "NO_PROXY", 1193 | "value": "http://my.local.com" 1194 | } 1195 | }, 1196 | { 1197 | "op": "add", 1198 | "path": "/spec/containers/1/env/1", 1199 | "value": { 1200 | "name": "HTTP_PROXY", 1201 | "value": "http://my.proxy.com" 1202 | } 1203 | }, 1204 | { 1205 | "op": "add", 1206 | "path": "/spec/containers/1/volumeMounts", 1207 | "value": [ 1208 | { 1209 | "mountPath": "/etc/ssl/certs", 1210 | "name": "ca-certs", 1211 | "readOnly": true 1212 | } 1213 | ] 1214 | } 1215 | ] ` 1216 | var expectedPatch []jsonpatch.JsonPatchOperation 1217 | err = json.Unmarshal([]byte(expectedJSON), &expectedPatch) 1218 | require.NoError(t, err) 1219 | 1220 | assert.ElementsMatch(t, expectedPatch, actualPatch) 1221 | }) 1222 | 1223 | it("only patches pods", func() { 1224 | testPod.Annotations = map[string]string{ 1225 | annotation: "some value", 1226 | } 1227 | 1228 | bytes, err := json.Marshal(testPod) 1229 | require.NoError(t, err) 1230 | 1231 | admissionRequest := &admissionv1.AdmissionRequest{ 1232 | Name: "testAdmissionRequest", 1233 | Object: runtime.RawExtension{ 1234 | Raw: bytes, 1235 | }, 1236 | Operation: admissionv1.Create, 1237 | Resource: metav1.GroupVersionResource{Version: "v1", Resource: "containers"}, 1238 | } 1239 | 1240 | ac, err := certinjectionwebhook.NewAdmissionController( 1241 | name, 1242 | path, 1243 | func(ctx context.Context) context.Context { return ctx }, 1244 | []string{}, 1245 | []string{annotation}, 1246 | envVars, 1247 | setupCACertsImage, 1248 | caCertsData, 1249 | corev1.LocalObjectReference{}, 1250 | ) 1251 | require.NoError(t, err) 1252 | 1253 | response := ac.Admit(ctx, admissionRequest) 1254 | wtesting.ExpectAllowed(t, response) 1255 | require.Nil(t, response.Patch) 1256 | }) 1257 | 1258 | it("only processes pods marked with the configured label or configured service annotation by default", func() { 1259 | testPod.Labels = nil 1260 | testPod.Annotations = nil 1261 | 1262 | bytes, err := json.Marshal(testPod) 1263 | require.NoError(t, err) 1264 | 1265 | admissionRequest := &admissionv1.AdmissionRequest{ 1266 | Name: "testAdmissionRequest", 1267 | Object: runtime.RawExtension{ 1268 | Raw: bytes, 1269 | }, 1270 | Operation: admissionv1.Create, 1271 | Resource: metav1.GroupVersionResource{Version: "v1", Resource: "pods"}, 1272 | } 1273 | 1274 | ac, err := certinjectionwebhook.NewAdmissionController( 1275 | name, 1276 | path, 1277 | func(ctx context.Context) context.Context { return ctx }, 1278 | []string{label}, 1279 | []string{annotation}, 1280 | envVars, 1281 | setupCACertsImage, 1282 | caCertsData, 1283 | corev1.LocalObjectReference{}, 1284 | ) 1285 | require.NoError(t, err) 1286 | 1287 | response := ac.Admit(ctx, admissionRequest) 1288 | wtesting.ExpectAllowed(t, response) 1289 | require.Nil(t, response.Patch) 1290 | }) 1291 | 1292 | it("sets the registry credentials on all containers on the pods that have the registry env vars", func() { 1293 | testPod.Labels = map[string]string{ 1294 | label: "some value", 1295 | } 1296 | 1297 | bytes, err := json.Marshal(testPod) 1298 | require.NoError(t, err) 1299 | 1300 | admissionRequest := &admissionv1.AdmissionRequest{ 1301 | Name: "testAdmissionRequest", 1302 | Object: runtime.RawExtension{ 1303 | Raw: bytes, 1304 | }, 1305 | Operation: admissionv1.Create, 1306 | Resource: metav1.GroupVersionResource{Version: "v1", Resource: "pods"}, 1307 | } 1308 | 1309 | ac, err := certinjectionwebhook.NewAdmissionController( 1310 | name, 1311 | path, 1312 | func(ctx context.Context) context.Context { return ctx }, 1313 | []string{label}, 1314 | []string{}, 1315 | []corev1.EnvVar{}, 1316 | setupCACertsImage, 1317 | caCertsData, 1318 | corev1.LocalObjectReference{ 1319 | Name: "system-registry-credentials", 1320 | }, 1321 | ) 1322 | require.NoError(t, err) 1323 | 1324 | response := ac.Admit(ctx, admissionRequest) 1325 | wtesting.ExpectAllowed(t, response) 1326 | 1327 | var actualPatch []jsonpatch.JsonPatchOperation 1328 | err = json.Unmarshal(response.Patch, &actualPatch) 1329 | require.NoError(t, err) 1330 | 1331 | expectedJSON := "[{\"op\":\"add\",\"path\":\"/spec/volumes\",\"value\":[{\"emptyDir\":{},\"name\":\"ca-certs\"}]},{\"op\":\"add\",\"path\":\"/spec/imagePullSecrets\",\"value\":[{\"name\":\"system-registry-credentials\"}]},{\"op\":\"add\",\"path\":\"/spec/initContainers/2\",\"value\":{\"env\":[{\"name\":\"EXISTING\",\"value\":\"VALUE\"}],\"image\":\"image\",\"name\":\"init-container-with-env\",\"resources\":{},\"volumeMounts\":[{\"mountPath\":\"/etc/ssl/certs\",\"name\":\"ca-certs\",\"readOnly\":true}]}},{\"op\":\"add\",\"path\":\"/spec/initContainers/0/env\",\"value\":[{\"name\":\"CA_CERTS_DATA_0\",\"value\":\"-----BEGIN CERTIFICATE-----\\n-----END CERTIFICATE-----\\n\"}]},{\"op\":\"add\",\"path\":\"/spec/initContainers/0/volumeMounts\",\"value\":[{\"mountPath\":\"/workspace\",\"name\":\"ca-certs\"}]},{\"op\":\"add\",\"path\":\"/spec/initContainers/0/imagePullPolicy\",\"value\":\"IfNotPresent\"},{\"op\": \"add\", \"path\": \"/spec/initContainers/0/securityContext\", \"value\": {\"allowPrivilegeEscalation\": false, \"capabilities\": {\"drop\": [\"ALL\"]}, \"privileged\": false, \"runAsNonRoot\": true, \"seccompProfile\": {\"type\": \"RuntimeDefault\"}}},{\"op\":\"replace\",\"path\":\"/spec/initContainers/0/name\",\"value\":\"setup-ca-certs\"},{\"op\":\"replace\",\"path\":\"/spec/initContainers/0/image\",\"value\":\"some-ca-certs-image\"},{\"op\":\"add\",\"path\":\"/spec/initContainers/0/workingDir\",\"value\":\"/workspace\"},{\"op\":\"replace\",\"path\":\"/spec/initContainers/1/name\",\"value\":\"init-container-without-env\"},{\"op\":\"add\",\"path\":\"/spec/initContainers/1/volumeMounts\",\"value\":[{\"mountPath\":\"/etc/ssl/certs\",\"name\":\"ca-certs\",\"readOnly\":true}]},{\"op\":\"remove\",\"path\":\"/spec/initContainers/1/env\"},{\"op\":\"add\",\"path\":\"/spec/containers/0/volumeMounts\",\"value\":[{\"mountPath\":\"/etc/ssl/certs\",\"name\":\"ca-certs\",\"readOnly\":true}]},{\"op\":\"add\",\"path\":\"/spec/containers/1/volumeMounts\",\"value\":[{\"mountPath\":\"/etc/ssl/certs\",\"name\":\"ca-certs\",\"readOnly\":true}]}]" 1332 | var expectedPatch []jsonpatch.JsonPatchOperation 1333 | err = json.Unmarshal([]byte(expectedJSON), &expectedPatch) 1334 | require.NoError(t, err) 1335 | 1336 | assert.ElementsMatch(t, expectedPatch, actualPatch) 1337 | }) 1338 | 1339 | it("sets the registry credentials on all containers on the pods that have the registry credentials env", func() { 1340 | testPod.Annotations = map[string]string{ 1341 | annotation: "some value", 1342 | } 1343 | testPod.Spec.ImagePullSecrets = []corev1.LocalObjectReference{ 1344 | {Name: "app-registry-credentials"}, 1345 | } 1346 | 1347 | bytes, err := json.Marshal(testPod) 1348 | require.NoError(t, err) 1349 | 1350 | admissionRequest := &admissionv1.AdmissionRequest{ 1351 | Name: "testAdmissionRequest", 1352 | Object: runtime.RawExtension{ 1353 | Raw: bytes, 1354 | }, 1355 | Operation: admissionv1.Create, 1356 | Resource: metav1.GroupVersionResource{Version: "v1", Resource: "pods"}, 1357 | } 1358 | 1359 | ac, err := certinjectionwebhook.NewAdmissionController( 1360 | name, 1361 | path, 1362 | func(ctx context.Context) context.Context { return ctx }, 1363 | []string{}, 1364 | []string{annotation}, 1365 | []corev1.EnvVar{}, 1366 | setupCACertsImage, 1367 | caCertsData, 1368 | corev1.LocalObjectReference{ 1369 | Name: "system-registry-credentials", 1370 | }, 1371 | ) 1372 | require.NoError(t, err) 1373 | 1374 | response := ac.Admit(ctx, admissionRequest) 1375 | wtesting.ExpectAllowed(t, response) 1376 | 1377 | var actualPatch []jsonpatch.JsonPatchOperation 1378 | err = json.Unmarshal(response.Patch, &actualPatch) 1379 | require.NoError(t, err) 1380 | 1381 | expectedJSON := "[{\"op\":\"add\",\"path\":\"/spec/volumes\",\"value\":[{\"emptyDir\":{},\"name\":\"ca-certs\"}]},{\"op\":\"add\",\"path\":\"/spec/imagePullSecrets/1\",\"value\":{\"name\":\"system-registry-credentials\"}},{\"op\":\"add\",\"path\":\"/spec/initContainers/2\",\"value\":{\"env\":[{\"name\":\"EXISTING\",\"value\":\"VALUE\"}],\"image\":\"image\",\"name\":\"init-container-with-env\",\"resources\":{},\"volumeMounts\":[{\"mountPath\":\"/etc/ssl/certs\",\"name\":\"ca-certs\",\"readOnly\":true}]}},{\"op\":\"add\",\"path\":\"/spec/initContainers/0/env\",\"value\":[{\"name\":\"CA_CERTS_DATA_0\",\"value\":\"-----BEGIN CERTIFICATE-----\\n-----END CERTIFICATE-----\\n\"}]},{\"op\":\"add\",\"path\":\"/spec/initContainers/0/volumeMounts\",\"value\":[{\"mountPath\":\"/workspace\",\"name\":\"ca-certs\"}]},{\"op\":\"add\",\"path\":\"/spec/initContainers/0/imagePullPolicy\",\"value\":\"IfNotPresent\"},{\"op\": \"add\", \"path\": \"/spec/initContainers/0/securityContext\", \"value\": {\"allowPrivilegeEscalation\": false, \"capabilities\": {\"drop\": [\"ALL\"]}, \"privileged\": false, \"runAsNonRoot\": true, \"seccompProfile\": {\"type\": \"RuntimeDefault\"}}},{\"op\":\"replace\",\"path\":\"/spec/initContainers/0/name\",\"value\":\"setup-ca-certs\"},{\"op\":\"replace\",\"path\":\"/spec/initContainers/0/image\",\"value\":\"some-ca-certs-image\"},{\"op\":\"add\",\"path\":\"/spec/initContainers/0/workingDir\",\"value\":\"/workspace\"},{\"op\":\"replace\",\"path\":\"/spec/initContainers/1/name\",\"value\":\"init-container-without-env\"},{\"op\":\"add\",\"path\":\"/spec/initContainers/1/volumeMounts\",\"value\":[{\"mountPath\":\"/etc/ssl/certs\",\"name\":\"ca-certs\",\"readOnly\":true}]},{\"op\":\"remove\",\"path\":\"/spec/initContainers/1/env\"},{\"op\":\"add\",\"path\":\"/spec/containers/0/volumeMounts\",\"value\":[{\"mountPath\":\"/etc/ssl/certs\",\"name\":\"ca-certs\",\"readOnly\":true}]},{\"op\":\"add\",\"path\":\"/spec/containers/1/volumeMounts\",\"value\":[{\"mountPath\":\"/etc/ssl/certs\",\"name\":\"ca-certs\",\"readOnly\":true}]}]" 1382 | var expectedPatch []jsonpatch.JsonPatchOperation 1383 | err = json.Unmarshal([]byte(expectedJSON), &expectedPatch) 1384 | require.NoError(t, err) 1385 | 1386 | assert.ElementsMatch(t, expectedPatch, actualPatch) 1387 | }) 1388 | 1389 | }) 1390 | 1391 | it("#Path returns path", func() { 1392 | ac, err := certinjectionwebhook.NewAdmissionController(name, path, nil, []string{"label"}, nil, nil, "", "", corev1.LocalObjectReference{}) 1393 | require.NoError(t, err) 1394 | 1395 | require.Equal(t, ac.Path(), path) 1396 | }) 1397 | 1398 | } 1399 | -------------------------------------------------------------------------------- /pkg/certinjectionwebhook/reconciler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-Present VMware, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package certinjectionwebhook 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/client-go/kubernetes" 12 | admissionlisters "k8s.io/client-go/listers/admissionregistration/v1" 13 | corelisters "k8s.io/client-go/listers/core/v1" 14 | "knative.dev/pkg/kmp" 15 | "knative.dev/pkg/logging" 16 | "knative.dev/pkg/ptr" 17 | "knative.dev/pkg/system" 18 | certresources "knative.dev/pkg/webhook/certificates/resources" 19 | ) 20 | 21 | // Implements controller.Reconciler 22 | type reconciler struct { 23 | name string 24 | path string 25 | 26 | k8sClient kubernetes.Interface 27 | mwhlister admissionlisters.MutatingWebhookConfigurationLister 28 | secretlister corelisters.SecretLister 29 | 30 | secretName string 31 | } 32 | 33 | func NewReconciler(name string, 34 | path string, 35 | k8sClient kubernetes.Interface, 36 | mwhlister admissionlisters.MutatingWebhookConfigurationLister, 37 | secretlister corelisters.SecretLister, 38 | secretName string) *reconciler { 39 | return &reconciler{ 40 | name: name, 41 | path: path, 42 | k8sClient: k8sClient, 43 | mwhlister: mwhlister, 44 | secretlister: secretlister, 45 | secretName: secretName, 46 | } 47 | } 48 | 49 | func (r *reconciler) Reconcile(ctx context.Context, key string) error { 50 | logger := logging.FromContext(ctx) 51 | 52 | secret, err := r.secretlister.Secrets(system.Namespace()).Get(r.secretName) 53 | if err != nil { 54 | logger.Errorf("Error fetching secret: %v", err) 55 | return err 56 | } 57 | caCert, ok := secret.Data[certresources.CACert] 58 | if !ok { 59 | return fmt.Errorf("secret %q is missing %q key", r.secretName, certresources.CACert) 60 | } 61 | 62 | return r.reconcileMutatingWebhook(ctx, caCert) 63 | } 64 | 65 | func (r *reconciler) reconcileMutatingWebhook(ctx context.Context, caCert []byte) error { 66 | logger := logging.FromContext(ctx) 67 | 68 | configuredWebhook, err := r.mwhlister.Get(r.name) 69 | if err != nil { 70 | return fmt.Errorf("error retrieving webhook: %v", err) 71 | } 72 | 73 | webhook := configuredWebhook.DeepCopy() 74 | 75 | for i, wh := range webhook.Webhooks { 76 | if wh.Name != webhook.Name { 77 | continue 78 | } 79 | webhook.Webhooks[i].ClientConfig.CABundle = caCert 80 | if webhook.Webhooks[i].ClientConfig.Service == nil { 81 | return fmt.Errorf("missing service reference for webhook: %s", wh.Name) 82 | } 83 | webhook.Webhooks[i].ClientConfig.Service.Path = ptr.String(r.path) 84 | } 85 | 86 | if ok, err := kmp.SafeEqual(configuredWebhook, webhook); err != nil { 87 | return fmt.Errorf("error diffing webhooks: %v", err) 88 | } else if !ok { 89 | logger.Info("Updating webhook") 90 | mwhclient := r.k8sClient.AdmissionregistrationV1().MutatingWebhookConfigurations() 91 | if _, err := mwhclient.Update(ctx, webhook, metav1.UpdateOptions{}); err != nil { 92 | return fmt.Errorf("failed to update webhook: %v", err) 93 | } 94 | } else { 95 | logger.Info("Webhook is valid") 96 | } 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /pkg/certinjectionwebhook/reconciler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-Present VMware, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package certinjectionwebhook_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/pivotal/kpack/pkg/reconciler/testhelpers" 10 | "github.com/sclevine/spec" 11 | admissionregistrationv1 "k8s.io/api/admissionregistration/v1" 12 | corev1 "k8s.io/api/core/v1" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/runtime" 15 | k8sfake "k8s.io/client-go/kubernetes/fake" 16 | clientgotesting "k8s.io/client-go/testing" 17 | "k8s.io/client-go/tools/record" 18 | "knative.dev/pkg/controller" 19 | rtesting "knative.dev/pkg/reconciler/testing" 20 | "knative.dev/pkg/system" 21 | certresources "knative.dev/pkg/webhook/certificates/resources" 22 | wtesting "knative.dev/pkg/webhook/testing" 23 | 24 | "github.com/vmware-tanzu/cert-injection-webhook/pkg/certinjectionwebhook" 25 | ) 26 | 27 | func TestReconciler(t *testing.T) { 28 | spec.Run(t, "Reconciler", testReconciler) 29 | } 30 | 31 | func testReconciler(t *testing.T, when spec.G, it spec.S) { 32 | const ( 33 | name = "some-webhook" 34 | caSecretName = "some-secret" 35 | ) 36 | var ( 37 | path = "/some-path" 38 | certData = []byte("some-cert") 39 | ) 40 | 41 | when("#Reconcile", func() { 42 | rt := testhelpers.ReconcilerTester(t, 43 | func(t *testing.T, row *rtesting.TableRow) (reconciler controller.Reconciler, lists rtesting.ActionRecorderList, list rtesting.EventList) { 44 | listers := wtesting.NewListers(row.Objects) 45 | secretLister := listers.GetSecretLister() 46 | mwhcLister := listers.GetMutatingWebhookConfigurationLister() 47 | 48 | k8sfakeClient := k8sfake.NewSimpleClientset(listers.GetKubeObjects()...) 49 | 50 | eventRecorder := record.NewFakeRecorder(10) 51 | actionRecorderList := rtesting.ActionRecorderList{k8sfakeClient} 52 | eventList := rtesting.EventList{Recorder: eventRecorder} 53 | 54 | r := certinjectionwebhook.NewReconciler( 55 | name, 56 | path, 57 | k8sfakeClient, 58 | mwhcLister, 59 | secretLister, 60 | caSecretName, 61 | ) 62 | 63 | return r, actionRecorderList, eventList 64 | }) 65 | 66 | it("Updates the webhook with the ca cert secret", func() { 67 | caSecret := &corev1.Secret{ 68 | ObjectMeta: metav1.ObjectMeta{ 69 | Name: caSecretName, 70 | Namespace: system.Namespace(), 71 | }, 72 | Data: map[string][]byte{ 73 | certresources.CACert: certData, 74 | }, 75 | } 76 | 77 | webhookConfig := &admissionregistrationv1.MutatingWebhookConfiguration{ 78 | ObjectMeta: metav1.ObjectMeta{ 79 | Name: name, 80 | }, 81 | Webhooks: []admissionregistrationv1.MutatingWebhook{ 82 | { 83 | Name: name, 84 | ClientConfig: admissionregistrationv1.WebhookClientConfig{ 85 | Service: &admissionregistrationv1.ServiceReference{}, 86 | }, 87 | }, 88 | }, 89 | } 90 | 91 | rt.Test(rtesting.TableRow{ 92 | Key: "some-namespace/pod-webhook", 93 | Objects: []runtime.Object{ 94 | caSecret, 95 | webhookConfig, 96 | }, 97 | WantErr: false, 98 | WantUpdates: []clientgotesting.UpdateActionImpl{ 99 | { 100 | Object: &admissionregistrationv1.MutatingWebhookConfiguration{ 101 | ObjectMeta: metav1.ObjectMeta{ 102 | Name: name, 103 | }, 104 | Webhooks: []admissionregistrationv1.MutatingWebhook{ 105 | { 106 | Name: name, 107 | ClientConfig: admissionregistrationv1.WebhookClientConfig{ 108 | Service: &admissionregistrationv1.ServiceReference{ 109 | Path: &path, 110 | }, 111 | CABundle: certData, 112 | }, 113 | }, 114 | }, 115 | }, 116 | }, 117 | }, 118 | }) 119 | }) 120 | }) 121 | } 122 | -------------------------------------------------------------------------------- /pkg/certinjectionwebhook/webhook.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-Present VMware, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package certinjectionwebhook 5 | 6 | import ( 7 | "context" 8 | 9 | corev1 "k8s.io/api/core/v1" 10 | // Injection stuff 11 | kubeclient "knative.dev/pkg/client/injection/kube/client" 12 | mwhinformer "knative.dev/pkg/client/injection/kube/informers/admissionregistration/v1/mutatingwebhookconfiguration" 13 | secretinformer "knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret" 14 | 15 | "k8s.io/client-go/tools/cache" 16 | "knative.dev/pkg/controller" 17 | "knative.dev/pkg/logging" 18 | "knative.dev/pkg/system" 19 | "knative.dev/pkg/webhook" 20 | ) 21 | 22 | type Webhook struct { 23 | *reconciler 24 | *admissionController 25 | } 26 | 27 | func NewController( 28 | ctx context.Context, 29 | name, path string, 30 | wc func(context.Context) context.Context, 31 | labels []string, 32 | annotations []string, 33 | envVars []corev1.EnvVar, 34 | caCertsData, setupCaCertsImage string, 35 | imagePullSecrets corev1.LocalObjectReference, 36 | ) (*controller.Impl, error) { 37 | client := kubeclient.Get(ctx) 38 | mwhInformer := mwhinformer.Get(ctx) 39 | secretInformer := secretinformer.Get(ctx) 40 | options := webhook.GetOptions(ctx) 41 | 42 | r := NewReconciler( 43 | name, 44 | path, 45 | client, 46 | mwhInformer.Lister(), 47 | secretInformer.Lister(), 48 | options.SecretName, 49 | ) 50 | 51 | ac, err := NewAdmissionController( 52 | name, 53 | path, 54 | wc, 55 | labels, 56 | annotations, 57 | envVars, 58 | setupCaCertsImage, 59 | caCertsData, 60 | imagePullSecrets, 61 | ) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | wh := Webhook{r, ac} 67 | 68 | logger := logging.FromContext(ctx) 69 | c := controller.NewContext(ctx, wh, controller.ControllerOptions{Logger: logger, WorkQueueName: "CertInjectionWebhook"}) 70 | 71 | mwhInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{ 72 | FilterFunc: controller.FilterWithName(name), 73 | Handler: controller.HandleAll(c.Enqueue), 74 | }) 75 | 76 | secretInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{ 77 | FilterFunc: controller.FilterWithNameAndNamespace(system.Namespace(), wh.secretName), 78 | Handler: controller.HandleAll(c.Enqueue), 79 | }) 80 | 81 | return c, nil 82 | } 83 | -------------------------------------------------------------------------------- /pkg/certs/cert.go: -------------------------------------------------------------------------------- 1 | package certs 2 | 3 | import ( 4 | "encoding/pem" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // Split a single string of multiple certs into multiple strings of single 10 | // certs. 11 | func Split(certs string) []string { 12 | var res []string 13 | for block, data := pem.Decode([]byte(certs)); block != nil; block, data = pem.Decode(data) { 14 | res = append(res, string(pem.EncodeToMemory(block))) 15 | } 16 | return res 17 | } 18 | 19 | // Parse the environment variables satsifying the pattern and construct a list 20 | // of certs. 21 | func Parse(pattern string, environ []string) ([]string, error) { 22 | envVars := map[string]string{} 23 | for _, e := range environ { 24 | parts := strings.SplitN(e, "=", 2) 25 | envVars[parts[0]] = parts[1] 26 | } 27 | 28 | var certs []string 29 | for i := 0; ; i++ { 30 | name := fmt.Sprintf("%s_%d", pattern, i) 31 | fragment, found := envVars[name] 32 | if !found { 33 | return certs, nil 34 | } 35 | 36 | block, _ := pem.Decode([]byte(fragment)) 37 | if block == nil { 38 | return nil, fmt.Errorf("cert not in pem format") 39 | } 40 | 41 | certs = append(certs, fragment) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/certs/cert_test.go: -------------------------------------------------------------------------------- 1 | package certs_test 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/x509" 7 | "crypto/x509/pkix" 8 | "encoding/pem" 9 | "fmt" 10 | "io" 11 | "math/big" 12 | "math/rand" 13 | "testing" 14 | "time" 15 | 16 | "github.com/sclevine/spec" 17 | "github.com/stretchr/testify/require" 18 | "github.com/vmware-tanzu/cert-injection-webhook/pkg/certs" 19 | ) 20 | 21 | func TestCerts(t *testing.T) { 22 | spec.Run(t, "Certs", testCerts) 23 | } 24 | 25 | func makeCaCert(t *testing.T, rng io.Reader) string { 26 | t.Helper() 27 | pKey, err := ecdsa.GenerateKey(elliptic.P256(), rng) 28 | require.NoError(t, err) 29 | 30 | cert := &x509.Certificate{ 31 | SerialNumber: big.NewInt(1), 32 | Subject: pkix.Name{ 33 | Organization: []string{""}, 34 | Country: []string{"US"}, 35 | Province: []string{""}, 36 | Locality: []string{""}, 37 | StreetAddress: []string{""}, 38 | PostalCode: []string{""}, 39 | }, 40 | NotBefore: time.Now(), 41 | NotAfter: time.Now().AddDate(10, 0, 0), 42 | IsCA: true, 43 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 44 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 45 | BasicConstraintsValid: true, 46 | } 47 | 48 | ca, err := x509.CreateCertificate(rng, cert, cert, &pKey.PublicKey, pKey) 49 | require.NoError(t, err) 50 | 51 | pem := pem.EncodeToMemory(&pem.Block{ 52 | Type: "CERTIFICATE", 53 | Bytes: ca, 54 | }) 55 | return string(pem) 56 | } 57 | 58 | func testCerts(t *testing.T, when spec.G, it spec.S) { 59 | // use insecure prng for certs since this is just a test 60 | source := rand.NewSource(time.Now().UnixNano()) 61 | prng := rand.New(source) 62 | 63 | when("Splitting", func() { 64 | it("splits single cert", func() { 65 | c1 := makeCaCert(t, prng) 66 | 67 | c := certs.Split(c1) 68 | require.Len(t, c, 1) 69 | require.Equal(t, c1, c[0]) 70 | }) 71 | 72 | it("splits multiple certs", func() { 73 | c1 := makeCaCert(t, prng) 74 | c2 := makeCaCert(t, prng) 75 | c3 := makeCaCert(t, prng) 76 | 77 | c := certs.Split(c1 + "\n" + c2 + "\n" + c3) 78 | require.Len(t, c, 3) 79 | require.Equal(t, c1, c[0]) 80 | require.Equal(t, c2, c[1]) 81 | require.Equal(t, c3, c[2]) 82 | }) 83 | 84 | it("ignores invalid certs", func() { 85 | c1 := makeCaCert(t, prng) 86 | c3 := makeCaCert(t, prng) 87 | 88 | c := certs.Split(c1 + "\n" + "not-a-cert" + "\n" + c3) 89 | require.Len(t, c, 2) 90 | require.Equal(t, c1, c[0]) 91 | require.Equal(t, c3, c[1]) 92 | }) 93 | }) 94 | 95 | when("Parsing", func() { 96 | it("parsing no cert", func() { 97 | envs := []string{ 98 | "SOME-OTHER=ENV", 99 | } 100 | 101 | certs, err := certs.Parse("CA_CERT_DATA", envs) 102 | require.NoError(t, err) 103 | require.Len(t, certs, 0) 104 | }) 105 | 106 | it("parsing single valid cert", func() { 107 | c1 := makeCaCert(t, prng) 108 | envs := []string{ 109 | fmt.Sprintf("CA_CERT_DATA_0=%v", c1), 110 | } 111 | 112 | certs, err := certs.Parse("CA_CERT_DATA", envs) 113 | require.NoError(t, err) 114 | require.Len(t, certs, 1) 115 | require.Equal(t, c1, certs[0]) 116 | }) 117 | 118 | it("parsing single invalid cert", func() { 119 | envs := []string{ 120 | "CA_CERT_DATA_0=not-a-cert", 121 | } 122 | 123 | _, err := certs.Parse("CA_CERT_DATA", envs) 124 | require.Error(t, err) 125 | }) 126 | 127 | it("parsing multiple certs", func() { 128 | c1 := makeCaCert(t, prng) 129 | c2 := makeCaCert(t, prng) 130 | c3 := makeCaCert(t, prng) 131 | envs := []string{ 132 | fmt.Sprintf("CA_CERT_DATA_0=%v", c1), 133 | fmt.Sprintf("CA_CERT_DATA_1=%v", c2), 134 | fmt.Sprintf("CA_CERT_DATA_2=%v", c3), 135 | } 136 | 137 | certs, err := certs.Parse("CA_CERT_DATA", envs) 138 | require.NoError(t, err) 139 | require.Len(t, certs, 3) 140 | require.Equal(t, []string{c1, c2, c3}, certs) 141 | }) 142 | 143 | it("parsing multiple invalid certs", func() { 144 | c1 := makeCaCert(t, prng) 145 | c3 := makeCaCert(t, prng) 146 | envs := []string{ 147 | fmt.Sprintf("CA_CERT_DATA_0=%v", c1), 148 | fmt.Sprintf("CA_CERT_DATA_1=not-a-cert"), 149 | fmt.Sprintf("CA_CERT_DATA_2=%v", c3), 150 | } 151 | 152 | _, err := certs.Parse("CA_CERT_DATA", envs) 153 | require.Error(t, err) 154 | }) 155 | }) 156 | } 157 | --------------------------------------------------------------------------------