Repository: bitnami-labs/sealed-secrets Branch: main Commit: e4154483f634 Files: 252 Total size: 934.0 KB Directory structure: gitextract_5lwfnjj2/ ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ ├── cosign.pub │ ├── helm-release.yaml │ ├── helm-vib-lint.yaml │ ├── helm-vib.yaml │ ├── publish-release.yaml │ ├── release.yaml │ └── stale.yml ├── .gitignore ├── .golangci.yaml ├── .goreleaser.yml ├── .vib/ │ ├── vib-pipeline.json │ └── vib-platform-verify-openshift.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── README.md ├── RELEASE-NOTES.md ├── SECURITY.md ├── carvel/ │ └── package.yaml ├── cmd/ │ ├── controller/ │ │ ├── main.go │ │ └── main_test.go │ └── kubeseal/ │ ├── main.go │ └── main_test.go ├── contrib/ │ └── prometheus-mixin/ │ ├── .gitignore │ ├── Makefile │ ├── README.md │ ├── alerts/ │ │ ├── alerts.libsonnet │ │ └── sealed-secrets-alerts.libsonnet │ ├── config.libsonnet │ ├── dashboards/ │ │ ├── dashboards.libsonnet │ │ └── sealed-secrets-controller.json │ ├── lib/ │ │ ├── alerts.jsonnet │ │ ├── dashboards.jsonnet │ │ └── rules.jsonnet │ ├── mixin.libsonnet │ ├── rules/ │ │ └── rules.libsonnet │ └── tests.yaml ├── controller-norbac.jsonnet ├── controller-podmonitor.jsonnet ├── controller.jsonnet ├── docker/ │ ├── controller.Dockerfile │ └── kubeseal.Dockerfile ├── docs/ │ ├── GKE.md │ ├── bring-your-own-certificates.md │ ├── developer/ │ │ ├── README.md │ │ ├── controller.md │ │ ├── crypto.md │ │ ├── kubeseal.md │ │ └── swagger.yml │ └── examples/ │ └── config-template/ │ ├── README.md │ ├── deployment.yaml │ └── sealedsecret.yaml ├── githooks/ │ └── pre-commit/ │ └── doc-toc ├── go.mod ├── go.sum ├── hack/ │ ├── boilerplate.go.txt │ ├── tools.go │ └── update-codegen.sh ├── helm/ │ └── sealed-secrets/ │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── crds/ │ │ └── bitnami.com_sealedsecrets.yaml │ ├── dashboards/ │ │ └── sealed-secrets-controller.json │ ├── templates/ │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── cluster-role-binding.yaml │ │ ├── cluster-role.yaml │ │ ├── configmap-dashboards.yaml │ │ ├── deployment.yaml │ │ ├── extra-list.yaml │ │ ├── ingress.yaml │ │ ├── networkpolicy.yaml │ │ ├── pdb.yaml │ │ ├── psp-clusterrole.yaml │ │ ├── psp-clusterrolebinding.yaml │ │ ├── psp.yaml │ │ ├── role-binding.yaml │ │ ├── role.yaml │ │ ├── service-account.yaml │ │ ├── service.yaml │ │ ├── servicemonitor.yaml │ │ └── tls-secret.yaml │ └── values.yaml ├── integration/ │ ├── controller_test.go │ ├── integration_suite_test.go │ └── kubeseal_test.go ├── jsonnetfile.json ├── jsonnetfile.lock.json ├── kube-fixes.libsonnet ├── pkg/ │ ├── apis/ │ │ └── sealedsecrets/ │ │ └── v1alpha1/ │ │ ├── doc.go │ │ ├── register.go │ │ ├── sealedsecret_expansion.go │ │ ├── sealedsecret_test.go │ │ ├── types.go │ │ └── zz_generated.deepcopy.go │ ├── buildinfo/ │ │ └── version.go │ ├── client/ │ │ ├── clientset/ │ │ │ └── versioned/ │ │ │ ├── clientset.go │ │ │ ├── fake/ │ │ │ │ ├── clientset_generated.go │ │ │ │ ├── doc.go │ │ │ │ └── register.go │ │ │ ├── scheme/ │ │ │ │ ├── doc.go │ │ │ │ └── register.go │ │ │ └── typed/ │ │ │ └── sealedsecrets/ │ │ │ └── v1alpha1/ │ │ │ ├── doc.go │ │ │ ├── fake/ │ │ │ │ ├── doc.go │ │ │ │ ├── fake_sealedsecret.go │ │ │ │ └── fake_sealedsecrets_client.go │ │ │ ├── generated_expansion.go │ │ │ ├── sealedsecret.go │ │ │ └── sealedsecrets_client.go │ │ ├── informers/ │ │ │ └── externalversions/ │ │ │ ├── factory.go │ │ │ ├── generic.go │ │ │ ├── internalinterfaces/ │ │ │ │ └── factory_interfaces.go │ │ │ └── sealedsecrets/ │ │ │ ├── interface.go │ │ │ └── v1alpha1/ │ │ │ ├── interface.go │ │ │ └── sealedsecret.go │ │ └── listers/ │ │ └── sealedsecrets/ │ │ └── v1alpha1/ │ │ ├── expansion_generated.go │ │ └── sealedsecret.go │ ├── controller/ │ │ ├── controller.go │ │ ├── controller_test.go │ │ ├── funcs.go │ │ ├── keyregistry.go │ │ ├── keyregistry_test.go │ │ ├── keys.go │ │ ├── keys_test.go │ │ ├── main.go │ │ ├── main_test.go │ │ ├── metrics.go │ │ ├── metrics_test.go │ │ ├── server.go │ │ ├── server_test.go │ │ ├── signal_notwin.go │ │ └── signal_windows.go │ ├── crypto/ │ │ ├── crypto.go │ │ ├── keys.go │ │ └── keys_test.go │ ├── flagenv/ │ │ ├── flagenv.go │ │ └── flagenv_test.go │ ├── kubeseal/ │ │ ├── kubeseal.go │ │ └── kubeseal_test.go │ ├── log/ │ │ └── log.go │ ├── multidocyaml/ │ │ ├── multidocyaml.go │ │ └── multidocyaml_test.go │ └── pflagenv/ │ ├── flagenv.go │ └── flagenv_test.go ├── schema-v1alpha1.yaml ├── scripts/ │ ├── check-k8s │ ├── kubeseal-sudo │ └── release-check ├── site/ │ ├── .gitignore │ ├── README.md │ ├── archetypes/ │ │ └── default.md │ ├── config.yaml │ ├── content/ │ │ ├── community/ │ │ │ └── _index.html │ │ ├── contributors/ │ │ │ ├── agarcia-oss.md │ │ │ ├── alvneiayu.md │ │ │ └── index.md │ │ ├── docs/ │ │ │ ├── CONTRIBUTING.md │ │ │ ├── _index.md │ │ │ ├── img/ │ │ │ │ └── _index.md │ │ │ └── latest/ │ │ │ ├── README.md │ │ │ ├── _index.md │ │ │ ├── background/ │ │ │ │ ├── README.md │ │ │ │ ├── _index.md │ │ │ │ └── cryptography.md │ │ │ ├── howto/ │ │ │ │ ├── README.md │ │ │ │ ├── _index.md │ │ │ │ └── validate-sealed-secrets.md │ │ │ ├── project/ │ │ │ │ ├── .placeholder │ │ │ │ └── _index.md │ │ │ ├── reference/ │ │ │ │ ├── README.md │ │ │ │ ├── _index.md │ │ │ │ └── faq.md │ │ │ └── tutorials/ │ │ │ ├── README.md │ │ │ ├── _index.md │ │ │ ├── getting-started.md │ │ │ └── install-sealed-secrets.md │ │ ├── posts/ │ │ │ └── _index.md │ │ └── resources/ │ │ └── _index.html │ ├── data/ │ │ └── docs/ │ │ ├── latest-toc.yml │ │ └── toc-mapping.yml │ ├── resources/ │ │ └── _gen/ │ │ └── assets/ │ │ └── scss/ │ │ └── scss/ │ │ ├── site.scss_8967e03afb92eb0cac064520bf021ba2.content │ │ └── site.scss_8967e03afb92eb0cac064520bf021ba2.json │ └── themes/ │ └── template/ │ ├── archetypes/ │ │ └── default.md │ ├── assets/ │ │ └── scss/ │ │ ├── _base.scss │ │ ├── _components.scss │ │ ├── _footer.scss │ │ ├── _header.scss │ │ ├── _mixins.scss │ │ ├── _variables.scss │ │ └── site.scss │ ├── layouts/ │ │ ├── _default/ │ │ │ ├── _markup/ │ │ │ │ ├── render-image.html │ │ │ │ └── render-link.html │ │ │ ├── baseof.html │ │ │ ├── docs.html │ │ │ ├── list.html │ │ │ ├── posts.html │ │ │ ├── search.html │ │ │ ├── section.html │ │ │ ├── single.html │ │ │ ├── summary.html │ │ │ ├── tag.html │ │ │ └── versions.html │ │ ├── index.html │ │ ├── index.redirects │ │ ├── partials/ │ │ │ ├── blog-post-card.html │ │ │ ├── contributors.html │ │ │ ├── docs-right-bar.html │ │ │ ├── docs-sidebar.html │ │ │ ├── footer.html │ │ │ ├── getting-started.html │ │ │ ├── header.html │ │ │ ├── hero.html │ │ │ ├── homepage-grid.html │ │ │ ├── pagination.html │ │ │ └── use-cases.html │ │ └── shortcodes/ │ │ └── readfile.html │ └── static/ │ ├── fonts/ │ │ ├── Open Font License.md │ │ └── README.md │ └── js/ │ └── main.js ├── vendor_jsonnet/ │ └── kube-libsonnet/ │ ├── .travis.yml │ ├── CODEOWNERS │ ├── LICENSE │ ├── Makefile │ ├── README.md │ ├── bitnami.libsonnet │ ├── examples/ │ │ ├── guestbook/ │ │ │ └── guestbook.jsonnet │ │ └── wordpress/ │ │ ├── backend.jsonnet │ │ ├── frontend.jsonnet │ │ └── wordpress.jsonnet │ ├── kube.libsonnet │ └── tests/ │ ├── Dockerfile │ ├── Makefile │ ├── docker-compose.yaml │ ├── golden/ │ │ ├── test-sealedsecrets-datalines.json │ │ ├── test-sealedsecrets.json │ │ ├── test-simple-validate.json │ │ └── unittests.json │ ├── test-sealedsecrets-datalines.jsonnet │ ├── test-sealedsecrets-datalines.txt │ ├── test-sealedsecrets.jsonnet │ ├── test-simple-validate.jsonnet │ └── unittests.jsonnet └── versions.env ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ vendor_jsonnet/ linguist-generated=true ================================================ FILE: .github/CODEOWNERS ================================================ # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence, # @alvneiayu @agarcia-oss @alemorcuq will be requested for # review when someone opens a pull request. * @alvneiayu @agarcia-oss ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: triage assignees: '' --- **Which component**: The name (and version) of the affected component (controller or kubeseal) **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Run the command '....' 3. Wait for '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Version of Kubernetes**: - Output of `kubectl version`: ``` (paste your output here) ``` **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: triage assignees: '' --- **Which component**: The name (and version) of the affected component (controller or kubeseal) **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ **Description of the change** **Benefits** **Possible drawbacks** **Applicable issues** - fixes # **Additional information** ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "gomod" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" # Enable version updates for Docker - package-ecosystem: "docker" directory: "/docker" schedule: interval: "weekly" ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [ main ] pull_request: branches: [ main ] env: controller_registry: docker.io controller_repository: bitnami/sealed-secrets-controller controller_tag: latest jobs: load-versions: name: Load versions.env runs-on: ubuntu-latest steps: - name: checkout repo uses: actions/checkout@v6.0.2 - id: load-versions run: | source $GITHUB_WORKSPACE/versions.env # env vars echo "GO_VERSION=$GO_VERSION" >> $GITHUB_ENV echo "GO_VERSION_LIST=$GO_VERSION_LIST" >> $GITHUB_ENV # outputs echo "go_version=${GO_VERSION}" >> $GITHUB_OUTPUT echo "go_version_list=${GO_VERSION_LIST}" >> $GITHUB_OUTPUT outputs: go_version: ${{ steps.load-versions.outputs.go_version }} go_version_list: ${{ steps.load-versions.outputs.go_version_list }} linter: needs: load-versions name: Run linters runs-on: ubuntu-latest strategy: matrix: go: ${{ fromJSON(needs.load-versions.outputs.go_version_list) }} os: [ubuntu-latest] golangci-lint: ["1.64.8"] gosec: ["2.22.2"] steps: - name: Set up Go 1.x uses: actions/setup-go@v6.3.0 with: go-version: ${{ matrix.go }} id: go - name: Check out code into the Go module directory uses: actions/checkout@v6.0.2 - name: Install dependencies run: | go install github.com/golangci/golangci-lint/cmd/golangci-lint@v${{ matrix.golangci-lint }} go install github.com/securego/gosec/v2/cmd/gosec@v${{ matrix.gosec }} - name: Run linter run: make lint - name: Run gosec run: make lint-gosec test: needs: load-versions name: Build runs-on: ${{ matrix.os }} strategy: matrix: go: ${{ fromJSON(needs.load-versions.outputs.go_version_list) }} os: [macos-latest, windows-latest, ubuntu-latest] gotestsum: ["1.8.1"] steps: - name: Set up Go 1.x uses: actions/setup-go@v6.3.0 with: go-version: ${{ matrix.go }} id: go - name: Check out code into the Go module directory uses: actions/checkout@v6.0.2 - name: Install dependencies run: | go install gotest.tools/gotestsum@v${{ matrix.gotestsum }} - name: Test run: make GO_FLAGS="--junitfile report.xml --format testname" test - name: Test Summary uses: test-summary/action@v2 with: paths: | report.xml container: needs: load-versions name: Build Container runs-on: ubuntu-latest steps: - name: "Set environmental variables" run: | echo "CONTROLLER_IMAGE=$controller_registry/$controller_repository:$controller_tag" >> $GITHUB_ENV - name: Check out code uses: actions/checkout@v6.0.2 - name: Install Cosign uses: sigstore/cosign-installer@v3.4.0 with: cosign-release: v2.2.3 - name: Distroless verify run: | diff <(grep FROM docker/kubeseal.Dockerfile | awk '{print $2}') \ <(grep FROM docker/controller.Dockerfile | awk '{print $2}') cosign verify "$(grep FROM docker/controller.Dockerfile | awk '{print $2}')" --certificate-oidc-issuer https://accounts.google.com --certificate-identity keyless@distroless.iam.gserviceaccount.com - name: Setup kubecfg run: | mkdir -p ~/bin curl -sLf https://github.com/kubecfg/kubecfg/releases/download/v0.26.0/kubecfg_Linux_X64 >~/bin/kubecfg chmod +x ~/bin/kubecfg - name: Set up Go 1.x uses: actions/setup-go@v6.3.0 with: go-version: ${{ needs.load-versions.outputs.go_version }} id: go - name: Docker build run: | export PATH=~/bin:$PATH make CONTROLLER_IMAGE=$CONTROLLER_IMAGE IMAGE_PULL_POLICY=Never controller.yaml make CONTROLLER_IMAGE=$CONTROLLER_IMAGE controller.image.linux-amd64 docker tag $CONTROLLER_IMAGE-linux-amd64 $CONTROLLER_IMAGE docker save $CONTROLLER_IMAGE -o /tmp/controller-image.tar - name: Upload manifest artifact uses: actions/upload-artifact@v6.0.0 with: name: controller-manifest path: controller.yaml - name: Upload container image artifact uses: actions/upload-artifact@v6.0.0 with: name: controller-image path: /tmp/controller-image.tar integration-yaml: needs: [ load-versions, container ] name: Integration (controller.yaml) runs-on: ubuntu-latest strategy: matrix: k8s: ["1.32.12","1.33.8","1.34.4","1.35.1"] env: MINIKUBE_WANTUPDATENOTIFICATION: "false" MINIKUBE_WANTREPORTERRORPROMPT: "false" CHANGE_MINIKUBE_NONE_USER: "true" steps: - name: "Set environmental variables" run: | echo "CONTROLLER_IMAGE=$controller_registry/$controller_repository:$controller_tag" >> $GITHUB_ENV - name: Set up Go 1.x uses: actions/setup-go@v6.3.0 with: go-version: ${{ needs.load-versions.outputs.go_version }} id: go - name: Set up Ginkgo run: | go install github.com/onsi/ginkgo/ginkgo@v1.16.4 - name: Check out code into the Go module directory uses: actions/checkout@v6.0.2 - uses: medyagh/setup-minikube@v0.0.21 with: minikube-version: 1.38.0 kubernetes-version: ${{ matrix.k8s }} # need to delete old state of the cluster, see: # https://github.com/kubernetes/minikube/issues/8765 - name: K8s setup run: | minikube delete minikube config set kubernetes-version v${{ matrix.k8s }} minikube start --vm-driver=docker minikube update-context kubectl cluster-info - name: Download manifest artifact uses: actions/download-artifact@v7.0.0 with: name: controller-manifest - name: Download container image artifact uses: actions/download-artifact@v7.0.0 with: name: controller-image - name: Load docker image run: | eval $(minikube docker-env) docker load -i controller-image.tar docker inspect $CONTROLLER_IMAGE - name: Testing environment setup run: | kubectl apply -f controller.yaml kubectl rollout status deployment/sealed-secrets-controller -n kube-system -w --timeout=1m || kubectl -n kube-system describe pod -lname=sealed-secrets-controller - name: Integration tests run: make integrationtest CONTROLLER_IMAGE=$CONTROLLER_IMAGE GINKGO="ginkgo -v --randomizeSuites --failOnPending --trace --progress --compilers=2 --nodes=4" integration-chart: needs: [ load-versions, container ] name: Integration (Helm Chart) runs-on: ubuntu-latest strategy: matrix: k8s: ["1.32.12","1.33.8","1.34.4","1.35.1"] env: MINIKUBE_WANTUPDATENOTIFICATION: "false" MINIKUBE_WANTREPORTERRORPROMPT: "false" CHANGE_MINIKUBE_NONE_USER: "true" steps: - name: "Set environmental variables" run: | echo "CONTROLLER_IMAGE=$controller_registry/$controller_repository:$controller_tag" >> $GITHUB_ENV - name: Set up Go 1.x uses: actions/setup-go@v6.3.0 with: go-version: ${{ needs.load-versions.outputs.go_version }} id: go - name: Set up Ginkgo run: | go install github.com/onsi/ginkgo/ginkgo@v1.16.4 - name: Check out code into the Go module directory uses: actions/checkout@v6.0.2 - uses: medyagh/setup-minikube@v0.0.21 with: minikube-version: 1.38.0 kubernetes-version: ${{ matrix.k8s }} - name: Install Helm uses: azure/setup-helm@v3.5 with: version: v3.12.0 # need to delete old state of the cluster, see: # https://github.com/kubernetes/minikube/issues/8765 - name: K8s setup run: | minikube delete minikube config set kubernetes-version v${{ matrix.k8s }} minikube start --vm-driver=docker minikube update-context kubectl cluster-info - name: Download container image artifact uses: actions/download-artifact@v7.0.0 with: name: controller-image - name: Load docker image run: | eval $(minikube docker-env) docker load -i controller-image.tar docker inspect $CONTROLLER_IMAGE - name: Testing environment setup run: | helm install sealed-secrets -n kube-system --set fullnameOverride=sealed-secrets-controller --set image.registry=$controller_registry --set image.repository=$controller_repository --set image.tag=$controller_tag --set image.pullPolicy=Never helm/sealed-secrets kubectl rollout status deployment/sealed-secrets-controller -n kube-system -w --timeout=1m || kubectl -n kube-system describe pod -lapp.kubernetes.io/name=sealed-secrets - name: Integration tests run: make integrationtest CONTROLLER_IMAGE=$CONTROLLER_IMAGE GINKGO="ginkgo -v --randomizeSuites --failOnPending --trace --progress --compilers=2 --nodes=4" ================================================ FILE: .github/workflows/cosign.pub ================================================ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEseWNtEaI73oDVgjfLzU4eQYHE11i MzRSNs1TA+cTT/Lw70ckfCC/vHnOXKACF2dnhsZsNNj647p9mAiYNVl9ug== -----END PUBLIC KEY----- ================================================ FILE: .github/workflows/helm-release.yaml ================================================ name: Release Helm Chart and Carvel package on: push: paths: # update this file to trigger helm chart release - 'helm/sealed-secrets/Chart.yaml' branches: - main workflow_dispatch: jobs: release: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - name: Configure Git run: | git config user.name "$GITHUB_ACTOR" git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - name: Install Helm uses: azure/setup-helm@v4.3.1 with: version: v4.1.1 - name: Run chart-releaser uses: helm/chart-releaser-action@v1.4.1 with: charts_dir: helm env: CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" CR_RELEASE_NAME_TEMPLATE: "helm-v{{ .Version }}" - name: Install Carvel uses: carvel-dev/setup-action@v1.3.0 with: only: kbld, imgpkg token: ${{ secrets.GITHUB_TOKEN }} - name: Install yq run: | mkdir -p ~/bin wget https://github.com/mikefarah/yq/releases/download/v4.30.8/yq_linux_amd64 -O ~/bin/yq chmod +x ~/bin/yq - name: Get chart version run: | export PATH=~/bin:$PATH echo "chart_version=$(yq .version < ./helm/sealed-secrets/Chart.yaml)" >> $GITHUB_ENV - name: Configure DNS for registry access run: | echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf > /dev/null echo "nameserver 8.8.4.4" | sudo tee -a /etc/resolv.conf > /dev/null - name: OCI Push env: OCI_PASS: ${{ secrets.OCI_PASSWORD }} OCI_USR: ${{ secrets.OCI_USERNAME }} HELM_EXPERIMENTAL_OCI: 1 run: | echo $OCI_PASS | helm registry login -u $OCI_USR --password-stdin registry-1.docker.io helm package helm/sealed-secrets/ helm push sealed-secrets-${{ env.chart_version }}.tgz oci://registry-1.docker.io/bitnamicharts/sealed-secrets - name: Create imglock file working-directory: ./helm run: | mkdir -p .imgpkg kbld -f <(helm template sealed-secrets) --imgpkg-lock-output .imgpkg/images.yml - name: Push imgpkg bundle working-directory: ./helm env: IMGPKG_REGISTRY_HOSTNAME: ghcr.io IMGPKG_REGISTRY_USERNAME: ${{ github.actor }} IMGPKG_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} run: | imgpkg push -b ghcr.io/${{ github.repository_owner }}/sealed-secrets-carvel:${{ env.chart_version }} -f . --json > output echo carvel_pkg=$(cat output | grep Pushed | cut -d "'" -f2 ) >> $GITHUB_ENV - name: Update package.yaml run: | yq -i '.spec.version = "${{ env.chart_version }}"' carvel/package.yaml yq -i '.metadata.name = "sealedsecrets.bitnami.com.${{ env.chart_version }}"' carvel/package.yaml yq -i '.spec.template.spec.fetch.0.imgpkgBundle.image = "${{ env.carvel_pkg }}"' carvel/package.yaml git checkout -B 'release-carvel-${{ env.chart_version }}' git add carvel/package.yaml git commit -sm 'Release carvel package ${{ env.chart_version }}' git push origin 'release-carvel-${{ env.chart_version }}' - name: Create PR run: gh pr create --fill --base main --repo $GITHUB_REPOSITORY env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/helm-vib-lint.yaml ================================================ name: Lint Helm Chart on: workflow_dispatch: pull_request_target: branches: - main - bitnami-labs:main paths: - 'helm/**' env: CSP_API_URL: https://console.tanzu.broadcom.com CSP_API_TOKEN: ${{ secrets.CSP_API_TOKEN }} VIB_PUBLIC_URL: https://cp.app-catalog.vmware.com jobs: # make sure chart is linted/safe vib-validate: runs-on: ubuntu-latest name: Lint chart steps: - uses: actions/checkout@v6.0.2 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - uses: vmware-labs/vmware-image-builder-action@v0.11.0 ================================================ FILE: .github/workflows/helm-vib.yaml ================================================ name: Verify Helm Chart on: workflow_dispatch: push: branches: - main paths: - 'helm/**' env: CSP_API_URL: https://console.tanzu.broadcom.com CSP_API_TOKEN: ${{ secrets.CSP_API_TOKEN }} VIB_PUBLIC_URL: https://cp.app-catalog.vmware.com jobs: # verify chart in multiple target platforms vib-k8s-verify: runs-on: ubuntu-latest environment: vmware-image-builder strategy: matrix: include: - name: Openshift target-platform: openshift target-platform-id: ebac9e0d-3931-4515-ba54-e6adada1f174 target-pipeline: vib-platform-verify-openshift.json fail-fast: false name: Verify chart (${{ matrix.name }}) steps: - uses: actions/checkout@v6.0.2 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} - uses: vmware-labs/vmware-image-builder-action@v0.11.0 with: pipeline: ${{ matrix.target-pipeline }} max-pipeline-duration: 7200 env: TARGET_PLATFORM: ${{ matrix.target-platform-id }} ================================================ FILE: .github/workflows/publish-release.yaml ================================================ name: Publish Release on: workflow_dispatch: inputs: chart: description: 'Chart version (e.g. 2.11.3)' required: true type: string jobs: chart-pr: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Config Git run: | git config user.name "$GITHUB_ACTOR" git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - name: Fetch Versions run: | echo NEW_VERSION=$(git describe --tags --match "v[0-9]*" --abbrev=0 | tr -d v) >> "$GITHUB_ENV" echo PREV_VERSION=$(grep appVersion helm/sealed-secrets/Chart.yaml | grep -o '[0-9.]*') >> "$GITHUB_ENV" - name: Update Version run: | sed -i "s/version: .*/version: ${{ inputs.chart }}/" helm/sealed-secrets/Chart.yaml sed -i "s/appVersion: .*/appVersion: $NEW_VERSION/" helm/sealed-secrets/Chart.yaml sed -i "s/tag: .*/tag: $NEW_VERSION/" helm/sealed-secrets/values.yaml sed -i "s/\`$PREV_VERSION\`/\`$NEW_VERSION\`/" helm/sealed-secrets/README.md git checkout -B 'release-chart-${{ inputs.chart }}' git add helm/sealed-secrets/Chart.yaml helm/sealed-secrets/values.yaml helm/sealed-secrets/README.md git commit -sm 'Release chart ${{ inputs.chart }}' git push origin 'release-chart-${{ inputs.chart }}' - name: Create PR run: gh pr create --fill --base main --repo $GITHUB_REPOSITORY env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/release.yaml ================================================ name: Prepare Release # Only release when a new GH release branch is pushed on: push: branches: - 'release/v[0-9]+.[0-9]+.[0-9]+' jobs: build: runs-on: ubuntu-latest env: controller_dockerhub_image_name: docker.io/bitnami/sealed-secrets-controller controller_ghcr_image_name: ghcr.io/bitnami-labs/sealed-secrets-controller kubeseal_dockerhub_image_name: docker.io/bitnami/sealed-secrets-kubeseal kubeseal_ghcr_image_name: ghcr.io/bitnami-labs/sealed-secrets-kubeseal steps: # Checkout and set env - name: Checkout uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - id: load-version run: | source $GITHUB_WORKSPACE/versions.env echo "GO_VERSION=$GO_VERSION" >> $GITHUB_ENV - name: Set up Go uses: actions/setup-go@v6.3.0 with: go-version: ${{ env.GO_VERSION }} - name: Setup kubecfg run: | mkdir -p ~/bin curl -sLf https://github.com/kubecfg/kubecfg/releases/download/v0.26.0/kubecfg_Linux_X64 >~/bin/kubecfg chmod +x ~/bin/kubecfg - name: Install dependencies run: | go install gotest.tools/gotestsum@v1.8.1 # Run tests - name: Tests run: make test # Generate K8s manifests - name: K8s manifests run: | export PATH=~/bin:$PATH RELEASE_BRANCH="${{ github.ref }}" VERSION_TAG=$(echo "${RELEASE_BRANCH}" | awk -F'/' '{print $NF}') echo "VERSION_TAG=$VERSION_TAG" >> $GITHUB_ENV IMAGE_TAG=$(echo "${VERSION_TAG}" | sed 's/^v//') echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV make CONTROLLER_IMAGE=${{ env.controller_dockerhub_image_name }}:${IMAGE_TAG} controller.yaml controller-norbac.yaml # Setup env for multi-arch builds - name: Set up QEMU uses: docker/setup-qemu-action@v2.0.0 with: image: tonistiigi/binfmt:latest platforms: arm64,arm - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2.0.0 # Setup Cosign - name: Install Cosign uses: sigstore/cosign-installer@v3.0.2 - name: Write Cosign key run: echo "$COSIGN_KEY" > /tmp/cosign.key env: COSIGN_KEY: ${{ secrets.COSIGN_KEY }} # Tag for GoReleaser from release branch name - name: Tag Release run: | git tag "${VERSION_TAG}" # Build & Release binaries - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 if: success() && startsWith(github.ref, 'refs/heads/') with: distribution: goreleaser version: v2.11.2 args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} # Build & Publish multi-arch image - name: Login to Docker Hub uses: docker/login-action@v2.0.0 with: username: ${{ secrets.BITNAMI_USERNAME }} password: ${{ secrets.BITNAMI_PASSWORD }} - name: Login to GHRC uses: docker/login-action@v2.0.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker controller image id: meta_controller uses: docker/metadata-action@v4.0.1 with: images: | ${{ env.controller_dockerhub_image_name }} ${{ env.controller_ghcr_image_name }} tags: | type=raw,value=${{ env.IMAGE_TAG }} type=raw,value=latest - name: Build and push controller image id: docker_build_controller uses: docker/build-push-action@v3.2.0 with: context: . file: ./docker/controller.Dockerfile platforms: linux/amd64,linux/arm64,linux/arm push: true tags: ${{ steps.meta_controller.outputs.tags }} - name: Extract metadata (tags, labels) for Docker kubeseal image id: meta_kubeseal uses: docker/metadata-action@v4.0.1 with: images: | ${{ env.kubeseal_dockerhub_image_name }} ${{ env.kubeseal_ghcr_image_name }} tags: | type=raw,value=${{ env.IMAGE_TAG }} type=raw,value=latest - name: Build and push kubeseal image id: docker_build_kubeseal uses: docker/build-push-action@v3.2.0 with: context: . file: ./docker/kubeseal.Dockerfile platforms: linux/amd64,linux/arm64,linux/arm push: true tags: ${{ steps.meta_kubeseal.outputs.tags }} - name: Sign controller image with a key in GHCR run: | echo -n "$COSIGN_PASSWORD" | cosign sign --key /tmp/cosign.key --yes $TAG_CURRENT env: COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} TAG_CURRENT: ${{ steps.meta_controller.outputs.tags }} COSIGN_REPOSITORY: ${{ env.controller_ghcr_image_name }}/signs - name: Sign kubeseal image with a key in GHCR run: | echo -n "$COSIGN_PASSWORD" | cosign sign --key /tmp/cosign.key --yes $TAG_CURRENT env: COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} TAG_CURRENT: ${{ steps.meta_kubeseal.outputs.tags }} COSIGN_REPOSITORY: ${{ env.kubeseal_ghcr_image_name }}/signs ================================================ FILE: .github/workflows/stale.yml ================================================ name: 'Close stale issues and PRs' on: schedule: # Stalebot will be executed at 1:00 AM every day - cron: '0 1 * * *' jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v10.1.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'This Issue has been automatically marked as "stale" because it has not had recent activity (for 15 days). It will be closed if no further activity occurs. Thanks for the feedback.' stale-pr-message: 'This Pull Request has been automatically marked as "stale" because it has not had recent activity (for 15 days). It will be closed if no further activity occurs. Thank you for your contribution.' close-issue-message: 'Due to the lack of activity in the last 7 days since it was marked as "stale", we proceed to close this Issue. Do not hesitate to reopen it later if necessary.' close-pr-message: 'Due to the lack of activity in the last 7 days since it was marked as "stale", we proceed to close this Pull Request. Do not hesitate to reopen it later if necessary.' days-before-stale: 15 days-before-close: 7 exempt-issue-labels: 'backlog,help wanted,triage' exempt-pr-labels: 'backlog,help wanted,triage' operations-per-run: 500 ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ # Project-local vscode config .vscode/ /controller /kubeseal /kubeseal-arm /kubeseal-arm64 /controller.image /controller.image.* /kubeseal.image /kubeseal.image.* /pushed.controller.image.* /pushed.kubeseal.image.* /controller-manifest-* /push-controller-image /*-static /*-static-* /controller.yaml /controller-norbac.yaml /controller-podmonitor.yaml /docker/controller *.iml .idea # GoReleaser output dir dist/ # Vendor folder vendor/ report.xml ================================================ FILE: .golangci.yaml ================================================ # Inspired by https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322 # output configuration options output: # Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions # Default: colored-line-number format: checkstyle:report.xml,colored-line-number:stdout # Options for analysis running. run: # Timeout for analysis, e.g. 30s, 5m. # Default: 1m timeout: 5m # This file contains only configs which differ from defaults. # All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml linters-settings: cyclop: # The maximal code complexity to report. # Default: 10 max-complexity: 30 # The maximal average package complexity. # If it's higher than 0.0 (float) the check is enabled # Default: 0.0 package-average: 10.0 errcheck: # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. # Such cases aren't reported by default. # Default: false check-type-assertions: true # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`. # Such cases aren't reported by default. # Default: false check-blank: true exhaustive: # Program elements to check for exhaustiveness. # Default: [ switch ] check: - switch - map exhaustruct: # List of regular expressions to exclude struct packages and names from check. # Default: [] exclude: # std libs - "^net/http.Client$" - "^net/http.Cookie$" - "^net/http.Request$" - "^net/http.Response$" - "^net/http.Server$" - "^net/http.Transport$" - "^net/url.URL$" - "^os/exec.Cmd$" - "^reflect.StructField$" # public libs (add more if needed) funlen: # Checks the number of lines in a function. # If lower than 0, disable the check. # Default: 60 lines: 100 # Checks the number of statements in a function. # If lower than 0, disable the check. # Default: 40 statements: 50 gocognit: # Minimal code complexity to report. # Default: 30 (but we recommend 10-20) min-complexity: 20 goconst: # Minimal length of string constant. # Default: 3 min-len: 2 # Minimum occurrences of constant string count to trigger issue. # Default: 3 min-occurrences: 2 # Search also for duplicated numbers. # Default: false numbers: true # Minimum value, only works with goconst.numbers # Default: 3 min: 2 gocritic: # Settings passed to gocritic. # The settings key is the name of a supported gocritic checker. # The list of supported checkers can be find in https://go-critic.github.io/overview. settings: captLocal: # Whether to restrict checker to params only. # Default: true paramsOnly: false underef: # Whether to skip (*x).method() calls where x is a pointer receiver. # Default: true skipRecvDeref: false gomnd: # List of function patterns to exclude from analysis. # Values always ignored: `time.Date`, # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`, # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`. # Default: [] ignored-functions: - os.Chmod - os.Mkdir - os.MkdirAll - os.OpenFile - os.WriteFile - math.* - http.StatusText govet: # Enable all analyzers. # Default: false enable-all: true # Disable analyzers by name. # Run `go tool vet help` to see all analyzers. # Default: [] disable: - fieldalignment # too strict, it warns about struct fields that are not aligned by size # Settings per analyzer. settings: shadow: # Whether to be strict about shadowing; can be noisy. # Default: false strict: true nakedret: # Make an issue if func has more lines of code than this setting, and it has naked returns. # Default: 30 max-func-lines: 0 nestif: # Minimal complexity of if statements to report. # Default: 5 min-complexity: 4 nolintlint: # Exclude following linters from requiring an explanation. # Default: [] allow-no-explanation: [funlen, gocognit, lll] # Enable to require an explanation of nonzero length after each nolint directive. # Default: false require-explanation: true # Enable to require nolint directives to mention the specific linter being suppressed. # Default: false require-specific: true lll: # Max line length, lines longer will be reported. # '\t' is counted as 1 character by default, and can be changed with the tab-width option. # Default: 120. line-length: 240 rowserrcheck: # database/sql is always checked # Default: [] packages: - github.com/jmoiron/sqlx tenv: # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. # Default: false all: true varnamelen: # The minimum length of a variable's name that is considered "long". # Variable names that are at least this long will be ignored. # Default: 3 min-name-length: 2 # Check method receivers. # Default: false # Ignore "ok" variables that hold the bool return value of a type assertion. # Default: false ignore-type-assert-ok: true # Ignore "ok" variables that hold the bool return value of a map index. # Default: false ignore-map-index-ok: true # Ignore "ok" variables that hold the bool return value of a channel receive. # Default: false ignore-chan-recv-ok: true godot: # Check periods at the end of sentences. period: false linters: disable-all: true enable: #- errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases - gosimple # specializes in simplifying a code #- govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string - ineffassign # detects when assignments to existing variables are not used - staticcheck # is a go vet on steroids, applying a ton of static analysis checks - typecheck # like the front-end of a Go compiler, parses and type-checks Go code - unused # checks for unused constants, variables, functions and types - asasalint # checks for pass []any as any in variadic func(...any) - asciicheck # checks that your code does not contain non-ASCII identifiers - bidichk # checks for dangerous unicode character sequences - bodyclose # checks whether HTTP response body is closed successfully #- cyclop # checks function and package cyclomatic complexity #- dupl # tool for code clone detection - durationcheck # checks for two durations multiplied together - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error #- errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 #- execinquery # checks query string in Query function which reads your Go src files and warning it finds - exhaustive # checks exhaustiveness of enum switch statements #- exportloopref # checks for pointers to enclosing loop variables #- forbidigo # forbids identifiers #- funlen # tool for detection of long functions #- gochecknoinits # checks that no init functions are present in Go code #- gocognit # computes and checks the cognitive complexity of functions #- goconst # finds repeated strings that could be replaced by a constant #- gocritic # provides diagnostics that check for bugs, performance and style issues #- gocyclo # computes and checks the cyclomatic complexity of functions - godot # checks if comments end in a period - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt #- gomnd # detects magic numbers - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations - goprintffuncname # checks that printf-like functions are named with f at the end #- gosec # inspects source code for security problems #- lll # reports long lines - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) #- makezero # finds slice declarations with non-zero initial length - nakedret # finds naked returns in functions greater than a specified function length #- nestif # reports deeply nested if statements #- nilerr # finds the code that returns nil even if it checks that the error is not nil - nilnil # checks that there is no simultaneous return of nil error and an invalid value #- noctx # finds sending http request without context.Context - nolintlint # reports ill-formed or insufficient nolint directives #- nonamedreturns # reports all named returns - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL #- predeclared # finds code that shadows one of Go's predeclared identifiers - promlinter # checks Prometheus metrics naming via promlint - reassign # checks that package variables are not reassigned #- revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint - rowserrcheck # checks whether Err of rows is checked successfully - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed #- stylecheck # is a replacement for golint - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 - testableexamples # checks if examples are testable (have an expected output) #- testpackage # makes you use a separate _test package - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes - unconvert # removes unnecessary type conversions #- unparam # reports unused function parameters - usestdlibvars # detects the possibility to use variables/constants from the Go standard library - wastedassign # finds wasted assignment statements - whitespace # detects leading and trailing whitespace ## you may want to enable - decorder # checks declaration order and count of types, constants, variables and functions #- gci # controls golang package import order and makes it always deterministic - goheader # checks is file header matches to pattern - interfacebloat # checks the number of methods inside an interface #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope #- wrapcheck # checks that errors returned from external packages are wrapped #- containedctx # detects struct contained context.Context field - contextcheck # [too many false positives] checks the function whether use a non-inherited context - dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) #- dupword # [useless without config] checks for duplicate words in the source code - errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted #- goerr113 # [too strict] checks the errors handling expressions - grouper # analyzes expression groups - importas # enforces consistent import aliases - maintidx # measures the maintainability index of each function - misspell # [useless] finds commonly misspelled English words in comments #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test - tagliatelle # checks the struct tags #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines ## disabled # - exhaustruct # [highly recommend to enable] checks if all structure fields are initialized # - godox # detects FIXME, TODO and other comment keywords # - gochecknoglobals # checks that no global variables exist # - ireturn # accept interfaces, return concrete types issues: # Maximum count of issues with the same text. # Set to 0 to disable. # Default: 3 max-same-issues: 0 ================================================ FILE: .goreleaser.yml ================================================ project_name: sealed-secrets env: - CGO_ENABLED=0 builds: - binary: controller id: controller main: ./cmd/controller ldflags: - -X main.VERSION={{ .Version }} targets: - darwin_amd64 - darwin_arm64 - linux_amd64 - linux_arm64 - linux_arm - windows_amd64 - binary: kubeseal id: kubeseal main: ./cmd/kubeseal ldflags: - -X main.VERSION={{ .Version }} targets: - darwin_amd64 - darwin_arm64 - linux_amd64 - linux_arm64 - linux_arm - windows_amd64 archives: - builds: - kubeseal name_template: "kubeseal-{{ .Version }}-{{ .Os }}-{{ .Arch }}" checksum: algorithm: sha256 changelog: sort: asc filters: exclude: - '^docs:' - '^helm:' - '^integration:' - '^vendor_jsonnet:' signs: - cmd: cosign stdin: '{{ .Env.COSIGN_PASSWORD }}' output: true artifacts: all args: - 'sign-blob' - '--key=/tmp/cosign.key' - '--output-signature=${signature}' - '--yes' - '${artifact}' release: name_template: "{{ .ProjectName }}-v{{ .Version }}" header: | ## v{{ .Version }} ({{ .Date }}) New v{{ .Version }} release! footer: | ## Installation Instructions ### Cluster-side Install the SealedSecret CRD and server-side controller into the `kube-system` namespace: ```sh kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v{{ .Version }}/controller.yaml ``` ### Client-side Install the client-side tool into `/usr/local/bin/`: **Linux x86_64:** ```sh curl -OL "https://github.com/bitnami-labs/sealed-secrets/releases/download/v{{ .Version }}/kubeseal-{{ .Version }}-linux-amd64.tar.gz" tar -xvzf kubeseal-{{ .Version }}-linux-amd64.tar.gz kubeseal sudo install -m 755 kubeseal /usr/local/bin/kubeseal ``` **macOS:** The `kubeseal` client is available on [homebrew](https://formulae.brew.sh/formula/kubeseal): ```sh brew install kubeseal ``` **MacPorts:** The `kubeseal` client is available on [MacPorts](https://ports.macports.org/port/kubeseal/summary): ```sh port install kubeseal ``` #### Nixpkgs The `kubeseal` client is available on [Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=kubeseal&from=0&size=50&sort=relevance&type=packages&query=kubeseal): (**DISCLAIMER**: Not maintained by bitnami-labs) ```sh nix-env -iA nixpkgs.kubeseal ``` **Other OS/Arch:** Binaries for other OS/arch combinations are attached to this release below. If you just want the latest client tool, it can be installed into `$GOPATH/bin` with: ```sh go install github.com/bitnami-labs/sealed-secrets/cmd/kubeseal@main ``` You can specify a release tag or a commit SHA instead of `main`. The `go install` command will place the `kubeseal` binary at `$GOPATH/bin`: ```sh $(go env GOPATH)/bin/kubeseal ``` ## Release Notes Please read the [RELEASE_NOTES](https://github.com/bitnami-labs/sealed-secrets/blob/main/RELEASE-NOTES.md) which contain among other things important information for those upgrading from previous releases. ## Thanks! extra_files: - glob: ./controller.yaml - glob: ./controller-norbac.yaml - glob: ./.github/workflows/cosign.pub ================================================ FILE: .vib/vib-pipeline.json ================================================ { "phases": { "package": { "context": { "resources": { "url": "{SHA_ARCHIVE}", "path": "/helm/sealed-secrets" } }, "actions": [ { "action_id": "helm-package" }, { "action_id": "helm-lint" } ] }, "verify": { "context": { "runtime_parameters": "IyMgQ3JlYXRlIFNlYWxlZCBTZWNyZXRzIGNvbnRyb2xsZXIgc2hvdWxkIGJlIGNyZWF0ZWQKY3JlYXRlQ29udHJvbGxlcjogdHJ1ZQojIyBTZWNyZXQgY29udGFpbmluZyB0aGUga2V5IHVzZWQgdG8gZW5jcnlwdCBzZWNyZXRzCnNlY3JldE5hbWU6ICJzZWFsZWQtc2VjcmV0cy1rZXkiCiMjIFJlbmV3IGtleXMgZXZlcnkgd2VlawprZXlyZW5ld3BlcmlvZDogIjE2OGgiCg==" }, "actions": [ { "action_id": "trivy", "params": { "threshold": "CRITICAL", "vuln_type": ["OS"] } } ] } } } ================================================ FILE: .vib/vib-platform-verify-openshift.json ================================================ { "phases": { "package": { "context": { "resources": { "url": "{SHA_ARCHIVE}", "path": "/helm/sealed-secrets" } }, "actions": [ { "action_id": "helm-package" } ] }, "verify": { "context": { "resources": { "url": "{SHA_ARCHIVE}", "path": "/.vib/" }, "runtime_parameters": "IyMgQ3JlYXRlIFNlYWxlZCBTZWNyZXRzIGNvbnRyb2xsZXIgc2hvdWxkIGJlIGNyZWF0ZWQKY3JlYXRlQ29udHJvbGxlcjogdHJ1ZQojIyBTZWNyZXQgY29udGFpbmluZyB0aGUga2V5IHVzZWQgdG8gZW5jcnlwdCBzZWNyZXRzCnNlY3JldE5hbWU6ICJzZWFsZWQtc2VjcmV0cy1rZXkiCiMjIFJlbmV3IGtleXMgZXZlcnkgd2VlawprZXlyZW5ld3BlcmlvZDogIjE2OGgiCmNvbnRhaW5lclNlY3VyaXR5Q29udGV4dDoKICBlbmFibGVkOiB0cnVlCiAgcmVhZE9ubHlSb290RmlsZXN5c3RlbTogdHJ1ZQogIHJ1bkFzTm9uUm9vdDogdHJ1ZQogIHJ1bkFzVXNlcjogbnVsbApwb2RTZWN1cml0eUNvbnRleHQ6CiAgZW5hYmxlZDogZmFsc2UKc2VydmljZToKICB0eXBlOiBMb2FkQmFsYW5jZXIKICBwb3J0OiA4MAo=", "target_platform": { "target_platform_id": "{TARGET_PLATFORM}" } }, "actions": [ { "action_id": "health-check", "params": { "endpoint": "lb-sealed-secrets-http" } } ] } } } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at sealed-secrets.pdl@broadcom.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Guidelines Contributions are welcome via GitHub Pull Requests. This document outlines the process to help get your contribution accepted. Any type of contribution is welcome; from new features, bug fixes, or documentation improvements. However, VMware/Bitnami will review the proposals and perform a triage over them. By doing so, we will ensure that the most valuable contributions for the community will be implemented in due time. ## How to Contribute 1. Fork this repository, develop, and test your changes. 2. Submit a pull request. ### Technical Requirements When submitting a PR make sure that it: - Must pass CI jobs for linting and test the changes on top of different k8s platforms. - Must follow [Golang best practices](https://go.dev/doc/effective_go). - Is signed off with the line `Signed-off-by: `. See [related GitHub blogpost about signing off](https://github.blog/changelog/2022-06-08-admins-can-require-sign-off-on-web-based-commits/). > Note: Signing off on a commit is different than signing a commit, such as with a GPG key. ### PR Approval 1. Changes are manually reviewed by VMware/Bitnami team members. 2. When the PR passes all tests, the PR is merged by the reviewer(s) in the GitHub `main` branch. ### Release process The release process is based upon periodic release trains. #### Schedule Releases happen monthly. A release train "leaves" on the 15th of each month, or the closest working date to that. #### Creation First of all, prepare the release notes as usual, and merge them. Once the release notes are ready, a release train is launched by *branching* from `main` to `release/vX.Y.Z`. #### Validation The `release/vX.Y.Z` branch will go through the release CI. GoReleaser requires a tag to build a release, so one will be produced automatically from the release branch name `vX.Y.Z`. If anything fails the release branch is dropped, the issue fixed in `main` and a new release train is started on a new branch. #### Tracking Once the release passes all validations and is published, it is merged into `released`. Note that currently the release process is done in 2 steps, first the container images, then the chart using them. Both events must be merged in the `released` branch. #### Hot-fixing releases If there is a need to urgently fix a show-stopper issue in the latest released version. There is no need to wait for the next release train for a new release to happen. Unless there is a strong reason not to, a fix can be merged into `main` directly, followed by a regular release process. If doing the fix in main is a "no go" for some reason, for instance, a new change already in `main` makes the bug to be urgently fixed even worse, then the fix must happen from the latest released code to proceed ASAP: * Create a `hotfix/YYYYMMDD` branch as a copy of `released`. The `YYYYMMDD` suffix is an ISO-8601 timestamp, for tracking purposes. * Branch off `hotfix/YYYYMMDD` to work on the fix. As a regular PR, you might name the fix branch with a descriptive name for the bug being fixed. * Once the fix is approved and tested as successful, merge into `hotfix/YYYYMMDD`. * Push `hostfix/YYYYMMDD` as a `release/vX.Y.Z` to kick off a release train. * If the release fails for any reason, fix it in `hostfix/YYYYMMDD`, merge and push another `release/vX.Y.Z'` branch. * Once a hotfix release completes successfully, merge the `release/vX.Y.Z` as `released` as per normal procedure. * *Backport the hotfix into the `main` including the tests added to detected regressions* of that bug going forward. * Finally, `hotfix/YYYYMMDD` can be kept around for tracking or historical purposes. Note that, in either case, the release notes must clarify this was a hotfix our of the regular release train schedule. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MAINTAINERS.md ================================================ # Sealed Secrets Maintainers ## Maintainers | Maintainer | GitHub ID | Affiliation | | ------------------ | --------------------------------------------------- | ---------------------------------------: | | Alvaro Neira Ayuso | [alvneiayu](https://github.com/alvneiayu) | [VMware](https://www.github.com/vmware/) | | Alejandro Moreno | [alemorcuq](https://github.com/alemorcuq) | [VMware](https://www.github.com/vmware/) | | Alfredo Garcia | [agarcia-oss](https://github.com/agarcia-oss) | [VMware](https://www.github.com/vmware/) | ## Emeritus Maintainers - Angus Lees ([anguslees](https://github.com/anguslees)) - Marko Mikulicic ([mkmik](https://github.com/mkmik)) - Juan Ariza ([juan131](https://github.com/juan131)) - Jose Vazquez ([josvazg](https://github.com/josvazg)) --- Full list of [Sealed Secrets contributors](https://github.com/bitnami-labs/sealed-secrets/graphs/contributors). ================================================ FILE: Makefile ================================================ GO = go GOTESTSUM = gotestsum GOFMT = gofmt GOLANGCILINT=golangci-lint -vv GOSEC=gosec export GO111MODULE = on GO_FLAGS = KUBECFG = kubecfg DOCKER = docker GINKGO = ginkgo -p CONTROLLER_GEN ?= go run sigs.k8s.io/controller-tools/cmd/controller-gen@latest REGISTRY ?= docker.io CONTROLLER_IMAGE = $(REGISTRY)/bitnami/sealed-secrets-controller:latest KUBESEAL_IMAGE = $(REGISTRY)/bitnami/sealed-secrets-kubeseal:latest INSECURE_REGISTRY = false # useful for local registry IMAGE_PULL_POLICY = KUBECONFIG ?= $(HOME)/.kube/config GO_PACKAGES = ./... GO_FILES := $(shell find $(shell $(GO) list -f '{{.Dir}}' $(GO_PACKAGES)) -name \*.go) COMMIT = $(shell git rev-parse HEAD) TAG = $(shell git describe --exact-match --abbrev=0 --tags '$(COMMIT)' 2> /dev/null || true) DIRTY = $(shell git diff --shortstat 2> /dev/null | tail -n1) # Use a tag if set, otherwise use the commit hash ifeq ($(TAG),) VERSION := $(COMMIT) else VERSION := $(TAG) endif GOOS = $(shell go env GOOS) GOARCH = $(shell go env GOARCH) # Check for changed files ifneq ($(DIRTY),) VERSION := $(VERSION)+dirty endif GO_LD_FLAGS = -X main.VERSION=$(VERSION) all: controller kubeseal generate: $(GO) mod vendor ./hack/update-codegen.sh rm -rf vendor manifests: $(CONTROLLER_GEN) crd:generateEmbeddedObjectMeta=true paths="./pkg/apis/..." output:stdout | tail -n +2 > helm/sealed-secrets/crds/bitnami.com_sealedsecrets.yaml yq '.spec.versions[0].schema' < helm/sealed-secrets/crds/bitnami.com_sealedsecrets.yaml > schema-v1alpha1.yaml controller: $(GO_FILES) $(GO) build -o $@ $(GO_FLAGS) -ldflags "$(GO_LD_FLAGS)" ./cmd/controller kubeseal: $(GO_FILES) $(GO) build -o $@ $(GO_FLAGS) -ldflags "$(GO_LD_FLAGS)" ./cmd/kubeseal define binary $(1)-static-$(2)-$(3): $(GO_FILES) GOOS=$(2) GOARCH=$(3) CGO_ENABLED=0 $(GO) build -o $$@ -installsuffix cgo $(GO_FLAGS) -ldflags "$(GO_LD_FLAGS)" ./cmd/$(1) endef define binaries $(call binary,controller,$1,$2) $(call binary,kubeseal,$1,$2) endef $(eval $(call binaries,linux,amd64)) $(eval $(call binaries,linux,arm64)) $(eval $(call binaries,linux,arm)) $(eval $(call binaries,darwin,amd64)) $(eval $(call binary,kubeseal,windows,amd64)) controller-static: controller-static-$(GOOS)-$(GOARCH) cp $< $@ kubeseal-static: kubeseal-static-$(GOOS)-$(GOARCH) cp $< $@ define image $(1).image.$(3)-$(4): docker/$(1).Dockerfile $(1)-static-$(3)-$(4) mkdir -p dist/$(1)_$(3)_$(4) cp $(1)-static-$(3)-$(4) dist/$(1)_$(3)_$(4)/$(1) $(DOCKER) build --build-arg TARGETARCH=$(4) -t $(2)-$(3)-$(4) -f docker/$(1).Dockerfile . @echo $(2)-$(3)-$(4) >$$@.tmp @mv $$@.tmp $$@ endef define images $(call image,controller,${CONTROLLER_IMAGE},$1,$2) $(call image,kubeseal,${KUBESEAL_IMAGE},$1,$2) endef $(eval $(call images,linux,amd64)) $(eval $(call images,linux,arm64)) $(eval $(call images,linux,arm)) %.yaml: %.jsonnet $(KUBECFG) show -V CONTROLLER_IMAGE=$(CONTROLLER_IMAGE) -V IMAGE_PULL_POLICY=$(IMAGE_PULL_POLICY) -o yaml $< > $@.tmp mv $@.tmp $@ controller.yaml: controller.jsonnet controller-norbac.jsonnet schema-v1alpha1.yaml kube-fixes.libsonnet controller-norbac.yaml: controller-norbac.jsonnet schema-v1alpha1.yaml kube-fixes.libsonnet controller-podmonitor.yaml: controller.jsonnet controller-norbac.jsonnet schema-v1alpha1.yaml kube-fixes.libsonnet test: $(GOTESTSUM) $(GO_FLAGS) --junitfile report.xml --format testname -- "-coverprofile=coverage.out" $(GO_PACKAGES) integrationtest: kubeseal controller # Assumes a k8s cluster exists, with controller already installed $(GINKGO) -tags 'integration' integration -- -kubeconfig $(KUBECONFIG) -kubeseal-bin $(abspath $<) -controller-bin $(abspath $(word 2,$^)) vet: # known issue: # pkg/client/clientset/versioned/fake/clientset_generated.go:46: literal copies lock value from fakePtr $(GO) vet $(GO_FLAGS) -copylocks=false $(GO_PACKAGES) fmt: $(GOFMT) -s -w $(GO_FILES) lint: $(GOLANGCILINT) run --enable goimports --timeout=5m lint-gosec: $(GOSEC) -r -severity low -exclude-generated clean: $(RM) ./controller ./kubeseal $(RM) *-static* $(RM) controller*.yaml $(RM) controller.image* check-k8s: scripts/check-k8s push-controller: clean check-k8s controller.image.$(OS)-$(ARCH) docker tag $(CONTROLLER_IMAGE)-$(OS)-$(ARCH) $(CONTROLLER_IMAGE) ifeq ($(REGISTRY),docker.io) echo "Skip push: docker.io registry means minikube" else docker push $(CONTROLLER_IMAGE) endif apply-controller-manifests: clean check-k8s controller.yaml kubectl apply -f controller.yaml kubectl rollout status deployment sealed-secrets-controller -n kube-system controller-tests: test push-controller apply-controller-manifests clean integrationtest .PHONY: all kubeseal controller test clean vet fmt lint-gosec .PHONY: controllertests check-k8s push-controller apply-controller-manifests ================================================ FILE: README.md ================================================ # "Sealed Secrets" for Kubernetes [![](https://img.shields.io/badge/install-docs-brightgreen.svg)](#Installation) [![](https://img.shields.io/github/release/bitnami-labs/sealed-secrets.svg)](https://github.com/bitnami-labs/sealed-secrets/releases/latest) [![](https://img.shields.io/homebrew/v/kubeseal)](https://formulae.brew.sh/formula/kubeseal) [![Build Status](https://github.com/bitnami-labs/sealed-secrets/actions/workflows/ci.yml/badge.svg)](https://github.com/bitnami-labs/sealed-secrets/actions/workflows/ci.yml) [![](https://img.shields.io/github/v/release/bitnami-labs/sealed-secrets?include_prereleases&label=helm&sort=semver)](https://github.com/bitnami-labs/sealed-secrets/releases) [![Download Status](https://img.shields.io/docker/pulls/bitnami/sealed-secrets-controller.svg)](https://hub.docker.com/r/bitnami/sealed-secrets-controller) [![Go Report Card](https://goreportcard.com/badge/github.com/bitnami-labs/sealed-secrets)](https://goreportcard.com/report/github.com/bitnami-labs/sealed-secrets) ![Downloads](https://img.shields.io/github/downloads/bitnami-labs/sealed-secrets/total.svg) **Problem:** "I can manage all my K8s config in git, except Secrets." **Solution:** Encrypt your Secret into a SealedSecret, which *is* safe to store - even inside a public repository. The SealedSecret can be decrypted only by the controller running in the target cluster and nobody else (not even the original author) is able to obtain the original Secret from the SealedSecret. - [Overview](#overview) - [SealedSecrets as templates for secrets](#sealedsecrets-as-templates-for-secrets) - [Public key / Certificate](#public-key--certificate) - [Scopes](#scopes) - [Installation](#installation) - [Installation in Restricted Environments (No RBAC)](#installation-in-restricted-environments-no-rbac) - [Controller](#controller) - [Kustomize](#kustomize) - [Helm Chart](#helm-chart) - [Helm Chart on a restricted environment](#helm-chart-on-a-restricted-environment) - [Kubeseal](#kubeseal) - [Homebrew](#homebrew) - [MacPorts](#macports) - [Nixpkgs](#nixpkgs) - [Linux](#linux) - [Installation from source](#installation-from-source) - [Upgrade](#upgrade) - [Supported Versions](#supported-versions) - [Compatibility with Kubernetes versions](#compatibility-with-kubernetes-versions) - [Usage](#usage) - [Managing existing secrets](#managing-existing-secrets) - [Patching existing secrets](#patching-existing-secrets) - [Seal secret which can skip set owner references](#seal-secret-which-can-skip-set-owner-references) - [Update existing secrets](#update-existing-secrets) - [Raw mode (experimental)](#raw-mode-experimental) - [Validate a Sealed Secret](#validate-a-sealed-secret) - [Secret Rotation](#secret-rotation) - [Sealing key renewal](#sealing-key-renewal) - [Key registry init priority order](#key-registry-init-priority-order) - [User secret rotation](#user-secret-rotation) - [Early key renewal](#early-key-renewal) - [Common misconceptions about key renewal](#common-misconceptions-about-key-renewal) - [Manual key management (advanced)](#manual-key-management-advanced) - [Re-encryption (advanced)](#re-encryption-advanced) - [Details (advanced)](#details-advanced) - [Crypto](#crypto) - [Developing](#developing) - [FAQ](#faq) - [Can I encrypt multiple secrets at once, in one YAML / JSON file?](#can-i-encrypt-multiple-secrets-at-once-in-one-yaml--json-file) - [Will you still be able to decrypt if you no longer have access to your cluster?](#will-you-still-be-able-to-decrypt-if-you-no-longer-have-access-to-your-cluster) - [How can I do a backup of my SealedSecrets?](#how-can-i-do-a-backup-of-my-sealedsecrets) - [Can I decrypt my secrets offline with a backup key?](#can-i-decrypt-my-secrets-offline-with-a-backup-key) - [What flags are available for kubeseal?](#what-flags-are-available-for-kubeseal) - [How do I update parts of JSON/YAML/TOML/.. file encrypted with sealed secrets?](#how-do-i-update-parts-of-jsonyamltoml-file-encrypted-with-sealed-secrets) - [Can I bring my own (pre-generated) certificates?](#can-i-bring-my-own-pre-generated-certificates) - [How to use kubeseal if the controller is not running within the `kube-system` namespace?](#how-to-use-kubeseal-if-the-controller-is-not-running-within-the-kube-system-namespace) - [How to verify the images?](#how-to-verify-the-images) - [How to use one controller for a subset of namespaces](#how-to-use-one-controller-for-a-subset-of-namespaces) - [Can I configure the Controller unseal retries?](#can-i-configure-the-controller-unseal-retries) - [How to manage SealedSecrets across the cluster or specific namespaces?](#how-to-manage-sealedsecrets-across-the-cluster-or-specific-namespaces) - [Community](#community) - [Related projects](#related-projects) ## Overview Sealed Secrets is composed of two parts: - A cluster-side controller / operator - A client-side utility: `kubeseal` The `kubeseal` utility uses asymmetric crypto to encrypt secrets that only the controller can decrypt. These encrypted secrets are encoded in a `SealedSecret` resource, which you can see as a recipe for creating a secret. Here is how it looks: ```yaml apiVersion: bitnami.com/v1alpha1 kind: SealedSecret metadata: name: mysecret namespace: mynamespace spec: encryptedData: foo: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq..... ``` Once unsealed this will produce a secret equivalent to this: ```yaml apiVersion: v1 kind: Secret metadata: name: mysecret namespace: mynamespace data: foo: YmFy # <- base64 encoded "bar" ``` This normal [kubernetes secret](https://kubernetes.io/docs/concepts/configuration/secret/) will appear in the cluster after a few seconds you can use it as you would use any secret that you would have created directly (e.g. reference it from a `Pod`). Jump to the [Installation](#installation) section to get up and running. The [Usage](#usage) section explores in more detail how you craft `SealedSecret` resources. ### SealedSecrets as templates for secrets The previous example only focused on the encrypted secret items themselves, but the relationship between a `SealedSecret` custom resource and the `Secret` it unseals into is similar in many ways (but not in all of them) to the familiar `Deployment` vs `Pod`. In particular, the annotations and labels of a `SealedSecret` resource are not the same as the annotations of the `Secret` that gets generated out of it. To capture this distinction, the `SealedSecret` object has a `template` section which encodes all the fields you want the controller to put in the unsealed `Secret`. The [Sprig function library](https://masterminds.github.io/sprig/) is available (except for `env`, `expandenv` and `getHostByName`) in addition to the default Go Text Template functions. The `metadata` block is copied as is (the `ownerReference` field will be updated [unless disabled](#seal-secret-which-can-skip-set-owner-references)). Other secret fields are handled individually. The `type` and `immutable` fields are copied, and the `data` field can be used to [template complex values](docs/examples/config-template) on the `Secret`. All other fields are currently ignored. ```yaml apiVersion: bitnami.com/v1alpha1 kind: SealedSecret metadata: name: mysecret namespace: mynamespace annotations: "kubectl.kubernetes.io/last-applied-configuration": .... spec: encryptedData: .dockerconfigjson: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq..... template: type: kubernetes.io/dockerconfigjson immutable: true # this is an example of labels and annotations that will be added to the output secret metadata: labels: "jenkins.io/credentials-type": usernamePassword annotations: "jenkins.io/credentials-description": credentials from Kubernetes ``` The controller would unseal that into something like: ```yaml apiVersion: v1 kind: Secret metadata: name: mysecret namespace: mynamespace labels: "jenkins.io/credentials-type": usernamePassword annotations: "jenkins.io/credentials-description": credentials from Kubernetes ownerReferences: - apiVersion: bitnami.com/v1alpha1 controller: true kind: SealedSecret name: mysecret uid: 5caff6a0-c9ac-11e9-881e-42010aac003e type: kubernetes.io/dockerconfigjson immutable: true data: .dockerconfigjson: ewogICJjcmVk... ``` As you can see, the generated `Secret` resource is a "dependent object" of the `SealedSecret` and as such it will be updated and deleted whenever the `SealedSecret` object gets updated or deleted. ### Public key / Certificate The key certificate (public key portion) is used for sealing secrets, and needs to be available wherever `kubeseal` is going to be used. The certificate is not secret information, although you need to ensure you are using the correct one. `kubeseal` will fetch the certificate from the controller at runtime (requires secure access to the Kubernetes API server), which is convenient for interactive use, but it's known to be brittle when users have clusters with special configurations such as [private GKE clusters](docs/GKE.md#private-gke-clusters) that have firewalls between control plane and nodes. An alternative workflow is to store the certificate somewhere (e.g. local disk) with `kubeseal --fetch-cert >mycert.pem`, and use it offline with `kubeseal --cert mycert.pem`. The certificate is also printed to the controller log on startup. Since v0.9.x certificates get automatically renewed every 30 days. It's good practice that you and your team update your offline certificate periodically. To help you with that, since v0.9.2 `kubeseal` accepts URLs too. You can set up your internal automation to publish certificates somewhere you trust. ```bash kubeseal --cert https://your.intranet.company.com/sealed-secrets/your-cluster.cert ``` It also recognizes the `SEALED_SECRETS_CERT` env var. (pro-tip: see also [direnv](https://github.com/direnv/direnv)). > **NOTE**: we are working on providing key management mechanisms that offload the encryption to HSM based modules or managed cloud crypto solutions such as KMS. ### Scopes SealedSecrets are from the POV of an end user a "write only" device. The idea is that the SealedSecret can be decrypted only by the controller running in the target cluster and nobody else (not even the original author) is able to obtain the original Secret from the SealedSecret. The user may or may not have direct access to the target cluster. More specifically, the user might or might not have access to the Secret unsealed by the controller. There are many ways to configure RBAC on k8s, but it's quite common to forbid low-privilege users from reading Secrets. It's also common to give users one or more namespaces where they have higher privileges, which would allow them to create and read secrets (and/or create deployments that can reference those secrets). Encrypted `SealedSecret` resources are designed to be safe to be looked at without gaining any knowledge about the secrets it conceals. This implies that we cannot allow users to read a SealedSecret meant for a namespace they wouldn't have access to and just push a copy of it in a namespace where they can read secrets from. Sealed-secrets thus behaves *as if* each namespace had its own independent encryption key and thus once you seal a secret for a namespace, it cannot be moved in another namespace and decrypted there. We don't technically use an independent private key for each namespace, but instead we *include* the namespace name during the encryption process, effectively achieving the same result. Furthermore, namespaces are not the only level at which RBAC configurations can decide who can see which secret. In fact, it's possible that users can access a secret called `foo` in a given namespace but not any other secret in the same namespace. We cannot thus by default let users freely rename `SealedSecret` resources otherwise a malicious user would be able to decrypt any SealedSecret for that namespace by just renaming it to overwrite the one secret user does have access to. We use the same mechanism used to include the namespace in the encryption key to also include the secret name. That said, there are many scenarios where you might not care about this level of protection. For example, the only people who have access to your clusters are either admins or they cannot read any `Secret` resource at all. You might have a use case for moving a sealed secret to other namespaces (e.g. you might not know the namespace name upfront), or you might not know the name of the secret (e.g. it could contain a unique suffix based on the hash of the contents etc). These are the possible scopes: - `strict` (default): the secret must be sealed with exactly the same *name* and *namespace*. These attributes become *part of the encrypted data* and thus changing name and/or namespace would lead to "decryption error". - `namespace-wide`: you can freely *rename* the sealed secret within a given namespace. - `cluster-wide`: the secret can be unsealed in *any* namespace and can be given *any* name. In contrast to the restrictions of *name* and *namespace*, secret *items* (i.e. JSON object keys like `spec.encryptedData.my-key`) can be renamed at will without losing the ability to decrypt the sealed secret. The scope is selected with the `--scope` flag: ```bash kubeseal --scope cluster-wide sealed-secret.json ``` It's also possible to request a scope via annotations in the input secret you pass to `kubeseal`: - `sealedsecrets.bitnami.com/namespace-wide: "true"` -> for `namespace-wide` - `sealedsecrets.bitnami.com/cluster-wide: "true"` -> for `cluster-wide` The lack of any of such annotations means `strict` mode. If both are set, `cluster-wide` takes precedence. > NOTE: Next release will consolidate this into a single `sealedsecrets.bitnami.com/scope` annotation. ## Installation See https://github.com/bitnami-labs/sealed-secrets/releases for the latest release and detailed installation instructions. Cloud platform specific notes and instructions: - [GKE](docs/GKE.md) ### Installation in Restricted Environments (No RBAC) In environments where you lack permissions to create cluster-wide RBAC resources (like `ClusterRoles`), you can use the **`controller-norbac.yaml`** manifest available on the Releases page. This version is a minimal deployment that includes only the **Deployment**, **Service**, and **CustomResourceDefinition**. It intentionally omits `ServiceAccount`, `ClusterRole`, and `ClusterRoleBinding`. **Requirements:** 1. A cluster administrator must have already installed the SealedSecret CRDs. 2. You must have an allocated Service Account to run the deployment ### Controller Once you deploy the manifest it will create the `SealedSecret` resource and install the controller into `kube-system` namespace, create a service account and necessary RBAC roles. After a few moments, the controller will start, generate a key pair, and be ready for operation. If it does not, check the controller logs. #### Kustomize The official controller manifest installation mechanism is just a YAML file. In some cases you might need to apply your own customizations, like set a custom namespace or set some env variables. `kubectl` has native support for that, see [kustomize](https://kustomize.io/). #### Helm Chart The Sealed Secrets helm chart is now officially supported and hosted in this GitHub repo. ```bash helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets ``` > NOTE: The versioning scheme of the helm chart differs from the versioning scheme of the sealed secrets project itself. Originally the helm chart was maintained by the community and the first version adopted a major version of 1 while the sealed secrets project itself is still at major 0. This is ok because the version of the helm chart itself is not meant to be necessarily the version of the app itself. However this is confusing, so our current versioning rule is: 1. The `SealedSecret` controller version scheme: 0.X.Y 2. The helm chart version scheme: 1.X.Y-rZ There can be thus multiple revisions of the helm chart, with fixes that apply only to the helm chart without affecting the static YAML manifests or the controller image itself. > NOTE: The helm chart by default installs the controller with the name `sealed-secrets`, while the `kubeseal` command line interface (CLI) tries to access the controller with the name `sealed-secrets-controller`. You can explicitly pass `--controller-name` to the CLI: ```bash kubeseal --controller-name sealed-secrets ``` Alternatively, you can set `fullnameOverride` when installing the chart to override the name. Note also that `kubeseal` assumes that the controller is installed within the `kube-system` namespace by default. So if you want to use the `kubeseal` CLI without having to pass the expected controller name and namespace you should install the Helm Chart like this: ```bash helm install sealed-secrets -n kube-system --set-string fullnameOverride=sealed-secrets-controller sealed-secrets/sealed-secrets ``` ##### Helm Chart on a restricted environment In some companies you might be given access only to a single namespace, not a full cluster. One of the most restrictive environments you can encounter is: - A `namespace` was allocated to you with some `service account`. - You do not have access to the rest of the cluster, not even cluster CRDs. - You may not even be able to create further service accounts or roles in your namespace. - You are required to include resource limits in all your deployments. Even with these restrictions you can still install the sealed secrets Helm Chart, there is only one pre-requisite: - *The cluster must already have the sealed secrets CRDs installed*. Once your admins installed the CRDs, if they were not there already, you can install the chart by preparing a YAML config file such as this: ```shell serviceAccount: create: false name: {allocated-service-account} rbac: create: false clusterRole: false resources: limits: cpu: 150m memory: 256Mi ``` Note that: - No service accounts are created, instead the one allocated to you will be used. - `{allocated-service-account}` is the name of the `service account` you were allocated on the cluster. - No RBAC roles are created neither in the namespace nor the cluster. - Resource limits must be specified. - The limits are samples that should work, but you might want to review them in your particular setup. Once that file is ready, if you named it `config.yaml` you now can install the sealed secrets Helm Chart like this: ```shell helm install sealed-secrets -n {allocated-namespace} sealed-secrets/sealed-secrets --skip-crds -f config.yaml ``` Where `{allocated-namespace}` is the name of the `namespace` you were allocated in the cluster. ### Kubeseal #### Homebrew The `kubeseal` client is also available on [homebrew](https://formulae.brew.sh/formula/kubeseal): ```bash brew install kubeseal ``` #### MacPorts The `kubeseal` client is also available on [MacPorts](https://ports.macports.org/port/kubeseal/summary): ```bash port install kubeseal ``` #### Nixpkgs The `kubeseal` client is also available on [Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=kubeseal&from=0&size=50&sort=relevance&type=packages&query=kubeseal): (**DISCLAIMER**: Not maintained by bitnami-labs) ```bash nix-env -iA nixpkgs.kubeseal ``` #### Linux The `kubeseal` client can be installed on Linux, using the below commands: ```bash KUBESEAL_VERSION='' # Set this to, for example, KUBESEAL_VERSION='0.23.0' curl -OL "https://github.com/bitnami-labs/sealed-secrets/releases/download/v${KUBESEAL_VERSION:?}/kubeseal-${KUBESEAL_VERSION:?}-linux-amd64.tar.gz" tar -xvzf kubeseal-${KUBESEAL_VERSION:?}-linux-amd64.tar.gz kubeseal sudo install -m 755 kubeseal /usr/local/bin/kubeseal ``` If you have `curl` and `jq` installed on your machine, you can get the version dynamically this way. This can be useful for environments used in automation and such. ``` # Fetch the latest sealed-secrets version using GitHub API KUBESEAL_VERSION=$(curl -s https://api.github.com/repos/bitnami-labs/sealed-secrets/tags | jq -r '.[0].name' | cut -c 2-) # Check if the version was fetched successfully if [ -z "$KUBESEAL_VERSION" ]; then echo "Failed to fetch the latest KUBESEAL_VERSION" exit 1 fi curl -OL "https://github.com/bitnami-labs/sealed-secrets/releases/download/v${KUBESEAL_VERSION}/kubeseal-${KUBESEAL_VERSION}-linux-amd64.tar.gz" tar -xvzf kubeseal-${KUBESEAL_VERSION}-linux-amd64.tar.gz kubeseal sudo install -m 755 kubeseal /usr/local/bin/kubeseal ``` where `KUBESEAL_VERSION` is the [version tag](https://github.com/bitnami-labs/sealed-secrets/tags) of the kubeseal release you want to use. For example: `v0.18.0`. #### Installation from source If you just want the latest client tool, it can be installed into `$GOPATH/bin` with: ```bash go install github.com/bitnami-labs/sealed-secrets/cmd/kubeseal@main ``` You can specify a release tag or a commit SHA instead of `main`. The `go install` command will place the `kubeseal` binary at `$GOPATH/bin`: ```bash $(go env GOPATH)/bin/kubeseal ``` ## Upgrade Don't forget to check the [release notes](RELEASE-NOTES.md) for guidance about possible breaking changes when you upgrade the client tool and/or the controller. ### Supported Versions Currently, only the latest version of Sealed Secrets is supported for production environments. ### Compatibility with Kubernetes versions The Sealed Secrets controller ensures compatibility with different versions of Kubernetes by relying on a stable Kubernetes API. Typically, Kubernetes versions above 1.16 are considered compatible. However, we officially support the [currently recommended Kubernetes versions](https://kubernetes.io/releases/). Additionally, versions above 1.24 undergo thorough verification through our CI process with every release. ## Usage ```bash # Create a json/yaml-encoded Secret somehow: # (note use of `--dry-run` - this is just a local file!) echo -n bar | kubectl create secret generic mysecret --dry-run=client --from-file=foo=/dev/stdin -o json >mysecret.json # This is the important bit: kubeseal -f mysecret.json -w mysealedsecret.json # At this point mysealedsecret.json is safe to upload to Github, # post on Twitter, etc. # Eventually: kubectl create -f mysealedsecret.json # Profit! kubectl get secret mysecret ``` Note the `SealedSecret` and `Secret` must have **the same namespace and name**. This is a feature to prevent other users on the same cluster from re-using your sealed secrets. See the [Scopes](#scopes) section for more info. `kubeseal` reads the namespace from the input secret, accepts an explicit `--namespace` argument, and uses the `kubectl` default namespace (in that order). Any labels, annotations, etc on the original `Secret` are preserved, but not automatically reflected in the `SealedSecret`. By design, this scheme *does not authenticate the user*. In other words, *anyone* can create a `SealedSecret` containing any `Secret` they like (provided the namespace/name matches). It is up to your existing config management workflow, cluster RBAC rules, etc to ensure that only the intended `SealedSecret` is uploaded to the cluster. The only change from existing Kubernetes is that the *contents* of the `Secret` are now hidden while outside the cluster. ### Managing existing secrets If you want the Sealed Secrets controller to manage an existing `Secret`, you can annotate your `Secret` with the `sealedsecrets.bitnami.com/managed: "true"` annotation. The existing `Secret` will be overwritten when unsealing a `SealedSecret` with the same name and namespace, and the `SealedSecret` will take ownership of the `Secret` (so that when the `SealedSecret` is deleted the `Secret` will also be deleted). ### Patching existing secrets > New in v0.23.0 There are some use cases in which you don't want to replace the whole `Secret` but just add or modify some keys from the existing `Secret`. For this, you can annotate your `Secret` with `sealedsecrets.bitnami.com/patch: "true"`. Using this annotation will make sure that secret keys, labels and annotations in the `Secret` that are not present in the `SealedSecret` won't be deleted, and those present in the `SealedSecret` will be added to the `Secret` (secret keys, labels and annotations that exist both in the `Secret` and the `SealedSecret` will be modified by the `SealedSecret`). This annotation does not make the `SealedSecret` take ownership of the `Secret`. You can add both the `patch` and `managed` annotations to obtain the patching behavior while also taking ownership of the `Secret`. ### Seal secret which can skip set owner references If you want `SealedSecret` and the `Secret` to be independent, which mean when you delete the `SealedSecret` the `Secret` won't disappear with it, then you have to annotate that Secret with the annotation `sealedsecrets.bitnami.com/skip-set-owner-references: "true"` ahead of applying the Usage steps. You still may also add `sealedsecrets.bitnami.com/managed: "true"` to your `Secret` so that your secret will be updated when `SealedSecret` is updated. ### Update existing secrets If you want to add or update existing sealed secrets without having the cleartext for the other items, you can just copy&paste the new encrypted data items and merge it into an existing sealed secret. You must take care of sealing the updated items with a compatible name and namespace (see note about scopes above). You can use the `--merge-into` command to update an existing sealed secrets if you don't want to copy&paste: ```bash echo -n bar | kubectl create secret generic mysecret --dry-run=client --from-file=foo=/dev/stdin -o json \ | kubeseal > mysealedsecret.json echo -n baz | kubectl create secret generic mysecret --dry-run=client --from-file=bar=/dev/stdin -o json \ | kubeseal --merge-into mysealedsecret.json ``` ### Raw mode (experimental) Creating temporary Secret with the `kubectl` command, only to throw it away once piped to `kubeseal` can be a quite unfriendly user experience. We're working on an overhaul of the CLI experience. In the meantime, we offer an alternative mode where kubeseal only cares about encrypting a value to stdout, and it's your responsibility to put it inside a `SealedSecret` resource (not unlike any of the other k8s resources). It can also be useful as a building block for editor/IDE integrations. The downside is that you have to be careful to be consistent with the sealing scope, the namespace and the name. See [Scopes](#scopes) `strict` scope (default): ```console $ echo -n foo | kubeseal --raw --namespace bar --name mysecret AgBChHUWLMx... ``` `namespace-wide` scope: ```console $ echo -n foo | kubeseal --raw --namespace bar --scope namespace-wide AgAbbFNkM54... ``` Include the `sealedsecrets.bitnami.com/namespace-wide` annotation in the `SealedSecret` ```yaml metadata: annotations: sealedsecrets.bitnami.com/namespace-wide: "true" ``` `cluster-wide` scope: ```console $ echo -n foo | kubeseal --raw --scope cluster-wide AgAjLKpIYV+... ``` Include the `sealedsecrets.bitnami.com/cluster-wide` annotation in the `SealedSecret` ```yaml metadata: annotations: sealedsecrets.bitnami.com/cluster-wide: "true" ``` ### Validate a Sealed Secret If you want to validate an existing sealed secret, `kubeseal` has the flag `--validate` to help you. Giving a file named `sealed-secrets.yaml` containing the following sealed secret: ```yaml apiVersion: bitnami.com/v1alpha1 kind: SealedSecret metadata: name: mysecret namespace: mynamespace spec: encryptedData: foo: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq..... ``` You can validate if the sealed secret was properly created or not: ```console $ cat sealed-secrets.yaml | kubeseal --validate ``` In case of an invalid sealed secret, `kubeseal` will show: ```console $ cat sealed-secrets.yaml | kubeseal --validate error: unable to decrypt sealed secret ``` ## Secret Rotation You should always rotate your secrets. But since your secrets are encrypted with another secret, you need to understand how these two layers relate to take the right decisions. TL;DR: > If a *sealing* private key is compromised, you need to follow the instructions below in "Early key renewal" > section before rotating any of your actual secret values. > > SealedSecret key renewal and re-encryption features are **not a substitute** for periodical rotation of your actual secret values. ### Sealing key renewal Sealing keys are automatically renewed every 30 days. Which means a new sealing key is created and appended to the set of active sealing keys the controller can use to unseal `SealedSecret` resources. The most recently created sealing key is the one used to seal new secrets when you use `kubeseal` and it's the one whose certificate is downloaded when you use `kubeseal --fetch-cert`. The renewal time of 30 days is a reasonable default, but it can be tweaked as needed with the `--key-renew-period=` flag for the command in the pod template of the `SealedSecret` controller. The `value` field can be given as golang duration flag (eg: `720h30m`). Assuming that you've installed Sealed Secrets into the `kube-system` namespace, use the following command to edit the Deployment controller, and add the `--key-renew-period` parameter. Once you close your text editor, and the Deployment controller has been modified, a new Pod will be automatically created to replace the old Pod. ``` kubectl edit deployment/sealed-secrets-controller --namespace=kube-system ``` A value of `0` will deactivate automatic key renewal. Of course, you may have a valid use case for deactivating automatic sealing key renewal but experience has shown that new users often tend to jump to conclusions that they want control over key renewal, before fully understanding how sealed secrets work. Read more about this in the [common misconceptions](#common-misconceptions-about-key-renewal) section below. > Unfortunately, you cannot use e.g. "d" as a unit for days because that's not supported by the Go stdlib. Instead of hitting your face with a palm, take this as an opportunity to meditate on the [falsehoods programmers believe about time](https://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time). A common misunderstanding is that key renewal is often thought of as a form of key rotation, where the old key is not only obsolete but actually bad and that you thus want to get rid of it. It doesn't help that this feature has been historically called "key rotation", which can add to the confusion. Sealed secrets are not automatically rotated and old keys are not deleted when new keys are generated. Old `SealedSecret` resources can be still decrypted (that's because old sealing keys are not deleted). ### Key registry init priority order When the controller starts, it will initialize the key registry. The most recent key is used to seal secrets. By default, this certificate is chosen based on the NotBefore attribute of the certificate. If you want to change the priority order of the keys in the registry, you can use the `--key-order-priority` flag. The `--key-order-priority` flag accepts the following values: - `CertNotBefore`: (default) The key registry will be ordered based on the NotBefore attribute of the key certificate. - `SecretCreationTimestamp`: The key registry will be ordered based on the creation timestamp of the secret. This flag influences the public key used to encrypt secrets and the certificate retrieved by `kubeseal --fetch-cert`. ### User secret rotation The *sealing key* renewal and SealedSecret rotation are **not a substitute** for rotating your actual secrets. A core value proposition of this tool is: > Encrypt your Secret into a SealedSecret, which *is* safe to store - even inside a public repository. If you store anything in a version control storage, and in a public one in particular, you must assume you cannot ever delete that information. *If* a sealing key somehow leaks out of the cluster you must consider all your `SealedSecret` resources encrypted with that key as compromised. No amount of sealing key rotation in the cluster or even re-encryption of existing SealedSecrets files can change that. The best practice is to periodically rotate all your actual secrets (e.g. change the password) **and** craft new `SealedSecret` resources with those new secrets. But if the `SealedSecret` controller was not renewing the *sealing key* that rotation would be moot, since the attacker could just decrypt the new secrets as well. Thus, you need to do both: periodically renew the sealing key and rotate your actual secrets! ### Early key renewal If you know or suspect a *sealing key* has been compromised you should renew the key ASAP before you start sealing your new rotated secrets, otherwise you'll be giving attackers access to your new secrets as well. A key can be generated early by passing the current timestamp to the controller into a flag called `--key-cutoff-time` or an env var called `SEALED_SECRETS_KEY_CUTOFF_TIME`. The expected format is RFC1123, you can generate it with the `date -R` unix command. ### Common misconceptions about key renewal Sealed secrets sealing keys are not access control keys (e.g. a password). They are more like the GPG key you might use to read encrypted mail sent to you. Let's continue with the email analogy for a bit: Imagine you have reasons to believe your private GPG key might have been compromised. You'd have more to lose than to gain if the first thing you do is just delete your private key. All the previous emails sent with that key are no longer accessible to you (unless you have a decrypted copy of those emails), nor are new emails sent by your friends whom you have not yet managed to tell to use the new key. Sure, the content of those encrypted emails is not secure, as an attacker might now be able to decrypt them, but what's done is done. Your sudden loss of the ability to read those emails surely doesn't undo the damage. If anything, it's worse because you no longer know for sure what secret the attacker got to know. What you really want to do is to make sure that your friend stops using your old key and that from now on all further communication is encrypted with a new key pair (i.e. your friend must know about that new key). The same logic applies to SealedSecrets. The ultimate goal is to secure your actual "user" secrets. The "sealing" secrets are just a mechanism, an "envelope". If a secret is leaked there is no going back, what's done is done. You first need to ensure that new secrets don't get encrypted with that old compromised key (in the email analogy above that's: create a new key pair and give all your friends your new public key). The second logical step is to neutralize the damage, which depends on the nature of the secret. A simple example is a database password: if you accidentally leak your database password, the thing you're supposed to do is simply to change your database password (on the database; and revoke the old one!) *and* update the `SealedSecret` resource with the new password (i.e. running `kubeseal` again). Both steps are described in the previous sections, albeit in a less verbose way. There is no shame in reading them again, now that you have a more in-depth grasp of the underlying rationale. ### Manual key management (advanced) The `SealedSecret` controller and the associated workflow are designed to keep old sealing keys around and periodically add new ones. You should not delete old keys unless you know what you're doing. That said, if you want you can manually manage (create, move, delete) *sealing keys*. They are just normal k8s secrets living in the same namespace where the `SealedSecret` controller lives (usually `kube-system`, but it's configurable). There are advanced use cases that you can address by creative management of the sealing keys. For example, you can share the same sealing key among a few clusters so that you can apply exactly the same sealed secret in multiple clusters. Since sealing keys are just normal k8s secrets you can even use sealed secrets themselves and use a GitOps workflow to manage your sealing keys (useful when you want to share the same key among different clusters)! Labeling a *sealing key* secret with anything other than `active` effectively deletes the key from the `SealedSecret` controller, but it is still available in k8s for manual encryption/decryption if need be. **NOTE** `SealedSecret` controller currently does not automatically pick up manually created, deleted or relabeled sealing keys. An admin must restart the controller before the effect will apply. ### Re-encryption (advanced) Before you can get rid of some old sealing keys you need to re-encrypt your SealedSecrets with the latest private key. ```bash kubeseal --re-encrypt tmp.json \ && mv tmp.json my_sealed_secret.json ``` The invocation above will produce a new sealed secret file freshly encrypted with the latest key, without making the secrets leave the cluster to the client. You can then save that file in your version control system (`kubeseal --re-encrypt` doesn't update the in-cluster object). Currently, old keys are not garbage collected automatically. It's a good idea to periodically re-encrypt your SealedSecrets. But as mentioned above, don't lull yourself in a false sense of security: you must assume the old version of the `SealedSecret` resource (the one encrypted with a key you think of as dead) is still potentially around and accessible to attackers. I.e. re-encryption is not a substitute for periodically rotating your actual secrets. ## Details (advanced) This controller adds a new `SealedSecret` custom resource. The interesting part of a `SealedSecret` is a base64-encoded asymmetrically encrypted `Secret`. The controller maintains a set of private/public key pairs as kubernetes secrets. Keys are labeled with `sealedsecrets.bitnami.com/sealed-secrets-key` and identified in the label as either `active` or `compromised`. On startup, The sealed secrets controller will... 1. Search for these keys and add them to its local store if they are labeled as active. 2. Create a new key 3. Start the key rotation cycle ### Crypto More details about crypto can be found [here](docs/developer/crypto.md). ## Developing Developing guidelines can be found [in the Developer Guide](docs/developer/README.md). ## FAQ ### Can I encrypt multiple secrets at once, in one YAML / JSON file? Yes, you can! Drop as many secrets as you like in one file. Make sure to separate them via `---` for YAML and as extra, single objects in JSON. ### Will you still be able to decrypt if you no longer have access to your cluster? No, the private keys are only stored in the Secret managed by the controller (unless you have some other backup of your k8s objects). There are no backdoors - without that private key used to encrypt a given SealedSecrets, you can't decrypt it. If you can't get to the Secrets with the encryption keys, and you also can't get to the decrypted versions of your Secrets live in the cluster, then you will need to regenerate new passwords for everything, seal them again with a new sealing key, etc. ### How can I do a backup of my SealedSecrets? If you do want to make a backup of the encryption private keys, it's easy to do from an account with suitable access: ```bash kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml >main.key echo "---" >> main.key kubectl get secret -n kube-system sealed-secrets-key -o yaml >>main.key ``` > NOTE: You need the second statement only if you ever installed sealed-secrets older than version 0.9.x on your cluster. > NOTE: This file will contain the controller's public + private keys and should be kept omg-safe! > NOTE: After sealing key renewal you should recreate your backup. Otherwise, your backup won't be able to decrypt new sealed secrets. To restore from a backup after some disaster, just put that secrets back before starting the controller - or if the controller was already started, replace the newly-created secrets and restart the controller: * For Helm deployment: ```bash kubectl apply -f main.key kubectl delete pod -n kube-system -l app.kubernetes.io/name=sealed-secrets ``` * For deployment via `controller.yaml` manifest ```bash kubectl apply -f main.key kubectl delete pod -n kube-system -l name=sealed-secrets-controller ``` ### Can I decrypt my secrets offline with a backup key? While treating sealed-secrets as long term storage system for secrets is not the recommended use case, some people do have a legitimate requirement for being able to recover secrets when the k8s cluster is down and restoring a backup into a new `SealedSecret` controller deployment is not practical. If you have backed up one or more of your private keys (see previous question), you can use the `kubeseal --recovery-unseal --recovery-private-key file1.key,file2.key,...` command to decrypt a sealed secrets file. ### What flags are available for kubeseal? You can check the flags available using `kubeseal --help`. ### How do I update parts of JSON/YAML/TOML/.. file encrypted with sealed secrets? A kubernetes `Secret` resource contains multiple items, basically a flat map of key/value pairs. SealedSecrets operate at that level, and does not care what you put in the values. In other words it cannot make sense of any structured configuration file you might have put in a secret and thus cannot help you update individual fields in it. Since this is a common problem, especially when dealing with legacy applications, we do offer an [example](docs/examples/config-template) of a possible workaround. ### Can I bring my own (pre-generated) certificates? Yes, you can provide the controller with your own certificates, and it will consume them. Please check [here](docs/bring-your-own-certificates.md) for a workaround. ### How to use kubeseal if the controller is not running within the `kube-system` namespace? If you installed the controller in a different namespace than the default `kube-system`, you need to provide this namespace to the `kubeseal` commandline tool. There are two options: 1. You can specify the namespace via the command line option `--controller-namespace `: ```bash kubeseal --controller-namespace sealed-secrets mysealedsecret.json ``` 2. Via the environment variable `SEALED_SECRETS_CONTROLLER_NAMESPACE`: ```bash export SEALED_SECRETS_CONTROLLER_NAMESPACE=sealed-secrets kubeseal mysealedsecret.json ``` ### How to verify the images? Our images are being signed using [cosign](https://github.com/sigstore/cosign). The signatures have been saved in our [GitHub Container Registry](https://ghcr.io/bitnami-labs/sealed-secrets-controller/signs). > Images up to and including v0.20.2 were signed using Cosign v1. Newer images are signed with Cosign v2. It is pretty simple to verify the images: ```bash # export the COSIGN_VARIABLE setting up the GitHub container registry signs path export COSIGN_REPOSITORY=ghcr.io/bitnami-labs/sealed-secrets-controller/signs # verify the image uploaded in GHCR cosign verify --key .github/workflows/cosign.pub ghcr.io/bitnami-labs/sealed-secrets-controller:latest # verify the image uploaded in Dockerhub cosign verify --key .github/workflows/cosign.pub docker.io/bitnami/sealed-secrets-controller:latest ``` ### How to use one controller for a subset of namespaces If you want to use one controller for more than one namespace, but not all namespaces, you can provide additional namespaces using the command line flag `--additional-namespaces=,,<...>`. Make sure you provide appropriate roles and rolebindings in the target namespaces, so the controller can manage the secrets in there. ### Can I configure the Controller unseal retries? The answer is yes, you can configure the number of retries in your controller using the flag `--max-unseal-retries`. This flag allows you to configure the number of maximum retries to unseal your Sealed Secrets. ### How to manage SealedSecrets across the cluster or specific namespaces? By default, the controller watches for `SealedSecret` resources across **all namespaces** using the `--all-namespaces` flag (which defaults to `true`). If you need to restrict the controller's scope, you have two options: - **Watch a subset of namespaces:** Use the `--additional-namespaces=,` flag to provide a comma-separated list of namespaces for the controller to manage. - **Watch only the local namespace:** Set `--all-namespaces=false` (or the environment variable `SEALED_SECRETS_ALL_NAMESPACES=false`). This is useful for multi-tenant clusters where you want isolated controllers with independent sealing keys in each namespace. ## Community - [#sealed-secrets on Kubernetes Slack](https://kubernetes.slack.com/messages/sealed-secrets) Click [here](http://slack.k8s.io) to sign up to the Kubernetes Slack org. ### Related projects - `kseal` A Kubeseal Companion: [https://github.com/eznix86/kseal](https://github.com/eznix86/kseal) - `kubeseal-convert`: [https://github.com/EladLeev/kubeseal-convert](https://github.com/EladLeev/kubeseal-convert) - Visual Studio Code extension: [https://marketplace.visualstudio.com/items?itemName=codecontemplator.kubeseal](https://marketplace.visualstudio.com/items?itemName=codecontemplator.kubeseal) - WebSeal: generates secrets in the browser: [https://socialgouv.github.io/webseal](https://socialgouv.github.io/webseal) - HybridEncrypt TypeScript implementation: [https://github.com/SocialGouv/aes-gcm-rsa-oaep](https://github.com/SocialGouv/aes-gcm-rsa-oaep) - [DEPRACATED] Sealed Secrets Operator: [https://github.com/disposab1e/sealed-secrets-operator-helm](https://github.com/disposab1e/sealed-secrets-operator-helm) ================================================ FILE: RELEASE-NOTES.md ================================================ # Release Notes Latest release: [![](https://img.shields.io/github/release/bitnami-labs/sealed-secrets.svg)](https://github.com/bitnami-labs/sealed-secrets/releases/latest) ## v0.36.1 - Doc/issue 501 all namespaces ([#1900](https://github.com/bitnami-labs/sealed-secrets/pull/1900)) - Bump go 1.26.1 ([#1914](https://github.com/bitnami-labs/sealed-secrets/pull/1914)) - Update actions/setup-go to v6.2.0 ([#1906](https://github.com/bitnami-labs/sealed-secrets/pull/1906)) - fix: explicitly specify TCP protocol for helm SSA compatibility (#692) ([#1901](https://github.com/bitnami-labs/sealed-secrets/pull/1901)) - docs: document GKE Warden and RBAC restrictions ([#1892](https://github.com/bitnami-labs/sealed-secrets/pull/1892)) - Bump k8s.io/klog/v2 from 2.130.1 to 2.140.0 ([#1913](https://github.com/bitnami-labs/sealed-secrets/pull/1913)) - chore: remove note about deprecation of helm chart. ([#1902](https://github.com/bitnami-labs/sealed-secrets/pull/1902)) - Bump k8s.io/code-generator from 0.35.1 to 0.35.2 ([#1909](https://github.com/bitnami-labs/sealed-secrets/pull/1909)) - Bump k8s.io/client-go from 0.35.1 to 0.35.2 ([#1908](https://github.com/bitnami-labs/sealed-secrets/pull/1908)) - Bump distroless/static from `d90359c` to `28efbe9` in /docker ([#1912](https://github.com/bitnami-labs/sealed-secrets/pull/1912)) - Fix oci push action ([#1899](https://github.com/bitnami-labs/sealed-secrets/pull/1899)) ## v0.36.0 - [Security] Preserve scope during Sealed Secret rotation ([#1886](https://github.com/bitnami-labs/sealed-secrets/pull/1886)) - [Security] Throw an error in case of inconsistencies in the Sealed Secrets ([#1885](https://github.com/bitnami-labs/sealed-secrets/pull/1885)) - Bump distroless/static from `972618c` to `d90359c` in /docker ([#1884](https://github.com/bitnami-labs/sealed-secrets/pull/1884)) - Set up OCI GH to release helm chart ([#1883](https://github.com/bitnami-labs/sealed-secrets/pull/1883)) ## v0.35.0 - my namespace as key namespace ([#1867](https://github.com/bitnami-labs/sealed-secrets/pull/1867)) - Bump go 1.25.7 ([#1880](https://github.com/bitnami-labs/sealed-secrets/pull/1880)) - Update client-go and api 0.35.0 ([#1868](https://github.com/bitnami-labs/sealed-secrets/pull/1868)) - Bump golang.org/x/crypto from 0.46.0 to 0.47.0 ([#1863](https://github.com/bitnami-labs/sealed-secrets/pull/1863)) - Bump github.com/onsi/gomega from 1.38.3 to 1.39.0 ([#1865](https://github.com/bitnami-labs/sealed-secrets/pull/1865)) - Bump github.com/onsi/ginkgo/v2 from 2.27.3 to 2.27.5 ([#1864](https://github.com/bitnami-labs/sealed-secrets/pull/1864)) - Bump distroless/static from `4b2a093` to `cd64bec` in /docker ([#1866](https://github.com/bitnami-labs/sealed-secrets/pull/1866)) - Bump k8s.io/code-generator from 0.34.3 to 0.35.0 ([#1858](https://github.com/bitnami-labs/sealed-secrets/pull/1858)) ## v0.34.0 - Add kseal to README ([#1852)](https://github.com/bitnami-labs/sealed-secrets/pull/1852)) - Bump golang version to the latest available 1.24 ([#1854](https://github.com/bitnami-labs/sealed-secrets/pull/1854)) - Bump k8s.io/code-generator from 0.34.2 to 0.34.3 ([#1850](https://github.com/bitnami-labs/sealed-secrets/pull/1850)) - Bump k8s.io/client-go from 0.34.2 to 0.34.3 ([#1848](https://github.com/bitnami-labs/sealed-secrets/pull/1848)) - Bump github.com/onsi/ginkgo/v2 from 2.27.2 to 2.27.3 ([#1843](https://github.com/bitnami-labs/sealed-secrets/pull/1843)) - Bump distroless/static from `87bce11` to `4b2a093` in /docker ([#1846](https://github.com/bitnami-labs/sealed-secrets/pull/1846)) - Bump github.com/onsi/gomega from 1.38.2 to 1.38.3 ([#1844](https://github.com/bitnami-labs/sealed-secrets/pull/1844)) - Bump golang.org/x/crypto from 0.45.0 to 0.46.0 ([#1845](https://github.com/bitnami-labs/sealed-secrets/pull/1845)) - Make controllers kubeclient QPS & Burst configurable. ([#1834](https://github.com/bitnami-labs/sealed-secrets/pull/1834)) - use default method to watch for key secrets ([#1831](https://github.com/bitnami-labs/sealed-secrets/pull/1831)) - Bump golang.org/x/crypto from 0.44.0 to 0.45.0 in the go_modules group across 1 directory ([#1840](https://github.com/bitnami-labs/sealed-secrets/pull/1840)) - Bump k8s.io/code-generator from 0.34.1 to 0.34.2 ([#1839](https://github.com/bitnami-labs/sealed-secrets/pull/1839)) - Bump golang.org/x/crypto from 0.43.0 to 0.44.0 ([#1835](https://github.com/bitnami-labs/sealed-secrets/pull/1835)) - Bump k8s.io/client-go from 0.34.1 to 0.34.2 ([#1837](https://github.com/bitnami-labs/sealed-secrets/pull/1837)) ## v0.33.1 - Release done to fix missing helm chart code. ## v0.33.0 - Bump Go to 1.25.4 ([#1823](https://github.com/bitnami-labs/sealed-secrets/pull/1823)) - Bump github.com/onsi/ginkgo/v2 from 2.26.0 to 2.27.2 ([#1820](https://github.com/bitnami-labs/sealed-secrets/pull/1820)) - Bump golang.org/x/crypto from 0.42.0 to 0.43.0 ([#1818](https://github.com/bitnami-labs/sealed-secrets/pull/1818)) - Bump github.com/onsi/ginkgo/v2 from 2.25.3 to 2.26.0 ([#1817](https://github.com/bitnami-labs/sealed-secrets/pull/1817)) ## v0.32.2 - Fix controller yaml ([#1811](https://github.com/bitnami-labs/sealed-secrets/pull/1811)) - Bump k8s.io/code-generator from 0.33.4 to 0.34.1 ([#1809](https://github.com/bitnami-labs/sealed-secrets/pull/1809)) ## v0.32.1 - Bump distroless version ([#1804](https://github.com/bitnami-labs/sealed-secrets/pull/1804)) ## v0.32.0 - Fix regression mismatching namespace ([#1798](https://github.com/bitnami-labs/sealed-secrets/pull/1798)) - Bump k8s.io/apimachinery from 0.33.4 to 0.34.0 ([#1795](https://github.com/bitnami-labs/sealed-secrets/pull/1795)) - Bump github.com/spf13/pflag from 1.0.7 to 1.0.10 ([#1794](https://github.com/bitnami-labs/sealed-secrets/pull/1794)) - Bump github.com/onsi/ginkgo/v2 from 2.25.1 to 2.25.3 ([#1793](https://github.com/bitnami-labs/sealed-secrets/pull/1793)) - Bump golang.org/x/crypto from 0.41.0 to 0.42.0 ([#1797](https://github.com/bitnami-labs/sealed-secrets/pull/1797)) - Bump github.com/prometheus/client_golang from 1.23.0 to 1.23.2 ([#1796](https://github.com/bitnami-labs/sealed-secrets/pull/1796)) - Bump github.com/onsi/gomega from 1.38.0 to 1.38.1 ([#1787](https://github.com/bitnami-labs/sealed-secrets/pull/1787)) - Bump k8s.io/client-go from 0.33.3 to 0.33.4 ([#1774](https://github.com/bitnami-labs/sealed-secrets/pull/1774)) - Bump k8s.io/api from 0.33.3 to 0.33.4 ([#1775](https://github.com/bitnami-labs/sealed-secrets/pull/1775)) - Bump github.com/onsi/ginkgo/v2 from 2.23.4 to 2.24.0 ([#1776](https://github.com/bitnami-labs/sealed-secrets/pull/1776)) - Bump k8s.io/apimachinery from 0.33.3 to 0.33.4 ([#1777](https://github.com/bitnami-labs/sealed-secrets/pull/1788)) - Bump k8s.io/code-generator from 0.33.3 to 0.33.4 ([#1778](https://github.com/bitnami-labs/sealed-secrets/pull/1778)) ## v0.31.0 - Helm: add watch for secrets ([#1758](https://github.com/bitnami-labs/sealed-secrets/pull/1758)) - Simplify VIB helm chart validation ([#1771](https://github.com/bitnami-labs/sealed-secrets/pull/1771)) - Fix: metrics cleanup for deleted SealedSecrets ([#1764](https://github.com/bitnami-labs/sealed-secrets/pull/1764)) - Fix keyrenewperiod template chart ([#1756](https://github.com/bitnami-labs/sealed-secrets/pull/1756)) - Fix namespace validation to prevent mismatch errors ([#1754](https://github.com/bitnami-labs/sealed-secrets/pull/1754)) - Bump VIB action version and updates the service URL ([#1770](https://github.com/bitnami-labs/sealed-secrets/pull/1770)) - Bump golang version to latest available one for 1.24 ([#1769](https://github.com/bitnami-labs/sealed-secrets/pull/1769)) - Bump golang.org/x/crypto from 0.40.0 to 0.41.0 ([#1768](https://github.com/bitnami-labs/sealed-secrets/pull/1768)) - Bump github.com/prometheus/client_golang from 1.22.0 to 1.23.0 ([#1767](https://github.com/bitnami-labs/sealed-secrets/pull/1767)) - Bump k8s.io/api from 0.33.2 to 0.33.3 ([#1766](https://github.com/bitnami-labs/sealed-secrets/pull/1766)) - Bump github.com/spf13/pflag from 1.0.6 to 1.0.7 ([#1765](https://github.com/bitnami-labs/sealed-secrets/pull/1765)) - Bump k8s.io/client-go from 0.33.2 to 0.33.3 ([#1761](https://github.com/bitnami-labs/sealed-secrets/pull/1761)) - Bump github.com/onsi/gomega from 1.37.0 to 1.38.0 ([#1760](https://github.com/bitnami-labs/sealed-secrets/pull/1760)) - Bump k8s.io/code-generator from 0.33.2 to 0.33.3 ([#1759](https://github.com/bitnami-labs/sealed-secrets/pull/1759)) - Bump golang.org/x/crypto from 0.39.0 to 0.40.0 ([#1755](https://github.com/bitnami-labs/sealed-secrets/pull/1755)) - Bump k8s.io/code-generator from 0.33.1 to 0.33.2 ([#1752](https://github.com/bitnami-labs/sealed-secrets/pull/1752)) ## v0.30.0 - Bump golang to 1.24.4 ([#1743](https://github.com/bitnami-labs/sealed-secrets/pull/1743)) - Fix typo in RBAC namespaced roles documentation ([#1720](https://github.com/bitnami-labs/sealed-secrets/pull/1720)) - Bump to go1.24.1 ([#1713](https://github.com/bitnami-labs/sealed-secrets/pull/1713)) - Fix potential controller sensitive data exposure by sprig template functions ([#1703](https://github.com/bitnami-labs/sealed-secrets/pull/1703)) - Bump golang.org/x/crypto from 0.38.0 to 0.39.0 ([#1742](https://github.com/bitnami-labs/sealed-secrets/pull/1742)) - Bump k8s.io/client-go from 0.33.0 to 0.33.1 ([#1734](https://github.com/bitnami-labs/sealed-secrets/pull/1734)) - Bump k8s.io/api from 0.33.0 to 0.33.1 ([#1733](https://github.com/bitnami-labs/sealed-secrets/pull/1733)) - Bump k8s.io/code-generator from 0.33.0 to 0.33.1 ([#1732](https://github.com/bitnami-labs/sealed-secrets/pull/1732)) - Bump golang.org/x/crypto from 0.37.0 to 0.38.0 ([#1731](https://github.com/bitnami-labs/sealed-secrets/pull/1731)) - Bump k8s.io/client-go from 0.32.3 to 0.33.0 ([#1729](https://github.com/bitnami-labs/sealed-secrets/pull/1729)) - Bump k8s.io/code-generator from 0.32.3 to 0.33.0 ([#1728](https://github.com/bitnami-labs/sealed-secrets/pull/1728)) - Bump k8s.io/api from 0.32.3 to 0.33.0 ([#1730](https://github.com/bitnami-labs/sealed-secrets/pull/1730)) - Bump golang.org/x/net from 0.37.0 to 0.38.0 in the go_modules group ([#1725](https://github.com/bitnami-labs/sealed-secrets/pull/1725)) - Bump github.com/prometheus/client_golang from 1.21.1 to 1.22.0 ([#1724](https://github.com/bitnami-labs/sealed-secrets/pull/1724)) - Bump github.com/onsi/gomega from 1.36.3 to 1.37.0 ([#1722](https://github.com/bitnami-labs/sealed-secrets/pull/1722)) - Bump github.com/onsi/ginkgo/v2 from 2.23.3 to 2.23.4 ([#1723](https://github.com/bitnami-labs/sealed-secrets/pull/1723)) - Bump golang.org/x/crypto from 0.36.0 to 0.37.0 ([#1721](https://github.com/bitnami-labs/sealed-secrets/pull/1721)) ## v0.29.0 - Fix register a key using secret creationTimestamp instead of certificate validity timestamp ([#1681](https://github.com/bitnami-labs/sealed-secrets/pull/1681)) - Bump to go1.23.7 ([#1714](https://github.com/bitnami-labs/sealed-secrets/pull/1714)) - Update environment k8s version on CI ([#1688](https://github.com/bitnami-labs/sealed-secrets/pull/1688)) - Update go tooling to 1.23.6 ([#1686](https://github.com/bitnami-labs/sealed-secrets/pull/1686)) - Bump github.com/onsi/gomega from 1.36.2 to 1.36.3 ([#1712](https://github.com/bitnami-labs/sealed-secrets/pull/1712)) - Bump github.com/onsi/ginkgo/v2 from 2.23.0 to 2.23.3 ([#1711](https://github.com/bitnami-labs/sealed-secrets/pull/1711)) - Bump k8s.io/code-generator from 0.32.2 to 0.32.3 ([#1708](https://github.com/bitnami-labs/sealed-secrets/pull/1708)) - Bump k8s.io/client-go from 0.32.2 to 0.32.3 ([#1705](https://github.com/bitnami-labs/sealed-secrets/pull/1705)) - Bump golang.org/x/net from 0.35.0 to 0.36.0 in the go_modules group ([#1702](https://github.com/bitnami-labs/sealed-secrets/pull/1702)) - Bump golang.org/x/crypto from 0.35.0 to 0.36.0 ([#1699](https://github.com/bitnami-labs/sealed-secrets/pull/1699)) - Bump github.com/prometheus/client_golang from 1.21.0 to 1.21.1 ([#1699](https://github.com/bitnami-labs/sealed-secrets/pull/1699)) - Bump github.com/onsi/ginkgo/v2 from 2.22.2 to 2.23.0 ([#1701](https://github.com/bitnami-labs/sealed-secrets/pull/1701)) - Bump github.com/prometheus/client_golang from 1.20.5 to 1.21.0 ([#1695](https://github.com/bitnami-labs/sealed-secrets/pull/1695)) - Bump github.com/google/go-cmp from 0.6.0 to 0.7.0 ([#1696](https://github.com/bitnami-labs/sealed-secrets/pull/1696)) - Bump golang.org/x/crypto from 0.33.0 to 0.35.0 ([#1697](https://github.com/bitnami-labs/sealed-secrets/pull/1697)) - Bump k8s.io/client-go from 0.32.1 to 0.32.2 ([#1691](https://github.com/bitnami-labs/sealed-secrets/pull/1691)) - Bump k8s.io/code-generator from 0.32.1 to 0.32.2 ([#1693](https://github.com/bitnami-labs/sealed-secrets/pull/1693)) - Bump golang.org/x/crypto from 0.32.0 to 0.33.0 ([#1685](https://github.com/bitnami-labs/sealed-secrets/pull/1685)) - Bump github.com/spf13/pflag from 1.0.5 to 1.0.6 ([#1683](https://github.com/bitnami-labs/sealed-secrets/pull/1683)) - Bump k8s.io/client-go from 0.32.0 to 0.32.1 ([#1678](https://github.com/bitnami-labs/sealed-secrets/pull/1678)) - Bump k8s.io/code-generator from 0.32.0 to 0.32.1 ([#1677](https://github.com/bitnami-labs/sealed-secrets/pull/1677)) ## v0.28.0 - fix: explicitly set resourceFieldRef.divisor ([#1655](https://github.com/bitnami-labs/sealed-secrets/pull/1655)) - Fix deprecated functions for bumping client-go ([#1667](https://github.com/bitnami-labs/sealed-secrets/pull/1667)) - Bump github.com/onsi/ginkgo/v2 from 2.22.1 to 2.22.2 ([#1670](https://github.com/bitnami-labs/sealed-secrets/pull/1670)) - Bump golang.org/x/crypto from 0.31.0 to 0.32.0 ([#1671](https://github.com/bitnami-labs/sealed-secrets/pull/1671)) - Bump github.com/onsi/gomega from 1.36.1 to 1.36.2 ([#1669](https://github.com/bitnami-labs/sealed-secrets/pull/1669)) - Bump github.com/onsi/ginkgo/v2 from 2.22.0 to 2.22.1 ([#1668](https://github.com/bitnami-labs/sealed-secrets/pull/1668)) - Bump github.com/onsi/gomega from 1.36.0 to 1.36.1 ([#1664](https://github.com/bitnami-labs/sealed-secrets/pull/1664)) - Bump golang.org/x/crypto from 0.30.0 to 0.31.0 ([#1659](https://github.com/bitnami-labs/sealed-secrets/pull/1659)) - Bump golang.org/x/crypto from 0.29.0 to 0.30.0 ([#1657](https://github.com/bitnami-labs/sealed-secrets/pull/1657)) ## v0.27.3 - Bump k8s.io/apimachinery from 0.31.2 to 0.31.3 ([#1642](https://github.com/bitnami-labs/sealed-secrets/pull/1642)) - Bump k8s.io/code-generator from 0.31.2 to 0.31.3 ([#1643](https://github.com/bitnami-labs/sealed-secrets/pull/1643)) - Bump github.com/onsi/gomega from 1.35.1 to 1.36.0 ([#1645](https://github.com/bitnami-labs/sealed-secrets/pull/1645)) - re-introduce install instructions with to releases ([#1649](https://github.com/bitnami-labs/sealed-secrets/pull/1649)) - Properly error out when input file doesn't exist ([#1640](https://github.com/bitnami-labs/sealed-secrets/pull/1640)) - Bump github.com/onsi/ginkgo/v2 from 2.21.0 to 2.22.0 ([#1641](https://github.com/bitnami-labs/sealed-secrets/pull/1641)) - Bump golang.org/x/crypto from 0.28.0 to 0.29.0 ([#1635](https://github.com/bitnami-labs/sealed-secrets/pull/1635)) - Configure max retries ([#1633](https://github.com/bitnami-labs/sealed-secrets/pull/1633)) - Label "app.kubernetes.io/instance" in the Prometheus metric ([#1620](https://github.com/bitnami-labs/sealed-secrets/pull/1620)) - Bump github.com/onsi/gomega from 1.34.2 to 1.35.1 ([#1624](https://github.com/bitnami-labs/sealed-secrets/pull/1624)) - Adding keyttl and keycutofftime options to helm chart ([#1610](https://github.com/bitnami-labs/sealed-secrets/pull/1610)) - Bump github.com/onsi/ginkgo/v2 from 2.20.2 to 2.21.0 ([#1623](https://github.com/bitnami-labs/sealed-secrets/pull/1623)) ## v0.27.2 - feature: Show error if there's no secret to encode ([#1580](https://github.com/bitnami-labs/sealed-secrets/pull/1580)) - feature: allow container port configuration ([#1606](https://github.com/bitnami-labs/sealed-secrets/pull/1606)) - chore: Update go version to 1.22.8 ([#1621](https://github.com/bitnami-labs/sealed-secrets/pull/1621)) - chore: Update the TCSP settings for helm testing ([#1608](https://github.com/bitnami-labs/sealed-secrets/pull/1608)) - chore: Redirect external site to the GitHub Repository ([#1589](https://github.com/bitnami-labs/sealed-secrets/pull/1589)) - chore: Update dependencies (Several automatic PRs) ## v0.27.1 - chore: Update dependencies ([#1565](https://github.com/bitnami-labs/sealed-secrets/pull/1565)) - chore: Bump golang.org/x/crypto from 0.24.0 to 0.25.0 ([#1561](https://github.com/bitnami-labs/sealed-secrets/pull/1561)) - chore: Bump k8s.io/klog/v2 from 2.130.0 to 2.130.1 ([#1558](https://github.com/bitnami-labs/sealed-secrets/pull/1558)) - chore: Improve release process ([#1559](https://github.com/bitnami-labs/sealed-secrets/pull/1559)) ## v0.27.0 - feature: loadbalancerclass ([#1545](https://github.com/bitnami-labs/sealed-secrets/pull/1545)) - Add sprig function library for templating ([#1542](https://github.com/bitnami-labs/sealed-secrets/pull/1542)) - Update install instructions for consistent HTTP request package ([#1546](https://github.com/bitnami-labs/sealed-secrets/pull/1546)) - Bump k8s.io/client-go from 0.30.1 to 0.30.2 ([#1552](https://github.com/bitnami-labs/sealed-secrets/pull/1552)) - Bump k8s.io/klog/v2 from 2.120.1 to 2.130.0 ([#1551](https://github.com/bitnami-labs/sealed-secrets/pull/1551)) - Bump k8s.io/code-generator from 0.30.1 to 0.30.2 ([#1550](https://github.com/bitnami-labs/sealed-secrets/pull/1550)) - Bump golang.org/x/crypto from 0.23.0 to 0.24.0 ([#1544](https://github.com/bitnami-labs/sealed-secrets/pull/1544)) - Bump github.com/onsi/ginkgo/v2 from 2.17.3 to 2.19.0 ([#1540](https://github.com/bitnami-labs/sealed-secrets/pull/1540)) ## v0.26.3 ### Changelog - fix: code generation ([#1536](https://github.com/bitnami-labs/sealed-secrets/pull/1536)) - fix: show field name in error message when base64 decoding fails ([#1519](https://github.com/bitnami-labs/sealed-secrets/pull/1519)) - helm: Set `GOMAXPROCS` and `GOMEMLIMIT` environment variables ([#1528](https://github.com/bitnami-labs/sealed-secrets/pull/1528)) - docs: mention limitation of backup with key renewal ([#1533](https://github.com/bitnami-labs/sealed-secrets/pull/1533)) - chore: update dependencies ([#1535](https://github.com/bitnami-labs/sealed-secrets/pull/1535)) - chore: Bump k8s.io/code-generator from 0.30.0 to 0.30.1 ([#1529](https://github.com/bitnami-labs/sealed-secrets/pull/1529)) - chore: Bump k8s.io/client-go from 0.30.0 to 0.30.1 ([#1532](https://github.com/bitnami-labs/sealed-secrets/pull/1532)) - chore: Bump github.com/onsi/ginkgo/v2 from 2.17.2 to 2.17.3 ([#1527](https://github.com/bitnami-labs/sealed-secrets/pull/1527)) - chore: Bump github.com/prometheus/client_golang from 1.19.0 to 1.19.1 ([#1526](https://github.com/bitnami-labs/sealed-secrets/pull/1526)) - chore: Bump k8s.io/code-generator from 0.29.3 to 0.30.0 ([#1513](https://github.com/bitnami-labs/sealed-secrets/pull/1513)) - chore: Update dependencies ([#1524](https://github.com/bitnami-labs/sealed-secrets/pull/1524)) - chore: Bump github.com/onsi/gomega from 1.33.0 to 1.33.1 ([#1522](https://github.com/bitnami-labs/sealed-secrets/pull/1522)) - chore: Bump github.com/onsi/ginkgo/v2 from 2.17.1 to 2.17.2 ([#1520](https://github.com/bitnami-labs/sealed-secrets/pull/1520)) - chore: Bump github.com/onsi/gomega from 1.32.0 to 1.33.0 ([#1512](https://github.com/bitnami-labs/sealed-secrets/pull/1512)) - chore: increase vib timeout ([#1509](https://github.com/bitnami-labs/sealed-secrets/pull/1509)) - chore: fix publish-release workflow ([#1508](https://github.com/bitnami-labs/sealed-secrets/pull/1508)) - chore: Bump golang.org/x/crypto from 0.21.0 to 0.22.0 ([#1505](https://github.com/bitnami-labs/sealed-secrets/pull/1505)) ## v0.26.2 ### Changelog - fix: update dependencies and version for CVE-2023-45288 ([#1501](https://github.com/bitnami-labs/sealed-secrets/pull/1501)) - fix(helm): role binding annotations ([#1494](https://github.com/bitnami-labs/sealed-secrets/pull/1494)) - chore: update cosign version ([#1495](https://github.com/bitnami-labs/sealed-secrets/pull/1495)) - chore: Bump github.com/onsi/ginkgo/v2 from 2.16.0 to 2.17.1 ([#1497](https://github.com/bitnami-labs/sealed-secrets/pull/1497)) - chore: Bump k8s.io/client-go from 0.29.2 to 0.29.3 ([#1486](https://github.com/bitnami-labs/sealed-secrets/pull/1486)) - chore: Bump k8s.io/code-generator from 0.29.2 to 0.29.3 ([#1488](https://github.com/bitnami-labs/sealed-secrets/pull/1488)) - chore: Bump github.com/onsi/gomega from 1.31.1 to 1.32.0 ([#1489](https://github.com/bitnami-labs/sealed-secrets/pull/1489)) - chore: Bump k8s.io/apimachinery from 0.29.2 to 0.29.3 ([#1490](https://github.com/bitnami-labs/sealed-secrets/pull/1490)) - chore: Update security contact and other references DL to the new team one ([#1500](https://github.com/bitnami-labs/sealed-secrets/pull/1500)) ## v0.26.1 ### Changelog - fix: panic when patching empty secret ([#1474](https://github.com/bitnami-labs/sealed-secrets/pull/1474)) - fix: Modify LastUpdateTime when the Sealed Secrets is being updated ([#1475](https://github.com/bitnami-labs/sealed-secrets/pull/1475)) - fix: Bring back private keys logging ([#1481](https://github.com/bitnami-labs/sealed-secrets/pull/1481)) - fix: missing common annotations in the helm chart ([#1471](https://github.com/bitnami-labs/sealed-secrets/pull/1471)) - fix: Add metrics port to allow ingress traffic in the netpols ([#1473](https://github.com/bitnami-labs/sealed-secrets/pull/1473)) - chore: Bump google.golang.org/protobuf from 1.32.0 to 1.33.0 ([#1480](https://github.com/bitnami-labs/sealed-secrets/pull/1480)) - chore: Bump golang.org/x/crypto from 0.20.0 to 0.21.0 ([#1477](https://github.com/bitnami-labs/sealed-secrets/pull/1477)) - chore: Bump github.com/onsi/ginkgo/v2 from 2.15.0 to 2.16.0 ([#1478](https://github.com/bitnami-labs/sealed-secrets/pull/1478)) - chore: Bump github.com/prometheus/client_golang from 1.18.0 to 1.19.0 ([#1476](https://github.com/bitnami-labs/sealed-secrets/pull/1476)) - chore: Bump golang.org/x/crypto from 0.19.0 to 0.20.0 ([#1472](https://github.com/bitnami-labs/sealed-secrets/pull/1472)) - chore: Bump k8s.io/code-generator from 0.29.1 to 0.29.2 ([#1467](https://github.com/bitnami-labs/sealed-secrets/pull/1467)) ## v0.26.0 ### Changelog - feat: Implement structured logging ([#1438](https://github.com/bitnami-labs/sealed-secrets/pull/1438)) - feat: [helm] add rbac.proxier config ([#1451](https://github.com/bitnami-labs/sealed-secrets/pull/1451)) - docs: Add clarity around template Secret fields ([#1456](https://github.com/bitnami-labs/sealed-secrets/pull/1456)) - docs: [helm] adding disable keyrenewperiod comment ([#1455](https://github.com/bitnami-labs/sealed-secrets/pull/1455)) - chore: Update Go version and dependencies ([#1460](https://github.com/bitnami-labs/sealed-secrets/pull/1460)) - chore: Bump golang.org/x/crypto from 0.18.0 to 0.19.0 ([#1458](https://github.com/bitnami-labs/sealed-secrets/pull/1458)) - chore: Bump k8s.io/client-go from 0.29.0 to 0.29.1 ([#1452](https://github.com/bitnami-labs/sealed-secrets/pull/1452)) - chore: Bump k8s.io/code-generator from 0.29.0 to 0.29.1 ([#1441](https://github.com/bitnami-labs/sealed-secrets/pull/1441)) - chore: Bump k8s.io/api from 0.29.0 to 0.29.1 ([#1443](https://github.com/bitnami-labs/sealed-secrets/pull/1443)) - chore: Bump k8s.io/klog/v2 from 2.120.0 to 2.120.1 ([#1439](https://github.com/bitnami-labs/sealed-secrets/pull/1439)) - chore: Bump github.com/onsi/gomega from 1.30.0 to 1.31.1 ([#1440](https://github.com/bitnami-labs/sealed-secrets/pull/1440)) ## v0.25.0 ### Changelog - feat: support immutable secrets ([#1395](https://github.com/bitnami-labs/sealed-secrets/pull/1395)) - Update dependencies ([#1411](https://github.com/bitnami-labs/sealed-secrets/pull/1411)) - Support fetching certificate URL via proxy environment variables ([#1419](https://github.com/bitnami-labs/sealed-secrets/pull/1419)) - Bump github.com/onsi/ginkgo/v2 from 2.13.2 to 2.14.0 ([#1432](https://github.com/bitnami-labs/sealed-secrets/pull/1432) - Bump k8s.io/klog/v2 from 2.110.1 to 2.120.0 ([#1431](https://github.com/bitnami-labs/sealed-secrets/pull/1431)) - Bump golang.org/x/crypto from 0.17.0 to 0.18.0 ([#1425](https://github.com/bitnami-labs/sealed-secrets/pull/1425)) - Bump github.com/prometheus/client_golang from 1.17.0 to 1.18.0 ([#1421](https://github.com/bitnami-labs/sealed-secrets/pull/1421)) - Bump k8s.io/code-generator from 0.28.4 to 0.29.0 ([#1406](https://github.com/bitnami-labs/sealed-secrets/pull/1406)) - Bump golang.org/x/crypto from 0.16.0 to 0.17.0 ([#1405](https://github.com/bitnami-labs/sealed-secrets/pull/1405)) ## v0.24.5 ### Changelog - feat: Helm - Add sources ([#1383](https://github.com/bitnami-labs/sealed-secrets/pull/1383)) - Update golang to the latest tooling version ([#1398](https://github.com/bitnami-labs/sealed-secrets/pull/1398)) - Bump github.com/onsi/ginkgo/v2 from 2.13.1 to 2.13.2 ([#1397](https://github.com/bitnami-labs/sealed-secrets/pull/1397)) - Bump golang.org/x/crypto from 0.15.0 to 0.16.0 ([#1394](https://github.com/bitnami-labs/sealed-secrets/pull/1394)) - Bump k8s.io/code-generator from 0.28.3 to 0.28.4 ([#1390](https://github.com/bitnami-labs/sealed-secrets/pull/1390)) - Bump k8s.io/client-go from 0.28.3 to 0.28.4 ([#1389](https://github.com/bitnami-labs/sealed-secrets/pull/1389)) - Bump k8s.io/client-go from 0.28.3 to 0.28.4 ([#1389](https://github.com/bitnami-labs/sealed-secrets/pull/1389)) ## v0.24.4 ### Changelog - kubeseal: write help message to stdout ([#1377](https://github.com/bitnami-labs/sealed-secrets/pull/1377)) - fix: Set up LastTransitionTime in case that it is empty ([#1370](https://github.com/bitnami-labs/sealed-secrets/pull/1370)) - Bump github.com/onsi/gomega from 1.29.0 to 1.30.0 ([#1376](https://github.com/bitnami-labs/sealed-secrets/pull/1376)) - Bump golang.org/x/crypto from 0.14.0 to 0.15.0 ([#1375](https://github.com/bitnami-labs/sealed-secrets/pull/1375)) - Bump github.com/onsi/ginkgo/v2 from 2.13.0 to 2.13.1 ([#1374](https://github.com/bitnami-labs/sealed-secrets/pull/1374)) - Bump k8s.io/klog/v2 from 2.100.1 to 2.110.1 ([#1367](https://github.com/bitnami-labs/sealed-secrets/pull/1367)) ## v0.24.3 ### Changelog - fix a bug that kept a sealed secret's generation and observedgeneration out of sync ([#1360](https://github.com/bitnami-labs/sealed-secrets/pull/1360)) - fix: add pdb ([#1340](https://github.com/bitnami-labs/sealed-secrets/pull/1340)) - Bump k8s.io/code-generator from 0.28.2 to 0.28.3 ([#1358](https://github.com/bitnami-labs/sealed-secrets/pull/1340)) - Bump github.com/onsi/gomega from 1.28.1 to 1.29.0 ([#1357](https://github.com/bitnami-labs/sealed-secrets/pull/1357)) - Bump github.com/mattn/go-isatty from 0.0.19 to 0.0.20 ([#1353](https://github.com/bitnami-labs/sealed-secrets/pull/1353)) - Bump github.com/onsi/gomega from 1.28.0 to 1.28.1 ([#1351](https://github.com/bitnami-labs/sealed-secrets/pull/1351)) - Bump k8s.io/client-go from 0.28.2 to 0.28.3 ([#1350](https://github.com/bitnami-labs/sealed-secrets/pull/1350)) - Bump k8s.io/api from 0.28.2 to 0.28.3 ([#1349](https://github.com/bitnami-labs/sealed-secrets/pull/1349)) - Bump github.com/google/go-cmp from 0.5.9 to 0.6.0 ([#1348](https://github.com/bitnami-labs/sealed-secrets/pull/1348)) ## v0.24.2 ### Changelog - Fix issue where sealed secrets status is not updated if sealed secret… ([#1295](https://github.com/bitnami-labs/sealed-secrets/pull/1295)) - Bump golang.org/x/crypto from 0.13.0 to 0.14.0([#1341](https://github.com/bitnami-labs/sealed-secrets/pull/1341)) - Bump github.com/onsi/ginkgo/v2 from 2.12.1 to 2.13.0 ([#1342](https://github.com/bitnami-labs/sealed-secrets/pull/1342)) - Bump golang.org/x/net from 0.14.0 to 0.17.0 ([#1344](https://github.com/bitnami-labs/sealed-secrets/pull/1344)) ## v0.24.1 ### Changelog - fix: remove trailing dashes for multidoc yaml ([#1335](https://github.com/bitnami-labs/sealed-secrets/pull/1335)) ## v0.24.0 ### Changelog - feat: multidoc support for yaml and json ([#1304](https://github.com/bitnami-labs/sealed-secrets/pull/1304)) - Delete repeating warning message ([#1303](https://github.com/bitnami-labs/sealed-secrets/pull/1303)) - Add dashboard configmap annotations ([#1302](https://github.com/bitnami-labs/sealed-secrets/pull/1302)) - Update the golang version to the latest available one ([#1318](https://github.com/bitnami-labs/sealed-secrets/pull/1318)) - Update Linux installation process on README to have a way to dynamically get kubeseal version number ([#1294](https://github.com/bitnami-labs/sealed-secrets/pull/1294)) - Bump golang.org/x/crypto from 0.12.0 to 0.13.0 ([#1319](https://github.com/bitnami-labs/sealed-secrets/pull/1319)) - Bump github.com/onsi/ginkgo/v2 from 2.11.0 to 2.12.0 ([#1310](https://github.com/bitnami-labs/sealed-secrets/pull/1310)) - Bump k8s.io/client-go from 0.28.0 to 0.28.1 ([#1308](https://github.com/bitnami-labs/sealed-secrets/pull/1308)) - Bump k8s.io/code-generator from 0.28.0 to 0.28.1 ([#1307](https://github.com/bitnami-labs/sealed-secrets/pull/1307)) - Bump k8s.io/code-generator from 0.27.4 to 0.28.0 ([#1300](https://github.com/bitnami-labs/sealed-secrets/pull/1300)) - Bump k8s.io/client-go from 0.27.4 to 0.28.0 ([#1297](https://github.com/bitnami-labs/sealed-secrets/pull/1297)) ## v0.23.1 ### Changelog - securityContext adjusted ([#1261](https://github.com/bitnami-labs/sealed-secrets/pull/1261)) - allow changing the default revisionHistoryLimit ([#1286](https://github.com/bitnami-labs/sealed-secrets/pull/1286)) - Bump k8s.io/client-go from 0.27.3 to 0.27.4 ([#1277](https://github.com/bitnami-labs/sealed-secrets/pull/1277)) - Bump k8s.io/code-generator from 0.27.3 to 0.27.4 ([#1278](https://github.com/bitnami-labs/sealed-secrets/pull/1278)) - Bump github.com/onsi/gomega from 1.27.8 to 1.27.10 ([#1279](https://github.com/bitnami-labs/sealed-secrets/pull/1279)) - Bump k8s.io/api from 0.27.3 to 0.27.4 ([#1281](https://github.com/bitnami-labs/sealed-secrets/pull/1281)) - Bump golang.org/x/crypto from 0.11.0 to 0.12.0 ([#1287](https://github.com/bitnami-labs/sealed-secrets/pull/1287) ## v0.23.0 ### Changelog - Add option for custom annotations and labels on sealing keypairs ([#1250](https://github.com/bitnami-labs/sealed-secrets/pull/1250)) - Add option to patch secrets instead of clobbering them ([#1259](https://github.com/bitnami-labs/sealed-secrets/pull/1259)) - Improve CLI UX error message while service is not found ([#1256](https://github.com/bitnami-labs/sealed-secrets/pull/1256)) - Add namespaced roles support to Helm chart ([#1240](https://github.com/bitnami-labs/sealed-secrets/pull/1240)) - add --log-info-stdout to chart ([#1238](https://github.com/bitnami-labs/sealed-secrets/pull/1238)) - Fix networkpolicy port + add egress ([#1243](https://github.com/bitnami-labs/sealed-secrets/pull/1243)) - Create index for Sealed Secrets public documentation ([#1264](https://github.com/bitnami-labs/sealed-secrets/pull/1264)) - Getting started page ([#1253](https://github.com/bitnami-labs/sealed-secrets/pull/1253)) - Create a FAQ document for Sealed Secrets public documentation ([#1269](https://github.com/bitnami-labs/sealed-secrets/pull/1269)) - Create a cryptography document for Sealed Secrets public documentation ([#1267](https://github.com/bitnami-labs/sealed-secrets/pull/1267)) - Validate existing Sealed Secrets document ([#1266](https://github.com/bitnami-labs/sealed-secrets/pull/1266)) - added support policy to readme ([#1265](https://github.com/bitnami-labs/sealed-secrets/pull/1265)) - Add missing document seperator ([#1260](https://github.com/bitnami-labs/sealed-secrets/pull/1260)) - Enable full linter support for golangci-lint ([#1262](https://github.com/bitnami-labs/sealed-secrets/pull/1262)) - Update minikube K8S versions ([#1251](https://github.com/bitnami-labs/sealed-secrets/pull/1251)) - Bump github.com/onsi/ginkgo/v2 from 2.10.0 to 2.11.0 ([#1254](https://github.com/bitnami-labs/sealed-secrets/pull/1254)) - Bump k8s.io/code-generator from 0.27.2 to 0.27.3 ([#1255](https://github.com/bitnami-labs/sealed-secrets/pull/1255)) - Bump golang.org/x/crypto from 0.10.0 to 0.11.0 ([#1268](https://github.com/bitnami-labs/sealed-secrets/pull/1268)) - Bump github.com/prometheus/client_golang from 1.15.1 to 1.16.0 ([#1247](https://github.com/bitnami-labs/sealed-secrets/pull/1247)) - Bump golang.org/x/crypto from 0.9.0 to 0.10.0 ([#1248](https://github.com/bitnami-labs/sealed-secrets/pull/1248)) - Bump k8s.io/client-go from 0.27.2 to 0.27.3 ([#1244](https://github.com/bitnami-labs/sealed-secrets/pull/1244)) ## v0.22.0 ### Changelog - Feature allow to skip set owner references ([#1200](https://github.com/bitnami-labs/sealed-secrets/pull/1200)) - Add additionalPrinterColumns for status and age ([#1217](https://github.com/bitnami-labs/sealed-secrets/pull/1217)) - Add replicas default value to the deployment manifest ([#1219](https://github.com/bitnami-labs/sealed-secrets/pull/1219)) - Create SECURITY.md ([#1226](https://github.com/bitnami-labs/sealed-secrets/pull/1226)) - Fix doc generated code directory ([#1227](https://github.com/bitnami-labs/sealed-secrets/pull/1227)) - Update generated code ([#1228](https://github.com/bitnami-labs/sealed-secrets/pull/1228)) - Update maintainers list ([#1237](https://github.com/bitnami-labs/sealed-secrets/pull/1237)) - Bump github.com/onsi/ginkgo/v2 from 2.9.4 to 2.9.5 ([#1215](https://github.com/bitnami-labs/sealed-secrets/pull/1215)) - Bump golang.org/x/crypto from 0.8.0 to 0.9.0 ([#1216](https://github.com/bitnami-labs/sealed-secrets/pull/1216)) - Bump k8s.io/apimachinery from 0.27.1 to 0.27.2 ([#1221](https://github.com/bitnami-labs/sealed-secrets/pull/1221)) - Bump k8s.io/client-go from 0.27.1 to 0.27.2 ([#1222](https://github.com/bitnami-labs/sealed-secrets/pull/1222)) - Bump github.com/mattn/go-isatty from 0.0.18 to 0.0.19 ([#1223](https://github.com/bitnami-labs/sealed-secrets/pull/1223)) - Bump k8s.io/code-generator from 0.27.1 to 0.27.2 ([#1225](https://github.com/bitnami-labs/sealed-secrets/pull/1225)) - Bump github.com/onsi/gomega from 1.27.6 to 1.27.7 ([#1229](https://github.com/bitnami-labs/sealed-secrets/pull/1229)) - Bump github.com/onsi/ginkgo/v2 from 2.9.5 to 2.9.7 ([#1231](https://github.com/bitnami-labs/sealed-secrets/pull/1231)) - Bump github.com/onsi/gomega from 1.27.7 to 1.27.8 ([#1234](https://github.com/bitnami-labs/sealed-secrets/pull/1234)) - Bump github.com/onsi/ginkgo/v2 from 2.9.7 to 2.10.0 ([#1235](https://github.com/bitnami-labs/sealed-secrets/pull/1235)) ## v0.21.0 ### Changelog - Enable logging info to stdout([#1195](https://github.com/bitnami-labs/sealed-secrets/pull/1195)) - Bump github.com/prometheus/client_golang from 1.15.0 to 1.15.1 ([#1204](https://github.com/bitnami-labs/sealed-secrets/pull/1204)) - Bump github.com/onsi/ginkgo/v2 from 2.9.2 to 2.9.4 ([#1203](https://github.com/bitnami-labs/sealed-secrets/pull/1203)) - Bump k8s.io/klog/v2 from 2.90.1 to 2.100.1 ([#1201](https://github.com/bitnami-labs/sealed-secrets/pull/1201)) - Bump k8s.io/code-generator from 0.26.3 to 0.27.1 ([#1188](https://github.com/bitnami-labs/sealed-secrets/pull/1188)) - Bump k8s.io/client-go from 0.26.3 to 0.27.1 ([#1187](https://github.com/bitnami-labs/sealed-secrets/pull/1187)) - Bump github.com/prometheus/client_golang from 1.14.0 to 1.15.0 ([#1189](https://github.com/bitnami-labs/sealed-secrets/pull/1189)) ## v0.20.5 ### Changelog - Generate embedded ObjectMeta in CRD ([#1177](https://github.com/bitnami-labs/sealed-secrets/pull/1177)) - Sign images using Cosign v2 ([#1176](https://github.com/bitnami-labs/sealed-secrets/pull/1176)) - ReProcess only on spec changes ([#1174](https://github.com/bitnami-labs/sealed-secrets/pull/1174)) - Upgrade sealed secrets to Go 1.20 ([#1173](https://github.com/bitnami-labs/sealed-secrets/pull/1173)) - Fix cosign command for goreleaser ([#1180](https://github.com/bitnami-labs/sealed-secrets/pull/1180)) - Fix kubeseal image sign for cosign v2 ([#1182](https://github.com/bitnami-labs/sealed-secrets/pull/1182)) - Remove automountServiceAccountToken parameter ([#1162](https://github.com/bitnami-labs/sealed-secrets/pull/1162)) - Verify chart with secret recreation disabled ([#1163](https://github.com/bitnami-labs/sealed-secrets/pull/1163)) - Bump golang.org/x/crypto from 0.7.0 to 0.8.0 ([#1175](https://github.com/bitnami-labs/sealed-secrets/pull/1175)) - Bump github.com/onsi/gomega from 1.27.5 to 1.27.6 ([#1169](https://github.com/bitnami-labs/sealed-secrets/pull/1169)) - Bump github.com/onsi/gomega from 1.27.4 to 1.27.5 ([#1168](https://github.com/bitnami-labs/sealed-secrets/pull/1168)) - Bump github.com/mattn/go-isatty from 0.0.17 to 0.0.18 ([#1167](https://github.com/bitnami-labs/sealed-secrets/pull/1167)) - Bump github.com/onsi/ginkgo/v2 from 2.9.1 to 2.9.2 ([#1166](https://github.com/bitnami-labs/sealed-secrets/pull/1166)) - Bump k8s.io/apimachinery from 0.26.2 to 0.26.3 ([#1160](https://github.com/bitnami-labs/sealed-secrets/pull/1160)) - Bump k8s.io/code-generator from 0.26.2 to 0.26.3 ([#1159](https://github.com/bitnami-labs/sealed-secrets/pull/1159)) - Bump k8s.io/api from 0.26.2 to 0.26.3 ([#1158](https://github.com/bitnami-labs/sealed-secrets/pull/1158)) - Bump k8s.io/client-go from 0.26.2 to 0.26.3 ([#1157](https://github.com/bitnami-labs/sealed-secrets/pull/1157)) - Update VIB release tag format ([#1165](https://github.com/bitnami-labs/sealed-secrets/pull/1165)) - Update VIB action ([#1164](https://github.com/bitnami-labs/sealed-secrets/pull/1164)) - Include dockerhub pull statistics in the project README ([#1172](https://github.com/bitnami-labs/sealed-secrets/pull/1172)) ## v0.20.4 Incomplete release ## v0.20.3 Incomplete release ## v0.20.2 ### Changelog - Fix panic when skip recreate is enabled ([#1152](https://github.com/bitnami-labs/sealed-secrets/pull/1152)) ## v0.20.1 ### Changelog - Parametrize cluster role name ([#1141](https://github.com/bitnami-labs/sealed-secrets/pull/1141)) - Allow automountServiceAccountToken to be set to false ([#1128](https://github.com/bitnami-labs/sealed-secrets/pull/1128)) - Allow to disable secret auto-recreation ([#1118](https://github.com/bitnami-labs/sealed-secrets/pull/1118)) - Bump github.com/onsi/gomega from 1.27.2 to 1.27.4 ([#1143](https://github.com/bitnami-labs/sealed-secrets/pull/1143)) - Bump k8s.io/client-go from 0.26.1 to 0.26.2 ([#1136](https://github.com/bitnami-labs/sealed-secrets/pull/1136)) - Bump k8s.io/code-generator from 0.26.1 to 0.26.2 ([#1137](https://github.com/bitnami-labs/sealed-secrets/pull/1137)) - Bump k8s.io/api from 0.26.1 to 0.26.2 ([#1135](https://github.com/bitnami-labs/sealed-secrets/pull/1135)) - Bump github.com/onsi/gomega from 1.27.1 to 1.27.2 ([#1134](https://github.com/bitnami-labs/sealed-secrets/pull/1134)) - Bump k8s.io/apimachinery from 0.26.1 to 0.26.2 ([#1133](https://github.com/bitnami-labs/sealed-secrets/pull/1133)) - Bump k8s.io/klog/v2 from 2.90.0 to 2.90.1 ([#1132](https://github.com/bitnami-labs/sealed-secrets/pull/1132)) - Bump github.com/onsi/ginkgo/v2 from 2.8.3 to 2.9.0 ([#1131](https://github.com/bitnami-labs/sealed-secrets/pull/1131)) - Bump golang.org/x/crypto from 0.6.0 to 0.7.0 ([#1130](https://github.com/bitnami-labs/sealed-secrets/pull/1130)) - Ensure vib runs only when PR is approved ([#1121](https://github.com/bitnami-labs/sealed-secrets/pull/1121)) - Run VIB Helm chart validations on push to main ([#1140](https://github.com/bitnami-labs/sealed-secrets/pull/1140)) - Update parameters table ([#1139](https://github.com/bitnami-labs/sealed-secrets/pull/1139)) - Update docs ([#1127](https://github.com/bitnami-labs/sealed-secrets/pull/1127)) ## v0.20.0 Incomplete release ## v0.19.5 ### Changelog - Automated controller test on Openshift platforms (using ([VMware Image Builder](https://tanzu.vmware.com/image-builder)) ([#1107](https://github.com/bitnami-labs/sealed-secrets/pull/1107)). - We now generate a Carvel package distribution of the controller ([#1104](https://github.com/bitnami-labs/sealed-secrets/pull/1104)). - Bump golang.org/x/crypto from 0.5.0 to 0.6.0 ([#1108](https://github.com/bitnami-labs/sealed-secrets/pull/1108)). - Bump github.com/onsi/gomega from 1.25.0 to 1.26.0 ([#1103](https://github.com/bitnami-labs/sealed-secrets/pull/1103)). - Bump k8s.io/code-generator from 0.26.0 to 0.26.1 ([#1102](https://github.com/bitnami-labs/sealed-secrets/pull/1102)). - Bump github.com/onsi/ginkgo/v2 from 2.7.0 to 2.8.0 ([#1101](https://github.com/bitnami-labs/sealed-secrets/pull/1101)). - Bump k8s.io/api from 0.26.0 to 0.26.1 ([#1097](https://github.com/bitnami-labs/sealed-secrets/pull/1097)). - Bump k8s.io/client-go from 0.26.0 to 0.26.1 ([#1096](https://github.com/bitnami-labs/sealed-secrets/pull/1096)). - Bump k8s.io/klog/v2 from 2.80.1 to 2.90.0 ([#1094](https://github.com/bitnami-labs/sealed-secrets/pull/1094)). - Bump k8s.io/apimachinery from 0.26.0 to 0.26.1 ([#1093](https://github.com/bitnami-labs/sealed-secrets/pull/1093)). ## v0.19.4 ### Changelog - Bump github.com/onsi/ginkgo/v2 from 2.6.1 to 2.7.0 ([#1086](https://github.com/bitnami-labs/sealed-secrets/pull/1086)). - Bump golang.org/x/crypto from 0.4.0 to 0.5.0 ([#1085](https://github.com/bitnami-labs/sealed-secrets/pull/1085)). - Bump github.com/mattn/go-isatty from 0.0.16 to 0.0.17 ([#1083](https://github.com/bitnami-labs/sealed-secrets/pull/1083)). - Bump github.com/onsi/gomega from 1.24.1 to 1.24.2 ([#1079](https://github.com/bitnami-labs/sealed-secrets/pull/1079)). - Bump k8s.io/code-generator from 0.25.4 to 0.26.0 ([#1078](https://github.com/bitnami-labs/sealed-secrets/pull/1078)). - Bump github.com/onsi/ginkgo/v2 from 2.6.0 to 2.6.1 ([#1077](https://github.com/bitnami-labs/sealed-secrets/pull/1077)). ## v0.19.3 ### Changelog - Update to Go 1.19.4 ([#1073](https://github.com/bitnami-labs/sealed-secrets/pull/1073)). - Bump k8s.io/client-go from 0.25.4 to 0.26.0 ([#1071](https://github.com/bitnami-labs/sealed-secrets/pull/1071)). - Bump golang.org/x/crypto from 0.3.0 to 0.4.0 ([#1072](https://github.com/bitnami-labs/sealed-secrets/pull/1072)). - Bump github.com/onsi/ginkgo/v2 from 2.5.1 to 2.6.0 ([#1069](https://github.com/bitnami-labs/sealed-secrets/pull/1069)). - Bump k8s.io/api from 0.25.4 to 0.26.0 ([#1068](https://github.com/bitnami-labs/sealed-secrets/pull/1068)). - Bump golang.org/x/crypto from 0.2.0 to 0.3.0 ([#1063](https://github.com/bitnami-labs/sealed-secrets/pull/1063)). - Bump k8s.io/client-go from 0.25.3 to 0.25.4 ([#1062](https://github.com/bitnami-labs/sealed-secrets/pull/1062)). - Bump github.com/onsi/ginkgo/v2 from 2.5.0 to 2.5.1 ([#1061](https://github.com/bitnami-labs/sealed-secrets/pull/1061)). ## v0.19.2 ### Changelog - Distinguish std & k8s errors ([#1046](https://github.com/bitnami-labs/sealed-secrets/pull/1046)). - Fix empty Group Version Kind ([#1044](https://github.com/bitnami-labs/sealed-secrets/pull/1044)). - Regenerate code - detected some dummy changes ([#1033](https://github.com/bitnami-labs/sealed-secrets/pull/1033)). - Decouple the kubeseal CLI from the kubeseal library ([#1030](https://github.com/bitnami-labs/sealed-secrets/pull/1030)). - Remove namespaceFn ([#1029](https://github.com/bitnami-labs/sealed-secrets/pull/1029)). ## v0.19.1 ### Changelog - Fix release dockerhub container image name([#1014](https://github.com/bitnami-labs/sealed-secrets/pull/1014)). ## v0.19.0 ### Changelog - FEATURE: Support to recreate a deleted secret generated by the controller([#963](https://github.com/bitnami-labs/sealed-secrets/pull/963)). - Update `golang.org/x/text` fixing CVE-2022-32149 ([#1008](https://github.com/bitnami-labs/sealed-secrets/pull/1008)). - Expired certificate error now prints expiration date in kubeseal([#986](https://github.com/bitnami-labs/sealed-secrets/pull/986)). ## v0.18.5 ### Changelog - Fix `controller.yaml` having no image reference ([#977](https://github.com/bitnami-labs/sealed-secrets/pull/977)) ## v0.18.4 ### Changelog - Upgrade Go version, dependencies and fix CVE-2022-27664 ([#960](https://github.com/bitnami-labs/sealed-secrets/pull/960)) - Move `kubeseal` to its own package ([#939](https://github.com/bitnami-labs/sealed-secrets/pull/939)) - Several refactors to the `controller` ([#940](https://github.com/bitnami-labs/sealed-secrets/pull/940) & [#947](https://github.com/bitnami-labs/sealed-secrets/pull/947)) - Generate a proper schema for the CRD ([#941](https://github.com/bitnami-labs/sealed-secrets/pull/941), [#957](https://github.com/bitnami-labs/sealed-secrets/pull/957), [#964](https://github.com/bitnami-labs/sealed-secrets/pull/964), [#966](https://github.com/bitnami-labs/sealed-secrets/pull/966) & [#970](https://github.com/bitnami-labs/sealed-secrets/pull/970)) - Publish `kubeseal` in a container image ([#921](https://github.com/bitnami-labs/sealed-secrets/pull/921)) ## v0.18.3 Incomplete release ## v0.18.2 ### Changelog - Replace ioutil with io or os ([#895](https://github.com/bitnami-labs/sealed-secrets/pull/895)) - Remove CLI global variables and refactor flag handling ([#901](https://github.com/bitnami-labs/sealed-secrets/pull/901) & [#920](https://github.com/bitnami-labs/sealed-secrets/pull/920)) - Upgrade Go version, dependencies and tooling ([#904](https://github.com/bitnami-labs/sealed-secrets/pull/904) & [#905](https://github.com/bitnami-labs/sealed-secrets/pull/905)) ## v0.18.1 ### Changelog - Add flags to set the rate limit for the verify endpoint ([#873](https://github.com/bitnami-labs/sealed-secrets/pull/873)) ## v0.18.0 ### Changelog - Add capability to watch multiple namespaces ([#572](https://github.com/bitnami-labs/sealed-secrets/pull/572)) - Bump `gopkg.in/yaml.v3` to avoid CVE-2022-28948 ([#852](https://github.com/bitnami-labs/sealed-secrets/pull/852)) - Bump `prometheus/client_golang` and `crypto` dependencies to avoid CVE-2022-21698 and CVE-2022-27191 ([#831](https://github.com/bitnami-labs/sealed-secrets/pull/831)) - Sign container images with cosign ([#810](https://github.com/bitnami-labs/sealed-secrets/pull/810) and [#851](https://github.com/bitnami-labs/sealed-secrets/pull/851)) ## v0.17.5 ### Changelog - Switch to dockerhub([#823](https://github.com/bitnami-labs/sealed-secrets/pull/823)) - Sign the release using cosign ([#814](https://github.com/bitnami-labs/sealed-secrets/pull/814)) ## v0.17.4 ### Changelog - Fix linter errors running golangci-lint ([#751](https://github.com/bitnami-labs/sealed-secrets/pull/751))([#771](https://github.com/bitnami-labs/sealed-secrets/pull/771)) - Added kubeseal support for darwin/arm64 ([#752](https://github.com/bitnami-labs/sealed-secrets/pull/752)) - Bump prometheus/client_golang dependency to avoid CVE-2022-21698 ([#783](https://github.com/bitnami-labs/sealed-secrets/pull/783)) ## v0.17.3 ### Changelog - Unseal templates even when encryptedData is empty ([#653](https://github.com/bitnami-labs/sealed-secrets/pull/653)) - Add new RBAC rules to make Sealed Secret compatible with K8s environments with RBAC enabled ([#715](https://github.com/bitnami-labs/sealed-secrets/pull/715)) - Allow re-encrypt/validate functionalities to work with named ports defined in the Sealed Secret service ([#726](https://github.com/bitnami-labs/sealed-secrets/pull/726)) - Fix verbose logging ([#727](https://github.com/bitnami-labs/sealed-secrets/pull/727)) ## v0.17.2 ### Changelog - Fix issue fetching the certificate when the Sealed Secrets service has a named port ([#648](https://github.com/bitnami-labs/sealed-secrets/pull/648)) - Drop support for Go < 1.16 and bump client-go version ([#705](https://github.com/bitnami-labs/sealed-secrets/pull/705)) ## v0.17.1 ### Changelog - Binaries to emit the proper version ([#683](https://github.com/bitnami-labs/sealed-secrets/pull/683)) - Re-enable publishing K8s manifests in GH releases ([#678](https://github.com/bitnami-labs/sealed-secrets/issues/678)) ## v0.17.0 ### Announcements This release finally turns on the `update-status` feature flag that was introduced back in v0.12.0. The feature is considered stable (if it doesn't work for you, you can deactivate it by setting `SEALED_SECRETS_UPDATE_STATUS=0` in the controller manifest). ### Changelog - Update rbac api version to `rbac.authorization.k8s.io/v1` ([#602](https://github.com/bitnami-labs/sealed-secrets/issues/602)) - Enable `--update-status` by default ([#583](https://github.com/bitnami-labs/sealed-secrets/pull/583)) ## v0.16.0 ### Changelog - Add ability to template arbitrary data keys within resulting secrets ([#445](https://github.com/bitnami-labs/sealed-secrets/issues/445)) - Fix status CRD in controller.yaml (backport from helm chart) ([#567](https://github.com/bitnami-labs/sealed-secrets/issues/567)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/26?closed=1 ## v0.15.0 This release contains a couple of fixes in the controller and manifests. Notable mention: You can give the `--update-status` (also available as env var `SEALED_SECRETS_UPDATE_STATUS=1`) feature flag another try. We'll turn it on by default in ~~the next release~~ v0.17.0. ### Changelog - Remove '{}' in CRD schema properties so that ArgoCD doesn't get confused ([#529](https://github.com/bitnami-labs/sealed-secrets/issues/529)) - Fix bug in status updates ([#223](https://github.com/bitnami-labs/sealed-secrets/issues/223)) - Add label-selector to filter Sealed Secrets ([#521](https://github.com/bitnami-labs/sealed-secrets/issues/521)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/28?closed=1 ## v0.14.1 ### Changelog - Fixed `condition_info` prometheus metric disappearance ([#504](https://github.com/bitnami-labs/sealed-secrets/issues/504)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/27?closed=1 ## v0.14.0 ### Changelog - Updated CustomResourceDefinition to apiextensions.k8s.io/v1 ([#490](https://github.com/bitnami-labs/sealed-secrets/issues/490)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/19?closed=1 ## v0.13.1 ### Changelog - Make it easier to upgrade from ancient (pre v0.9.0) controllers ([#466](https://github.com/bitnami-labs/sealed-secrets/issues/466)) - Prometheus: add namespace to unseal error metric ([#463](https://github.com/bitnami-labs/sealed-secrets/issues/463)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/17?closed=1 ## v0.12.6 # Announcements This release contains a fix for [CVE-2020-14040](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-14040), which could have opened the possibility for an attacker to cause a DoS on the sealed-secret controller (provided the attacker can cause the controller to process a malicious sealed secret resource). ### Changelog - Fix CVE-2020-14040 ([#456](https://github.com/bitnami-labs/sealed-secrets/issues/456)) - Don't require a namespace when using --raw and cluster-wide scope ([#451](https://github.com/bitnami-labs/sealed-secrets/issues/451)) - Unregister Prometheus Gauges associated to removed SealedSecrets conditions ([#422](https://github.com/bitnami-labs/sealed-secrets/issues/422)) - Add -f and -w flags as an alternative to stdin/out ([#439](https://github.com/bitnami-labs/sealed-secrets/issues/439)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/24?closed=1 ## v0.12.5 ### Changelog - Add `condition_info` metric to expose SealedSecrets status ([#421](https://github.com/bitnami-labs/sealed-secrets/issues/421)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/23?closed=1 ## v0.12.4 ### Announcements The binaries in this release have been rebuilt with the Go 1.14.3 toolchain. No other changes in binaries nor k8s manifests. ### Changelog - Build with latest Go 1.14.x version ([#411](https://github.com/bitnami-labs/sealed-secrets/issues/411)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/22?closed=1 ## v0.12.3 ### Announcements This release contains only a change in the `kubeseal` binary since v0.12.2. No controller nor k8s manifest changes. ### Changelog - Fix `--merge-into` file permissions on Windows ([#407](https://github.com/bitnami-labs/sealed-secrets/issues/407)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/21?closed=1 ## v0.12.2 ### Announcements This release contains important changes in manifests since v0.12.1. It also contains a minor fix in kubeseal client. Previously, users upgrading to v0.12.x from previous versions would experience: ``` The Deployment "sealed-secrets-controller" is invalid: spec.selector: Invalid value: v1.LabelSelector{MatchLabels:map[string]string{"app.kubernetes.io/managed-by":"jsonnet", "app.kubernetes.io/name":"kubeseal", "app.kubernetes.io/part-of":"kubeseal", "app.kubernetes.io/version":"v0.12.1", "name":"sealed-secrets-controller"}, MatchExpressions:[]v1.LabelSelectorRequirement(nil)}: field is immutable ``` This was caused by a bug in our official yaml manifests introduced in v0.12.0. Users of the Helm chart were unaffected. By reverting this issue we're are going to cause the same bad experience for users who did perform a clean install of v0.12.x. However, we believe such users are a minority. ### Changelog - Revert "Add recommended labels" ([#404](https://github.com/bitnami-labs/sealed-secrets/issues/404)) - remove kubeconfig deps from recovery-unseal ([#394](https://github.com/bitnami-labs/sealed-secrets/issues/394)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/19?closed=1 ## v0.12.1 ### Announcements This release contains changes in `kubeseal` and `controller` binaries but no changes in manifests since v0.12.0. This release is a fixup release that turns off the status update feature introduced in v0.12.0. Several users have reported a severe bug (an infinite feedback loop where the controller kept updating SealedSecrets and consuming lots of CPU). In order to turn it back on you need to manually pass the `--update-status` flag to the *controller* (or pass the `SEALED_SECRETS_UPDATE_STATUS=1` env var) ### Changelog - Make it easier to use --raw from stdin ([#386](https://github.com/bitnami-labs/sealed-secrets/issues/386)) - Deactivate status updates unless a feature flag is explicitly passed ([#388](https://github.com/bitnami-labs/sealed-secrets/issues/388)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/18?closed=1 ## v0.12.0 ### Announcements This release contains changes in `kubeseal` and `controller` binaries as well as a minor change to the k8s manifest (see [#381](https://github.com/bitnami-labs/sealed-secrets/issues/381)); keep that in mind if you don't rely on the official k8s manifests, including the community-maintained Helm chart. # Status field Now the Sealed Secrets controller updates the `Status` field of the `SealedSecrets` resources. This makes it easier for automation like ArgoCD to detect whether (and when) the controller has reacted to changes in the SealedSecret resources and produced a Secret. It also shows an error message in case it fails (many users are not familiar with k8s events and they may find it easier to see the error message in the status). # Prometheus The Sealed Secrets controller now exports prometheus metrics. See also [contrib/prometheus-mixin](contrib/prometheus-mixin) and `controller-podmonitor.yaml`. ### Changelog - Update Status field ([#346](https://github.com/bitnami-labs/sealed-secrets/issues/346)) - Add prometheus metrics ([#177](https://github.com/bitnami-labs/sealed-secrets/issues/177)) - Upgrade k8s client-go to v0.16.8 ([#380](https://github.com/bitnami-labs/sealed-secrets/issues/380)) - kubeseal no longer emits empty `status: {}` field ([#383](https://github.com/bitnami-labs/sealed-secrets/issues/383)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/16?closed=1 ## v0.11.0 ### Announcements This release contains only changes in kubeseal binary (no k8s manifest changes required). ### For those who choose the name and namespace after sealing the secret Creating secrets with namespace-wide and cluster-wide scopes is now easier as it no longer requires manually adding annotations in the input Secret before passing it to `kubeseal`. This was often the root cause of many support requests. Now all you need to do is to: ``` $ kubeseal --scope namespace-wide output-sealed-secret.json ``` ### Changelog - Honour --scope flag ([#371](https://github.com/bitnami-labs/sealed-secrets/issues/371)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/15?closed=1 ## v0.10.0 ### Announcements This release supports the ARM 32 bit and 64 bit architectures, both on the client and the controller sides. We also end the silly streak of patch level releases that actually contained features. We'll try to bump the minor version on every release except true hotfixes. ### Changelog - Provide multi-arch Container image for Sealed Secrets controller ([#349](https://github.com/bitnami-labs/sealed-secrets/issues/349)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/2?closed=1 ## v0.9.8 ### Announcements This release contains only changes in Linux `kubeseal-arm` and `kubeseal-arm64` binaries. There are no changes in the docker images, nor in the `x86_64` binaries for any of the supported OS. ### Changelog - Fix bad release of Linux ARM7 and ARM64 binaries ([#362](https://github.com/bitnami-labs/sealed-secrets/issues/362)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/14?closed=1 ## v0.9.7 ### Announcements This release contains changes in `kubeseal` and `controller` binaries as well as a minor change to the k8s manifest (see [#338](https://github.com/bitnami-labs/sealed-secrets/issues/338)); keep that in mind if you don't rely on the official k8s manifests, including the community-maintained Helm chart. ### Allow overwriting existing secrets By default, the sealed-secrets controller doesn't unseal a SealedSecret over an existing Secret resource (i.e. a resource that has not been created by the sealed-secrets controller in the first place). This is an important safeguard, not only to catch accidental overwrites due to typos etc, but also as a security measure: the sealed-secrets controller can create/update Secret resources even if the user who has the RBAC rights to create the SealedSecret resource doesn't have the right to create/update a Secret resource. We didn't want the sealed-secret controller to give its users more effective rights than what they would otherwise have without the sealed-secrets controller. A simple way to achieve that was permit only updates (overwrites) to Secret resources that were already owned by the sealed-secrets controller (which also seemed a sensible thing to do since it protects from accidental overwrites). However, this behavior gets in the way when you're just starting to use SealedSecrets and want to migrate your existing Secrets into SealedSecrets. You now can just annotate your `Secret`s with `sealedsecrets.bitnami.com/managed: true` thus indicating that they can be safely overwritten by the sealed-secrets controller. This doesn't loosen our security model since you'd have to have RBAC rights to annotate the existing secrets (e.g. with `kubectl annotate`) or you can ask your friendly admins to do it on your behalf. ### Changelog - Release includes ARMv7 and ARM64 binaries (although no docker images yet) ([#173](https://github.com/bitnami-labs/sealed-secrets/issues/173)) - Set `fsGroup` to `nobody` in order to support `BoundServiceAccountTokenVolume` ([#338](https://github.com/bitnami-labs/sealed-secrets/issues/338)) - Add `--force-empty-data` flag to allow (un)sealing an empty secret ([#334](https://github.com/bitnami-labs/sealed-secrets/issues/334)) - Avoid forcing the default namespace when sealing a cluster-wide secret ([#323](https://github.com/bitnami-labs/sealed-secrets/issues/323)) - Introduce the `sealedsecrets.bitnami.com/managed: true` annotation which controls overwriting existing secrets ([#331](https://github.com/bitnami-labs/sealed-secrets/issues/331)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/13?closed=1 ## v0.9.6 ### Announcements This release contains only changes in `kubeseal` and `controller` binaries (no k8s manifest changes required). ### Preliminary support for running multiple controllers It always been possible in theory to run multiple controller instance in multiple namespaces, each with their own sealing encryption keys and thus each able to unseal secrets intended for it. However, doing so created a lot of noise in the logs, since each controller wouldn't know which secrets are meant to be decryptable, but failed to decrypt, and which it ought to ignore. Since v0.9.6 you can reduce this noise by setting the `--all-namespaces` flag to false (also via the env var `SEALED_SECRETS_ALL_NAMESPACES=false`). ### Changelog - Give an option to search only the current namespace ([#316](https://github.com/bitnami-labs/sealed-secrets/issues/316)) - Support parsing multiple private keys in --recovery-private-key ([#325](https://github.com/bitnami-labs/sealed-secrets/issues/325)) - Add klog flags so we can troubleshoot k8s client ([#320](https://github.com/bitnami-labs/sealed-secrets/issues/320)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/12?closed=1 ## v0.9.5 ### Announcements This release contains only changes in `kubeseal` binary (no k8s manifest changes required). ### Changelog - Improve error reporting in case of missing kubeconfig when inferring namespace ([#313](https://github.com/bitnami-labs/sealed-secrets/issues/313)) - Teach kubeseal to decrypt using backed up secrets ([#312](https://github.com/bitnami-labs/sealed-secrets/issues/312)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/11?closed=1 ## v0.9.4 ### Announcements This release contains only changes in `kubeseal` and `controller` binaries (no k8s manifest changes required). ### Changelog - Remove tty warning in `--fetch-cert` (regression caused by #303 released in v0.9.3) ([#306](https://github.com/bitnami-labs/sealed-secrets/issues/306)) - Implement `--recovery-unseal` to help with disaster recovery ([#307](https://github.com/bitnami-labs/sealed-secrets/issues/307)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/10?closed=1 ## v0.9.3 ### Announcements This release contains only changes in `kubeseal` and `controller` binaries (no k8s manifest changes required). ### Changelog - Implement `--key-cutoff-time` ([#299](https://github.com/bitnami-labs/sealed-secrets/issues/299)) - Warn if stdin is a terminal ([#303](https://github.com/bitnami-labs/sealed-secrets/issues/303)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/9?closed=1 ## v0.9.2 ### Announcements This release contains only changes in `kubeseal` and `controller` binaries (no k8s manifest changes required). ### Periodic key renewal and offline certificates A few people have raised concerns of how will automatic key+certificate renewal affect the offline signing workflow. First, a clarification: nothing changed. You can keep using your old certificates; it's just that if you do, you won't benefit from the additional security given from the periodic key renewal. In order to simplify the workflow for those who do want to benefit from the key renewal, but at the same time cannot access the target cluster (while not being completely offline), we offer a little feature that will help: `--cert` has learned to accept http(s) URLs. You can point it to a place where you serve up-to-date certificates for your clusters (tip/idea: you can expose the controller's cert.pem files with an Ingress). ### Changelog - Accept URLs in `--cert` ([#281](https://github.com/bitnami-labs/sealed-secrets/issues/281)) - Improve logs/events in case of decryption error ([#274](https://github.com/bitnami-labs/sealed-secrets/issues/274)) - Reduce likelihood of name/namespace mismatch when using `--merge-into` ([#286](https://github.com/bitnami-labs/sealed-secrets/issues/286)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/8?closed=1 ## v0.9.1 - Make manifests compatible with k8s 1.16.x ([#269](https://github.com/bitnami-labs/sealed-secrets/issues/269)) - Fix non-strict scopes with --raw ([#276](https://github.com/bitnami-labs/sealed-secrets/issues/276)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/7?closed=1 ## v0.9.0 ## Announcement ### Private key renewal This release turns on an important security feature: a new private key will be now created every 30 days by default. Existing sealed-secrets resources will still be decrypted until the keys are manually phased out. You can read more about this feature and the problem of **secret rotation** and how it interacts with Sealed Secrets in this [README section](https://github.com/bitnami-labs/sealed-secrets#secret-rotation) or in the original GH issue #137. This feature alone is not technically a breaking change for people who use the offline workflow with `kubeseal --cert`, since old keys are not rotated out automatically. Users would be required to update their offline certs only when they purge old keys manually (we might introduce automatic purging in the future). That said, to reap the benefits of key renewal, users of the offline workflow are encouraged to update their offline certificates every time a new key is generated (by default every 30 days). ### Pre-v0.7.0 clients If you are using kubeseal clients older than v0.7.0, please upgrade. Since this release the controller will no longer accept the "v1" format of the encrypted "data" field and instead it will only support the "encryptedData" field. If you have old sealed secret resources lying around, you can easily upgrade them by invoking: ```bash kubeseal --re-encrypt new.yaml ``` ### Update items Since version v0.7.0 it was possible to update individual items in the `encryptedData` field of the Sealed Secret resource, but you had to manually copy&paste the encrypted items into an existing resource file. The required steps were never spelled out in the documentation and to be fair it always felt quite awkward. Now `kubectl` has learned how to update an existing secret, whilst preserving the same general operation principles, namely staying out of the business of actually crafting the secret itself (`kubectl create secret ...` and its various flags like `--from-file`, `--from-literal`, etc). Example: ```bash $ kubectl create secret generic mysecret --dry-run -o json --from-file=foo=/tmp/foo \ | kubeseal >sealed.json $ kubectl create secret generic mysecret --dry-run -o json --from-file=bar=/tmp/bar \ | kubeseal --merge-into sealed.json ``` ### Changelog - Doc improvements. - Rename "key rotation" to "key renewal" since the terminology was confusing. - Key renewal is enabled by default every 30 days ([#236](https://github.com/bitnami-labs/sealed-secrets/issues/236)) - You can now use env vars such as SEALED_SECRETS_FOO_BAR to customize the controller ([#234](https://github.com/bitnami-labs/sealed-secrets/issues/234)) - Deactivating by default deprecated "v1" encrypted data format (used by pre-v0.7.0 clients) ([#235](https://github.com/bitnami-labs/sealed-secrets/issues/235)) - Fix RBAC rules for /v1/rotate and /v1/validate fixing #166 for good ([#249](https://github.com/bitnami-labs/sealed-secrets/issues/249)) - Implement the --merge-into command ([#253](https://github.com/bitnami-labs/sealed-secrets/issues/253)) - Add the `-o` alias for `--format` ([#261](https://github.com/bitnami-labs/sealed-secrets/issues/261)) - Add the `--raw` command for only encrypting single items ([#257](https://github.com/bitnami-labs/sealed-secrets/issues/257)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/1?closed=1 ## v0.8.3 ### Announcements This release contains a fix for a possible secret leak that can happen when sealing existing secrets that have been retrieved from a cluster (e.g. with `kubectl get`) where they have been created with `kubectl apply` (as opposed to `kubectl create`). This potential problem has been introduced v0.8.0 when kubeseal learned how to preserve annotations and labels. Please check your existing sealed secret sources for any annotation `kubectl.kubernetes.io/last-applied-configuration`, because that annotation would contain your original secrets in clear. This release strips this annotation (and a similar annotation created by the `kubecfg` tool) ### Changelog Fixes in this release: - Round-tripping secrets can leak clear-text in last-applied-configuration ([#227](https://github.com/bitnami-labs/sealed-secrets/issues/227)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/6?closed=1 ## v0.8.2 Fixes in this release: - Endless loop in controller on invalid base64 encrypted data bug ([#201](https://github.com/bitnami-labs/sealed-secrets/issues/201)) - Fix RBAC for /v1/cert.pem public key in isolated namespaces, removes most use cases for offline sealing with `--cert` ([#208](https://github.com/bitnami-labs/sealed-secrets/issues/208),[#166](https://github.com/bitnami-labs/sealed-secrets/issues/166)) - Accept and seal stringData into secret ([#221](https://github.com/bitnami-labs/sealed-secrets/issues/221)) - Fix a couple of blockers for enabling (still experimental) key rotation ([#185](https://github.com/bitnami-labs/sealed-secrets/issues/185), [#219](https://github.com/bitnami-labs/sealed-secrets/issues/219), [#218](https://github.com/bitnami-labs/sealed-secrets/issues/218)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/5?closed=1 ## v0.8.1 Fixes in this release: - Solve kubectl auth issues with clusters using `client.authentication.k8s.io/v1beta1` config by upgrading to client-go v12.0.0 ([#183](https://github.com/bitnami-labs/sealed-secrets/issues/183)) - Fix controller crash when writing logs due to read-only root FS ([#200](https://github.com/bitnami-labs/sealed-secrets/issues/200)) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/4?closed=1 ## v0.8.0 The main improvements in this release are: - support for annotations and labels ([#92](https://github.com/bitnami-labs/sealed-secrets/issues/92)) - support for secrets rotation opt-in ([#137](https://github.com/bitnami-labs/sealed-secrets/issues/137)) - fix bug with OwnerReferences handling ([#127](https://github.com/bitnami-labs/sealed-secrets/issues/127)) - EKS support; client-go version bump to release-7.0 ([#110](https://github.com/bitnami-labs/sealed-secrets/issues/110)) - Instructions to run on GKE when user is not cluster-admin ([#111](https://github.com/bitnami-labs/sealed-secrets/issues/111)) - Windows binary of kubeseal ([#85](https://github.com/bitnami-labs/sealed-secrets/issues/85)) - Internal codebase modernization (e.g. switch to Go modules) The full Changelog is maintained in https://github.com/bitnami-labs/sealed-secrets/milestone/3?closed=1 Many thanks for all the folks who contributed to this release! ## v0.7.0 Big change for this release is the switch to **per-key encrypted values**. - ("Keys" as in "object key/value", not as in "encryption key". English is hard.)* - Previously we generated a single big encrypted blob for each Secret, now we encrypt each value in the Secret separately, with the keys in plain text. This allows: - Existing keys can now be renamed and deleted without re-encrypting the value(s). - New keys/values can be added to the SealedSecret without re-encrypting (or even having access to!) the existing values. - Note that (as before) the encrypted values are still tied to the namespace/name of the enclosing Secret/SealedSecret, so can't be moved to another Secret. (The [cluster-wide annotation](https://github.com/bitnami-labs/sealed-secrets/blob/bda0af6a6a8abebc9ff359dd2e5e22d54cb40798/pkg/apis/sealed-secrets/v1alpha1/types.go#L16) _does_ allow this, with the corresponding caveats, as before) - The `kubeseal` tool does not yet have an option to output _just_ a single value, but you can safely mix+match the individual values from `kubeseal` output with an existing SealedSecret. Improving `kubeseal` support for this feature is still an open action item. - Existing/older "all-in-one" SealedSecrets are declared deprecated, but will continue to be supported by the controller for the foreseeable future. New invocations of the `kubeseal` tool now produce per-key encrypted output - if you need to produce the older format, just use an older `kubeseal`. Please raise a github issue if you have a use-case that requires supporting "all-in-one" SealedSecrets going forward. - Note the CRD schema used for server-side validation in k8s >=1.9 has been temporarily removed, because it was unable to support the new per-key structure correctly (see [kubernetes/kubernetes#59485](https://github.com/kubernetes/kubernetes/issues/59485)). - Huge thanks to @sullerandras for the code and his persistence in getting this merged! ## v0.6.0 - Support "cluster wide" secrets, that are not restricted to the original namespace - Set `sealedsecrets.bitnami.com/cluster-wide: "true"` annotation - Warning: cluster-wide SealedSecrets can be decrypted by anyone who can create a SealedSecret in your cluster - Move to client-go v5.0 - Move to bitnami-labs github org - Fix bug in schema validation for k8s 1.9 ## v0.5.1 **Note:** this version moves TPR/CRD definition into a separate file. To install, you need `controller.yaml` *and* either `sealedsecret-tpr.yaml` or `sealedsecret-crd.yaml` - Add CRD definition and TPR->CRD migration documentation - Add `kubeseal --fetch-cert` to dump server cert to stdout, for later offline use with `kubeseal --cert` - Better sanitization of input object to `kubeseal` (v0.5.1 fixes a travis/github release issue with v0.5.0) ## v0.5.0 ## v0.4.0 - controller: deployment security hardening: non-root uid and read-only rootfs - `kubeseal`: Include oidc and gcp auth provider plugins - `kubeseal`: Add support for YAML output ## v0.3.1 - Add `controller-norbac.yaml` to the release build. This is `controller.yaml` without RBAC rules and related service account - for environments where RBAC is not yet supported, [like Azure](https://github.com/Azure/acs-engine/issues/680). - Fix missing controller RBAC ClusterRoleBinding in v0.3.0 ## v0.3.0 Rename everything to better represent project scope. Better to do this early (now) and apologies for the disruption. - Rename repo and golang import path -> `bitnami/sealed-secrets` - Rename cli tool -> `kubeseal` - Rename `SealedSecret` apiGroup -> `bitnami.com` ## v0.2.1 - Fix invalid field `resourceName` in v0.2.0 controller.yaml (thanks @Globegitter) ## v0.2.0 - Client tool has better defaults, and can fetch the certificate automatically from the controller. - Improve release process to include pre-built Linux and OSX x86-64 binaries. ## v0.1.0 Basic functionality is complete. ## v0.0.1 - Clean up controller.jsonnet - Switch to quay.io (docker hub doesn't offer robot accounts??) - Add deploy section to .travis.yml ================================================ FILE: SECURITY.md ================================================ # Security Release Process The community has adopted this security disclosure and response policy to ensure we responsibly handle critical issues. ## Supported Versions For a list of support versions that this project will potentially create security fixes for, please refer to the [Releases page](https://github.com/bitnami-labs/sealed-secrets/blob/main/CONTRIBUTING.md#release-process) on this project's GitHub and/or project related documentation on release cadence and support. ## Reporting a Vulnerability - Private Disclosure Process Security is of the highest importance and all security vulnerabilities or suspected security vulnerabilities should be reported to this project privately, to minimize attacks against current users before they are fixed. Vulnerabilities will be investigated and patched on the next patch (or minor) release as soon as possible. This information could be kept entirely internal to the project. If you know of a publicly disclosed security vulnerability for this project, please **IMMEDIATELY** contact the [maintainers](mailto:sealed-secrets.pdl@broadcom.com) of this project privately. The use of encrypted email is encouraged. >**IMPORTANT:** Do not file public issues on GitHub for security vulnerabilities To report a vulnerability or a security-related issue, please contact the maintainers with enough details through one of the following channels: * Directly via the team email addresses (sealed-secrets.pdl@broadcom.com) * Open a [GitHub Security Advisory](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability). This allows for anyone to report security vulnerabilities directly and privately to the maintainers via GitHub. Note that this option may not be present for every repository. The report will be fielded by the [maintainers](https://github.com/bitnami-labs/sealed-secrets/blob/main/MAINTAINERS.md) who have committer and release permissions. Feedback will be sent within 3 business days, including a detailed plan to investigate the issue and any potential workarounds to perform in the meantime. Do not report non-security-impacting bugs through this channel. Use GitHub issues for all non-security-impacting bugs. ## Proposed Report Content Provide a descriptive title and in the description of the report include the following information: * Basic identity information, such as your name and your affiliation or company. * Detailed steps to reproduce the vulnerability (POC scripts, screenshots, and logs are all helpful to us). * Description of the effects of the vulnerability on this project and the related hardware and software configurations, so that the maintainers can reproduce it. * How the vulnerability affects this project's usage and an estimation of the attack surface, if there is one. * List other projects or dependencies that were used in conjunction with this project to produce the vulnerability. ## When to report a vulnerability * When you think this project has a potential security vulnerability. * When you suspect a potential vulnerability but you are unsure that it impacts this project. * When you know of or suspect a potential vulnerability on another project that is used by this project. ## Patch, Release, and Disclosure The maintainers will respond to vulnerability reports as follows: 1. The maintainers will investigate the vulnerability and determine its effects and criticality. 2. If the issue is not deemed to be a vulnerability, the maintainers will follow up with a detailed reason for rejection. 3. The maintainers will initiate a conversation with the reporter within 3 business days. 4. If a vulnerability is acknowledged and the timeline for a fix is determined, the maintainers will work on a plan to communicate with the appropriate community, including identifying mitigating steps that affected users can take to protect themselves until the fix is rolled out. 5. The maintainers will also create a [Security Advisory](https://docs.github.com/en/code-security/repository-security-advisories/publishing-a-repository-security-advisory) using the [CVSS Calculator](https://www.first.org/cvss/calculator/3.0), if it is not created yet. The maintainers make the final call on the calculated CVSS; it is better to move quickly than making the CVSS perfect. Issues may also be reported to [Mitre](https://cve.mitre.org/) using this [scoring calculator](https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator). The draft advisory will initially be set to private. 6. The maintainers will work on fixing the vulnerability and perform internal testing before preparing to roll out the fix. 7. Once the fix is confirmed, the maintainers will patch the vulnerability in the next patch or minor release, and backport a patch release into all earlier supported releases. ## Public Disclosure Process The maintainers publish the public advisory to this project's community via GitHub. In most cases, additional communication via Slack, Twitter, mailing lists, blog, and other channels will assist in educating the project's users and rolling out the patched release to affected users. The maintainers will also publish any mitigating steps users can take until the fix can be applied to their instances. This project's distributors will handle creating and publishing their own security advisories. ## Confidentiality, integrity and availability We consider vulnerabilities leading to the compromise of data confidentiality, elevation of privilege, or integrity to be our highest priority concerns. Availability, in particular in areas relating to DoS and resource exhaustion, is also a serious security concern. The maintainer team takes all vulnerabilities, potential vulnerabilities, and suspected vulnerabilities seriously and will investigate them in an urgent and expeditious manner. Note that we do not currently consider the default settings for this project to be secure-by-default. It is necessary for operators to explicitly configure settings, role based access control, and other resource related features in this project to provide a hardened environment. We will not act on any security disclosure that relates to a lack of safe defaults. Over time, we will work towards improved safe-by-default configuration, taking into account backwards compatibility. ================================================ FILE: carvel/package.yaml ================================================ apiVersion: data.packaging.carvel.dev/v1alpha1 kind: Package metadata: name: "sealedsecrets.bitnami.com.2.18.1" spec: refName: "sealedsecrets.bitnami.com" version: "2.18.1" valuesSchema: openAPIv3: title: Chart Values type: object properties: kubeVersion: type: string description: Override Kubernetes version default: "" nameOverride: type: string description: String to partially override sealed-secrets.fullname default: "" fullnameOverride: type: string description: String to fully override sealed-secrets.fullname default: "" namespace: type: string description: Namespace where to deploy the Sealed Secrets controller default: "" extraDeploy: type: array description: Array of extra objects to deploy with the release default: [] items: {} image: type: object properties: registry: type: string description: Sealed Secrets image registry default: docker.io repository: type: string description: Sealed Secrets image repository default: bitnami/sealed-secrets-controller tag: type: string description: Sealed Secrets image tag (immutable tags are recommended) default: v0.24.5 pullPolicy: type: string description: Sealed Secrets image pull policy default: IfNotPresent pullSecrets: type: array description: Sealed Secrets image pull secrets default: [] items: {} createController: type: boolean description: Specifies whether the Sealed Secrets controller should be created default: true secretName: type: string description: The name of an existing TLS secret containing the key used to encrypt secrets default: sealed-secrets-key updateStatus: type: boolean description: Specifies whether the Sealed Secrets controller should update the status subresource default: true skipRecreate: type: boolean description: Specifies whether the Sealed Secrets controller should skip recreating removed secrets default: false keyrenewperiod: type: string description: Specifies key renewal period. Default 30 days default: "" rateLimit: type: string description: Number of allowed sustained request per second for verify endpoint default: "" rateLimitBurst: type: string description: Number of requests allowed to exceed the rate limit per second for verify endpoint default: "" additionalNamespaces: type: array description: List of namespaces used to manage the Sealed Secrets default: [] items: {} command: type: array description: Override default container command default: [] items: {} args: type: array description: Override default container args default: [] items: {} livenessProbe: type: object properties: enabled: type: boolean description: Enable livenessProbe on Sealed Secret containers default: true initialDelaySeconds: type: number description: Initial delay seconds for livenessProbe default: 0 periodSeconds: type: number description: Period seconds for livenessProbe default: 10 timeoutSeconds: type: number description: Timeout seconds for livenessProbe default: 1 failureThreshold: type: number description: Failure threshold for livenessProbe default: 3 successThreshold: type: number description: Success threshold for livenessProbe default: 1 readinessProbe: type: object properties: enabled: type: boolean description: Enable readinessProbe on Sealed Secret containers default: true initialDelaySeconds: type: number description: Initial delay seconds for readinessProbe default: 0 periodSeconds: type: number description: Period seconds for readinessProbe default: 10 timeoutSeconds: type: number description: Timeout seconds for readinessProbe default: 1 failureThreshold: type: number description: Failure threshold for readinessProbe default: 3 successThreshold: type: number description: Success threshold for readinessProbe default: 1 startupProbe: type: object properties: enabled: type: boolean description: Enable startupProbe on Sealed Secret containers default: false initialDelaySeconds: type: number description: Initial delay seconds for startupProbe default: 0 periodSeconds: type: number description: Period seconds for startupProbe default: 10 timeoutSeconds: type: number description: Timeout seconds for startupProbe default: 1 failureThreshold: type: number description: Failure threshold for startupProbe default: 3 successThreshold: type: number description: Success threshold for startupProbe default: 1 customLivenessProbe: type: object description: Custom livenessProbe that overrides the default one default: {} customReadinessProbe: type: object description: Custom readinessProbe that overrides the default one default: {} customStartupProbe: type: object description: Custom startupProbe that overrides the default one default: {} podSecurityContext: type: object properties: enabled: type: boolean description: Enabled Sealed Secret pods' Security Context default: true fsGroup: type: number description: Set Sealed Secret pod's Security Context fsGroup default: 65534 containerSecurityContext: type: object properties: enabled: type: boolean description: Enabled Sealed Secret containers' Security Context default: true readOnlyRootFilesystem: type: boolean description: Whether the Sealed Secret container has a read-only root filesystem default: true runAsNonRoot: type: boolean description: Indicates that the Sealed Secret container must run as a non-root user default: true runAsUser: type: number description: Set Sealed Secret containers' Security Context runAsUser default: 1001 automountServiceAccountToken: type: string description: whether to automatically mount the service account API-token to a particular pod default: "" priorityClassName: type: string description: Sealed Secret pods' priorityClassName default: "" runtimeClassName: type: string description: Sealed Secret pods' runtimeClassName default: "" tolerations: type: array description: Tolerations for Sealed Secret pods assignment default: [] items: {} hostNetwork: type: boolean description: Sealed Secrets pods' hostNetwork default: false dnsPolicy: type: string description: Sealed Secrets pods' dnsPolicy default: "" service: type: object properties: type: type: string description: Sealed Secret service type default: ClusterIP port: type: number description: Sealed Secret service HTTP port default: 8080 nodePort: type: string description: Node port for HTTP default: "" ingress: type: object properties: enabled: type: boolean description: Enable ingress record generation for Sealed Secret default: false pathType: type: string description: Ingress path type default: ImplementationSpecific apiVersion: type: string description: Force Ingress API version (automatically detected if not set) default: "" ingressClassName: type: string description: IngressClass that will be be used to implement the Ingress default: "" hostname: type: string description: Default host for the ingress record default: sealed-secrets.local path: type: string description: Default path for the ingress record default: /v1/cert.pem tls: type: boolean description: Enable TLS configuration for the host defined at `ingress.hostname` parameter default: false selfSigned: type: boolean description: Create a TLS secret for this ingress record using self-signed certificates generated by Helm default: false extraHosts: type: array description: An array with additional hostname(s) to be covered with the ingress record default: [] items: {} extraPaths: type: array description: An array with additional arbitrary paths that may need to be added to the ingress under the main host default: [] items: {} extraTls: type: array description: TLS configuration for additional hostname(s) to be covered with this ingress record default: [] items: {} secrets: type: array description: Custom TLS certificates as secrets default: [] items: {} networkPolicy: type: object properties: enabled: type: boolean description: Specifies whether a NetworkPolicy should be created default: false serviceAccount: type: object properties: create: type: boolean description: Specifies whether a ServiceAccount should be created default: true labels: type: object description: Extra labels to be added to the ServiceAccount default: {} name: type: string description: The name of the ServiceAccount to use. default: "" automountServiceAccountToken: type: string description: Specifies, whether to mount the service account API-token default: "" rbac: type: object properties: create: type: boolean description: Specifies whether RBAC resources should be created default: true clusterRole: type: boolean description: Specifies whether the Cluster Role resource should be created default: true labels: type: object description: Extra labels to be added to RBAC resources default: {} pspEnabled: type: boolean description: PodSecurityPolicy default: false metrics: type: object properties: serviceMonitor: type: object properties: enabled: type: boolean description: Specify if a ServiceMonitor will be deployed for Prometheus Operator default: false namespace: type: string description: Namespace where Prometheus Operator is running in default: "" labels: type: object description: Extra labels for the ServiceMonitor default: {} annotations: type: object description: Extra annotations for the ServiceMonitor default: {} interval: type: string description: How frequently to scrape metrics default: "" scrapeTimeout: type: string description: Timeout after which the scrape is ended default: "" honorLabels: type: boolean description: Specify if ServiceMonitor endPoints will honor labels default: true metricRelabelings: type: array description: Specify additional relabeling of metrics default: [] items: {} relabelings: type: array description: Specify general relabeling default: [] items: {} dashboards: type: object properties: create: type: boolean description: Specifies whether a ConfigMap with a Grafana dashboard configuration should be created default: false labels: type: object description: Extra labels to be added to the Grafana dashboard ConfigMap default: {} namespace: type: string description: Namespace where Grafana dashboard ConfigMap is deployed default: "" template: spec: fetch: - imgpkgBundle: image: ghcr.io/bitnami-labs/sealed-secrets-carvel@sha256:9dd602e7653ef7979a67eeab60bd58fe1059de8cc208d40d5293279bd80f6478 template: - helmTemplate: path: sealed-secrets - kbld: paths: - "-" - .imgpkg/images.yml deploy: - kapp: {} ================================================ FILE: cmd/controller/main.go ================================================ package main import ( goflag "flag" "fmt" "io" "log/slog" "os" "time" flag "github.com/spf13/pflag" "github.com/bitnami-labs/sealed-secrets/pkg/controller" "github.com/bitnami-labs/sealed-secrets/pkg/flagenv" "github.com/bitnami-labs/sealed-secrets/pkg/log" "github.com/bitnami-labs/sealed-secrets/pkg/pflagenv" ssv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" "github.com/bitnami-labs/sealed-secrets/pkg/buildinfo" ) const ( flagEnvPrefix = "SEALED_SECRETS" defaultKeyRenewPeriod = 30 * 24 * time.Hour defaultKeyOrderPriority = "CertNotBefore" ) var ( // VERSION set from Makefile. VERSION = buildinfo.DefaultVersion ) func bindControllerFlags(f *controller.Flags, fs *flag.FlagSet) { fs.StringVar(&f.KeyPrefix, "key-prefix", "sealed-secrets-key", "Prefix used to name keys.") fs.IntVar(&f.KeySize, "key-size", 4096, "Size of encryption key.") fs.DurationVar(&f.ValidFor, "key-ttl", 10*365*24*time.Hour, "Duration that certificate is valid for.") fs.StringVar(&f.MyCN, "my-cn", "", "Common name to be used as issuer/subject DN in generated certificate.") fs.DurationVar(&f.KeyRenewPeriod, "key-renew-period", defaultKeyRenewPeriod, "New key generation period (automatic rotation deactivated if 0)") fs.StringVar(&f.KeyOrderPriority, "key-order-priority", defaultKeyOrderPriority, "Ordering of keys based on NotBefore certificate attribute or secret creation timestamp.") fs.BoolVar(&f.AcceptV1Data, "accept-deprecated-v1-data", true, "Accept deprecated V1 data field.") fs.StringVar(&f.KeyCutoffTime, "key-cutoff-time", "", "Create a new key if latest one is older than this cutoff time. RFC1123 format with numeric timezone expected.") fs.BoolVar(&f.NamespaceAll, "all-namespaces", true, "Scan all namespaces or only the current namespace (default=true).") fs.StringVar(&f.AdditionalNamespaces, "additional-namespaces", "", "Comma-separated list of additional namespaces to be scanned.") fs.StringVar(&f.LabelSelector, "label-selector", "", "Label selector which can be used to filter sealed secrets.") fs.IntVar(&f.RateLimitPerSecond, "rate-limit", 2, "Number of allowed sustained request per second for verify endpoint") fs.IntVar(&f.RateLimitBurst, "rate-limit-burst", 2, "Number of requests allowed to exceed the rate limit per second for verify endpoint") fs.StringVar(&f.PrivateKeyAnnotations, "privatekey-annotations", "", "Comma-separated list of additional annotations to be put on renewed sealing keys.") fs.StringVar(&f.PrivateKeyLabels, "privatekey-labels", "", "Comma-separated list of additional labels to be put on renewed sealing keys.") fs.BoolVar(&f.OldGCBehavior, "old-gc-behavior", false, "Revert to old GC behavior where the controller deletes secrets instead of delegating that to k8s itself.") fs.BoolVar(&f.UpdateStatus, "update-status", true, "beta: if true, the controller will update the status sub-resource whenever it processes a sealed secret") fs.BoolVar(&f.WatchForSecrets, "watch-for-secrets", false, "beta: If this is true, the controller will watch for key secrets. This is useful if you create the key secrets externally.") fs.BoolVar(&f.SkipRecreate, "skip-recreate", false, "if true the controller will skip listening for managed secret changes to recreate them. This helps on limited permission environments.") fs.BoolVar(&f.LogInfoToStdout, "log-info-stdout", false, "if true the controller will log info to stdout.") fs.StringVar(&f.LogLevel, "log-level", "INFO", "Log level (INFO|ERROR).") fs.StringVar(&f.LogFormat, "log-format", "text", "Log format (text|json).") fs.DurationVar(&f.KeyRenewPeriod, "rotate-period", defaultKeyRenewPeriod, "") _ = fs.MarkDeprecated("rotate-period", "please use key-renew-period instead") fs.IntVar(&f.MaxRetries, "max-unseal-retries", 5, "Max unseal retries.") fs.Float32Var(&f.KubeClientQPS, "kubeclient-qps", 5, "Kubeclient QPS (negative value disables ratelimiting)") fs.IntVar(&f.KubeClientBurst, "kubeclient-burst", 10, "Kubeclient Burst") } func bindFlags(f *controller.Flags, fs *flag.FlagSet, gofs *goflag.FlagSet) { bindControllerFlags(f, fs) flagenv.SetFlagsFromEnv(flagEnvPrefix, gofs) pflagenv.SetFlagsFromEnv(flagEnvPrefix, fs) // Standard goflags (glog in particular) fs.AddGoFlagSet(gofs) if f := fs.Lookup("logtostderr"); f != nil { f.DefValue = "true" _ = f.Value.Set(f.DefValue) } } func mainE(w io.Writer, fs *flag.FlagSet, gofs *goflag.FlagSet, args []string) error { var printVersion bool var flags controller.Flags buildinfo.FallbackVersion(&VERSION, buildinfo.DefaultVersion) fs.BoolVar(&printVersion, "version", false, "Print version information and exit") bindFlags(&flags, fs, gofs) if err := fs.Parse(args); err != nil { return err } if err := gofs.Parse([]string{}); err != nil { return err } // Set logging logLevel := slog.Level(0) _ = logLevel.UnmarshalText([]byte(flags.LogLevel)) opts := &slog.HandlerOptions{ Level: logLevel, } if flags.LogInfoToStdout { slog.SetDefault(slog.New(log.New(os.Stdout, os.Stderr, flags.LogFormat, opts))) } else { slog.SetDefault(slog.New(log.New(os.Stderr, os.Stderr, flags.LogFormat, opts))) } ssv1alpha1.AcceptDeprecatedV1Data = flags.AcceptV1Data if printVersion { fmt.Fprintf(w, "controller version: %s\n", VERSION) return nil } slog.Info("Starting sealed-secrets controller", "version", VERSION) if err := controller.Main(&flags, VERSION); err != nil { panic(err) } return nil } func main() { if err := mainE(os.Stdout, flag.CommandLine, goflag.CommandLine, os.Args); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } } ================================================ FILE: cmd/controller/main_test.go ================================================ package main import ( "bytes" goflag "flag" "testing" flag "github.com/spf13/pflag" ) func TestVersion(t *testing.T) { buf := bytes.NewBufferString("") testVersionFlags := flag.NewFlagSet("testVersionFlags", flag.ExitOnError) testNopFlags := goflag.NewFlagSet("nop", goflag.ExitOnError) err := mainE(buf, testVersionFlags, testNopFlags, []string{"--version"}) if err != nil { t.Fatal(err) } if got, want := buf.String(), "controller version: UNKNOWN\n"; got != want { t.Errorf("got: %q, want: %q", got, want) } } ================================================ FILE: cmd/kubeseal/main.go ================================================ package main import ( "context" "fmt" "io" "os" "path/filepath" goflag "flag" ssv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" "github.com/google/renameio" "github.com/mattn/go-isatty" flag "github.com/spf13/pflag" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/tools/clientcmd" "k8s.io/klog/v2" "github.com/bitnami-labs/sealed-secrets/pkg/buildinfo" "github.com/bitnami-labs/sealed-secrets/pkg/flagenv" "github.com/bitnami-labs/sealed-secrets/pkg/kubeseal" "github.com/bitnami-labs/sealed-secrets/pkg/pflagenv" // Register Auth providers. _ "k8s.io/client-go/plugin/pkg/client/auth" ) const ( flagEnvPrefix = "SEALED_SECRETS" ) var ( // VERSION set from Makefile. VERSION = buildinfo.DefaultVersion ) type cliFlags struct { certURL string controllerNs string controllerName string outputFormat string outputFileName string inputFileName string kubeconfig string dumpCert bool allowEmptyData bool validateSecret bool mergeInto string raw bool secretName string fromFile []string sealingScope ssv1alpha1.SealingScope reEncrypt bool unseal bool privKeys []string help bool } type config struct { flags *cliFlags clientConfig kubeseal.ClientConfig ctx context.Context } func newConfig(clientConfig clientcmd.ClientConfig, flags *cliFlags) *config { return &config{ flags: flags, clientConfig: clientConfig, ctx: context.Background(), } } func initClient(kubeConfigPath string, cfgOverrides *clientcmd.ConfigOverrides, r io.Reader) clientcmd.ClientConfig { loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig loadingRules.ExplicitPath = kubeConfigPath return clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, cfgOverrides, r) } func bindFlags(f *cliFlags, fs *flag.FlagSet) { // TODO: Verify k8s server signature against cert in kube client config. fs.StringVar(&f.certURL, "cert", "", "Certificate / public key file/URL to use for encryption. Overrides --controller-*") fs.StringVar(&f.controllerNs, "controller-namespace", metav1.NamespaceSystem, "Namespace of sealed-secrets controller.") fs.StringVar(&f.controllerName, "controller-name", "sealed-secrets-controller", "Name of sealed-secrets controller.") fs.StringVarP(&f.outputFormat, "format", "o", "json", "Output format for sealed secret. Either json or yaml") fs.StringVarP(&f.outputFileName, "sealed-secret-file", "w", "", "Sealed-secret (output) file") fs.StringVarP(&f.inputFileName, "secret-file", "f", "", "Secret (input) file") fs.BoolVar(&f.dumpCert, "fetch-cert", false, "Write certificate to stdout. Useful for later use with --cert") fs.BoolVar(&f.allowEmptyData, "allow-empty-data", false, "Allow empty data in the secret object") fs.BoolVar(&f.validateSecret, "validate", false, "Validate that the sealed secret can be decrypted") fs.StringVar(&f.mergeInto, "merge-into", "", "Merge items from secret into an existing sealed secret file, updating the file in-place instead of writing to stdout.") fs.BoolVar(&f.raw, "raw", false, "Encrypt a raw value passed via the --from-* flags instead of the whole secret object") fs.StringVar(&f.secretName, "name", "", "Name of the sealed secret (required with --raw and default (strict) scope)") fs.StringSliceVar(&f.fromFile, "from-file", nil, "(only with --raw) Secret items can be sourced from files. Pro-tip: you can use /dev/stdin to read pipe input. This flag tries to follow the same syntax as in kubectl") fs.StringVar(&f.kubeconfig, "kubeconfig", "", "Path to a kube config. Only required if out-of-cluster") fs.Var(&f.sealingScope, "scope", "Set the scope of the sealed secret: strict, namespace-wide, cluster-wide (defaults to strict). Mandatory for --raw, otherwise the 'sealedsecrets.bitnami.com/cluster-wide' and 'sealedsecrets.bitnami.com/namespace-wide' annotations on the input secret can be used to select the scope.") fs.BoolVar(&f.reEncrypt, "rotate", false, "") fs.BoolVar(&f.reEncrypt, "re-encrypt", false, "Re-encrypt the given sealed secret to use the latest cluster key.") _ = fs.MarkDeprecated("rotate", "please use --re-encrypt instead") fs.BoolVar(&f.unseal, "recovery-unseal", false, "Decrypt a sealed secrets file obtained from stdin, using the private key passed with --recovery-private-key. Intended to be used in disaster recovery mode.") fs.StringSliceVar(&f.privKeys, "recovery-private-key", nil, "Private key filename used by the --recovery-unseal command. Multiple files accepted either via comma separated list or by repetition of the flag. Either PEM encoded private keys or a backup of a json/yaml encoded k8s sealed-secret controller secret (and v1.List) are accepted. ") fs.BoolVar(&f.help, "help", false, "Print this help message") fs.SetOutput(os.Stdout) } func bindClientFlags(fs *flag.FlagSet, gofs *goflag.FlagSet, overrides *clientcmd.ConfigOverrides) { flagenv.SetFlagsFromEnv(flagEnvPrefix, gofs) initUsualKubectlFlags(overrides, fs) pflagenv.SetFlagsFromEnv(flagEnvPrefix, fs) // add klog flags to goflags flagset klog.InitFlags(nil) // Standard goflags (glog in particular) fs.AddGoFlagSet(gofs) } func initUsualKubectlFlags(overrides *clientcmd.ConfigOverrides, fs *flag.FlagSet) { kflags := clientcmd.RecommendedConfigOverrideFlags("") clientcmd.BindOverrideFlags(overrides, fs, kflags) } func runCLI(w io.Writer, cfg *config) (err error) { flags := cfg.flags if flags.help { fmt.Fprintf(os.Stdout, "Usage of %s:\n", os.Args[0]) flag.PrintDefaults() return nil } if len(flags.fromFile) != 0 && !flags.raw { return fmt.Errorf("--from-file requires --raw") } var input io.Reader = os.Stdin if flags.inputFileName != "" { // #nosec G304 -- should open user provided file f, err := os.Open(flags.inputFileName) if err != nil { return fmt.Errorf("Could not read file specified with --secret-file") } // #nosec: G307 -- this deferred close is fine because it is not on a writable file defer f.Close() input = f } else if !flags.raw && !flags.dumpCert { if isatty.IsTerminal(os.Stdin.Fd()) { fmt.Fprintf(os.Stderr, "(tty detected: expecting json/yaml k8s resource in stdin)\n") } } // reEncrypt is the only "in-place" update subcommand. When the user only provides one file (the input file) // we'll use the same file for output (see #405). if flags.reEncrypt && (flags.outputFileName == "" && flags.inputFileName != "") { flags.outputFileName = flags.inputFileName } if flags.outputFileName != "" { if ext := filepath.Ext(flags.outputFileName); ext == ".yaml" || ext == ".yml" { flags.outputFormat = "yaml" } var f *renameio.PendingFile f, err = renameio.TempFile("", flags.outputFileName) if err != nil { return err } // only write the output file if the run function exits without errors. defer func() { if err == nil { _ = f.CloseAtomicallyReplace() } }() w = f } if flags.unseal { return kubeseal.UnsealSealedSecret(w, input, flags.privKeys, flags.outputFormat, scheme.Codecs) } if len(flags.privKeys) != 0 && isatty.IsTerminal(os.Stderr.Fd()) { fmt.Fprintf(os.Stderr, "warning: ignoring --recovery-private-key because unseal command not chosen with --recovery-unseal\n") } if flags.validateSecret { return kubeseal.ValidateSealedSecret(cfg.ctx, cfg.clientConfig, flags.controllerNs, flags.controllerName, input) } if flags.reEncrypt { return kubeseal.ReEncryptSealedSecret(cfg.ctx, cfg.clientConfig, flags.controllerNs, flags.controllerName, flags.outputFormat, input, w, scheme.Codecs) } f, err := kubeseal.OpenCert(cfg.ctx, cfg.clientConfig, flags.controllerNs, flags.controllerName, flags.certURL) if err != nil { return err } // #nosec: G307 -- this deferred close is fine because it is not on a writable file defer f.Close() if flags.dumpCert { _, err := io.Copy(w, f) return err } pubKey, err := kubeseal.ParseKey(f) if err != nil { return err } if flags.mergeInto != "" { return kubeseal.SealMergingInto(cfg.clientConfig, flags.outputFormat, input, flags.mergeInto, scheme.Codecs, pubKey, flags.sealingScope, flags.allowEmptyData) } if flags.raw { var ( ns string err error ) if flags.sealingScope < ssv1alpha1.ClusterWideScope { ns, _, err = cfg.clientConfig.Namespace() if err != nil { return err } if ns == "" { return fmt.Errorf("must provide the --namespace flag with --raw and --scope %s", flags.sealingScope.String()) } if flags.secretName == "" && flags.sealingScope < ssv1alpha1.NamespaceWideScope { return fmt.Errorf("must provide the --name flag with --raw and --scope %s", flags.sealingScope.String()) } } var data []byte if len(flags.fromFile) > 0 { if len(flags.fromFile) > 1 { return fmt.Errorf("must provide only one --from-file when encrypting a single item with --raw") } _, filename := kubeseal.ParseFromFile(flags.fromFile[0]) // #nosec G304 -- should open user provided file data, err = os.ReadFile(filename) } else { if isatty.IsTerminal(os.Stdin.Fd()) { fmt.Fprintf(os.Stderr, "(tty detected: expecting a secret to encrypt in stdin)\n") } data, err = io.ReadAll(os.Stdin) } if err != nil { return err } return kubeseal.EncryptSecretItem(w, flags.secretName, ns, data, flags.sealingScope, pubKey) } return kubeseal.Seal(cfg.clientConfig, flags.outputFormat, input, w, scheme.Codecs, pubKey, flags.sealingScope, flags.allowEmptyData, flags.secretName, "") } func mainE(w io.Writer, fs *flag.FlagSet, gofs *goflag.FlagSet, args []string) error { var flags cliFlags var printVersion bool var overrides clientcmd.ConfigOverrides buildinfo.FallbackVersion(&VERSION, buildinfo.DefaultVersion) fs.BoolVar(&printVersion, "version", false, "Print version information and exit") bindFlags(&flags, fs) bindClientFlags(fs, gofs, &overrides) if err := fs.Parse(args); err != nil { return err } if err := gofs.Parse([]string{}); err != nil { return err } if printVersion { fmt.Fprintf(w, "kubeseal version: %s\n", VERSION) return nil } clientConfig := initClient(flags.kubeconfig, &overrides, os.Stdout) cfg := newConfig(clientConfig, &flags) return runCLI(w, cfg) } func main() { if err := mainE(os.Stdout, flag.CommandLine, goflag.CommandLine, os.Args); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } } ================================================ FILE: cmd/kubeseal/main_test.go ================================================ package main import ( "bytes" "context" "crypto/rand" "crypto/rsa" "encoding/pem" goflag "flag" "fmt" "io" "os" "path/filepath" "strings" "testing" "time" ssv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" "github.com/bitnami-labs/sealed-secrets/pkg/crypto" "github.com/bitnami-labs/sealed-secrets/pkg/kubeseal" flag "github.com/spf13/pflag" _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" certUtil "k8s.io/client-go/util/cert" "k8s.io/client-go/util/keyutil" ) // mockClientConfig implements clientcmd.ClientConfig for testing type mockClientConfig struct { namespace string namespaceSet bool } func (m *mockClientConfig) Namespace() (string, bool, error) { return m.namespace, m.namespaceSet, nil } func (m *mockClientConfig) ClientConfig() (*rest.Config, error) { return &rest.Config{}, nil } func (m *mockClientConfig) ConfigAccess() clientcmd.ConfigAccess { return nil } func (m *mockClientConfig) RawConfig() (clientcmdapi.Config, error) { return clientcmdapi.Config{}, nil } func TestVersion(t *testing.T) { buf := bytes.NewBufferString("") testVersionFlags := flag.NewFlagSet("testVersionFlags", flag.ExitOnError) nopFlags := goflag.NewFlagSet("nop", goflag.ExitOnError) err := mainE(buf, testVersionFlags, nopFlags, []string{"--version"}) if err != nil { t.Fatal(err) } if got, want := buf.String(), "kubeseal version: UNKNOWN\n"; got != want { t.Errorf("got: %q, want: %q", got, want) } } func testClientConfig() clientcmd.ClientConfig { return &mockClientConfig{namespace: "default", namespaceSet: false} } func testConfig(flags *cliFlags) *config { clientConfig := testClientConfig() return &config{ flags: flags, clientConfig: clientConfig, ctx: context.Background(), } } func TestMainError(t *testing.T) { badFileName := filepath.Join("this", "file", "cannot", "possibly", "exist", "can", "it?") flags := cliFlags{certURL: badFileName} err := runCLI(io.Discard, testConfig(&flags)) if err == nil || !os.IsNotExist(err) { t.Fatalf("expecting not exist error, got: %v", err) } } // writeTempFile creates a temporary file, writes data into it and closes it. func writeTempFile(b []byte) (string, error) { tmp, err := os.CreateTemp("", "") if err != nil { return "", err } defer tmp.Close() if _, err := tmp.Write(b); err != nil { os.RemoveAll(tmp.Name()) return "", err } return tmp.Name(), nil } func newTestKeyPairSingle(t *testing.T) (*rsa.PublicKey, *rsa.PrivateKey) { privKey, _, err := crypto.GeneratePrivateKeyAndCert(2048, time.Hour, "testcn") if err != nil { t.Fatal(err) } return &privKey.PublicKey, privKey } // testingKeypairFiles returns a path to a PEM encoded certificate and a PEM encoded private key // along with a function to be called to cleanup those files. func testingKeypairFiles(t *testing.T) (string, string, func()) { _, pk := newTestKeyPairSingle(t) cert, err := crypto.SignKey(rand.Reader, pk, time.Hour, "testcn") if err != nil { t.Fatal(err) } certFile, err := writeTempFile(pem.EncodeToMemory(&pem.Block{Type: certUtil.CertificateBlockType, Bytes: cert.Raw})) if err != nil { t.Fatal(err) } pkPEM, err := keyutil.MarshalPrivateKeyToPEM(pk) if err != nil { t.Fatal(err) } pkFile, err := writeTempFile(pkPEM) if err != nil { t.Fatal(err) } return certFile, pkFile, func() { os.RemoveAll(certFile) os.RemoveAll(pkFile) } } func TestWriteToFile(t *testing.T) { certFilename, _, cleanup := testingKeypairFiles(t) defer cleanup() in, err := os.CreateTemp("", "") if err != nil { t.Fatal(err) } defer os.RemoveAll(in.Name()) fmt.Fprintf(in, `apiVersion: v1 kind: Secret metadata: name: foo namespace: bar data: super: c2VjcmV0 `) in.Close() out, err := os.CreateTemp("", "*.yaml") if err != nil { t.Fatal(err) } out.Close() defer os.RemoveAll(out.Name()) var buf bytes.Buffer flags := cliFlags{ inputFileName: in.Name(), outputFileName: out.Name(), certURL: certFilename, } if err := runCLI(&buf, testConfig(&flags)); err != nil { t.Fatal(err) } if got, want := buf.Len(), 0; got != want { t.Errorf("got: %d, want: %d", got, want) } b, err := os.ReadFile(out.Name()) if err != nil { t.Fatal(err) } if sub := "kind: SealedSecret"; !bytes.Contains(b, []byte(sub)) { t.Errorf("expecting to find %q in %q", sub, b) } } func TestFailToWriteToFile(t *testing.T) { certFilename, _, cleanup := testingKeypairFiles(t) defer cleanup() in, err := os.CreateTemp("", "") if err != nil { t.Fatal(err) } defer os.RemoveAll(in.Name()) fmt.Fprintf(in, `apiVersion: v1 kind: BadInput metadata: name: foo namespace: bar `) in.Close() out, err := os.CreateTemp("", "") if err != nil { t.Fatal(err) } // if sealing error happens, the old content of the output file shouldn't be truncated. const testOldContent = "previous content" fmt.Fprint(out, testOldContent) out.Close() defer os.RemoveAll(out.Name()) var buf bytes.Buffer flags := cliFlags{ inputFileName: in.Name(), outputFileName: out.Name(), certURL: certFilename, } if err := runCLI(&buf, testConfig(&flags)); err == nil { t.Errorf("expecting error") } if got, want := buf.Len(), 0; got != want { t.Errorf("got: %d, want: %d", got, want) } b, err := os.ReadFile(out.Name()) if err != nil { t.Fatal(err) } if got, want := string(b), testOldContent; got != want { t.Errorf("got: %q, want: %q", got, want) } } func Test_runCLI(t *testing.T) { type args struct { cfg *config } tests := []struct { name string args args wantW string wantErr bool }{ // TODO: Add test cases. } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { w := &bytes.Buffer{} if err := runCLI(w, tt.args.cfg); (err != nil) != tt.wantErr { t.Errorf("runCLI() error = %v, wantErr %v", err, tt.wantErr) return } if gotW := w.String(); gotW != tt.wantW { t.Errorf("runCLI() = %v, want %v", gotW, tt.wantW) } }) } } type tweakedClientConfig struct { ccfg kubeseal.ClientConfig namespace string } func (tcc *tweakedClientConfig) Namespace() (string, bool, error) { return tcc.namespace, false, nil } func (tcc *tweakedClientConfig) ClientConfig() (*rest.Config, error) { return tcc.ccfg.ClientConfig() } func trySealTestItem(certFilename, secretNS, secretName, secretValue string, scope ssv1alpha1.SealingScope) (string, error) { dataFile, err := writeTempFile([]byte(secretValue)) if err != nil { return "", err } defer os.RemoveAll(dataFile) fromFile := []string{dataFile} var buf bytes.Buffer flags := cliFlags{ sealingScope: scope, secretName: secretName, certURL: certFilename, raw: true, fromFile: fromFile, } cfg := testConfig(&flags) cfg.clientConfig = &tweakedClientConfig{cfg.clientConfig, secretNS} if err := runCLI(&buf, cfg); err != nil { return "", err } return buf.String(), nil } func TestRawSealErrors(t *testing.T) { certFilename, _, cleanup := testingKeypairFiles(t) defer cleanup() const ( secretNS = "myns" secretName = "mysecret" secretValue = "supersecret" ) testCases := []struct { ns string name string scope ssv1alpha1.SealingScope sealErr string }{ {ns: "", name: "", sealErr: "must provide the --namespace flag with --raw and --scope strict"}, {ns: secretNS, name: "", sealErr: "must provide the --name flag with --raw and --scope strict"}, {scope: ssv1alpha1.NamespaceWideScope, name: secretName, sealErr: "must provide the --namespace flag with --raw and --scope namespace-wide"}, } for i, tc := range testCases { // try to encrypt an item and check error response t.Run(fmt.Sprint(i), func(t *testing.T) { _, err := trySealTestItem(certFilename, tc.ns, tc.name, secretValue, tc.scope) if got, want := fmt.Sprint(err), tc.sealErr; !strings.HasPrefix(got, want) { t.Fatalf("got: %v, want: %v", err, want) } }) } } ================================================ FILE: contrib/prometheus-mixin/.gitignore ================================================ manifests/ ================================================ FILE: contrib/prometheus-mixin/Makefile ================================================ # Prometheus Mixin Makefile # Heavily copied from upstream project kubenetes-mixin PROMETHEUS_IMAGE := prom/prometheus:v2.21.0 JSONNET_FMT := jsonnetfmt all: fmt prometheus_alerts.yaml prometheus_rules.yaml dashboards_out lint test ## Generate files, lint and test fmt: ## Format Jsonnet find . -name 'vendor' -prune -o -name '*.libsonnet' -print -o -name '*.jsonnet' -print | \ xargs -n 1 -- $(JSONNET_FMT) -i prometheus_alerts.yaml: mixin.libsonnet lib/alerts.jsonnet alerts/*.libsonnet ## Generate Alerts YAML @mkdir -p manifests jsonnet -S lib/alerts.jsonnet > manifests/$@ prometheus_rules.yaml: mixin.libsonnet lib/rules.jsonnet rules/*.libsonnet ## Generate Rules YAML @mkdir -p manifests jsonnet -S lib/rules.jsonnet > manifests/$@ dashboards_out: mixin.libsonnet lib/dashboards.jsonnet dashboards/*.libsonnet ## Generate Dashboards JSON jsonnet -J vendor -m manifests lib/dashboards.jsonnet lint: prometheus_alerts.yaml prometheus_rules.yaml ## Lint and check YAML find . -name 'vendor' -prune -o -name '*.libsonnet' -print -o -name '*.jsonnet' -print | \ while read f; do \ $(JSONNET_FMT) "$$f" | diff -u "$$f" -; \ done docker run \ -v $(PWD)/manifests:/tmp \ --entrypoint '/bin/promtool' \ $(PROMETHEUS_IMAGE) \ check rules /tmp/prometheus_rules.yaml; \ docker run \ -v $(PWD)/manifests:/tmp \ --entrypoint '/bin/promtool' \ $(PROMETHEUS_IMAGE) \ check rules /tmp/prometheus_alerts.yaml clean: ## Clean up generated files rm -rf manifests/ # TODO: Find out why official prom images segfaults during `test rules` if not root test: prometheus_alerts.yaml prometheus_rules.yaml ## Test generated files docker run \ -v $(PWD):/tmp \ --user root \ --entrypoint '/bin/promtool' \ $(PROMETHEUS_IMAGE) \ test rules /tmp/tests.yaml .PHONY: help help: @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' ================================================ FILE: contrib/prometheus-mixin/README.md ================================================ # Sealed Secrets Metrics The Sealed Secrets Controller running in Kubernetes exposes Prometheus metrics on `*:8081/metrics`. These metrics enable operators to observe how it is performing. For example how many `SealedSecret` unseals have been attempted and how many errors may have occured due to RBAC permissions, wrong key, corrupted data, etc. These metrics can be scraped by a Prometheus server and viewed in Prometheus, displayed on a Grafana dashboard and/or trigger alerts to Slack/etc. ## Prometheus Mixin A Prometheus mixin bundles all of the metric related concerns into a single package for users of the application to consume. Typically this includes dashboards, recording rules, alerts and alert logic tests. By creating a mixin, application maintainers and contributors to the project can enshrine knowledge about operating the application and potential SLO's that users may wish to use. For more details about this concept see the [monitoring-mixins](https://github.com/monitoring-mixins/docs) project on GitHub. ## Scraping the metrics manually After installing the Sealed Secrets Controller you can access the metrics via Kubernetes port-forward to your pod: ``` $ kubectl port-forward sealed-secrets-controller-6566dc69c6-lqr6x 8081 & [1] 293283 ``` Then query the metrics endpoint: ``` $ curl localhost:8081/metrics # HELP sealed_secrets_controller_build_info Build information. # TYPE sealed_secrets_controller_build_info gauge sealed_secrets_controller_build_info{revision="v0.12.1"} 0 # HELP sealed_secrets_controller_unseal_errors_total Total number of sealed secret unseal errors by reason # TYPE sealed_secrets_controller_unseal_errors_total counter sealed_secrets_controller_unseal_errors_total{reason="fetch"} 0 sealed_secrets_controller_unseal_errors_total{reason="status"} 0 sealed_secrets_controller_unseal_errors_total{reason="unmanaged"} 0 sealed_secrets_controller_unseal_errors_total{reason="unseal"} 0 sealed_secrets_controller_unseal_errors_total{reason="update"} 0 # HELP sealed_secrets_controller_unseal_requests_total Total number of sealed secret unseal requests # TYPE sealed_secrets_controller_unseal_requests_total counter sealed_secrets_controller_unseal_requests_total 86 ``` ## Scraping metrics with the Prometheus Operator The [Prometheus Operator](https://github.com/coreos/prometheus-operator) supports a couple of Kubernetes native scrape target `CustomResourceDefinitions`. This project includes a [PodMonitor](../../controller-podmonitor.jsonnet ) CRD definition in jsonnet. To use this: Compile jsonnet to yaml: ``` $ make controller-podmonitor.yaml kubecfg show -V CONTROLLER_IMAGE=docker.io/bitnami/sealed-secrets-controller:latest -V IMAGE_PULL_POLICY=Always -o yaml controller-podmonitor.jsonnet > controller-podmonitor.yaml.tmp mv controller-podmonitor.yaml.tmp controller-podmonitor.yaml ``` Submit the `PodMonitor` CustomResourceDefinition to Kubernetes API: ``` $ kubectl apply -f controller-podmonitor.yaml ``` The Prometheus Operator will trigger a reload of Prometheus configuration and you should see the Sealed Secrets Controller in your Prometheus UI under `Service Discovery` and `Targets`. ## Grafana dashboard The [dashboard](./dashboards/sealed-secrets-controller.json) can be imported standalone into Grafana. You may need to edit the datasource if you have configured your Prometheus datasource with a different name. ## Using the mixin with kube-prometheus See the [kube-prometheus](https://github.com/coreos/kube-prometheus#kube-prometheus) project documentation for instructions on importing mixins. ## Using the mixin as raw YAML files If you don't use the jsonnet based `kube-prometheus` project then you will need to generate the raw yaml files for inclusion in your Prometheus installation. Install the `jsonnet` dependencies: ``` $ go get github.com/google/go-jsonnet/cmd/jsonnet $ go get github.com/google/go-jsonnet/cmd/jsonnetfmt ``` Generate yaml: ``` $ make ``` ================================================ FILE: contrib/prometheus-mixin/alerts/alerts.libsonnet ================================================ // Sealed Secrets Alertmanager Alerts (import 'sealed-secrets-alerts.libsonnet') ================================================ FILE: contrib/prometheus-mixin/alerts/sealed-secrets-alerts.libsonnet ================================================ { prometheusAlerts+:: { groups+: [{ name: 'sealed-secrets', rules: [ // SealedSecretsErrorRateHigh: // Method: Alert on occurence of errors by looking for a non-zero rate of errors over past 5 minutes // Pros: // - An app deploy is likely broken if a secret can't be updated by Controller. // Caveats: // - Probably better to leave app deploy breakages to the app or CD systems monitoring. // - Potentially noisy. Controller attempts to unseal 5 times, so if it exceeds on the 4th attempt then all is fine but this alert will trigger. // - Usage of an invalid cert.pem with kubeseal will trigger this alert, it would be better to distinguish alerts due to controller or user // - 'for' clause not used because we are unlikely to have a sustained rate of errors unless there is a LOT of secret churn in cluster. // Rob Ewaschuk - My Philosophy on Alerting: https://docs.google.com/document/d/199PqyG3UsyXlwieHaqbGiWVa8eMWi8zzAn0YfcApr8Q/edit { alert: 'SealedSecretsUnsealErrorHigh', expr: ||| sum by (reason, namespace) (rate(sealed_secrets_controller_unseal_errors_total{}[5m])) > 0 ||| % $._config, // 'for': '5m', // Not used, see caveats above. labels: { severity: 'warning', }, annotations: { summary: 'Sealed Secrets Unseal Error High', description: 'High number of errors during unsealing Sealed Secrets in {{ $labels.namespace }} namespace.', runbook_url: 'https://github.com/bitnami-labs/sealed-secrets', }, }, ], }], }, } ================================================ FILE: contrib/prometheus-mixin/config.libsonnet ================================================ // Sealed Secrets Prometheus Mixin Config { _config+:: {}, } ================================================ FILE: contrib/prometheus-mixin/dashboards/dashboards.libsonnet ================================================ // Sealed Secrets Grafana Dashboards { grafanaDashboards+:: { 'sealed-secrets-controller.json': (import 'sealed-secrets-controller.json'), }, } ================================================ FILE: contrib/prometheus-mixin/dashboards/sealed-secrets-controller.json ================================================ { "annotations": { "list": [ { "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" } ] }, "description": "Sealed Secrets Controller", "editable": true, "gnetId": null, "graphTooltip": 0, "id": 3, "iteration": 1585599163503, "links": [ { "icon": "external link", "tags": [], "title": "GitHub", "tooltip": "View Project on GitHub", "type": "link", "url": "https://github.com/bitnami-labs/sealed-secrets" } ], "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "description": "Rate of requests to unseal a SealedSecret.\n\nThis can include non-obvious operations such as deleting a SealedSecret.", "fill": 1, "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 0, "y": 0 }, "hiddenSeries": false, "id": 2, "legend": { "avg": true, "current": false, "max": true, "min": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "dataLinks": [] }, "percentage": false, "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(sealed_secrets_controller_unseal_requests_total{}[1m]))", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "rps", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Unseal Request Rate/s", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "description": "Rate of errors when unsealing a SealedSecret. \n\nReason for error included as label value, eg:\n- unseal = cryptography issue (key/namespace) or RBAC\n- unmanaged = destination Secret wasn't created by SealedSecrets\n- update = potentially RBAC\n- status = potentially RBAC\n- fetch = potentially RBAC\n", "fill": 1, "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 12, "y": 0 }, "hiddenSeries": false, "id": 3, "legend": { "avg": false, "current": false, "hideEmpty": false, "hideZero": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null as zero", "options": { "dataLinks": [] }, "percentage": false, "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(sealed_secrets_controller_unseal_errors_total{pod=~\"$pod\"}[1m])) by (reason)", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{ reason }}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Unseal Error Rate/s", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "refresh": false, "schemaVersion": 22, "style": "dark", "tags": [], "templating": { "list": [ { "current": { "text": "prometheus", "value": "prometheus" }, "hide": 0, "includeAll": false, "label": null, "multi": false, "name": "datasource", "options": [], "query": "prometheus", "refresh": 1, "regex": "", "skipUrlSync": false, "type": "datasource" }, { "allValue": null, "current": { "selected": false, "text": "All", "value": "$__all" }, "datasource": "$datasource", "definition": "label_values(kube_pod_info, pod)", "hide": 0, "includeAll": true, "label": null, "multi": false, "name": "pod", "options": [], "query": "label_values(kube_pod_info, pod)", "refresh": 1, "regex": "/^sealed-secrets-controller.*$/", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "time": { "from": "now-1h", "to": "now" }, "timepicker": { "refresh_intervals": [ "5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d" ], "time_options": [ "5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d" ] }, "timezone": "", "title": "Sealed Secrets Controller", "uid": "UuEtZCVWz", "version": 2 } ================================================ FILE: contrib/prometheus-mixin/lib/alerts.jsonnet ================================================ std.manifestYamlDoc((import '../mixin.libsonnet').prometheusAlerts) ================================================ FILE: contrib/prometheus-mixin/lib/dashboards.jsonnet ================================================ local dashboards = (import '../mixin.libsonnet').grafanaDashboards; { [name]: dashboards[name] for name in std.objectFields(dashboards) } ================================================ FILE: contrib/prometheus-mixin/lib/rules.jsonnet ================================================ std.manifestYamlDoc((import '../mixin.libsonnet').prometheusRules) ================================================ FILE: contrib/prometheus-mixin/mixin.libsonnet ================================================ // Prometheus Mixin // Follows the kubernetes-mixin project pattern here: https://github.com/kubernetes-monitoring/kubernetes-mixin // Mixin design doc: https://docs.google.com/document/d/1A9xvzwqnFVSOZ5fD3blKODXfsat5fg6ZhnKu9LK3lB4/edit // This file will be imported during build for all Promethei (import 'config.libsonnet') + (import 'alerts/alerts.libsonnet') + (import 'dashboards/dashboards.libsonnet') + (import 'rules/rules.libsonnet') ================================================ FILE: contrib/prometheus-mixin/rules/rules.libsonnet ================================================ // Sealed Secrets Prometheus Recording Rules { prometheusRules+:: { groups+: [ // import ('sealed-secrets-rules.libsonnet') ], }, } ================================================ FILE: contrib/prometheus-mixin/tests.yaml ================================================ --- rule_files: - /tmp/manifests/prometheus_alerts.yaml - /tmp/manifests/prometheus_rules.yaml evaluation_interval: 1m tests: - interval: 1m input_series: - series: 'sealed_secrets_controller_unseal_errors_total{reason="update",namespace="test"}' values: '0+0x5 1+1x5' - series: 'sealed_secrets_controller_unseal_errors_total{reason="unseal",namespace="test"}' values: '0+0x10' alert_rule_test: - eval_time: 5m alertname: SealedSecretsUnsealErrorHigh - eval_time: 10m alertname: SealedSecretsUnsealErrorHigh exp_alerts: - exp_labels: severity: warning namespace: test reason: update exp_annotations: summary: 'Sealed Secrets Unseal Error High' description: 'High number of errors during unsealing Sealed Secrets in test namespace.' runbook_url: 'https://github.com/bitnami-labs/sealed-secrets' ================================================ FILE: controller-norbac.jsonnet ================================================ // Minimal required deployment for a functional controller. local kubecfg = import 'kubecfg.libsonnet'; local namespace = 'kube-system'; { kube:: (import 'vendor_jsonnet/kube-libsonnet/kube.libsonnet'), local kube = self.kube + import 'kube-fixes.libsonnet', controllerImage:: std.extVar('CONTROLLER_IMAGE'), imagePullPolicy:: local ext = std.extVar('IMAGE_PULL_POLICY'); if ext == '' then if std.endsWith($.controllerImage, ':latest') then 'Always' else 'IfNotPresent' else ext, crd: kube.CustomResourceDefinition('bitnami.com', 'v1alpha1', 'SealedSecret') { spec+: { versions_+: { v1alpha1+: { served: true, storage: true, subresources: { status: {}, }, schema: kubecfg.parseYaml(importstr 'schema-v1alpha1.yaml')[0], }, }, }, }, namespace:: { metadata+: { namespace: namespace } }, service: kube.Service('sealed-secrets-controller') + $.namespace { target_pod: $.controller.spec.template, }, service_metrics: kube.Service('sealed-secrets-controller-metrics') + $.namespace { local service = self, target_pod: $.controller.spec.template, spec: { selector: service.target_pod.metadata.labels, ports: [ { port: 8081, targetPort: 8081, }, ], type: "ClusterIP", }, }, controller: kube.Deployment('sealed-secrets-controller') + $.namespace { spec+: { template+: { spec+: { securityContext+: { fsGroup: 65534, runAsNonRoot: true, runAsUser: 1001, seccompProfile+: { type: 'RuntimeDefault', } }, containers_+: { controller: kube.Container('sealed-secrets-controller') { image: $.controllerImage, imagePullPolicy: $.imagePullPolicy, command: ['controller'], readinessProbe: { httpGet: { path: '/healthz', port: 'http' }, }, livenessProbe: self.readinessProbe, ports_+: { http: { containerPort: 8080 }, metrics: { containerPort: 8081 }, }, securityContext+: { allowPrivilegeEscalation: false, capabilities+: { drop: [ 'ALL' ], }, readOnlyRootFilesystem: true, }, volumeMounts_+: { tmp: { mountPath: '/tmp', }, }, }, }, volumes_+: { tmp: { emptyDir: {}, }, }, }, }, }, }, } ================================================ FILE: controller-podmonitor.jsonnet ================================================ // Prometheus Pod Monitor manifest // ref: https://github.com/prometheus-operator/prometheus-operator#customresourcedefinitions local controller = import 'controller.jsonnet'; controller { podMonitor: { apiVersion: 'monitoring.coreos.com/v1', kind: 'PodMonitor', metadata: { name: 'sealed-secrets-controller', namespace: $.namespace.metadata.namespace, labels: { name: 'sealed-secrets-controller', }, }, spec: { jobLabel: 'name', selector: { matchLabels: { name: 'sealed-secrets-controller', }, }, namespaceSelector: { matchNames: [ $.namespace.metadata.namespace, ], }, podMetricsEndpoints: [ { honorLabels: true, // prefer controller metric namespace port: 'http', interval: '30s', }, ], sampleLimit: 1000, }, }, } ================================================ FILE: controller.jsonnet ================================================ // This is the recommended cluster deployment of sealed-secrets. // See controller-norbac.jsonnet for the bare minimum functionality. local controller = import 'controller-norbac.jsonnet'; controller { local kube = self.kube, account: kube.ServiceAccount('sealed-secrets-controller') + $.namespace, unsealerRole: kube.ClusterRole('secrets-unsealer') { rules: [ { apiGroups: ['bitnami.com'], resources: ['sealedsecrets'], verbs: ['get', 'list', 'watch'], }, { apiGroups: ['bitnami.com'], resources: ['sealedsecrets/status'], verbs: ['update'], }, { apiGroups: [''], resources: ['secrets'], verbs: ['get', 'list', 'create', 'update', 'delete', 'watch'], }, { apiGroups: [''], resources: ['events'], verbs: ['create', 'patch'], }, { apiGroups: [''], resources: ['namespaces'], verbs: ['get'], }, ], }, unsealKeyRole: kube.Role('sealed-secrets-key-admin') + $.namespace { rules: [ { apiGroups: [''], resources: ['secrets'], // Can't limit create by resource name as keys are produced on the fly verbs: ['create', 'list'], }, ], }, serviceProxierRole: kube.Role('sealed-secrets-service-proxier') + $.namespace { rules: [ { apiGroups: [ '', ], resources: [ 'services', ], resourceNames: [ 'sealed-secrets-controller', ], // kubeseal dynamically obtains the service port name so later on // can access the service using a proxy verbs: [ 'get', ], }, { apiGroups: [ '', ], resources: [ 'services/proxy', ], resourceNames: [ 'http:sealed-secrets-controller:', // kubeseal uses net.JoinSchemeNamePort when crafting proxy subresource URLs 'http:sealed-secrets-controller:http', 'sealed-secrets-controller', // but often services are referred by name only, let's not make it unnecessarily cryptic ], verbs: [ 'create', // rotate and validate endpoints expect POST, see https://kubernetes.io/docs/reference/access-authn-authz/authorization/#determine-the-request-verb 'get', ], }, ], }, unsealerBinding: kube.ClusterRoleBinding('sealed-secrets-controller') { roleRef_: $.unsealerRole, subjects_+: [$.account], }, unsealKeyBinding: kube.RoleBinding('sealed-secrets-controller') + $.namespace { roleRef_: $.unsealKeyRole, subjects_+: [$.account], }, serviceProxierBinding: kube.RoleBinding('sealed-secrets-service-proxier') + $.namespace { roleRef_: $.serviceProxierRole, // kube.libsonnet assumes object here have a namespace, but system groups don't // thus are not supposed to use the magic "_" here. subjects+: [kube.Group('system:authenticated')], }, controller+: { spec+: { template+: { spec+: { serviceAccountName: $.account.metadata.name, }, }, }, }, } ================================================ FILE: docker/controller.Dockerfile ================================================ FROM gcr.io/distroless/static@sha256:47b2d72ff90843eb8a768b5c2f89b40741843b639d065b9b937b07cd59b479c6 LABEL maintainer "Sealed Secrets " USER 1001 ARG TARGETARCH COPY dist/controller_linux_${TARGETARCH}*/controller /usr/local/bin/ EXPOSE 8080 8081 ENTRYPOINT ["controller"] ================================================ FILE: docker/kubeseal.Dockerfile ================================================ FROM gcr.io/distroless/static@sha256:47b2d72ff90843eb8a768b5c2f89b40741843b639d065b9b937b07cd59b479c6 LABEL maintainer "Sealed Secrets " USER 1001 ARG TARGETARCH COPY dist/kubeseal_linux_${TARGETARCH}*/kubeseal /usr/local/bin/ ENTRYPOINT ["kubeseal"] ================================================ FILE: docs/GKE.md ================================================ **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - [GKE](#gke) - [Install](#install) - [Private GKE clusters](#private-gke-clusters) - [Offline sealing](#offline-sealing) - [Control Plane to Node firewall](#control-plane-to-node-firewall) - [RBAC and GKE Warden Restrictions](#rbac-and-gke-warden-restrictions) - [Workarounds](#workarounds) - [Option 1: Disable the service-proxier (Simplest)](#option-1-disable-the-service-proxier-simplest) - [Option 2: Use Google Groups for RBAC (Recommended)](#option-2-use-google-groups-for-rbac-recommended) # GKE ## Install If installing on a GKE cluster you don't have admin rights for, a ClusterRoleBinding may be needed to successfully deploy the controller in the final command. Replace with a valid email, and then deploy the cluster role binding: ```bash USER_EMAIL= kubectl create clusterrolebinding $USER-cluster-admin-binding --clusterrole=cluster-admin --user=$USER_EMAIL ``` ## Private GKE clusters If you are using a **private GKE cluster**, `kubeseal` won't be able to fetch the public key from the controller because there is firewall that prevents the control plane to talk directly to the nodes. There are currently two workarounds: ### Offline sealing If you have the public key for your controller, you can seal secrets without talking to the controller. Normally `kubeseal --fetch-cert` can be used to obtain the certificate for later use, but in this case the firewall prevents us from doing it. The controller outputs the certificate to the logs so you can copy paste it from there. Once you have the cert this is how you seal secrets: ```bash kubeseal --cert=cert.pem - [Prerequisites](#prerequisites) - [The Sealed Secrets Components](#the-sealed-secrets-components) - [Controller](#controller) - [Kubeseal](#kubeseal) - [git-hooks](#git-hooks) ## Prerequisites To be able to develop on this project, you need to have the following tools installed: - [Git](https://git-scm.com/) - [Make](https://www.gnu.org/software/make/) - [Go programming language](https://golang.org/dl/) - [Docker CE](https://www.docker.com/community-edition) - [Kubernetes cluster (v1.16+)](https://kubernetes.io/docs/setup/). [Minikube](https://github.com/kubernetes/minikube) is recommended. - [Kubecfg](https://github.com/bitnami/kubecfg) - [Ginkgo](https://onsi.github.io/ginkgo/) - [git-hooks](https://github.com/git-hooks/git-hooks) - [doctoc](https://github.com/thlorenz/doctoc) ## The Sealed Secrets Components Sealed Secrets is composed of three parts: - A [custom resource](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources) named `SealedSecret` - A cluster-side controller / operator that manages the `SealedSecret` objects - A client-side utility: kubeseal ### Controller The controller is in charge of keeping the current state of `SealedSecret` objects in sync with the declared desired state. Please refer to the [Sealed Secrets Controller](controller.md) for the developer setup. ### Kubeseal The `kubeseal` utility uses asymmetric crypto to encrypt secrets that only the controller can decrypt. Please refer to the [Kubeseal Developer Guide](kubeseal.md) for the developer setup. ## git-hooks To avoid easily detectable issues and prevent them from reaching main, some validations have been implemented via [git hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks). To have those hooks committed in the repository you need to install a third party tool `git-hooks` (check [prerequisites](#prerequisites)), because the hooks provided by Git are stored in the `.git` directory that is not included as part of the repositories. Currently, there's a single hook at pre-commit level. This hook ensures the Table of Contents (TOC) is updated using `doctoc` (check [prerequisites](#prerequisites)) in every `.md` and `.txt` file that uses this tool. Configure git-hooks for this specific repository by running `git hooks install`. You can check with the following command if everything was configured properly: ```console $ git hooks list Git hooks ARE installed in this repository. project hooks pre-commit - doc-toc Contrib hooks ``` ================================================ FILE: docs/developer/controller.md ================================================ # Controller Developer Guide The controller is in charge of keeping the current state of `SealedSecret` objects in sync with the declared desired state. The controller exposes an API defined using the Swagger or OpenAPI v3 specification. You can download the definition from the link below: - [swagger.yml](swagger.yml) **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - [Download the controller source code](#download-the-controller-source-code) - [Setup a kubernetes cluster to run the tests](#setup-a-kubernetes-cluster-to-run-the-tests) - [Run all controller tests with a single command](#run-all-controller-tests-with-a-single-command) - [Run tests step by step](#run-tests-step-by-step) - [Building the `controller` binary](#building-the-controller-binary) - [Running unit tests](#running-unit-tests) - [Push the controller image](#push-the-controller-image) - [Building & applying the controller manifests](#building--applying-the-controller-manifests) - [Running integration tests](#running-integration-tests) ## Download the controller source code ```bash git clone https://github.com/bitnami-labs/sealed-secrets.git $SEALED_SECRETS_DIR ``` The controller sources are located under `cmd/controller/` and use packages from the `pkg` directory. ### Setup a kubernetes cluster to run the tests You need a kubernetes cluster to run the integration tests. For instance: When using a local minikube, configure your local environment to re-use the local Docker daemon: ```bash minikube start eval $(minikube docker-env) ``` If you use `kind` instead, you can setup a local companion image registry and allow kind to access it. Sample to run a registry locally: ```bash export LOCAL_REGISTRY_PORT='5000' export LOCAL_REGISTRY_NAME='kind-registry' docker run --rm -d -p "127.0.0.1:${LOCAL_REGISTRY_PORT}:5000" --name "${LOCAL_REGISTRY_NAME}" registry:2 ``` Then to have launch `kind` with access to that registry: ```bash cat </dev/null || echo ../code-generator)} source "${CODEGEN_PKG}/kube_codegen.sh" THIS_PKG="github.com/bitnami-labs/sealed-secrets" kube::codegen::gen_helpers \ --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ "${SCRIPT_ROOT}/pkg/apis" kube::codegen::gen_client \ --with-watch \ --output-dir "${SCRIPT_ROOT}/pkg/client" \ --output-pkg "${THIS_PKG}/pkg/client" \ --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ "${SCRIPT_ROOT}/pkg/apis" ================================================ FILE: helm/sealed-secrets/.helmignore ================================================ # Patterns to ignore when building packages. # This supports shell glob matching, relative path matching, and # negation (prefixed with !). Only one pattern per line. .DS_Store # Common VCS dirs .git/ .gitignore .bzr/ .bzrignore .hg/ .hgignore .svn/ # Common backup files *.swp *.bak *.tmp *~ # Various IDEs .project .idea/ *.tmproj ================================================ FILE: helm/sealed-secrets/Chart.yaml ================================================ annotations: category: DeveloperTools apiVersion: v2 appVersion: 0.36.1 description: Helm chart for the sealed-secrets controller. home: https://github.com/bitnami-labs/sealed-secrets icon: https://bitnami.com/assets/stacks/sealed-secrets/img/sealed-secrets-stack-220x234.png keywords: - secrets - sealed-secrets kubeVersion: ">=1.16.0-0" maintainers: - name: Bitnami url: https://github.com/bitnami-labs/sealed-secrets name: sealed-secrets type: application version: 2.18.4 sources: - https://github.com/bitnami-labs/sealed-secrets ================================================ FILE: helm/sealed-secrets/README.md ================================================ # Sealed Secrets Sealed Secrets are "one-way" encrypted K8s Secrets that can be created by anyone, but can only be decrypted by the controller running in the target cluster recovering the original object. - [TL;DR](#tldr) - [Introduction](#introduction) - [Prerequisites](#prerequisites) - [Installing the Chart](#installing-the-chart) - [Uninstalling the Chart](#uninstalling-the-chart) - [Parameters](#parameters) - [Common parameters](#common-parameters) - [Sealed Secrets Parameters](#sealed-secrets-parameters) - [Traffic Exposure Parameters](#traffic-exposure-parameters) - [Other Parameters](#other-parameters) - [Metrics parameters](#metrics-parameters) - [Using kubeseal](#using-kubeseal) - [Configuration and installation details](#configuration-and-installation-details) - [Troubleshooting](#troubleshooting) - [Upgrading](#upgrading) - [To 2.0.0](#to-200) ## TL;DR ```console $ helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets $ helm install my-release sealed-secrets/sealed-secrets ``` ## Introduction Bitnami charts for Helm are carefully engineered, actively maintained and are the quickest and easiest way to deploy containers on a Kubernetes cluster that are ready to handle production workloads. This chart bootstraps a [Sealed Secret Controller](https://github.com/bitnami-labs/sealed-secrets) Deployment in [Kubernetes](http://kubernetes.io) using the [Helm](https://helm.sh) package manager. Bitnami charts can be used with [Kubeapps](https://kubeapps.com/) for the deployment and management of Helm Charts in clusters. ## Prerequisites - Kubernetes 1.16+ - Helm 3.1.0 ## Installing the Chart To install the chart with the release name `my-release`: ```console helm install my-release sealed-secrets/sealed-secrets ``` The command deploys the Sealed Secrets controller on the Kubernetes cluster in the default configuration. The [Parameters](#parameters) section lists the parameters that can be configured during installation. > **Tip**: List all releases using `helm list` ## Uninstalling the Chart To uninstall/delete the `my-release` deployment: ```console helm delete my-release ``` The command removes all the Kubernetes components associated with the chart and deletes the release. ## Parameters ### Common parameters | Name | Description | Value | | ------------------- | ------------------------------------------------------- | ----- | | `kubeVersion` | Override Kubernetes version | `""` | | `nameOverride` | String to partially override sealed-secrets.fullname | `""` | | `fullnameOverride` | String to fully override sealed-secrets.fullname | `""` | | `namespace` | Namespace where to deploy the Sealed Secrets controller | `""` | | `extraDeploy` | Array of extra objects to deploy with the release | `[]` | | `commonAnnotations` | Annotations to add to all deployed resources | `{}` | | `commonLabels` | Labels to add to all deployed resources | `{}` | ### Sealed Secrets Parameters | Name | Description | Value | | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------- | | `image.registry` | Sealed Secrets image registry | `docker.io` | | `image.repository` | Sealed Secrets image repository | `bitnami/sealed-secrets-controller` | | `image.tag` | Sealed Secrets image tag (immutable tags are recommended) | `0.36.1` | | `image.pullPolicy` | Sealed Secrets image pull policy | `IfNotPresent` | | `image.pullSecrets` | Sealed Secrets image pull secrets | `[]` | | `revisionHistoryLimit` | Number of old history to retain to allow rollback (If not set, default Kubernetes value is set to 10) | `""` | | `createController` | Specifies whether the Sealed Secrets controller should be created | `true` | | `secretName` | The name of an existing TLS secret containing the key used to encrypt secrets | `sealed-secrets-key` | | `updateStatus` | Specifies whether the Sealed Secrets controller should update the status subresource | `true` | | `skipRecreate` | Specifies whether the Sealed Secrets controller should skip recreating removed secrets | `false` | | `keyrenewperiod` | Specifies key renewal period. Default 30 days | `""` | | `keyttl` | Specifies the certificate validity duration. Default 10 years. | `""` | | `keycutofftime` | Specifies a date at which the controller should generate a new certificate. Useful in early key renewal scenarios. | `""` | | `rateLimit` | Number of allowed sustained request per second for verify endpoint | `""` | | `rateLimitBurst` | Number of requests allowed to exceed the rate limit per second for verify endpoint | `""` | | `additionalNamespaces` | List of namespaces used to manage the Sealed Secrets | `[]` | | `privateKeyAnnotations` | Map of annotations to be set on the sealing keypairs | `{}` | | `privateKeyLabels` | Map of labels to be set on the sealing keypairs | `{}` | | `logInfoStdout` | Specifies whether the Sealed Secrets controller will log info to stdout | `false` | | `logLevel` | Specifies log level of controller (INFO,ERROR) | `""` | | `logFormat` | Specifies log format (text,json) | `""` | | `maxRetries` | Number of maximum retries | `""` | | `watchForSecrets` | Specifies whether the Sealed Secrets controller will watch for new secrets | `false` | | `kubeClientQPS` | Kubeclient QPS (negative value disables ratelimiting) | `""` | | `kubeClientBurst` | Kubeclient Burst | `""` | | `command` | Override default container command | `[]` | | `args` | Override default container args | `[]` | | `livenessProbe.enabled` | Enable livenessProbe on Sealed Secret containers | `true` | | `livenessProbe.initialDelaySeconds` | Initial delay seconds for livenessProbe | `0` | | `livenessProbe.periodSeconds` | Period seconds for livenessProbe | `10` | | `livenessProbe.timeoutSeconds` | Timeout seconds for livenessProbe | `1` | | `livenessProbe.failureThreshold` | Failure threshold for livenessProbe | `3` | | `livenessProbe.successThreshold` | Success threshold for livenessProbe | `1` | | `readinessProbe.enabled` | Enable readinessProbe on Sealed Secret containers | `true` | | `readinessProbe.initialDelaySeconds` | Initial delay seconds for readinessProbe | `0` | | `readinessProbe.periodSeconds` | Period seconds for readinessProbe | `10` | | `readinessProbe.timeoutSeconds` | Timeout seconds for readinessProbe | `1` | | `readinessProbe.failureThreshold` | Failure threshold for readinessProbe | `3` | | `readinessProbe.successThreshold` | Success threshold for readinessProbe | `1` | | `startupProbe.enabled` | Enable startupProbe on Sealed Secret containers | `false` | | `startupProbe.initialDelaySeconds` | Initial delay seconds for startupProbe | `0` | | `startupProbe.periodSeconds` | Period seconds for startupProbe | `10` | | `startupProbe.timeoutSeconds` | Timeout seconds for startupProbe | `1` | | `startupProbe.failureThreshold` | Failure threshold for startupProbe | `3` | | `startupProbe.successThreshold` | Success threshold for startupProbe | `1` | | `customLivenessProbe` | Custom livenessProbe that overrides the default one | `{}` | | `customReadinessProbe` | Custom readinessProbe that overrides the default one | `{}` | | `customStartupProbe` | Custom startupProbe that overrides the default one | `{}` | | `resources.limits` | The resources limits for the Sealed Secret containers | `{}` | | `resources.requests` | The requested resources for the Sealed Secret containers | `{}` | | `podSecurityContext.enabled` | Enabled Sealed Secret pods' Security Context | `true` | | `podSecurityContext.fsGroup` | Set Sealed Secret pod's Security Context fsGroup | `65534` | | `containerSecurityContext.enabled` | Enabled Sealed Secret containers' Security Context | `true` | | `containerSecurityContext.readOnlyRootFilesystem` | Whether the Sealed Secret container has a read-only root filesystem | `true` | | `containerSecurityContext.runAsNonRoot` | Indicates that the Sealed Secret container must run as a non-root user | `true` | | `containerSecurityContext.runAsUser` | Set Sealed Secret containers' Security Context runAsUser | `1001` | | `containerSecurityContext.capabilities` | Adds and removes POSIX capabilities from running containers (see `values.yaml`) | | | `podLabels` | Extra labels for Sealed Secret pods | `{}` | | `podAnnotations` | Annotations for Sealed Secret pods | `{}` | | `priorityClassName` | Sealed Secret pods' priorityClassName | `""` | | `runtimeClassName` | Sealed Secret pods' runtimeClassName | `""` | | `affinity` | Affinity for Sealed Secret pods assignment | `{}` | | `nodeSelector` | Node labels for Sealed Secret pods assignment | `{}` | | `tolerations` | Tolerations for Sealed Secret pods assignment | `[]` | | `additionalVolumes` | Extra Volumes for the Sealed Secrets Controller Deployment | `{}` | | `additionalVolumeMounts` | Extra volumeMounts for the Sealed Secrets Controller container | `{}` | | `hostNetwork` | Sealed Secrets pods' hostNetwork | `false` | | `containerPorts.http` | Controller HTTP Port on the Host and Container | `8080` | | `containerPorts.metrics` | Metrics HTTP Port on the Host and Container | `8081` | | `hostPorts.http` | Controller HTTP Port on the Host | `""` | | `hostPorts.metrics` | Metrics HTTP Port on the Host | `""` | | `dnsPolicy` | Sealed Secrets pods' dnsPolicy | `""` | ### Traffic Exposure Parameters | Name | Description | Value | | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | | `service.type` | Sealed Secret service type | `ClusterIP` | | `service.loadBalancerClass` | Sealed Secret service loadBalancerClass | `""` | | `service.port` | Sealed Secret service HTTP port | `8080` | | `service.nodePort` | Node port for HTTP | `""` | | `service.annotations` | Additional custom annotations for Sealed Secret service | `{}` | | `ingress.enabled` | Enable ingress record generation for Sealed Secret | `false` | | `ingress.pathType` | Ingress path type | `ImplementationSpecific` | | `ingress.apiVersion` | Force Ingress API version (automatically detected if not set) | `""` | | `ingress.ingressClassName` | IngressClass that will be be used to implement the Ingress | `""` | | `ingress.hostname` | Default host for the ingress record | `sealed-secrets.local` | | `ingress.path` | Default path for the ingress record | `/v1/cert.pem` | | `ingress.annotations` | Additional annotations for the Ingress resource. To enable certificate autogeneration, place here your cert-manager annotations. | `{}` | | `ingress.tls` | Enable TLS configuration for the host defined at `ingress.hostname` parameter | `false` | | `ingress.selfSigned` | Create a TLS secret for this ingress record using self-signed certificates generated by Helm | `false` | | `ingress.extraHosts` | An array with additional hostname(s) to be covered with the ingress record | `[]` | | `ingress.extraPaths` | An array with additional arbitrary paths that may need to be added to the ingress under the main host | `[]` | | `ingress.extraTls` | TLS configuration for additional hostname(s) to be covered with this ingress record | `[]` | | `ingress.secrets` | Custom TLS certificates as secrets | `[]` | | `networkPolicy.enabled` | Specifies whether a NetworkPolicy should be created | `false` | | `networkPolicy.egress.enabled` | Specifies wheter a egress is set in the NetworkPolicy | `false` | | `networkPolicy.egress.kubeapiCidr` | Specifies the kubeapiCidr, which is the only egress allowed. If not set, kubeapiCidr will be found using Helm lookup | `""` | | `networkPolicy.egress.kubeapiPort` | Specifies the kubeapiPort, which is the only egress allowed. If not set, kubeapiPort will be found using Helm lookup | `""` | ### Other Parameters | Name | Description | Value | | ------------------------------ | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | `serviceAccount.annotations` | Annotations for Sealed Secret service account | `{}` | | `serviceAccount.create` | Specifies whether a ServiceAccount should be created | `true` | | `serviceAccount.labels` | Extra labels to be added to the ServiceAccount | `{}` | | `serviceAccount.name` | The name of the ServiceAccount to use. | `""` | | `rbac.create` | Specifies whether RBAC resources should be created | `true` | | `rbac.clusterRole` | Specifies whether the Cluster Role resource should be created | `true` | | `rbac.clusterRoleName` | Specifies the name for the Cluster Role resource | `secrets-unsealer` | | `rbac.namespacedRoles` | Specifies whether the namespaced Roles should be created (in each of the specified additionalNamespaces) | `false` | | `rbac.namespacedRolesName` | Specifies the name for the namespaced Role resource | `secrets-unsealer` | | `rbac.labels` | Extra labels to be added to RBAC resources | `{}` | | `rbac.pspEnabled` | PodSecurityPolicy | `false` | | `rbac.serviceProxier.create` | Specifies whether to create the "proxier" role, to allow external users to access the SealedSecret API | `true` | | `rbac.serviceProxier.bind` | Specifies whether to create a RoleBinding for the "proxier" role | `true` | | `rbac.serviceProxier.subjects` | Specifies the RBAC subjects to grant the "proxier" role to, in the created RoleBinding |
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: system:authenticated
| ### Metrics parameters | Name | Description | Value | | ------------------------------------------ | -------------------------------------------------------------------------------------- | ----------- | | `metrics.serviceMonitor.enabled` | Specify if a ServiceMonitor will be deployed for Prometheus Operator | `false` | | `metrics.serviceMonitor.namespace` | Namespace where Prometheus Operator is running in | `""` | | `metrics.serviceMonitor.labels` | Extra labels for the ServiceMonitor | `{}` | | `metrics.serviceMonitor.annotations` | Extra annotations for the ServiceMonitor | `{}` | | `metrics.serviceMonitor.interval` | How frequently to scrape metrics | `""` | | `metrics.serviceMonitor.scrapeTimeout` | Timeout after which the scrape is ended | `""` | | `metrics.serviceMonitor.honorLabels` | Specify if ServiceMonitor endPoints will honor labels | `true` | | `metrics.serviceMonitor.metricRelabelings` | Specify additional relabeling of metrics | `[]` | | `metrics.serviceMonitor.relabelings` | Specify general relabeling | `[]` | | `metrics.dashboards.create` | Specifies whether a ConfigMap with a Grafana dashboard configuration should be created | `false` | | `metrics.dashboards.labels` | Extra labels to be added to the Grafana dashboard ConfigMap | `{}` | | `metrics.dashboards.annotations` | Annotations to be added to the Grafana dashboard ConfigMap | `{}` | | `metrics.dashboards.namespace` | Namespace where Grafana dashboard ConfigMap is deployed | `""` | | `metrics.service.type` | Sealed Secret Metrics service type | `ClusterIP` | | `metrics.service.loadBalancerClass` | Sealed Secret Metrics service loadBalancerClass | `""` | | `metrics.service.port` | Sealed Secret service Metrics HTTP port | `8081` | | `metrics.service.nodePort` | Node port for HTTP | `""` | | `metrics.service.annotations` | Additional custom annotations for Sealed Secret Metrics service | `{}` | ### PodDisruptionBudget Parameters | Name | Description | Value | | -------------------- | ----------------------------------------------------------- | ------- | | `pdb.create` | Specifies whether a PodDisruptionBudget should be created | `false` | | `pdb.minAvailable` | The minimum number of pods (non number to omit) | `1` | | `pdb.maxUnavailable` | The maximum number of unavailable pods (non number to omit) | `""` | Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, ```console $ helm install my-release \ --set resources.requests.cpu=25m \ sealed-secrets/sealed-secrets ``` The above command sets the `resources.requests.cpu` parameter to `25m`. Alternatively, a YAML file that specifies the values for the above parameters can be provided while installing the chart. For example, ```console helm install my-release -f values.yaml sealed-secrets/sealed-secrets ``` ## Using kubeseal Install the kubeseal CLI by downloading the binary from [sealed-secrets/releases](https://github.com/bitnami-labs/sealed-secrets/releases). Fetch the public key by passing the release name and namespace: ```bash kubeseal --fetch-cert \ --controller-name=my-release \ --controller-namespace=my-release-namespace \ > pub-cert.pem ``` Read about kubeseal usage on [sealed-secrets docs](https://github.com/bitnami-labs/sealed-secrets#usage). NOTE: the helm chart by default installs the controller with the name `sealed-secrets`, while the `kubeseal` command line interface (CLI) tries to access the controller with the name `sealed-secrets-controller`. You can explicitly pass `--controller-name` to the CLI: ```bash kubeseal --controller-name sealed-secrets ``` Alternatively, you can override `fullnameOverride` on the helm chart install. ## Configuration and installation details - In the case that **serviceAccount.create** is `false` and **rbac.create** is `true` it is expected for a ServiceAccount with the name **serviceAccount.name** to exist _in the same namespace as this chart_ before the installation. - If **rbac.create** is `true, by default *clusterRoles* are created. To switch to namespaced *Roles*: 1. set the required namespaces in **additionalNamespaces** 2. set **rbac.clusterRole** to `false` 3. set **rbac.namespacedRoles** to `true` - If **serviceAccount.create** is `true` there cannot be an existing service account with the name **serviceAccount.name**. - If a secret with name **secretName** does not exist _in the same namespace as this chart_, then on install one will be created. If a secret already exists with this name the keys inside will be used. - OpenShift: unset the runAsUser and fsGroup like this when installing in a custom namespace: ```yaml podSecurityContext: fsGroup: containerSecurityContext: runAsUser: ``` ## Troubleshooting Find more information about how to deal with common errors related to Bitnami's Helm charts in [this troubleshooting guide](https://docs.bitnami.com/general/how-to/troubleshoot-helm-chart-issues). ## Upgrading ### To 2.0.0 A major refactoring of the chart has been performed to adopt several common practices for Helm charts. Upgrades from previous chart versions should work, however, the values structure experienced several changes and you'll have to adapt your custom values/parameters so they're aligned with the new structure. For instance, these are a couple of examples: - `controller.create` renamed as `createController`. - `securityContext.*` parameters are deprecated in favor of `podSecurityContext.*`, and `containerSecurityContext.*` ones. - `image.repository` changed to `image.registry`/`image.repository`. - `ingress.hosts[0]` changed to `ingress.hostname`. Consult the [Parameters](#parameters) section to obtain more info about the available parameters. [On November 13, 2020, Helm v2 support was formally finished](https://github.com/helm/charts#status-of-the-project), this new major version is no longer compatible with Helm v2. ================================================ FILE: helm/sealed-secrets/crds/bitnami.com_sealedsecrets.yaml ================================================ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.15.0 name: sealedsecrets.bitnami.com spec: group: bitnami.com names: kind: SealedSecret listKind: SealedSecretList plural: sealedsecrets singular: sealedsecret scope: Namespaced versions: - additionalPrinterColumns: - jsonPath: .status.conditions[0].message name: Status type: string - jsonPath: .status.conditions[0].status name: Synced type: string - jsonPath: .metadata.creationTimestamp name: Age type: date name: v1alpha1 schema: openAPIV3Schema: description: |- SealedSecret is the K8s representation of a "sealed Secret" - a regular k8s Secret that has been sealed (encrypted) using the controller's key. properties: apiVersion: description: |- APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: description: |- Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object spec: description: SealedSecretSpec is the specification of a SealedSecret. properties: data: description: Data is deprecated and will be removed eventually. Use per-value EncryptedData instead. format: byte type: string encryptedData: additionalProperties: type: string type: object x-kubernetes-preserve-unknown-fields: true template: description: |- Template defines the structure of the Secret that will be created from this sealed secret. properties: data: additionalProperties: type: string description: Keys that should be templated using decrypted data. nullable: true type: object immutable: description: |- Immutable, if set to true, ensures that data stored in the Secret cannot be updated (only object metadata can be modified). If not set to true, the field can be modified at any time. Defaulted to nil. type: boolean metadata: description: |- Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata nullable: true properties: annotations: additionalProperties: type: string type: object finalizers: items: type: string type: array labels: additionalProperties: type: string type: object name: type: string namespace: type: string type: object x-kubernetes-preserve-unknown-fields: true type: description: Used to facilitate programmatic handling of secret data. type: string type: object required: - encryptedData type: object status: description: SealedSecretStatus is the most recently observed status of the SealedSecret. properties: conditions: description: Represents the latest available observations of a sealed secret's current state. items: description: SealedSecretCondition describes the state of a sealed secret at a certain point. properties: lastTransitionTime: description: Last time the condition transitioned from one status to another. format: date-time type: string lastUpdateTime: description: The last time this condition was updated. format: date-time type: string message: description: A human readable message indicating details about the transition. type: string reason: description: The reason for the condition's last transition. type: string status: description: |- Status of the condition for a sealed secret. Valid values for "Synced": "True", "False", or "Unknown". type: string type: description: |- Type of condition for a sealed secret. Valid value: "Synced" type: string required: - status - type type: object type: array observedGeneration: description: ObservedGeneration reflects the generation most recently observed by the sealed-secrets controller. format: int64 type: integer type: object required: - spec type: object served: true storage: true subresources: status: {} ================================================ FILE: helm/sealed-secrets/dashboards/sealed-secrets-controller.json ================================================ { "annotations": { "list": [ { "builtIn": 1, "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" } ] }, "description": "Sealed Secrets Controller", "editable": true, "gnetId": null, "graphTooltip": 0, "id": 3, "iteration": 1585599163503, "links": [ { "icon": "external link", "tags": [], "title": "GitHub", "tooltip": "View Project on GitHub", "type": "link", "url": "https://github.com/bitnami-labs/sealed-secrets" } ], "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "description": "Rate of requests to unseal a SealedSecret.\n\nThis can include non-obvious operations such as deleting a SealedSecret.", "fill": 1, "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 0, "y": 0 }, "hiddenSeries": false, "id": 2, "legend": { "avg": true, "current": false, "max": true, "min": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "dataLinks": [] }, "percentage": false, "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(sealed_secrets_controller_unseal_requests_total{}[1m]))", "format": "time_series", "instant": false, "intervalFactor": 1, "legendFormat": "rps", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Unseal Request Rate/s", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", "description": "Rate of errors when unsealing a SealedSecret. \n\nReason for error included as label value, eg:\n- unseal = cryptography issue (key/namespace) or RBAC\n- unmanaged = destination Secret wasn't created by SealedSecrets\n- update = potentially RBAC\n- status = potentially RBAC\n- fetch = potentially RBAC\n", "fill": 1, "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 12, "y": 0 }, "hiddenSeries": false, "id": 3, "legend": { "avg": false, "current": false, "hideEmpty": false, "hideZero": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null as zero", "options": { "dataLinks": [] }, "percentage": false, "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(sealed_secrets_controller_unseal_errors_total{pod=~\"$pod\"}[1m])) by (reason)", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{ reason }}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Unseal Error Rate/s", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "refresh": false, "schemaVersion": 22, "style": "dark", "tags": [], "templating": { "list": [ { "current": { "text": "prometheus", "value": "prometheus" }, "hide": 0, "includeAll": false, "label": null, "multi": false, "name": "datasource", "options": [], "query": "prometheus", "refresh": 1, "regex": "", "skipUrlSync": false, "type": "datasource" }, { "allValue": null, "current": { "selected": false, "text": "All", "value": "$__all" }, "datasource": "$datasource", "definition": "label_values(kube_pod_info, pod)", "hide": 0, "includeAll": true, "label": null, "multi": false, "name": "pod", "options": [], "query": "label_values(kube_pod_info, pod)", "refresh": 1, "regex": "/^sealed-secrets-controller.*$/", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "time": { "from": "now-1h", "to": "now" }, "timepicker": { "refresh_intervals": [ "5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d" ], "time_options": [ "5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d" ] }, "timezone": "", "title": "Sealed Secrets Controller", "uid": "UuEtZCVWz", "version": 2 } ================================================ FILE: helm/sealed-secrets/templates/NOTES.txt ================================================ {{ if .Values.createController -}} ** Please be patient while the chart is being deployed ** You should now be able to create sealed secrets. 1. Install the client-side tool (kubeseal) as explained in the docs below: https://github.com/bitnami-labs/sealed-secrets#installation-from-source 2. Create a sealed secret file running the command below: kubectl create secret generic secret-name --dry-run=client --from-literal=foo=bar -o [json|yaml] | \ kubeseal \ --controller-name={{ include "sealed-secrets.fullname" . }} \ --controller-namespace={{ include "sealed-secrets.namespace" . }} \ --format yaml > mysealedsecret.[json|yaml] The file mysealedsecret.[json|yaml] is a commitable file. If you would rather not need access to the cluster to generate the sealed secret you can run: kubeseal \ --controller-name={{ include "sealed-secrets.fullname" . }} \ --controller-namespace={{ include "sealed-secrets.namespace" . }} \ --fetch-cert > mycert.pem to retrieve the public cert used for encryption and store it locally. You can then run 'kubeseal --cert mycert.pem' instead to use the local cert e.g. kubectl create secret generic secret-name --dry-run=client --from-literal=foo=bar -o [json|yaml] | \ kubeseal \ --controller-name={{ include "sealed-secrets.fullname" . }} \ --controller-namespace={{ include "sealed-secrets.namespace" . }} \ --format [json|yaml] --cert mycert.pem > mysealedsecret.[json|yaml] 3. Apply the sealed secret kubectl create -f mysealedsecret.[json|yaml] Running 'kubectl get secret secret-name -o [json|yaml]' will show the decrypted secret that was generated from the sealed secret. Both the SealedSecret and generated Secret must have the same name and namespace. {{- else }} Sealed Secrets controller not installed, You need to install controller before sealed secrets can be created. {{- end }} ================================================ FILE: helm/sealed-secrets/templates/_helpers.tpl ================================================ {{/* Expand the name of the chart. */}} {{- define "sealed-secrets.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} {{- end -}} {{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} {{- define "sealed-secrets.fullname" -}} {{- if .Values.fullnameOverride -}} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} {{- else -}} {{- $name := default .Chart.Name .Values.nameOverride -}} {{- if contains $name .Release.Name -}} {{- .Release.Name | trunc 63 | trimSuffix "-" -}} {{- else -}} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} {{- end -}} {{- end -}} {{- end -}} {{/* Expand to the namespace sealed-secrets installs into. */}} {{- define "sealed-secrets.namespace" -}} {{- default .Release.Namespace .Values.namespace -}} {{- end -}} {{/* Create chart name and version as used by the chart label. */}} {{- define "sealed-secrets.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} {{- end -}} {{/* Create the name of the service account to use */}} {{- define "sealed-secrets.serviceAccountName" -}} {{- if .Values.serviceAccount.create -}} {{ default (include "sealed-secrets.fullname" .) .Values.serviceAccount.name }} {{- else -}} {{ default "default" .Values.serviceAccount.name }} {{- end -}} {{- end -}} {{/* Kubernetes standard labels */}} {{- define "sealed-secrets.labels" -}} app.kubernetes.io/name: {{ include "sealed-secrets.name" . }} helm.sh/chart: {{ include "sealed-secrets.chart" . }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/version: {{ .Chart.AppVersion }} app.kubernetes.io/part-of: sealed-secrets {{- end -}} {{/* Labels to use on deploy.spec.selector.matchLabels and svc.spec.selector */}} {{- define "sealed-secrets.matchLabels" -}} app.kubernetes.io/name: {{ include "sealed-secrets.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end -}} {{/* Return true if cert-manager required annotations for TLS signed certificates are set in the Ingress annotations Ref: https://cert-manager.io/docs/usage/ingress/#supported-annotations */}} {{- define "sealed-secrets.ingress.certManagerRequest" -}} {{ if or (hasKey . "cert-manager.io/cluster-issuer") (hasKey . "cert-manager.io/issuer") }} {{- true -}} {{- end -}} {{- end -}} {{/* Renders a value that contains template. Usage: {{ include "sealed-secrets.render" ( dict "value" .Values.path.to.the.Value "context" $) }} */}} {{- define "sealed-secrets.render" -}} {{- if typeIs "string" .value }} {{- tpl .value .context }} {{- else }} {{- tpl (.value | toYaml) .context }} {{- end }} {{- end -}} {{/* Return the target Kubernetes version */}} {{- define "sealed-secrets.kubeVersion" -}} {{- if .Values.global }} {{- if .Values.global.kubeVersion }} {{- .Values.global.kubeVersion -}} {{- else }} {{- default .Capabilities.KubeVersion.Version .Values.kubeVersion -}} {{- end -}} {{- else }} {{- default .Capabilities.KubeVersion.Version .Values.kubeVersion -}} {{- end -}} {{- end -}} {{/* Return the appropriate apiVersion for deployment. */}} {{- define "sealed-secrets.deployment.apiVersion" -}} {{- if semverCompare "<1.14-0" (include "sealed-secrets.kubeVersion" .) -}} {{- print "extensions/v1beta1" -}} {{- else -}} {{- print "apps/v1" -}} {{- end -}} {{- end -}} {{/* Return the appropriate apiVersion for ingress. */}} {{- define "sealed-secrets.ingress.apiVersion" -}} {{- if .Values.ingress -}} {{- if .Values.ingress.apiVersion -}} {{- .Values.ingress.apiVersion -}} {{- else if semverCompare "<1.14-0" (include "sealed-secrets.kubeVersion" .) -}} {{- print "extensions/v1beta1" -}} {{- else if semverCompare "<1.19-0" (include "sealed-secrets.kubeVersion" .) -}} {{- print "networking.k8s.io/v1beta1" -}} {{- else -}} {{- print "networking.k8s.io/v1" -}} {{- end }} {{- else if semverCompare "<1.14-0" (include "sealed-secrets.kubeVersion" .) -}} {{- print "extensions/v1beta1" -}} {{- else if semverCompare "<1.19-0" (include "sealed-secrets.kubeVersion" .) -}} {{- print "networking.k8s.io/v1beta1" -}} {{- else -}} {{- print "networking.k8s.io/v1" -}} {{- end -}} {{- end -}} {{/* Return the appropriate apiVersion for networkpolicy. */}} {{- define "sealed-secrets.networkPolicy.apiVersion" -}} {{- if semverCompare "<1.7-0" (include "sealed-secrets.kubeVersion" .) -}} {{- print "extensions/v1beta1" -}} {{- else -}} {{- print "networking.k8s.io/v1" -}} {{- end -}} {{- end -}} Usage: {{ include "sealed-secrets.backend" (dict "serviceName" "backendName" "servicePort" "backendPort" "context" $) }} Params: - serviceName - String. Name of an existing service backend - servicePort - String/Int. Port name (or number) of the service. It will be translated to different yaml depending if it is a string or an integer. - context - Dict - Required. The context for the template evaluation. */}} {{- define "sealed-secrets.backend" -}} {{- $apiVersion := (include "sealed-secrets.ingress.apiVersion" .context) -}} {{- if or (eq $apiVersion "extensions/v1beta1") (eq $apiVersion "networking.k8s.io/v1beta1") -}} serviceName: {{ .serviceName }} servicePort: {{ .servicePort }} {{- else -}} service: name: {{ .serviceName }} port: {{- if typeIs "string" .servicePort }} name: {{ .servicePort }} {{- else if or (typeIs "int" .servicePort) (typeIs "float64" .servicePort) }} number: {{ .servicePort | int }} {{- end }} {{- end -}} {{- end -}} {{/* Print "true" if the API pathType field is supported Usage: {{ include "sealed-secrets.supportsPathType" . }} */}} {{- define "sealed-secrets.supportsPathType" -}} {{- if (semverCompare "<1.18-0" (include "sealed-secrets.kubeVersion" .)) -}} {{- print "false" -}} {{- else -}} {{- print "true" -}} {{- end -}} {{- end -}} {{/* Returns true if the ingressClassname field is supported Usage: {{ include "sealed-secrets.supportsIngressClassname" . }} */}} {{- define "sealed-secrets.supportsIngressClassname" -}} {{- if semverCompare "<1.18-0" (include "sealed-secrets.kubeVersion" .) -}} {{- print "false" -}} {{- else -}} {{- print "true" -}} {{- end -}} {{- end -}} ================================================ FILE: helm/sealed-secrets/templates/cluster-role-binding.yaml ================================================ {{ if and .Values.rbac.create (not .Values.rbac.namespacedRoles)}} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: {{ include "sealed-secrets.fullname" . }} labels: {{- include "sealed-secrets.labels" . | nindent 4 }} {{- if .Values.rbac.labels }} {{- include "sealed-secrets.render" ( dict "value" .Values.rbac.labels "context" $) | nindent 4 }} {{- end }} {{- if .Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} {{- end }} annotations: {{- if .Values.commonAnnotations }} {{- include "sealed-secrets.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: {{ .Values.rbac.clusterRoleName }} subjects: - apiGroup: "" kind: ServiceAccount name: {{ include "sealed-secrets.serviceAccountName" . }} namespace: {{ include "sealed-secrets.namespace" . }} {{ end }} ================================================ FILE: helm/sealed-secrets/templates/cluster-role.yaml ================================================ {{ if and (and .Values.rbac.create .Values.rbac.clusterRole) (not .Values.rbac.namespacedRoles) }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: {{ .Values.rbac.clusterRoleName }} labels: {{- include "sealed-secrets.labels" . | nindent 4 }} {{- if .Values.rbac.labels }} {{- include "sealed-secrets.render" ( dict "value" .Values.rbac.labels "context" $) | nindent 4 }} {{- end }} {{- if .Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} {{- end }} annotations: {{- if .Values.commonAnnotations }} {{- include "sealed-secrets.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} rules: - apiGroups: - bitnami.com resources: - sealedsecrets verbs: - get - list - watch - apiGroups: - bitnami.com resources: - sealedsecrets/status verbs: - update - apiGroups: - "" resources: - secrets verbs: - get - list - create - update - delete - watch - apiGroups: - "" resources: - events verbs: - create - patch {{- if .Values.additionalNamespaces }} - apiGroups: - "" resources: - namespaces resourceNames: {{- include "sealed-secrets.render" (dict "value" .Values.additionalNamespaces "context" $) | nindent 6 }} verbs: - get {{- end }} {{ end }} ================================================ FILE: helm/sealed-secrets/templates/configmap-dashboards.yaml ================================================ {{- if .Values.metrics.dashboards.create }} {{- $namespace := .Values.metrics.dashboards.namespace | default $.Release.Namespace }} {{- range $path, $_ := .Files.Glob "dashboards/*.json" }} {{- $filename := trimSuffix (ext $path) (base $path) }} apiVersion: v1 kind: ConfigMap metadata: name: {{ printf "%s-%s" (include "sealed-secrets.fullname" $) $filename }} namespace: {{ $namespace }} labels: {{- include "sealed-secrets.labels" $ | nindent 4 }} {{- if $.Values.metrics.dashboards.labels }} {{- include "sealed-secrets.render" ( dict "value" $.Values.metrics.dashboards.labels "context" $) | nindent 4 }} {{- end }} {{- if $.Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" $.Values.commonLabels "context" $) | nindent 4 }} {{- end }} annotations: {{- if $.Values.metrics.dashboards.annotations }} {{- include "sealed-secrets.render" ( dict "value" $.Values.metrics.dashboards.annotations "context" $) | nindent 4 }} {{- end }} {{- if $.Values.commonAnnotations }} {{- include "sealed-secrets.render" ( dict "value" $.Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} data: {{ base $path }}: |- {{ $.Files.Get $path | indent 4 }} --- {{- end }} {{- end }} ================================================ FILE: helm/sealed-secrets/templates/deployment.yaml ================================================ {{- if .Values.createController }} apiVersion: {{ include "sealed-secrets.deployment.apiVersion" . }} kind: Deployment metadata: name: {{ include "sealed-secrets.fullname" . }} namespace: {{ include "sealed-secrets.namespace" . }} labels: {{- include "sealed-secrets.labels" . | nindent 4 }} {{- if .Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} {{- end }} annotations: {{- if .Values.commonAnnotations }} {{- include "sealed-secrets.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} spec: replicas: 1 {{- if .Values.revisionHistoryLimit }} revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} {{- end }} selector: matchLabels: {{- include "sealed-secrets.matchLabels" . | nindent 6 }} template: metadata: {{- if .Values.podAnnotations }} annotations: {{- toYaml .Values.podAnnotations | nindent 8 }} {{- end }} labels: {{- include "sealed-secrets.matchLabels" . | nindent 8 }} {{- if .Values.podLabels }} {{- toYaml .Values.podLabels | nindent 8 }} {{- end }} spec: {{- if .Values.image.pullSecrets }} imagePullSecrets: {{- range .Values.image.pullSecrets }} - name: {{ . }} {{- end }} {{- end }} {{- if .Values.affinity }} affinity: {{- toYaml .Values.affinity | nindent 8 }} {{- end }} {{- if .Values.nodeSelector }} nodeSelector: {{- toYaml .Values.nodeSelector | nindent 8 }} {{- end }} {{- if .Values.tolerations }} tolerations: {{- toYaml .Values.tolerations | nindent 8 }} {{- end }} {{- if .Values.priorityClassName }} priorityClassName: {{ .Values.priorityClassName | quote }} {{- end }} {{- if .Values.runtimeClassName }} runtimeClassName: {{ .Values.runtimeClassName | quote }} {{- end }} {{- if .Values.podSecurityContext.enabled }} securityContext: {{- omit .Values.podSecurityContext "enabled" | toYaml | nindent 8 }} {{- end }} serviceAccountName: {{ include "sealed-secrets.serviceAccountName" . }} {{- if .Values.hostNetwork }} hostNetwork: true {{- end }} {{- if .Values.dnsPolicy }} dnsPolicy: {{ .Values.dnsPolicy }} {{- end }} containers: - name: controller command: {{- if .Values.command }} {{- include "sealed-secrets.render" (dict "value" .Values.command "context" $) | nindent 12 }} {{- else }} - controller {{- end }} args: {{- if .Values.args }} {{- include "sealed-secrets.render" (dict "value" .Values.args "context" $) | nindent 12 }} {{- else }} {{- if .Values.updateStatus }} - --update-status {{- end }} {{- if .Values.skipRecreate }} - --skip-recreate {{- end }} {{- if ne (.Values.keyrenewperiod | toString) "" }} - --key-renew-period - {{ .Values.keyrenewperiod | quote }} {{- end }} {{- if .Values.keyttl }} - --key-ttl - {{ .Values.keyttl | quote }} {{- end }} {{- if .Values.keycutofftime }} - --key-cutoff-time - {{ .Values.keycutofftime | quote }} {{- end }} {{- if .Values.rateLimit }} - --rate-limit - {{ .Values.rateLimit | quote }} {{- end }} {{- if .Values.rateLimitBurst }} - --rate-limit-burst - {{ .Values.rateLimitBurst | quote }} {{- end }} - --key-prefix - {{ .Values.secretName | quote }} {{- if .Values.additionalNamespaces }} - --additional-namespaces - {{ join "," .Values.additionalNamespaces | quote }} {{- end }} {{- if $.Values.privateKeyAnnotations }} {{- $privatekeyAnnotations := ""}} {{- range $k, $v := $.Values.privateKeyAnnotations }} {{- if not (and $v (kindIs "string" $v)) }} {{ fail "Annotation values have to be strings"}} {{- end }} {{- $privatekeyAnnotations = printf "%s=%s,%s" $k $v $privatekeyAnnotations}} {{- end }} - --privatekey-annotations - {{ trimSuffix "," $privatekeyAnnotations | quote }} {{- end }} {{- if $.Values.privateKeyLabels }} {{- $privateKeyLabels := ""}} {{- range $k, $v := $.Values.privateKeyLabels }} {{- if not (and $v (kindIs "string" $v)) }} {{ fail "Label values have to be strings"}} {{- end }} {{- $privateKeyLabels = printf "%s=%s,%s" $k $v $privateKeyLabels}} {{- end }} - --privatekey-labels - {{ trimSuffix "," $privateKeyLabels | quote }} {{- end }} {{- if .Values.logInfoStdout }} - --log-info-stdout {{- end }} {{- if .Values.logLevel }} - --log-level - {{ .Values.logLevel }} {{- end }} {{- if .Values.logFormat }} - --log-format - {{ .Values.logFormat }} {{- end }} {{- if .Values.containerPorts.http }} - --listen-addr - {{ printf ":%s" (.Values.containerPorts.http | toString ) }} {{- end }} {{- if .Values.containerPorts.metrics }} - --listen-metrics-addr - {{ printf ":%s" (.Values.containerPorts.metrics | toString) }} {{- end }} {{- if .Values.maxRetries }} - --max-unseal-retries - {{ .Values.maxRetries | quote }} {{- end }} {{- if .Values.watchForSecrets }} - --watch-for-secrets {{- end }} {{- if .Values.kubeClientQPS }} - --kubeclient-qps - {{ .Values.kubeClientQPS | quote }} {{- end }} {{- if .Values.kubeClientBurst }} - --kubeclient-burst - {{ .Values.kubeClientBurst | quote }} {{- end }} {{- end }} image: {{ printf "%s/%s:%s" .Values.image.registry .Values.image.repository .Values.image.tag }} imagePullPolicy: {{ .Values.image.pullPolicy }} env: {{- if (.Values.resources.limits).cpu }} - name: GOMAXPROCS valueFrom: resourceFieldRef: resource: limits.cpu divisor: "1" {{- end }} {{- if (.Values.resources.limits).memory }} - name: GOMEMLIMIT valueFrom: resourceFieldRef: resource: limits.memory divisor: "1" {{- end }} ports: - name: http containerPort: {{ .Values.containerPorts.http | default "8080" }} protocol: TCP {{- if .Values.hostNetwork }} hostPort: {{ .Values.containerPorts.http }} {{- else if .Values.hostPorts.http }} hostPort: {{ .Values.hostPorts.http }} {{- end }} - name: metrics containerPort: {{ .Values.containerPorts.metrics | default "8081" }} protocol: TCP {{- if .Values.hostNetwork }} hostPort: {{ .Values.containerPorts.metrics }} {{- else if .Values.hostPorts.metrics }} hostPort: {{ .Values.hostPorts.metrics }} {{- end }} {{- if .Values.startupProbe.enabled }} startupProbe: {{- include "sealed-secrets.render" (dict "value" (omit .Values.startupProbe "enabled") "context" $) | nindent 12 }} tcpSocket: port: http {{- else if .Values.customStartupProbe }} startupProbe: {{- include "sealed-secrets.render" (dict "value" .Values.customStartupProbe "context" $) | nindent 12 }} {{- end }} {{- if .Values.livenessProbe.enabled }} livenessProbe: {{- include "sealed-secrets.render" (dict "value" (omit .Values.livenessProbe "enabled") "context" $) | nindent 12 }} httpGet: path: /healthz port: http {{- else if .Values.customLivenessProbe }} livenessProbe: {{- include "sealed-secrets.render" (dict "value" .Values.customLivenessProbe "context" $) | nindent 12 }} {{- end }} {{- if .Values.readinessProbe.enabled }} readinessProbe: {{- include "sealed-secrets.render" (dict "value" (omit .Values.readinessProbe "enabled") "context" $) | nindent 12 }} httpGet: path: /healthz port: http {{- else if .Values.customReadinessProbe }} readinessProbe: {{- include "sealed-secrets.render" (dict "value" .Values.customReadinessProbe "context" $) | nindent 12 }} {{- end }} {{- if .Values.resources }} resources: {{- toYaml .Values.resources | nindent 12 }} {{- end }} {{- if .Values.containerSecurityContext.enabled }} securityContext: {{- omit .Values.containerSecurityContext "enabled" | toYaml | nindent 12 }} {{- end }} volumeMounts: {{- if .Values.additionalVolumeMounts }} {{- toYaml .Values.additionalVolumeMounts | nindent 12 }} {{- end }} - mountPath: /tmp name: tmp volumes: {{- if .Values.additionalVolumes }} {{- toYaml .Values.additionalVolumes | nindent 8 }} {{- end }} - name: tmp emptyDir: {} {{- end }} ================================================ FILE: helm/sealed-secrets/templates/extra-list.yaml ================================================ {{- range .Values.extraDeploy }} --- {{ include "sealed-secrets.render" (dict "value" . "context" $) }} {{- end }} ================================================ FILE: helm/sealed-secrets/templates/ingress.yaml ================================================ {{- if and .Values.createController .Values.ingress.enabled }} apiVersion: {{ include "sealed-secrets.ingress.apiVersion" . }} kind: Ingress metadata: name: {{ include "sealed-secrets.fullname" . }} namespace: {{ include "sealed-secrets.namespace" . }} labels: {{- include "sealed-secrets.labels" . | nindent 4 }} {{- if .Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} {{- end }} annotations: {{- if .Values.ingress.annotations }} {{- include "sealed-secrets.render" ( dict "value" .Values.ingress.annotations "context" $) | nindent 4 }} {{- end }} {{- if .Values.commonAnnotations }} {{- include "sealed-secrets.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} spec: {{- if and .Values.ingress.ingressClassName (eq "true" (include "sealed-secrets.supportsIngressClassname" .)) }} ingressClassName: {{ .Values.ingress.ingressClassName | quote }} {{- end }} rules: {{- if .Values.ingress.hostname }} - host: {{ .Values.ingress.hostname }} http: paths: {{- if .Values.ingress.extraPaths }} {{- toYaml .Values.ingress.extraPaths | nindent 10 }} {{- end }} - path: {{ .Values.ingress.path }} {{- if eq "true" (include "sealed-secrets.supportsPathType" .) }} pathType: {{ .Values.ingress.pathType }} {{- end }} backend: {{- include "sealed-secrets.backend" (dict "serviceName" (include "sealed-secrets.fullname" .) "servicePort" "http" "context" $) | nindent 14 }} {{- end }} {{- range .Values.ingress.extraHosts }} - host: {{ .name | quote }} http: paths: - path: {{ default "/" .path }} {{- if eq "true" (include "sealed-secrets.supportsPathType" $) }} pathType: {{ default "ImplementationSpecific" .pathType }} {{- end }} backend: {{- include "sealed-secrets.backend" (dict "serviceName" (include "sealed-secrets.fullname" $) "servicePort" "http" "context" $) | nindent 14 }} {{- end }} {{- if or (and .Values.ingress.tls (or (include "sealed-secrets.ingress.certManagerRequest" .Values.ingress.annotations) .Values.ingress.selfSigned)) .Values.ingress.extraTls }} tls: {{- if and .Values.ingress.tls (or (include "sealed-secrets.ingress.certManagerRequest" .Values.ingress.annotations) .Values.ingress.selfSigned) }} - hosts: - {{ .Values.ingress.hostname | quote }} secretName: {{ printf "%s-tls" .Values.ingress.hostname }} {{- end }} {{- if .Values.ingress.extraTls }} {{- include "sealed-secrets.render" (dict "value" .Values.ingress.extraTls "context" $) | nindent 4 }} {{- end }} {{- end }} {{- end }} ================================================ FILE: helm/sealed-secrets/templates/networkpolicy.yaml ================================================ {{- if .Values.networkPolicy.enabled }} apiVersion: {{ include "sealed-secrets.networkPolicy.apiVersion" . }} kind: NetworkPolicy metadata: name: {{ include "sealed-secrets.fullname" . }} namespace: {{ include "sealed-secrets.namespace" . }} labels: {{- include "sealed-secrets.labels" . | nindent 4 }} {{- if .Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} {{- end }} annotations: {{- if .Values.commonAnnotations }} {{- include "sealed-secrets.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} spec: podSelector: matchLabels: {{- include "sealed-secrets.matchLabels" . | nindent 6 }} ingress: - ports: - port: {{ .Values.service.port }} - port: {{ .Values.metrics.service.port }} {{- if .Values.networkPolicy.egress.enabled }} egress: - to: {{- if not .Values.networkPolicy.egress.kubeapiCidr }} {{- $kubernetesEndpoint := lookup "v1" "Endpoints" "default" "kubernetes" }} {{- if $kubernetesEndpoint }} {{- range $kubernetesAddress := (first $kubernetesEndpoint.subsets).addresses }} - ipBlock: cidr: {{ $kubernetesAddress.ip }}/32 {{- end}} ports: {{- range $kubernetesPort := (first $kubernetesEndpoint.subsets).ports }} - protocol: {{ $kubernetesPort.protocol }} port: {{ $kubernetesPort.port }} {{- end }} {{- end}} {{- else }} - ipBlock: cidr: {{ .Values.networkPolicy.egress.kubeapiCidr }} ports: - protocol: TCP port: {{ .Values.networkPolicy.egress.kubeapiPort }} {{- end }} {{- end }} {{- end }} ================================================ FILE: helm/sealed-secrets/templates/pdb.yaml ================================================ {{- if .Values.pdb.create }} kind: PodDisruptionBudget apiVersion: policy/v1 metadata: name: {{ include "sealed-secrets.fullname" . }} namespace: {{ include "sealed-secrets.namespace" . }} labels: {{- include "sealed-secrets.labels" . | nindent 4 }} {{- if .Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} {{- end }} annotations: {{- if .Values.commonAnnotations }} {{- include "sealed-secrets.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} spec: {{- if regexMatch "64$" (typeOf .Values.pdb.minAvailable) }} minAvailable: {{ .Values.pdb.minAvailable }} {{- end }} {{- if regexMatch "64$" (typeOf .Values.pdb.maxUnavailable) }} maxUnavailable: {{ .Values.pdb.maxUnavailable }} {{- end }} selector: matchLabels: {{- include "sealed-secrets.matchLabels" . | nindent 6 }} {{- end }} ================================================ FILE: helm/sealed-secrets/templates/psp-clusterrole.yaml ================================================ {{- if .Values.rbac.pspEnabled }} kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: {{ printf "%s-psp" (include "sealed-secrets.fullname" .) }} labels: {{- include "sealed-secrets.labels" . | nindent 4 }} {{- if .Values.rbac.labels }} {{- include "sealed-secrets.render" ( dict "value" .Values.rbac.labels "context" $) | nindent 4 }} {{- end }} {{- if .Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} {{- end }} annotations: {{- if .Values.commonAnnotations }} {{- include "sealed-secrets.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} rules: - apiGroups: ['extensions'] resources: ['podsecuritypolicies'] verbs: ['use'] resourceNames: - {{ include "sealed-secrets.fullname" . }} {{- end }} ================================================ FILE: helm/sealed-secrets/templates/psp-clusterrolebinding.yaml ================================================ {{- if .Values.rbac.pspEnabled }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: {{ printf "%s-psp" (include "sealed-secrets.fullname" .) }} labels: {{- include "sealed-secrets.labels" . | nindent 4 }} {{- if .Values.rbac.labels }} {{- include "sealed-secrets.render" ( dict "value" .Values.rbac.labels "context" $) | nindent 4 }} {{- end }} {{- if .Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} {{- end }} annotations: {{- if .Values.commonAnnotations }} {{- include "sealed-secrets.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: {{ printf "%s-psp" (include "sealed-secrets.fullname" .) }} subjects: - kind: ServiceAccount name: {{ include "sealed-secrets.serviceAccountName" . }} namespace: {{ include "sealed-secrets.namespace" . }} {{- end }} ================================================ FILE: helm/sealed-secrets/templates/psp.yaml ================================================ {{- if .Values.rbac.pspEnabled }} apiVersion: policy/v1beta1 kind: PodSecurityPolicy metadata: name: {{ include "sealed-secrets.fullname" . }} labels: {{- include "sealed-secrets.labels" . | nindent 4 }} {{- if .Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} {{- end }} annotations: {{- if .Values.commonAnnotations }} {{- include "sealed-secrets.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} spec: privileged: false allowPrivilegeEscalation: false allowedCapabilities: [] volumes: - 'configMap' - 'emptyDir' - 'projected' - 'secret' - 'downwardAPI' - 'persistentVolumeClaim' {{- if not .Values.hostNetwork }} hostNetwork: false {{- end }} hostIPC: false hostPID: false runAsUser: rule: 'RunAsAny' seLinux: rule: 'RunAsAny' supplementalGroups: rule: 'RunAsAny' fsGroup: rule: 'RunAsAny' {{- end }} ================================================ FILE: helm/sealed-secrets/templates/role-binding.yaml ================================================ {{ if .Values.rbac.create }} apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: {{ printf "%s-key-admin" (include "sealed-secrets.fullname" .) }} namespace: {{ include "sealed-secrets.namespace" . }} labels: {{- include "sealed-secrets.labels" . | nindent 4 }} {{- if .Values.rbac.labels }} {{- include "sealed-secrets.render" ( dict "value" .Values.rbac.labels "context" $) | nindent 4 }} {{- end }} {{- if .Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} {{- end }} annotations: {{- if .Values.commonAnnotations }} {{- include "sealed-secrets.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: {{ printf "%s-key-admin" (include "sealed-secrets.fullname" .) }} subjects: - apiGroup: "" kind: ServiceAccount name: {{ include "sealed-secrets.serviceAccountName" . }} namespace: {{ include "sealed-secrets.namespace" . }} --- {{ end }} {{ if and (and .Values.rbac.create .Values.rbac.serviceProxier.create) .Values.rbac.serviceProxier.bind }} apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: {{ printf "%s-service-proxier" (include "sealed-secrets.fullname" .) }} namespace: {{ include "sealed-secrets.namespace" . }} labels: {{- include "sealed-secrets.labels" . | nindent 4 }} {{- if .Values.rbac.labels }} {{- include "sealed-secrets.render" ( dict "value" .Values.rbac.labels "context" $) | nindent 4 }} {{- end }} {{- if .Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} {{- end }} annotations: {{- if .Values.commonAnnotations }} {{- include "sealed-secrets.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: {{ printf "%s-service-proxier" (include "sealed-secrets.fullname" .) }} subjects: {{- include "sealed-secrets.render" (dict "value" .Values.rbac.serviceProxier.subjects "context" $) | nindent 2 }} --- {{ end }} {{ if and (and .Values.rbac.create .Values.rbac.namespacedRoles) (not $.Values.rbac.clusterRole) }} {{- range $additionalNamespace := $.Values.additionalNamespaces }} apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: {{ include "sealed-secrets.fullname" $ }} namespace: {{ $additionalNamespace }} labels: {{- include "sealed-secrets.labels" $ | nindent 4 }} {{- if $.Values.rbac.labels }} {{- include "sealed-secrets.render" ( dict "value" $.Values.rbac.labels "context" $) | nindent 4 }} {{- end }} annotations: {{- if $.Values.commonAnnotations }} {{- include "sealed-secrets.render" ( dict "value" $.Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: {{ $.Values.rbac.namespacedRolesName }} subjects: - apiGroup: "" kind: ServiceAccount name: {{ include "sealed-secrets.serviceAccountName" $ }} namespace: {{ include "sealed-secrets.namespace" $ }} --- {{ end }} {{ end }} ================================================ FILE: helm/sealed-secrets/templates/role.yaml ================================================ {{ if .Values.rbac.create }} apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: {{ printf "%s-key-admin" (include "sealed-secrets.fullname" .) }} namespace: {{ include "sealed-secrets.namespace" . }} labels: {{- include "sealed-secrets.labels" . | nindent 4 }} {{- if .Values.rbac.labels }} {{- include "sealed-secrets.render" ( dict "value" .Values.rbac.labels "context" $) | nindent 4 }} {{- end }} {{- if .Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} {{- end }} annotations: {{- if .Values.commonAnnotations }} {{- include "sealed-secrets.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} rules: - apiGroups: - "" resourceNames: - {{ .Values.secretName }} resources: - secrets verbs: - get - apiGroups: - "" resources: - secrets verbs: - create - list --- {{- end }} {{- if and .Values.rbac.create .Values.rbac.serviceProxier.create }} apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: {{ printf "%s-service-proxier" (include "sealed-secrets.fullname" .) }} namespace: {{ include "sealed-secrets.namespace" . }} labels: {{- include "sealed-secrets.labels" . | nindent 4 }} {{- if .Values.rbac.labels }} {{- include "sealed-secrets.render" ( dict "value" .Values.rbac.labels "context" $) | nindent 4 }} {{- end }} {{- if .Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} {{- end }} rules: - apiGroups: - "" resourceNames: - {{ include "sealed-secrets.fullname" . }} resources: - services verbs: - get - apiGroups: - "" resourceNames: - 'http:{{ include "sealed-secrets.fullname" . }}:' - 'http:{{ include "sealed-secrets.fullname" . }}:http' - {{ include "sealed-secrets.fullname" . }} resources: - services/proxy verbs: - create - get --- {{- end }} {{ if and (and .Values.rbac.create .Values.rbac.namespacedRoles) (not $.Values.rbac.clusterRole) }} {{- range $additionalNamespace := $.Values.additionalNamespaces }} apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: {{ $.Values.rbac.namespacedRolesName }} namespace: {{ $additionalNamespace }} labels: {{- include "sealed-secrets.labels" $ | nindent 4 }} {{- if $.Values.rbac.labels }} {{- include "sealed-secrets.render" ( dict "value" $.Values.rbac.labels "context" $) | nindent 4 }} {{- end }} rules: - apiGroups: - bitnami.com resources: - sealedsecrets verbs: - get - list - watch - apiGroups: - bitnami.com resources: - sealedsecrets/status verbs: - update - apiGroups: - "" resources: - secrets verbs: - get - list - create - update - delete - watch - apiGroups: - "" resources: - events verbs: - create - patch - apiGroups: - "" resources: - namespaces resourceNames: {{- include "sealed-secrets.render" (dict "value" $.Values.additionalNamespaces "context" $) | nindent 6 }} verbs: - get --- {{- end }} {{ end }} ================================================ FILE: helm/sealed-secrets/templates/service-account.yaml ================================================ {{ if .Values.serviceAccount.create }} apiVersion: v1 kind: ServiceAccount metadata: name: {{ include "sealed-secrets.serviceAccountName" . }} namespace: {{ include "sealed-secrets.namespace" . }} {{- if or (.Values.commonAnnotations) (.Values.serviceAccount.annotations) }} annotations: {{- if .Values.commonAnnotations }} {{- toYaml .Values.commonAnnotations | nindent 4 }} {{- end}} {{- if .Values.serviceAccount.annotations }} {{- toYaml .Values.serviceAccount.annotations | nindent 4 }} {{- end}} {{- end }} labels: {{- include "sealed-secrets.labels" . | nindent 4 }} {{- if .Values.serviceAccount.labels }} {{- include "sealed-secrets.render" ( dict "value" .Values.serviceAccount.labels "context" $) | nindent 4 }} {{- end }} {{- if .Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} {{- end }} {{ end }} ================================================ FILE: helm/sealed-secrets/templates/service.yaml ================================================ {{- if .Values.createController -}} apiVersion: v1 kind: Service metadata: name: {{ include "sealed-secrets.fullname" . }} namespace: {{ include "sealed-secrets.namespace" . }} {{- if or .Values.service.annotations .Values.commonAnnotations }} annotations: {{- if .Values.service.annotations }} {{- include "sealed-secrets.render" (dict "value" .Values.service.annotations "context" $) | nindent 4 }} {{- end }} {{- if .Values.commonAnnotations }} {{- include "sealed-secrets.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} {{- end }} labels: {{- include "sealed-secrets.labels" . | nindent 4 }} {{- if .Values.service.labels }} {{- include "sealed-secrets.render" ( dict "value" .Values.service.labels "context" $) | nindent 4 }} {{- end }} {{- if .Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} {{- end }} spec: type: {{ .Values.service.type }} {{- with .Values.service.loadBalancerClass }} loadBalancerClass: {{ . }} {{- end }} ports: - name: http port: {{ .Values.service.port }} targetPort: http protocol: TCP {{- if and (or (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer")) (not (empty .Values.service.nodePort)) }} nodePort: {{ .Values.service.nodePort }} {{- else if eq .Values.service.type "ClusterIP" }} nodePort: null {{- end }} selector: {{- include "sealed-secrets.matchLabels" . | nindent 4 }} --- apiVersion: v1 kind: Service metadata: name: {{ include "sealed-secrets.fullname" . }}-metrics namespace: {{ include "sealed-secrets.namespace" . }} {{- if or .Values.metrics.service.annotations .Values.commonAnnotations }} annotations: {{- if .Values.metrics.service.annotations }} {{- include "sealed-secrets.render" (dict "value" .Values.metrics.service.annotations "context" $) | nindent 4 }} {{- end }} {{- if .Values.commonAnnotations }} {{- include "sealed-secrets.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} {{- end }} labels: {{- include "sealed-secrets.labels" . | nindent 4 }} {{- if .Values.metrics.service.labels }} {{- include "sealed-secrets.render" ( dict "value" .Values.metrics.service.labels "context" $) | nindent 4 }} {{- end }} {{- if .Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} {{- end }} app.kubernetes.io/component: metrics spec: type: {{ .Values.metrics.service.type }} {{- with .Values.metrics.service.loadBalancerClass }} loadBalancerClass: {{ . }} {{- end }} ports: - name: metrics port: {{ .Values.metrics.service.port }} targetPort: metrics protocol: TCP {{- if and (or (eq .Values.metrics.service.type "NodePort") (eq .Values.metrics.service.type "LoadBalancer")) (not (empty .Values.metrics.service.nodePort)) }} nodePort: {{ .Values.metrics.service.nodePort }} {{- else if eq .Values.metrics.service.type "ClusterIP" }} nodePort: null {{- end }} selector: {{- include "sealed-secrets.matchLabels" . | nindent 4 }} {{- end }} ================================================ FILE: helm/sealed-secrets/templates/servicemonitor.yaml ================================================ {{- if .Values.metrics.serviceMonitor.enabled }} apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: {{ include "sealed-secrets.fullname" . }} {{- if .Values.metrics.serviceMonitor.namespace }} namespace: {{ .Values.metrics.serviceMonitor.namespace }} {{- else }} namespace: {{ include "sealed-secrets.namespace" . }} {{- end }} labels: {{- include "sealed-secrets.labels" . | nindent 4 }} {{- if .Values.metrics.serviceMonitor.labels }} {{- include "sealed-secrets.render" ( dict "value" .Values.metrics.serviceMonitor.labels "context" $) | nindent 4 }} {{- end }} {{- if .Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} {{- end }} annotations: {{- if .Values.metrics.serviceMonitor.annotations }} {{- include "sealed-secrets.render" (dict "value" .Values.metrics.serviceMonitor.annotations "context" $) | nindent 4 }} {{- end }} {{- if .Values.commonAnnotations }} {{- include "sealed-secrets.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} spec: endpoints: - port: metrics {{- if .Values.metrics.serviceMonitor.honorLabels }} honorLabels: {{ .Values.metrics.serviceMonitor.honorLabels }} {{- end }} {{- if .Values.metrics.serviceMonitor.interval }} interval: {{ .Values.metrics.serviceMonitor.interval }} {{- end }} {{- if .Values.metrics.serviceMonitor.scrapeTimeout }} scrapeTimeout: {{ .Values.metrics.serviceMonitor.scrapeTimeout }} {{- end }} {{- if .Values.metrics.serviceMonitor.metricRelabelings }} metricRelabelings: {{ toYaml .Values.metrics.serviceMonitor.metricRelabelings | nindent 8 }} {{- end }} {{- if .Values.metrics.serviceMonitor.relabelings }} relabelings: {{ toYaml .Values.metrics.serviceMonitor.relabelings | nindent 8 }} {{- end }} namespaceSelector: matchNames: - {{ include "sealed-secrets.namespace" . }} selector: matchLabels: {{- include "sealed-secrets.matchLabels" . | nindent 6 }} app.kubernetes.io/component: metrics {{- end }} ================================================ FILE: helm/sealed-secrets/templates/tls-secret.yaml ================================================ {{- if and .Values.createController .Values.ingress.enabled }} {{- if .Values.ingress.secrets }} {{- range .Values.ingress.secrets }} apiVersion: v1 kind: Secret metadata: name: {{ .name }} namespace: {{ include "sealed-secrets.namespace" $ | quote }} labels: {{- include "sealed-secrets.labels" $ | nindent 4 }} {{- if .Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} {{- end }} annotations: {{- if .Values.commonAnnotations }} {{- include "sealed-secrets.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} type: kubernetes.io/tls data: tls.crt: {{ .certificate | b64enc }} tls.key: {{ .key | b64enc }} --- {{- end }} {{- end }} {{- if and .Values.ingress.tls .Values.ingress.selfSigned }} {{- $ca := genCA "sealed-secrets-ca" 365 }} {{- $cert := genSignedCert .Values.ingress.hostname nil (list .Values.ingress.hostname) 365 $ca }} apiVersion: v1 kind: Secret metadata: name: {{ printf "%s-tls" .Values.ingress.hostname }} namespace: {{ include "sealed-secrets.namespace" . }} labels: {{- include "sealed-secrets.labels" . | nindent 4 }} {{- if .Values.commonLabels }} {{- include "sealed-secrets.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} {{- end }} annotations: {{- if .Values.commonAnnotations }} {{- include "sealed-secrets.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} {{- end }} type: kubernetes.io/tls data: tls.crt: {{ $cert.Cert | b64enc | quote }} tls.key: {{ $cert.Key | b64enc | quote }} ca.crt: {{ $ca.Cert | b64enc | quote }} {{- end }} {{- end }} ================================================ FILE: helm/sealed-secrets/values.yaml ================================================ ## @section Common parameters ## @param kubeVersion Override Kubernetes version ## kubeVersion: "" ## @param nameOverride String to partially override sealed-secrets.fullname ## nameOverride: "" ## @param fullnameOverride String to fully override sealed-secrets.fullname ## fullnameOverride: "" ## @param namespace Namespace where to deploy the Sealed Secrets controller ## namespace: "" ## @param extraDeploy [array] Array of extra objects to deploy with the release ## extraDeploy: [] ## @param commonAnnotations [object] Annotations to add to all deployed resources ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ ## commonAnnotations: {} ## @param commonLabels [object] Labels to add to all deployed resources ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ ## commonLabels: {} ## @section Sealed Secrets Parameters ## Sealed Secrets image ## ref: https://hub.docker.com/r/bitnami/sealed-secrets-controller/tags ## @param image.registry Sealed Secrets image registry ## @param image.repository Sealed Secrets image repository ## @param image.tag Sealed Secrets image tag (immutable tags are recommended) ## @param image.pullPolicy Sealed Secrets image pull policy ## @param image.pullSecrets [array] Sealed Secrets image pull secrets ## image: registry: docker.io repository: bitnami/sealed-secrets-controller tag: 0.36.1 ## Specify a imagePullPolicy ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images ## pullPolicy: IfNotPresent ## Optionally specify an array of imagePullSecrets. ## Secrets must be manually created in the namespace. ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ ## e.g: ## pullSecrets: ## - myRegistryKeySecretName ## pullSecrets: [] ## @param revisionHistoryLimit Number of old history to retain to allow rollback (If not set, default Kubernetes value is set to 10) ## e.g: revisionHistoryLimit: "" ## @param createController Specifies whether the Sealed Secrets controller should be created ## createController: true ## @param secretName The name of an existing TLS secret containing the key used to encrypt secrets ## secretName: "sealed-secrets-key" ## @param updateStatus Specifies whether the Sealed Secrets controller should update the status subresource ## updateStatus: true ## @param skipRecreate Specifies whether the Sealed Secrets controller should skip recreating removed secrets ## Setting it to true allows to optionally restore backward compatibility in low priviledge ## environments when old versions of the controller did not require watch permissions on secrets ## for secret re-creation. ## skipRecreate: false ## @param keyrenewperiod Specifies key renewal period. Default 30 days ## e.g ## keyrenewperiod: "720h30m" ## To disable use "0", with quotes! ## keyrenewperiod: "" ## @param keyttl Specifies the certificate validity duration. Default 10 years. ## e.g for one year ## keyttl: "8760h00m00s" ## keyttl: "" ## @param keycutofftime Specifies a date at which the controller should generate a new certificate. Useful in early key renewal scenarios. ## Takes a date formated according to RFC1123. Can be obtained with the 'date -R' command on a unix system. ## e.g ## keycutofftime: "Mon, 14 Oct 2024 21:45:30 +0200" ## keycutofftime: "" ## @param rateLimit Number of allowed sustained request per second for verify endpoint ## rateLimit: "" ## @param rateLimitBurst Number of requests allowed to exceed the rate limit per second for verify endpoint ## rateLimitBurst: "" ## @param additionalNamespaces List of namespaces used to manage the Sealed Secrets ## additionalNamespaces: [] ## @param privateKeyAnnotations Map of annotations to be set on the sealing keypairs ## privateKeyAnnotations: {} ## @param privateKeyLabels Map of labels to be set on the sealing keypairs ## privateKeyLabels: {} ## @param logInfoStdout Specifies whether the Sealed Secrets controller will log info to stdout ## logInfoStdout: false ## @param logLevel Specifies log level of controller (INFO,ERROR) ## logLevel: "" ## @param logFormat Specifies log format (text,json) ## logFormat: "" ## @param maxRetries Number of maximum retries ## maxRetries: "" ## @param watchForSecrets Specifies whether the Sealed Secrets controller will watch for new secrets ## watchForSecrets: false ## @param kubeClientQPS Kubeclient QPS (negative value disables ratelimiting) ## kubeClientQPS: "" ## @param kubeClientBurst Kubeclient Burst ## kubeClientBurst: "" ## @param command Override default container command ## command: [] ## @param args Override default container args ## args: [] ## Configure extra options for Sealed Secret containers' liveness, readiness and startup probes ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#configure-probes ## @param livenessProbe.enabled Enable livenessProbe on Sealed Secret containers ## @param livenessProbe.initialDelaySeconds Initial delay seconds for livenessProbe ## @param livenessProbe.periodSeconds Period seconds for livenessProbe ## @param livenessProbe.timeoutSeconds Timeout seconds for livenessProbe ## @param livenessProbe.failureThreshold Failure threshold for livenessProbe ## @param livenessProbe.successThreshold Success threshold for livenessProbe ## livenessProbe: enabled: true initialDelaySeconds: 0 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1 ## @param readinessProbe.enabled Enable readinessProbe on Sealed Secret containers ## @param readinessProbe.initialDelaySeconds Initial delay seconds for readinessProbe ## @param readinessProbe.periodSeconds Period seconds for readinessProbe ## @param readinessProbe.timeoutSeconds Timeout seconds for readinessProbe ## @param readinessProbe.failureThreshold Failure threshold for readinessProbe ## @param readinessProbe.successThreshold Success threshold for readinessProbe ## readinessProbe: enabled: true initialDelaySeconds: 0 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1 ## @param startupProbe.enabled Enable startupProbe on Sealed Secret containers ## @param startupProbe.initialDelaySeconds Initial delay seconds for startupProbe ## @param startupProbe.periodSeconds Period seconds for startupProbe ## @param startupProbe.timeoutSeconds Timeout seconds for startupProbe ## @param startupProbe.failureThreshold Failure threshold for startupProbe ## @param startupProbe.successThreshold Success threshold for startupProbe ## startupProbe: enabled: false initialDelaySeconds: 0 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1 ## @param customLivenessProbe Custom livenessProbe that overrides the default one ## customLivenessProbe: {} ## @param customReadinessProbe Custom readinessProbe that overrides the default one ## customReadinessProbe: {} ## @param customStartupProbe Custom startupProbe that overrides the default one ## customStartupProbe: {} ## Sealed Secret resource requests and limits ## ref: http://kubernetes.io/docs/user-guide/compute-resources/ ## @param resources.limits [object] The resources limits for the Sealed Secret containers ## @param resources.requests [object] The requested resources for the Sealed Secret containers ## resources: limits: {} requests: {} ## Configure Pods Security Context ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod ## @param podSecurityContext.enabled Enabled Sealed Secret pods' Security Context ## @param podSecurityContext.fsGroup Set Sealed Secret pod's Security Context fsGroup ## podSecurityContext: enabled: true fsGroup: 65534 ## Configure Container Security Context ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod ## @param containerSecurityContext.enabled Enabled Sealed Secret containers' Security Context ## @param containerSecurityContext.readOnlyRootFilesystem Whether the Sealed Secret container has a read-only root filesystem ## @param containerSecurityContext.runAsNonRoot Indicates that the Sealed Secret container must run as a non-root user ## @param containerSecurityContext.runAsUser Set Sealed Secret containers' Security Context runAsUser ## @extra containerSecurityContext.capabilities Adds and removes POSIX capabilities from running containers (see `values.yaml`) ## @skip containerSecurityContext.capabilities.drop ## containerSecurityContext: enabled: true readOnlyRootFilesystem: true runAsNonRoot: true runAsUser: 1001 capabilities: drop: - ALL ## @param podLabels [object] Extra labels for Sealed Secret pods ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ ## podLabels: {} ## @param podAnnotations [object] Annotations for Sealed Secret pods ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ ## podAnnotations: {} ## @param priorityClassName Sealed Secret pods' priorityClassName ## priorityClassName: "" ## @param runtimeClassName Sealed Secret pods' runtimeClassName ## runtimeClassName: "" ## @param affinity [object] Affinity for Sealed Secret pods assignment ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity ## affinity: {} ## @param nodeSelector [object] Node labels for Sealed Secret pods assignment ## ref: https://kubernetes.io/docs/user-guide/node-selection/ ## nodeSelector: {} ## @param tolerations [array] Tolerations for Sealed Secret pods assignment ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ ## tolerations: [] ## @param additionalVolumes [object] Extra Volumes for the Sealed Secrets Controller Deployment ## ref: https://kubernetes.io/docs/concepts/storage/volumes/ ## additionalVolumes: [] ## @param additionalVolumeMounts [object] Extra volumeMounts for the Sealed Secrets Controller container ## ref: https://kubernetes.io/docs/concepts/storage/volumes/ ## additionalVolumeMounts: [] ## @param hostNetwork Sealed Secrets pods' hostNetwork hostNetwork: false ## Sealed Secrets controller ports to open ## If hostNetwork true: the hostPort is set identical to the containerPort ## @param containerPorts.http Controller HTTP Port on the Host and Container ## @param containerPorts.metrics Metrics HTTP Port on the Host and Container ## containerPorts: http: 8080 metrics: 8081 ## Sealed Secrets controller ports to be exposed as hostPort ## If hostNetwork is false, only the ports specified here will be exposed (or not if set to an empty string) ## @param hostPorts.http Controller HTTP Port on the Host ## @param hostPorts.metrics Metrics HTTP Port on the Host ## hostPorts: http: "" metrics: "" ## @param dnsPolicy Sealed Secrets pods' dnsPolicy dnsPolicy: "" ## @section Traffic Exposure Parameters ## Sealed Secret service parameters ## service: ## @param service.type Sealed Secret service type ## type: ClusterIP ## @param service.loadBalancerClass Sealed Secret service loadBalancerClass ## loadBalancerClass: "" ## @param service.port Sealed Secret service HTTP port ## port: 8080 ## @param service.nodePort Node port for HTTP ## Specify the nodePort value for the LoadBalancer and NodePort service types ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport ## NOTE: choose port between <30000-32767> ## nodePort: "" ## @param service.annotations [object] Additional custom annotations for Sealed Secret service ## annotations: {} ## Sealed Secret ingress parameters ## ref: http://kubernetes.io/docs/user-guide/ingress/ ## ingress: ## @param ingress.enabled Enable ingress record generation for Sealed Secret ## enabled: false ## @param ingress.pathType Ingress path type ## pathType: ImplementationSpecific ## @param ingress.apiVersion Force Ingress API version (automatically detected if not set) ## apiVersion: "" ## @param ingress.ingressClassName IngressClass that will be be used to implement the Ingress ## This is supported in Kubernetes 1.18+ and required if you have more than one IngressClass marked as the default for your cluster. ## ref: https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/ ## ingressClassName: "" ## @param ingress.hostname Default host for the ingress record ## hostname: sealed-secrets.local ## @param ingress.path Default path for the ingress record ## path: /v1/cert.pem ## @param ingress.annotations [object] Additional annotations for the Ingress resource. To enable certificate autogeneration, place here your cert-manager annotations. ## Use this parameter to set the required annotations for cert-manager, see ## ref: https://cert-manager.io/docs/usage/ingress/#supported-annotations ## e.g: ## annotations: ## kubernetes.io/ingress.class: nginx ## cert-manager.io/cluster-issuer: cluster-issuer-name ## annotations: {} ## @param ingress.tls Enable TLS configuration for the host defined at `ingress.hostname` parameter ## TLS certificates will be retrieved from a TLS secret with name: `{{- printf "%s-tls" .Values.ingress.hostname }}` ## You can: ## - Use the `ingress.secrets` parameter to create this TLS secret ## - Relay on cert-manager to create it by setting the corresponding annotations ## - Relay on Helm to create self-signed certificates by setting `ingress.selfSigned=true` ## tls: false ## @param ingress.selfSigned Create a TLS secret for this ingress record using self-signed certificates generated by Helm ## selfSigned: false ## @param ingress.extraHosts [array] An array with additional hostname(s) to be covered with the ingress record ## e.g: ## extraHosts: ## - name: sealed-secrets.local ## path: / ## extraHosts: [] ## @param ingress.extraPaths [array] An array with additional arbitrary paths that may need to be added to the ingress under the main host ## e.g: ## extraPaths: ## - path: /* ## backend: ## serviceName: ssl-redirect ## servicePort: use-annotation ## extraPaths: [] ## @param ingress.extraTls [array] TLS configuration for additional hostname(s) to be covered with this ingress record ## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls ## e.g: ## extraTls: ## - hosts: ## - sealed-secrets.local ## secretName: sealed-secrets.local-tls ## extraTls: [] ## @param ingress.secrets [array] Custom TLS certificates as secrets ## NOTE: 'key' and 'certificate' are expected in PEM format ## NOTE: 'name' should line up with a 'secretName' set further up ## If it is not set and you're using cert-manager, this is unneeded, as it will create a secret for you with valid certificates ## If it is not set and you're NOT using cert-manager either, self-signed certificates will be created valid for 365 days ## It is also possible to create and manage the certificates outside of this helm chart ## Please see README.md for more information ## e.g: ## secrets: ## - name: sealed-secrets.local-tls ## key: |- ## -----BEGIN RSA PRIVATE KEY----- ## ... ## -----END RSA PRIVATE KEY----- ## certificate: |- ## -----BEGIN CERTIFICATE----- ## ... ## -----END CERTIFICATE----- ## secrets: [] ## Network policies ## Ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/ ## networkPolicy: ## @param networkPolicy.enabled Specifies whether a NetworkPolicy should be created ## enabled: false ## NetworkPolicy Egress configuration ## egress: ## @param networkPolicy.egress.enabled Specifies wheter a egress is set in the NetworkPolicy ## enabled: false ## @param networkPolicy.egress.kubeapiCidr Specifies the kubeapiCidr, which is the only egress allowed. If not set, kubeapiCidr will be found using Helm lookup ## kubeapiCidr: "" ## @param networkPolicy.egress.kubeapiPort Specifies the kubeapiPort, which is the only egress allowed. If not set, kubeapiPort will be found using Helm lookup ## kubeapiPort: "" ## @section Other Parameters ## ServiceAccount configuration ## serviceAccount: ## @param serviceAccount.annotations [object] Annotations for Sealed Secret service account ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ ## annotations: {} ## @param serviceAccount.create Specifies whether a ServiceAccount should be created ## create: true ## @param serviceAccount.labels Extra labels to be added to the ServiceAccount ## labels: {} ## @param serviceAccount.name The name of the ServiceAccount to use. ## If not set and create is true, a name is generated using the sealed-secrets.fullname template ## name: "" ## RBAC configuration ## rbac: ## @param rbac.create Specifies whether RBAC resources should be created ## create: true ## @param rbac.clusterRole Specifies whether the Cluster Role resource should be created ## clusterRole: true ## @param rbac.clusterRoleName Specifies the name for the Cluster Role resource ## clusterRoleName: "secrets-unsealer" ## @param rbac.namespacedRoles Specifies whether the namespaced Roles should be created (in each of the specified additionalNamespaces) ## namespacedRoles: false ## @param rbac.namespacedRolesName Specifies the name for the namespaced Role resource ## namespacedRolesName: "secrets-unsealer" ## @param rbac.labels Extra labels to be added to RBAC resources ## labels: {} ## @param rbac.pspEnabled PodSecurityPolicy ## pspEnabled: false ## "Proxier" RBAC Role configuration ## serviceProxier: ## @param rbac.serviceProxier.create Specifies whether to create the "proxier" role, to allow external users to access the SealedSecret API ## create: true ## @param rbac.serviceProxier.bind Specifies whether to create a RoleBinding for the "proxier" role ## bind: true ## @param rbac.serviceProxier.subjects Specifies the RBAC subjects to grant the "proxier" role to, in the created RoleBinding ## It is best to change this to something narrower, as the default binding gives `system:authenticated` access, which is very broad ## subjects: | - apiGroup: rbac.authorization.k8s.io kind: Group name: system:authenticated ## @section Metrics parameters metrics: ## Prometheus Operator ServiceMonitor configuration ## serviceMonitor: ## @param metrics.serviceMonitor.enabled Specify if a ServiceMonitor will be deployed for Prometheus Operator ## enabled: false ## @param metrics.serviceMonitor.namespace Namespace where Prometheus Operator is running in ## namespace: "" ## @param metrics.serviceMonitor.labels Extra labels for the ServiceMonitor ## labels: {} ## @param metrics.serviceMonitor.annotations Extra annotations for the ServiceMonitor ## annotations: {} ## @param metrics.serviceMonitor.interval How frequently to scrape metrics ## e.g: ## interval: 10s ## interval: "" ## @param metrics.serviceMonitor.scrapeTimeout Timeout after which the scrape is ended ## e.g: ## scrapeTimeout: 10s ## scrapeTimeout: "" ## @param metrics.serviceMonitor.honorLabels Specify if ServiceMonitor endPoints will honor labels ## honorLabels: true ## @param metrics.serviceMonitor.metricRelabelings [array] Specify additional relabeling of metrics ## metricRelabelings: [] ## @param metrics.serviceMonitor.relabelings [array] Specify general relabeling ## relabelings: [] ## Grafana dashboards configuration ## dashboards: ## @param metrics.dashboards.create Specifies whether a ConfigMap with a Grafana dashboard configuration should be created ## ref https://github.com/helm/charts/tree/master/stable/grafana#configuration ## create: false ## @param metrics.dashboards.labels Extra labels to be added to the Grafana dashboard ConfigMap ## labels: {} ## @param metrics.dashboards.annotations Annotations to be added to the Grafana dashboard ConfigMap ## annotations: {} ## @param metrics.dashboards.namespace Namespace where Grafana dashboard ConfigMap is deployed ## namespace: "" ## Sealed Secret Metrics service parameters ## service: ## @param metrics.service.type Sealed Secret Metrics service type ## type: ClusterIP ## @param metrics.service.loadBalancerClass Sealed Secret Metrics service loadBalancerClass ## loadBalancerClass: "" ## @param metrics.service.port Sealed Secret service Metrics HTTP port ## port: 8081 ## @param metrics.service.nodePort Node port for HTTP ## Specify the nodePort value for the LoadBalancer and NodePort service types ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport ## NOTE: choose port between <30000-32767> ## nodePort: "" ## @param metrics.service.annotations [object] Additional custom annotations for Sealed Secret Metrics service ## annotations: {} ## @section PodDisruptionBudget Parameters pdb: ## @param pdb.create Specifies whether a PodDisruptionBudget should be created ## create: false ## @param pdb.minAvailable The minimum number of pods (non number to omit) ## minAvailable: 1 ## @param pdb.maxUnavailable The maximum number of unavailable pods (non number to omit) ## maxUnavailable: "" ================================================ FILE: integration/controller_test.go ================================================ //go:build integration // +build integration package integration import ( "bytes" "context" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/base64" "fmt" "io" "sort" "time" "github.com/onsi/gomega/types" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/client-go/kubernetes/scheme" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" certUtil "k8s.io/client-go/util/cert" "k8s.io/client-go/util/keyutil" ssv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" ssclient "github.com/bitnami-labs/sealed-secrets/pkg/client/clientset/versioned" "github.com/bitnami-labs/sealed-secrets/pkg/crypto" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var keySelector = fields.OneTermEqualSelector("sealedsecrets.bitnami.com/sealed-secrets-key", "active").String() const ( Timeout = 15 * time.Second PollingInterval = "100ms" ) func getData(s *v1.Secret) map[string][]byte { return s.Data } func getAnnotations(s *v1.Secret) map[string]string { return s.ObjectMeta.Annotations } func getLabels(s *v1.Secret) map[string]string { return s.ObjectMeta.Labels } func getStatus(ss *ssv1alpha1.SealedSecret) *ssv1alpha1.SealedSecretStatus { return ss.Status } func getObservedGeneration(ss *ssv1alpha1.SealedSecret) int64 { return ss.Status.ObservedGeneration } // get the first owner name assuming there is only one owner which is the sealed-secret object func getFirstOwnerName(s *v1.Secret) string { return s.OwnerReferences[0].Name } func getNumberOfOwners(s *v1.Secret) int { return len(s.OwnerReferences) } func getSecretType(s *v1.Secret) v1.SecretType { return s.Type } func getSecretImmutable(s *v1.Secret) bool { return *s.Immutable } func compareLastTimes(ss *ssv1alpha1.SealedSecret) bool { for i := range ss.Status.Conditions { if ss.Status.Conditions[i].Type == ssv1alpha1.SealedSecretSynced { return ss.Status.Conditions[i].LastTransitionTime == ss.Status.Conditions[i].LastUpdateTime } } return false } func fetchKeys(ctx context.Context, c corev1.SecretsGetter) (map[string]*rsa.PrivateKey, []*x509.Certificate, error) { list, err := c.Secrets(*controllerNs).List(ctx, metav1.ListOptions{ LabelSelector: keySelector, }) if err != nil { return nil, nil, err } if len(list.Items) == 0 { return nil, nil, fmt.Errorf("found 0 keys") } sort.Sort(ssv1alpha1.ByCreationTimestamp(list.Items)) latestKey := &list.Items[len(list.Items)-1] privKey, err := keyutil.ParsePrivateKeyPEM(latestKey.Data[v1.TLSPrivateKeyKey]) if err != nil { return nil, nil, err } certs, err := certUtil.ParseCertsPEM(latestKey.Data[v1.TLSCertKey]) if err != nil { return nil, nil, err } if len(certs) == 0 { return nil, nil, fmt.Errorf("failed to read any certificates") } rsaPrivKey := privKey.(*rsa.PrivateKey) fp, err := crypto.PublicKeyFingerprint(&rsaPrivKey.PublicKey) if err != nil { return nil, nil, err } privKeys := map[string]*rsa.PrivateKey{fp: rsaPrivKey} return privKeys, certs, nil } func containEventWithReason(matcher types.GomegaMatcher) types.GomegaMatcher { return WithTransform( func(l *v1.EventList) []v1.Event { return l.Items }, ContainElement(WithTransform( func(e v1.Event) string { return e.Reason }, matcher, )), ) } func containEventWithMessage(matcher types.GomegaMatcher) types.GomegaMatcher { return WithTransform( func(l *v1.EventList) []v1.Event { return l.Items }, ContainElement(WithTransform( func(e v1.Event) string { return e.Message }, matcher, )), ) } var _ = Describe("create", func() { var c corev1.CoreV1Interface var ssc ssclient.Interface var ns string const secretName = "testsecret" var ss *ssv1alpha1.SealedSecret var s *v1.Secret var pubKey *rsa.PublicKey var ( ctx context.Context cancelLog context.CancelFunc ) BeforeEach(func() { ctx, cancelLog = context.WithCancel(context.Background()) conf := clusterConfigOrDie() c = corev1.NewForConfigOrDie(conf) ssc = ssclient.NewForConfigOrDie(conf) ns = createNsOrDie(ctx, c, "create") go streamLog(ctx, c, ns, "sealed-secrets-controller", "sealed-secrets-controller", GinkgoWriter, fmt.Sprintf("[%s] ", ns)) s = &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: ns, Name: secretName, Labels: map[string]string{ "mylabel": "myvalue", }, }, Data: map[string][]byte{ "foo": []byte("bar"), }, } _, certs, err := fetchKeys(ctx, c) Expect(err).NotTo(HaveOccurred()) pubKey = certs[0].PublicKey.(*rsa.PublicKey) fmt.Fprintf(GinkgoWriter, "Sealing Secret %#v\n", s) ss, err = ssv1alpha1.NewSealedSecret(scheme.Codecs, pubKey, s) Expect(err).NotTo(HaveOccurred()) }) AfterEach(func() { deleteNsOrDie(ctx, c, ns) cancelLog() }) JustBeforeEach(func() { var err error fmt.Fprintf(GinkgoWriter, "Creating SealedSecret: %#v\n", ss) ss, err = ssc.BitnamiV1alpha1().SealedSecrets(ss.Namespace).Create(context.Background(), ss, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) }) Describe("Simple change", func() { Context("With no existing object (create)", func() { It("should produce expected Secret", func() { expected := map[string][]byte{ "foo": []byte("bar"), } Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getData, Equal(expected))) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(metav1.Object.GetLabels, HaveKeyWithValue("mylabel", "myvalue"))) Eventually(func() (*ssv1alpha1.SealedSecret, error) { return ssc.BitnamiV1alpha1().SealedSecrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).ShouldNot(WithTransform(getStatus, BeNil())) Eventually(func() (*ssv1alpha1.SealedSecret, error) { return ssc.BitnamiV1alpha1().SealedSecrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(compareLastTimes, Equal(true))) Eventually(func() (*v1.EventList, error) { return c.Events(ns).Search(scheme.Scheme, ss) }, Timeout, PollingInterval).Should( containEventWithReason(Equal("Unsealed"))) }) }) Context("With existing object (update)", func() { JustBeforeEach(func() { var err error Eventually(func() (*ssv1alpha1.SealedSecret, error) { return ssc.BitnamiV1alpha1().SealedSecrets(ss.Namespace).Get(context.Background(), secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).ShouldNot(WithTransform(getStatus, BeNil())) ss, err = ssc.BitnamiV1alpha1().SealedSecrets(ss.Namespace).Get(context.Background(), secretName, metav1.GetOptions{}) Expect(err).NotTo(HaveOccurred()) resVer := ss.ResourceVersion // update s.Data["foo"] = []byte("baz") ss, err = ssv1alpha1.NewSealedSecret(scheme.Codecs, pubKey, s) Expect(err).NotTo(HaveOccurred()) ss.ResourceVersion = resVer time.Sleep(1 * time.Second) fmt.Fprintf(GinkgoWriter, "Updating to SealedSecret: %#v\n", ss) ss, err = ssc.BitnamiV1alpha1().SealedSecrets(ss.Namespace).Update(context.Background(), ss, metav1.UpdateOptions{}) Expect(err).NotTo(HaveOccurred()) }) It("should produce updated Secret", func() { expected := map[string][]byte{ "foo": []byte("baz"), } Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getData, Equal(expected))) Eventually(func() (*ssv1alpha1.SealedSecret, error) { return ssc.BitnamiV1alpha1().SealedSecrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).ShouldNot(WithTransform(getStatus, BeNil())) Eventually(func() (*ssv1alpha1.SealedSecret, error) { return ssc.BitnamiV1alpha1().SealedSecrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getObservedGeneration, Equal(int64(2)))) Eventually(func() (*ssv1alpha1.SealedSecret, error) { return ssc.BitnamiV1alpha1().SealedSecrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(compareLastTimes, Equal(false))) }) }) Context("With renamed encrypted keys", func() { BeforeEach(func() { ss.Spec.EncryptedData = map[string]string{ "xyzzy": ss.Spec.EncryptedData["foo"], } }) It("should produce expected Secret", func() { expected := map[string][]byte{ // renamed key "xyzzy": []byte("bar"), } Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getData, Equal(expected))) Eventually(func() (*ssv1alpha1.SealedSecret, error) { return ssc.BitnamiV1alpha1().SealedSecrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).ShouldNot(WithTransform(getStatus, BeNil())) }) }) Context("With appended encrypted keys", func() { BeforeEach(func() { label := fmt.Sprintf("%s/%s", s.Namespace, s.Name) ciphertext, err := crypto.HybridEncrypt(rand.Reader, pubKey, []byte("new!"), []byte(label)) Expect(err).NotTo(HaveOccurred()) ss.Spec.EncryptedData["foo2"] = base64.StdEncoding.EncodeToString(ciphertext) }) It("should produce expected Secret", func() { expected := map[string][]byte{ "foo": []byte("bar"), "foo2": []byte("new!"), } Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getData, Equal(expected))) Eventually(func() (*ssv1alpha1.SealedSecret, error) { return ssc.BitnamiV1alpha1().SealedSecrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).ShouldNot(WithTransform(getStatus, BeNil())) }) }) }) Describe("Secret already exists", func() { Context("With managed annotation", func() { BeforeEach(func() { s.Data = map[string][]byte{ "foo": []byte("bar1"), "foo2": []byte("bar2"), } s.Annotations = map[string]string{ ssv1alpha1.SealedSecretManagedAnnotation: "true", } s.Labels["anotherlabel"] = "anothervalue" c.Secrets(ns).Create(ctx, s, metav1.CreateOptions{}) }) It("should take ownership of the existing Secret overwriting the whole Secret", func() { expectedData := map[string][]byte{ "foo": []byte("bar"), } var expectedAnnotations map[string]string expectedLabels := map[string]string{ "mylabel": "myvalue", } Eventually(func() (*v1.EventList, error) { return c.Events(ns).Search(scheme.Scheme, ss) }, Timeout, PollingInterval).Should( containEventWithReason(Equal("Unsealed")), ) Eventually(func() (*ssv1alpha1.SealedSecret, error) { return ssc.BitnamiV1alpha1().SealedSecrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).ShouldNot(WithTransform(getStatus, BeNil())) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getFirstOwnerName, Equal(ss.GetName()))) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getData, Equal(expectedData))) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getAnnotations, Equal(expectedAnnotations))) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getLabels, Equal(expectedLabels))) }) }) Context("With managed and patch annotation", func() { BeforeEach(func() { s.Data = map[string][]byte{ "foo": []byte("bar1"), "foo2": []byte("bar2"), } s.Annotations = map[string]string{ ssv1alpha1.SealedSecretManagedAnnotation: "true", ssv1alpha1.SealedSecretPatchAnnotation: "true", } s.Labels["anotherlabel"] = "anothervalue" c.Secrets(ns).Create(ctx, s, metav1.CreateOptions{}) }) It("should take ownership of the existing Secret patching instead of overwriting the whole Secret", func() { expectedData := map[string][]byte{ "foo": []byte("bar"), "foo2": []byte("bar2"), } expectedAnnotations := map[string]string{ ssv1alpha1.SealedSecretManagedAnnotation: "true", ssv1alpha1.SealedSecretPatchAnnotation: "true", } expectedLabels := map[string]string{ "mylabel": "myvalue", "anotherlabel": "anothervalue", } Eventually(func() (*v1.EventList, error) { return c.Events(ns).Search(scheme.Scheme, ss) }, Timeout, PollingInterval).Should( containEventWithReason(Equal("Unsealed")), ) Eventually(func() (*ssv1alpha1.SealedSecret, error) { return ssc.BitnamiV1alpha1().SealedSecrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).ShouldNot(WithTransform(getStatus, BeNil())) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getFirstOwnerName, Equal(ss.GetName()))) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getData, Equal(expectedData))) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getAnnotations, Equal(expectedAnnotations))) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getLabels, Equal(expectedLabels))) }) }) Context("With patch annotation", func() { BeforeEach(func() { s.Data = map[string][]byte{ "foo": []byte("bar1"), "foo2": []byte("bar2"), } s.Annotations = map[string]string{ ssv1alpha1.SealedSecretPatchAnnotation: "true", } s.Labels["anotherlabel"] = "anothervalue" c.Secrets(ns).Create(ctx, s, metav1.CreateOptions{}) }) It("should not take ownership of existing Secret while patching the Secret", func() { expectedData := map[string][]byte{ "foo": []byte("bar"), "foo2": []byte("bar2"), } expectedAnnotations := map[string]string{ ssv1alpha1.SealedSecretPatchAnnotation: "true", } expectedLabels := map[string]string{ "mylabel": "myvalue", "anotherlabel": "anothervalue", } Eventually(func() (*v1.EventList, error) { return c.Events(ns).Search(scheme.Scheme, ss) }, Timeout, PollingInterval).Should( containEventWithReason(Equal("Unsealed")), ) Eventually(func() (*ssv1alpha1.SealedSecret, error) { return ssc.BitnamiV1alpha1().SealedSecrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).ShouldNot(WithTransform(getStatus, BeNil())) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getNumberOfOwners, Equal(0))) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getData, Equal(expectedData))) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getAnnotations, Equal(expectedAnnotations))) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getLabels, Equal(expectedLabels))) }) }) Context("With patch annotation and empty secret", func() { BeforeEach(func() { // Empty secret has no data nor labels field s.Data = nil s.Labels = nil s.Annotations = map[string]string{ ssv1alpha1.SealedSecretPatchAnnotation: "true", } c.Secrets(ns).Create(ctx, s, metav1.CreateOptions{}) }) It("should not take ownership of existing Secret while patching the Secret", func() { expectedData := map[string][]byte{ "foo": []byte("bar"), } expectedAnnotations := map[string]string{ ssv1alpha1.SealedSecretPatchAnnotation: "true", } expectedLabels := map[string]string{ "mylabel": "myvalue", } Eventually(func() (*v1.EventList, error) { return c.Events(ns).Search(scheme.Scheme, ss) }, Timeout, PollingInterval).Should( containEventWithReason(Equal("Unsealed")), ) Eventually(func() (*ssv1alpha1.SealedSecret, error) { return ssc.BitnamiV1alpha1().SealedSecrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).ShouldNot(WithTransform(getStatus, BeNil())) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getNumberOfOwners, Equal(0))) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getData, Equal(expectedData))) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getAnnotations, Equal(expectedAnnotations))) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getLabels, Equal(expectedLabels))) }) }) }) Describe("Secret Recreation", func() { Context("With owned secret", func() { JustBeforeEach(func() { Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getFirstOwnerName, Equal(ss.GetName()))) err := c.Secrets(ns).Delete(ctx, secretName, metav1.DeleteOptions{}) Expect(err).NotTo(HaveOccurred()) }) It("should recreate the secret", func() { expected := map[string][]byte{ "foo": []byte("bar"), } Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getData, Equal(expected))) Eventually(func() (*v1.EventList, error) { return c.Events(ns).Search(scheme.Scheme, ss) }, Timeout, PollingInterval).Should( containEventWithReason(Equal("Unsealed")), ) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getFirstOwnerName, Equal(ss.GetName()))) }) }) Context("With unowned secret with managed annotation", func() { BeforeEach(func() { s.Data["foo2"] = []byte("bar2") s.Annotations = map[string]string{ ssv1alpha1.SealedSecretManagedAnnotation: "true", } c.Secrets(ns).Create(ctx, s, metav1.CreateOptions{}) }) JustBeforeEach(func() { err := c.Secrets(ns).Delete(ctx, secretName, metav1.DeleteOptions{}) Expect(err).NotTo(HaveOccurred()) }) It("should recreate the secret", func() { expected := map[string][]byte{ "foo": []byte("bar"), } Eventually(func() (*v1.EventList, error) { return c.Events(ns).Search(scheme.Scheme, ss) }, Timeout, PollingInterval).Should( containEventWithReason(Equal("Unsealed")), ) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getFirstOwnerName, Equal(ss.GetName()))) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getData, Equal(expected))) }) }) Context("With unowned secret without managed annotation", func() { BeforeEach(func() { s.Annotations = map[string]string{} c.Secrets(ns).Create(ctx, s, metav1.CreateOptions{}) }) JustBeforeEach(func() { err := c.Secrets(ns).Delete(ctx, secretName, metav1.DeleteOptions{}) Expect(err).NotTo(HaveOccurred()) }) It("should not recreate the secret", func() { Consistently(func() error { _, err := c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) return err }).Should(WithTransform(errors.IsNotFound, Equal(true))) }) }) Context("With unowned secret with patch annotation", func() { BeforeEach(func() { s.Data = map[string][]byte{ "foo": []byte("bar1"), "foo2": []byte("bar2"), } s.Annotations = map[string]string{ ssv1alpha1.SealedSecretPatchAnnotation: "true", } s.Labels["anotherlabel"] = "anothervalue" c.Secrets(ns).Create(ctx, s, metav1.CreateOptions{}) }) JustBeforeEach(func() { err := c.Secrets(ns).Delete(ctx, secretName, metav1.DeleteOptions{}) Expect(err).NotTo(HaveOccurred()) }) It("should not recreate the secret", func() { Consistently(func() error { _, err := c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) return err }).Should(WithTransform(errors.IsNotFound, Equal(true))) }) }) }) Describe("Same name, wrong key", func() { BeforeEach(func() { // NB: weak key-size - this is just a test case wrongKey, err := rsa.GenerateKey(rand.Reader, 1024) Expect(err).NotTo(HaveOccurred()) fmt.Fprintf(GinkgoWriter, "Resealing with wrong key\n") ss, err = ssv1alpha1.NewSealedSecret(scheme.Codecs, &wrongKey.PublicKey, s) Expect(err).NotTo(HaveOccurred()) }) It("should *not* produce a Secret", func() { Consistently(func() error { _, err := c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) return err }).Should(WithTransform(errors.IsNotFound, Equal(true))) }) It("should produce an error Event", func() { // Check for a suitable error event on the // SealedSecret Eventually(func() (*v1.EventList, error) { return c.Events(ns).Search(scheme.Scheme, ss) }, Timeout, PollingInterval).Should( containEventWithReason(Equal("ErrUnsealFailed")), ) }) }) Describe("Custom Secret Type", func() { BeforeEach(func() { label := fmt.Sprintf("%s/%s", s.Namespace, s.Name) ciphertext, err := crypto.HybridEncrypt(rand.Reader, pubKey, []byte("{\"auths\": {\"https://index.docker.io/v1/\": {\"auth\": \"c3R...zE2\"}}}"), []byte(label)) Expect(err).NotTo(HaveOccurred()) ss.Spec.EncryptedData[".dockerconfigjson"] = base64.StdEncoding.EncodeToString(ciphertext) delete(ss.Spec.EncryptedData, "foo") ss.Spec.Template.Type = "kubernetes.io/dockerconfigjson" }) It("should produce expected Secret", func() { expected := map[string][]byte{ ".dockerconfigjson": []byte("{\"auths\": {\"https://index.docker.io/v1/\": {\"auth\": \"c3R...zE2\"}}}"), } var expectedType v1.SecretType = "kubernetes.io/dockerconfigjson" Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getData, Equal(expected))) Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getSecretType, Equal(expectedType))) Eventually(func() (*ssv1alpha1.SealedSecret, error) { return ssc.BitnamiV1alpha1().SealedSecrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).ShouldNot(WithTransform(getStatus, BeNil())) Eventually(func() (*v1.EventList, error) { return c.Events(ns).Search(scheme.Scheme, ss) }, Timeout, PollingInterval).Should( containEventWithReason(Equal("Unsealed")), ) }) }) Describe("Immutable Secret", func() { BeforeEach(func() { ss.Spec.Template.Immutable = new(bool) *ss.Spec.Template.Immutable = true }) It("should produce expected Secret", func() { Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getSecretImmutable, Equal(true))) Eventually(func() (*ssv1alpha1.SealedSecret, error) { return ssc.BitnamiV1alpha1().SealedSecrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).ShouldNot(WithTransform(getStatus, BeNil())) Eventually(func() (*v1.EventList, error) { return c.Events(ns).Search(scheme.Scheme, ss) }, Timeout, PollingInterval).Should( containEventWithReason(Equal("Unsealed")), ) }) }) Describe("Immutable Secret Error", func() { BeforeEach(func() { ss.Spec.Template.Immutable = new(bool) *ss.Spec.Template.Immutable = true }) JustBeforeEach(func() { var err error Eventually(func() (*ssv1alpha1.SealedSecret, error) { return ssc.BitnamiV1alpha1().SealedSecrets(ss.Namespace).Get(context.Background(), secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).ShouldNot(WithTransform(getStatus, BeNil())) ss, err = ssc.BitnamiV1alpha1().SealedSecrets(ss.Namespace).Get(context.Background(), secretName, metav1.GetOptions{}) Expect(err).NotTo(HaveOccurred()) resVer := ss.ResourceVersion // update s.Data["foo"] = []byte("baz") ss, err = ssv1alpha1.NewSealedSecret(scheme.Codecs, pubKey, s) Expect(err).NotTo(HaveOccurred()) ss.ResourceVersion = resVer fmt.Fprintf(GinkgoWriter, "Updating to SealedSecret: %#v\n", ss) ss, err = ssc.BitnamiV1alpha1().SealedSecrets(ss.Namespace).Update(context.Background(), ss, metav1.UpdateOptions{}) Expect(err).NotTo(HaveOccurred()) }) It("should record update failure as an event", func() { Eventually(func() (*ssv1alpha1.SealedSecret, error) { return ssc.BitnamiV1alpha1().SealedSecrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).ShouldNot(WithTransform(getStatus, BeNil())) Eventually(func() (*ssv1alpha1.SealedSecret, error) { return ssc.BitnamiV1alpha1().SealedSecrets(ns).Get(context.Background(), secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getObservedGeneration, Equal(int64(2)))) Eventually(func() (*v1.EventList, error) { return c.Events(ns).Search(scheme.Scheme, ss) }, Timeout, PollingInterval).Should( containEventWithReason(Equal("ErrUpdateFailed")), ) Eventually(func() (*v1.EventList, error) { return c.Events(ns).Search(scheme.Scheme, ss) }, Timeout, PollingInterval).Should( containEventWithMessage(ContainSubstring("the target Secret is immutable")), ) }) }) Describe("Different name/namespace", func() { Context("With wrong name", func() { const secretName2 = "not-testsecret" BeforeEach(func() { ss.Name = secretName2 }) It("should *not* produce a Secret", func() { Consistently(func() error { _, err := c.Secrets(ns).Get(ctx, secretName2, metav1.GetOptions{}) return err }).Should(WithTransform(errors.IsNotFound, Equal(true))) }) It("should produce an error Event", func() { // Check for a suitable error event on the // SealedSecret Eventually(func() (*v1.EventList, error) { return c.Events(ns).Search(scheme.Scheme, ss) }, Timeout, PollingInterval).Should( containEventWithReason(Equal("ErrUnsealFailed")), ) }) }) Context("With wrong namespace", func() { var ns2 string BeforeEach(func() { ns2 = createNsOrDie(ctx, c, "create") ss.Namespace = ns2 }) AfterEach(func() { deleteNsOrDie(ctx, c, ns2) }) It("should *not* produce a Secret", func() { Consistently(func() error { _, err := c.Secrets(ns2).Get(ctx, secretName, metav1.GetOptions{}) return err }).Should(WithTransform(errors.IsNotFound, Equal(true))) }) It("should produce an error Event", func() { // Check for a suitable error event on the // SealedSecret Eventually(func() (*v1.EventList, error) { return c.Events(ns2).Search(scheme.Scheme, ss) }, Timeout, PollingInterval).Should( containEventWithReason(Equal("ErrUnsealFailed")), ) }) }) Context("With wrong name and cluster-wide annotation", func() { const secretName2 = "not-testsecret" BeforeEach(func() { var err error s.Annotations = map[string]string{ ssv1alpha1.SealedSecretClusterWideAnnotation: "true", } fmt.Fprintf(GinkgoWriter, "Re-sealing secret %#v\n", s) ss, err = ssv1alpha1.NewSealedSecret(scheme.Codecs, pubKey, s) Expect(err).NotTo(HaveOccurred()) }) BeforeEach(func() { ss.Name = secretName2 }) It("should produce expected Secret", func() { expected := map[string][]byte{ "foo": []byte("bar"), } Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName2, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getData, Equal(expected))) Eventually(func() (*ssv1alpha1.SealedSecret, error) { return ssc.BitnamiV1alpha1().SealedSecrets(ns).Get(context.Background(), secretName2, metav1.GetOptions{}) }, Timeout, PollingInterval).ShouldNot(WithTransform(getStatus, BeNil())) }) }) Context("With wrong namespace and cluster-wide annotation", func() { var ns2 string BeforeEach(func() { ns2 = createNsOrDie(ctx, c, "create") }) BeforeEach(func() { var err error s.Annotations = map[string]string{ ssv1alpha1.SealedSecretClusterWideAnnotation: "true", } fmt.Fprintf(GinkgoWriter, "Re-sealing secret %#v\n", s) ss, err = ssv1alpha1.NewSealedSecret(scheme.Codecs, pubKey, s) ss.Namespace = ns2 Expect(err).NotTo(HaveOccurred()) }) AfterEach(func() { deleteNsOrDie(ctx, c, ns2) }) It("should produce expected Secret", func() { expected := map[string][]byte{ "foo": []byte("bar"), } Eventually(func() (*v1.Secret, error) { return c.Secrets(ns2).Get(ctx, secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getData, Equal(expected))) Eventually(func() (*ssv1alpha1.SealedSecret, error) { return ssc.BitnamiV1alpha1().SealedSecrets(ns2).Get(context.Background(), secretName, metav1.GetOptions{}) }, Timeout, PollingInterval).ShouldNot(WithTransform(getStatus, BeNil())) }) }) Context("With wrong name and namespace-wide annotation", func() { const secretName2 = "not-testsecret" BeforeEach(func() { var err error s.Annotations = map[string]string{ ssv1alpha1.SealedSecretNamespaceWideAnnotation: "true", } fmt.Fprintf(GinkgoWriter, "Re-sealing secret %#v\n", s) ss, err = ssv1alpha1.NewSealedSecret(scheme.Codecs, pubKey, s) Expect(err).NotTo(HaveOccurred()) }) BeforeEach(func() { ss.Name = secretName2 }) It("should produce expected Secret", func() { expected := map[string][]byte{ "foo": []byte("bar"), } Eventually(func() (*v1.Secret, error) { return c.Secrets(ns).Get(ctx, secretName2, metav1.GetOptions{}) }, Timeout, PollingInterval).Should(WithTransform(getData, Equal(expected))) }) }) Context("With wrong namespace and namespace-wide annotation", func() { var ns2 string BeforeEach(func() { ns2 = createNsOrDie(ctx, c, "create") }) BeforeEach(func() { var err error s.Annotations = map[string]string{ ssv1alpha1.SealedSecretNamespaceWideAnnotation: "true", } fmt.Fprintf(GinkgoWriter, "Re-sealing secret %#v\n", s) ss, err = ssv1alpha1.NewSealedSecret(scheme.Codecs, pubKey, s) ss.Namespace = ns2 Expect(err).NotTo(HaveOccurred()) }) AfterEach(func() { deleteNsOrDie(ctx, c, ns2) }) It("should *not* produce a Secret", func() { Consistently(func() error { _, err := c.Secrets(ns2).Get(ctx, secretName, metav1.GetOptions{}) return err }).Should(WithTransform(errors.IsNotFound, Equal(true))) }) }) }) }) var _ = Describe("controller --version", func() { var input io.Reader var output *bytes.Buffer var args []string BeforeEach(func() { args = []string{"--version"} output = &bytes.Buffer{} }) JustBeforeEach(func() { err := runController(args, input, output) Expect(err).NotTo(HaveOccurred()) }) It("should produce the version", func() { Expect(output.String()).Should(MatchRegexp("^controller version: (v[0-9]+\\.[0-9]+\\.[0-9]+|[0-9a-f]{40})(\\+dirty)?")) }) }) ================================================ FILE: integration/integration_suite_test.go ================================================ //go:build integration // +build integration package integration import ( "bufio" "bytes" "context" "flag" "fmt" "io" "os/exec" "testing" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" ssv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" // For client auth plugins _ "k8s.io/client-go/plugin/pkg/client/auth" ) var kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file") var controllerNs = flag.String("namespace", "kube-system", "namespace where the controller is installed") var kubesealBin = flag.String("kubeseal-bin", "kubeseal", "path to kubeseal executable under test") var controllerBin = flag.String("controller-bin", "controller", "path to controller executable under test") func clusterConfigOrDie() *rest.Config { var config *rest.Config var err error if *kubeconfig != "" { config, err = clientcmd.BuildConfigFromFlags("", *kubeconfig) } else { config, err = rest.InClusterConfig() } if err != nil { panic(err.Error()) } return config } func createNsOrDie(ctx context.Context, c corev1.NamespacesGetter, ns string) string { result, err := c.Namespaces().Create( ctx, &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ GenerateName: ns, }, }, metav1.CreateOptions{}) if err != nil { panic(err.Error()) } name := result.GetName() fmt.Fprintf(GinkgoWriter, "Created namespace %s\n", name) return name } func deleteNsOrDie(ctx context.Context, c corev1.NamespacesGetter, ns string) { err := c.Namespaces().Delete(ctx, ns, metav1.DeleteOptions{}) if err != nil { panic(err.Error()) } } func containsString(haystack []string, needle string) bool { for _, s := range haystack { if s == needle { return true } } return false } func runKubeseal(flags []string, input io.Reader, output io.Writer, opts ...runAppOpt) error { args := []string{} if *kubeconfig != "" && !containsString(flags, "--kubeconfig") { args = append(args, "--kubeconfig", *kubeconfig) } args = append(args, flags...) return runApp(*kubesealBin, args, input, output, opts...) } type interruptableReader struct { ctx context.Context r io.Reader } func (r interruptableReader) Read(p []byte) (int, error) { if err := r.ctx.Err(); err != nil { return 0, err } n, err := r.r.Read(p) if err != nil { return n, err } return n, r.ctx.Err() } func streamLog(ctx context.Context, c corev1.PodsGetter, namespace, name, container string, output io.Writer, prefix string) error { zero := int64(0) readCloser, err := c.Pods(namespace).GetLogs(name, &v1.PodLogOptions{ Container: container, Follow: true, SinceSeconds: &zero, }).Stream(ctx) if err != nil { return err } defer readCloser.Close() scanner := bufio.NewScanner(interruptableReader{ctx, readCloser}) for scanner.Scan() { fmt.Fprintf(output, "%s%s\n", prefix, scanner.Text()) } return scanner.Err() } func runController(flags []string, input io.Reader, output io.Writer) error { return runApp(*controllerBin, flags, input, output) } type runAppOpt func(*runAppOpts) type runAppOpts struct { stderr io.Writer } func runAppWithStderr(w io.Writer) runAppOpt { return func(o *runAppOpts) { o.stderr = w } } func runApp(app string, flags []string, input io.Reader, output io.Writer, opts ...runAppOpt) error { options := runAppOpts{ stderr: GinkgoWriter, } for _, o := range opts { o(&options) } fmt.Fprintf(GinkgoWriter, "Running %q %q\n", app, flags) cmd := exec.Command(app, flags...) cmd.Stdin = input cmd.Stdout = output cmd.Stderr = options.stderr return cmd.Run() } func runKubesealWith(flags []string, input runtime.Object, opts ...runAppOpt) (runtime.Object, error) { enc := scheme.Codecs.LegacyCodec(v1.SchemeGroupVersion) indata, err := runtime.Encode(enc, input) if err != nil { return nil, err } fmt.Fprintf(GinkgoWriter, "kubeseal input:\n%s\n", indata) outbuf := bytes.Buffer{} if err := runKubeseal(flags, bytes.NewReader(indata), &outbuf, opts...); err != nil { return nil, err } fmt.Fprintf(GinkgoWriter, "kubeseal output:\n%s\n", outbuf.Bytes()) outputObj, err := runtime.Decode(scheme.Codecs.UniversalDecoder(ssv1alpha1.SchemeGroupVersion), outbuf.Bytes()) if err != nil { return nil, err } return outputObj, nil } func TestE2e(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "sealed-secrets integration tests") } ================================================ FILE: integration/kubeseal_test.go ================================================ //go:build integration // +build integration package integration import ( "bytes" "context" "crypto/rsa" "crypto/x509" "encoding/json" "encoding/pem" "io" "os" ssv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" clientcmdlatest "k8s.io/client-go/tools/clientcmd/api/latest" certUtil "k8s.io/client-go/util/cert" ) var _ = Describe("kubeseal", func() { var c corev1.CoreV1Interface const secretName = "testSecret" var ns string var input *v1.Secret var ss *ssv1alpha1.SealedSecret var args []string var privKeys map[string]*rsa.PrivateKey var certs []*x509.Certificate var config *clientcmdapi.Config var kubeconfigFile string var ( ctx context.Context cancel context.CancelFunc ) BeforeEach(func() { ctx, cancel = context.WithCancel(context.Background()) clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( &clientcmd.ClientConfigLoadingRules{ExplicitPath: *kubeconfig}, &clientcmd.ConfigOverrides{}) rawconf, err := clientConfig.RawConfig() Expect(err).NotTo(HaveOccurred()) config = rawconf.DeepCopy() }) JustBeforeEach(func() { f, err := os.CreateTemp("", "kubeconfig") Expect(err).NotTo(HaveOccurred()) buf, err := runtime.Encode(clientcmdlatest.Codec, config) Expect(err).NotTo(HaveOccurred()) _, err = f.Write(buf) Expect(err).NotTo(HaveOccurred()) err = f.Close() Expect(err).NotTo(HaveOccurred()) kubeconfigFile = f.Name() args = append(args, "--kubeconfig", kubeconfigFile) }) AfterEach(func() { os.Remove(kubeconfigFile) cancel() }) BeforeEach(func() { c = corev1.NewForConfigOrDie(clusterConfigOrDie()) input = &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: ns, Name: secretName, }, Data: map[string][]byte{ "foo": []byte("bar"), }, } var err error privKeys, certs, err = fetchKeys(ctx, c) Expect(err).NotTo(HaveOccurred()) }) JustBeforeEach(func() { outobj, err := runKubesealWith(args, input) Expect(err).NotTo(HaveOccurred()) ss = outobj.(*ssv1alpha1.SealedSecret) }) Context("Without args", func() { const testNs = "testns" BeforeEach(func() { input.Namespace = testNs }) It("should have the right objectmeta", func() { Expect(ss.Kind).To(Equal("SealedSecret")) Expect(ss.GetName()).To(Equal(secretName)) Expect(ss.GetNamespace()).To(Equal(testNs)) }) It("should contain the right value", func() { s, err := ss.Unseal(scheme.Codecs, privKeys) Expect(err).NotTo(HaveOccurred()) Expect(s.Data).To(HaveKeyWithValue("foo", []byte("bar"))) }) }) Context("No input namespace", func() { const testNs = "nons" BeforeEach(func() { // set kubeconfig default namespace to testNs config.Contexts[config.CurrentContext].Namespace = testNs }) It("should use namespace from kubeconfig", func() { Expect(ss.GetNamespace()).To(Equal(testNs)) }) It("should qualify the Secret", func() { s, err := ss.Unseal(scheme.Codecs, privKeys) Expect(err).NotTo(HaveOccurred()) Expect(s.GetNamespace()).To(Equal(testNs)) }) }) Context("With --namespace", func() { const testNs = "argns" BeforeEach(func() { args = append(args, "-n", testNs) }) It("should qualify the output SealedSecret", func() { Expect(ss.GetNamespace()).To(Equal(testNs)) }) It("should qualify the Secret", func() { s, err := ss.Unseal(scheme.Codecs, privKeys) Expect(err).NotTo(HaveOccurred()) Expect(s.GetNamespace()).To(Equal(testNs)) }) }) Context("Offline, with --cert", func() { var certfile *os.File BeforeEach(func() { // Invalidate address of current cluster cluster := config.Contexts[config.CurrentContext].Cluster config.Clusters[cluster].Server = "http://0.0.0.0:1" }) BeforeEach(func() { var err error certfile, err = os.CreateTemp("", "kubeseal-test") Expect(err).NotTo(HaveOccurred()) for _, cert := range certs { certfile.Write(pem.EncodeToMemory(&pem.Block{Type: certUtil.CertificateBlockType, Bytes: cert.Raw})) } certfile.Close() args = append(args, "--cert", certfile.Name()) }) AfterEach(func() { if certfile != nil { os.Remove(certfile.Name()) certfile = nil } }) It("should output the right value", func() { s, err := ss.Unseal(scheme.Codecs, privKeys) Expect(err).NotTo(HaveOccurred()) Expect(s.Data).To(HaveKeyWithValue("foo", []byte("bar"))) }) }) }) var _ = Describe("kubeseal (with invalid input)", func() { var input io.Reader var output *bytes.Buffer var args []string BeforeEach(func() { output = &bytes.Buffer{} }) It("should throw an error", func() { err := runKubeseal(args, input, output) Expect(err).To(HaveOccurred()) }) }) var _ = Describe("kubeseal --fetch-cert", func() { var c corev1.CoreV1Interface var input io.Reader var output *bytes.Buffer var args []string var ( ctx context.Context cancel context.CancelFunc ) BeforeEach(func() { ctx, cancel = context.WithCancel(context.Background()) c = corev1.NewForConfigOrDie(clusterConfigOrDie()) args = append(args, "--fetch-cert") output = &bytes.Buffer{} }) JustBeforeEach(func() { err := runKubeseal(args, input, output) Expect(err).NotTo(HaveOccurred()) }) AfterEach(func() { cancel() }) It("should produce the certificate", func() { _, certs, err := fetchKeys(ctx, c) Expect(err).NotTo(HaveOccurred()) Expect(certUtil.ParseCertsPEM(output.Bytes())). Should(Equal(certs)) }) }) var _ = Describe("kubeseal --version", func() { var input io.Reader var output *bytes.Buffer var args []string BeforeEach(func() { args = []string{"--version"} output = &bytes.Buffer{} }) JustBeforeEach(func() { err := runKubeseal(args, input, output) Expect(err).NotTo(HaveOccurred()) }) It("should produce the version", func() { Expect(output.String()).Should(MatchRegexp("^kubeseal version: (v[0-9]+\\.[0-9]+\\.[0-9]+|[0-9a-f]{40})(\\+dirty)?")) }) }) var _ = Describe("kubeseal --verify", func() { const secretName = "testSecret" const testNs = "testverifyns" var input io.Reader var output *bytes.Buffer var ss *ssv1alpha1.SealedSecret var args []string var err error BeforeEach(func() { args = append(args, "--validate") output = &bytes.Buffer{} }) BeforeEach(func() { input := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: testNs, Name: secretName, }, Data: map[string][]byte{ "foo": []byte("bar"), }, } outobj, err := runKubesealWith([]string{}, input) Expect(err).NotTo(HaveOccurred()) ss = outobj.(*ssv1alpha1.SealedSecret) }) JustBeforeEach(func() { enc := scheme.Codecs.LegacyCodec(ssv1alpha1.SchemeGroupVersion) indata, err := runtime.Encode(enc, ss) Expect(err).NotTo(HaveOccurred()) input = bytes.NewReader(indata) }) JustBeforeEach(func() { err = runKubeseal(args, input, output) }) Context("valid sealed secret", func() { It("should see the sealed secret as valid", func() { Expect(err).NotTo(HaveOccurred()) }) }) Context("invalid sealed secret", func() { BeforeEach(func() { ss.Name = "a-completely-different-name" }) It("should see the sealed secret as invalid", func() { Expect(err).To(HaveOccurred()) }) }) }) var _ = Describe("kubeseal --cert", func() { var input io.Reader var output *bytes.Buffer var args []string BeforeEach(func() { args = []string{"--cert", "/?this/file/cannot/possibly/exist/right?"} output = &bytes.Buffer{} }) JustBeforeEach(func() { err := runKubeseal(args, input, io.Discard, runAppWithStderr(output)) Expect(err).To(HaveOccurred()) }) It("should return an error", func() { Expect(output.String()).Should(MatchRegexp("^error:.*no such file or directory")) }) }) var _ = Describe("kubeseal --recovery-unseal", func() { const ns = "default" const secretName = "testSecret" var args []string var backupKeysFile *os.File var c corev1.CoreV1Interface var err error var sealedSecretInput []byte var ss *ssv1alpha1.SealedSecret var stderr *bytes.Buffer var stdout *bytes.Buffer var ( ctx context.Context cancel context.CancelFunc ) BeforeEach(func() { ctx, cancel = context.WithCancel(context.Background()) c = corev1.NewForConfigOrDie(clusterConfigOrDie()) input := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: ns, Name: secretName, }, Data: map[string][]byte{ "foo": []byte("bar"), }, } outobj, err := runKubesealWith([]string{}, input) Expect(err).NotTo(HaveOccurred()) ss = outobj.(*ssv1alpha1.SealedSecret) enc := scheme.Codecs.LegacyCodec(ssv1alpha1.SchemeGroupVersion) sealedSecretInput, err = runtime.Encode(enc, ss) Expect(err).NotTo(HaveOccurred()) }) BeforeEach(func() { key, err := c.Secrets(*controllerNs).List(ctx, metav1.ListOptions{ LabelSelector: keySelector, }) Expect(err).NotTo(HaveOccurred()) backupKeysFile, err = os.CreateTemp("", "key") Expect(err).NotTo(HaveOccurred()) defer backupKeysFile.Close() json, err := json.Marshal(key) Expect(err).NotTo(HaveOccurred()) backupKeysFile.Write(json) }) BeforeEach(func() { args = []string{"--recovery-unseal", "--kubeconfig", "/?this/file/cannot/possibly/exist/right?"} stderr = &bytes.Buffer{} stdout = &bytes.Buffer{} }) JustBeforeEach(func() { err = runKubeseal(args, bytes.NewReader(sealedSecretInput), stdout, runAppWithStderr(stderr)) }) AfterEach(func() { cancel() }) Context("without --recovery-private-key", func() { It("should return an error", func() { Expect(err).To(HaveOccurred()) Expect(stderr.String()).Should(MatchRegexp("^error:.*key could decrypt secret (.*)")) }) }) Context("with valid --recovery-private-key", func() { var secret v1.Secret BeforeEach(func() { args = append(args, "--recovery-private-key", backupKeysFile.Name()) }) It("should successfully unseal the secret", func() { json.Unmarshal(stdout.Bytes(), &secret) Expect(err).NotTo(HaveOccurred()) Expect(secret.Data).To(HaveKeyWithValue("foo", []byte("bar"))) }) }) }) ================================================ FILE: jsonnetfile.json ================================================ { "dependencies": [ { "name": "kube-libsonnet", "source": { "git": { "remote": "https://github.com/bitnami-labs/kube-libsonnet", "subdir": "" } }, "version": "master" } ] } ================================================ FILE: jsonnetfile.lock.json ================================================ { "dependencies": [ { "name": "kube-libsonnet", "source": { "git": { "remote": "https://github.com/bitnami-labs/kube-libsonnet", "subdir": "" } }, "version": "7df1459e6d890d54eb96ea3df70d7c84b8b3fb0e" } ] } ================================================ FILE: kube-fixes.libsonnet ================================================ { CustomResourceDefinition(group, version, kind): { local this = self, apiVersion: 'apiextensions.k8s.io/v1', kind: 'CustomResourceDefinition', metadata+: { name: this.spec.names.plural + '.' + this.spec.group, }, spec: { scope: 'Namespaced', group: group, versions_:: { [version]: { served: true, storage: true, }, }, versions: $.mapToNamedList(self.versions_), names: { kind: kind, singular: $.toLower(self.kind), plural: self.singular + 's', listKind: self.kind + 'List', }, }, }, } ================================================ FILE: pkg/apis/sealedsecrets/v1alpha1/doc.go ================================================ // +k8s:deepcopy-gen=package,register // +groupName=bitnami.com // Package v1alpha1 contains the definition of the sealed-secrets v1alpha1 API. Some of the code in this package is generated. package v1alpha1 ================================================ FILE: pkg/apis/sealedsecrets/v1alpha1/register.go ================================================ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/kubernetes/scheme" ) // GroupName is the group name used in this package. const GroupName = "bitnami.com" var ( // SchemeGroupVersion is the group version used to register these objects. SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} // SchemeBuilder adds this group to scheme. SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) // AddToScheme is a global function that registers this API group & version to a scheme. AddToScheme = SchemeBuilder.AddToScheme ) func init() { utilruntime.Must(SchemeBuilder.AddToScheme(scheme.Scheme)) } // Resource takes an unqualified resource and returns a Group qualified GroupResource. func Resource(resource string) schema.GroupResource { return SchemeGroupVersion.WithResource(resource).GroupResource() } func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &SealedSecret{}, &SealedSecretList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil } ================================================ FILE: pkg/apis/sealedsecrets/v1alpha1/sealedsecret_expansion.go ================================================ package v1alpha1 import ( "bytes" "crypto/rand" "crypto/rsa" "encoding/base64" "errors" "fmt" "text/template" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" "github.com/Masterminds/sprig/v3" "github.com/mkmik/multierror" "github.com/bitnami-labs/sealed-secrets/pkg/crypto" ) const ( // The StrictScope pins the sealed secret to a specific namespace and a specific name. StrictScope SealingScope = iota // The NamespaceWideScope only pins a sealed secret to a specific namespace. NamespaceWideScope // The ClusterWideScope allows the sealed secret to be unsealed in any namespace of the cluster. ClusterWideScope // The DefaultScope is currently the StrictScope. DefaultScope = StrictScope ) var ( // TODO(mkm): remove after a release. AcceptDeprecatedV1Data = false sprigFuncMap = sprig.GenericFuncMap() // a singleton for better performance ) func init() { // Avoid allowing the user to learn things about the environment. delete(sprigFuncMap, "env") delete(sprigFuncMap, "expandenv") delete(sprigFuncMap, "getHostByName") } // SealedSecretExpansion has methods to work with SealedSecrets resources. type SealedSecretExpansion interface { Unseal(codecs runtimeserializer.CodecFactory, privKeys map[string]*rsa.PrivateKey) (*v1.Secret, error) } // SealingScope is an enum that declares the mobility of a sealed secret by defining // in which scopes. type SealingScope int func (s *SealingScope) String() string { switch *s { case StrictScope: return "strict" case NamespaceWideScope: return "namespace-wide" case ClusterWideScope: return "cluster-wide" default: return fmt.Sprintf("undefined-%d", *s) } } func (s *SealingScope) Set(v string) error { switch v { case "": *s = DefaultScope case "strict": *s = StrictScope case "namespace-wide": *s = NamespaceWideScope case "cluster-wide": *s = ClusterWideScope default: return fmt.Errorf("must be one of: strict, namespace-wide, cluster-wide") } return nil } // Type implements the pflag.Value interface. func (s *SealingScope) Type() string { return "string" } // EncryptionLabel returns the label meant to be used for encrypting a sealed secret according to scope. func EncryptionLabel(namespace, name string, scope SealingScope) []byte { var l string switch scope { case ClusterWideScope: l = "" case NamespaceWideScope: l = namespace case StrictScope: fallthrough default: l = fmt.Sprintf("%s/%s", namespace, name) } return []byte(l) } // Returns labels followed by clusterWide followed by namespaceWide. func labelFor(o metav1.Object) []byte { return EncryptionLabel(o.GetNamespace(), o.GetName(), SecretScope(o)) } // SecretScope returns the scope of a secret to be sealed, as annotated in its metadata. func SecretScope(o metav1.Object) SealingScope { if o.GetAnnotations()[SealedSecretClusterWideAnnotation] == "true" { return ClusterWideScope } if o.GetAnnotations()[SealedSecretNamespaceWideAnnotation] == "true" { return NamespaceWideScope } return StrictScope } // Scope returns the scope of the sealed secret, as annotated in its metadata. func (s *SealedSecret) Scope() SealingScope { return SecretScope(&s.Spec.Template) } // NewSealedSecretV1 creates a new SealedSecret object wrapping the // provided secret. This encrypts all the secrets into a single encrypted // blob and stores it in the `Data` attribute. Keeping this for backward // compatibility. func NewSealedSecretV1(codecs runtimeserializer.CodecFactory, pubKey *rsa.PublicKey, secret *v1.Secret) (*SealedSecret, error) { info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), runtime.ContentTypeJSON) if !ok { return nil, fmt.Errorf("binary can't serialize JSON") } if SecretScope(secret) != ClusterWideScope && secret.GetNamespace() == "" { return nil, fmt.Errorf("secret must declare a namespace") } codec := codecs.EncoderForVersion(info.Serializer, v1.SchemeGroupVersion) plaintext, err := runtime.Encode(codec, secret) if err != nil { return nil, err } // RSA-OAEP will fail to decrypt unless the same label is used // during decryption. label := labelFor(secret) ciphertext, err := crypto.HybridEncrypt(rand.Reader, pubKey, plaintext, label) if err != nil { return nil, err } s := &SealedSecret{ ObjectMeta: metav1.ObjectMeta{ Name: secret.GetName(), Namespace: secret.GetNamespace(), }, Spec: SealedSecretSpec{ Data: ciphertext, }, } s.Annotations = UpdateScopeAnnotations(s.Annotations, SecretScope(secret)) return s, nil } // UpdateScopeAnnotations updates the annotation map so that it reflects the desired scope. // It does so by updating and/or deleting existing annotations. func UpdateScopeAnnotations(anno map[string]string, scope SealingScope) map[string]string { if anno == nil { anno = map[string]string{} } delete(anno, SealedSecretNamespaceWideAnnotation) delete(anno, SealedSecretClusterWideAnnotation) if scope == NamespaceWideScope { anno[SealedSecretNamespaceWideAnnotation] = "true" } if scope == ClusterWideScope { anno[SealedSecretClusterWideAnnotation] = "true" } return anno } // StripLastAppliedAnnotations strips annotations added by tools such as kubectl and kubecfg // that contain a full copy of the original object kept in the annotation for strategic-merge-patch // purposes. We need to remove these annotations when sealing an existing secret otherwise we'd leak // the secrets. func StripLastAppliedAnnotations(annotations map[string]string) { if annotations == nil { return } keys := []string{ "kubectl.kubernetes.io/last-applied-configuration", "kubecfg.ksonnet.io/last-applied-configuration", } for _, k := range keys { delete(annotations, k) } } // NewSealedSecret creates a new SealedSecret object wrapping the // provided secret. This encrypts only the values of each secrets // individually, so secrets can be updated one by one. func NewSealedSecret(codecs runtimeserializer.CodecFactory, pubKey *rsa.PublicKey, secret *v1.Secret) (*SealedSecret, error) { if SecretScope(secret) != ClusterWideScope && secret.GetNamespace() == "" { return nil, fmt.Errorf("secret must declare a namespace") } s := &SealedSecret{ ObjectMeta: metav1.ObjectMeta{ Name: secret.GetName(), Namespace: secret.GetNamespace(), }, Spec: SealedSecretSpec{ Template: SecretTemplateSpec{ // ObjectMeta copied below Type: secret.Type, Immutable: secret.Immutable, }, EncryptedData: map[string]string{}, }, } secret.ObjectMeta.DeepCopyInto(&s.Spec.Template.ObjectMeta) // the input secret could come from a real secret object applied with `kubectl apply` or similar tools // which put a copy of the object version at application time in an annotation in order to support // strategic merge patch in subsequent updates. We need to strip those annotations or else we would // be leaking secrets in clear in a way that might be non obvious to users. // See https://github.com/bitnami-labs/sealed-secrets/issues/227 StripLastAppliedAnnotations(s.Spec.Template.ObjectMeta.Annotations) // Cleanup ownerReference (See #243) s.Spec.Template.ObjectMeta.OwnerReferences = nil // RSA-OAEP will fail to decrypt unless the same label is used // during decryption. label := labelFor(secret) for key, value := range secret.Data { ciphertext, err := crypto.HybridEncrypt(rand.Reader, pubKey, value, label) if err != nil { return nil, err } s.Spec.EncryptedData[key] = base64.StdEncoding.EncodeToString(ciphertext) } for key, value := range secret.StringData { ciphertext, err := crypto.HybridEncrypt(rand.Reader, pubKey, []byte(value), label) if err != nil { return nil, err } s.Spec.EncryptedData[key] = base64.StdEncoding.EncodeToString(ciphertext) } s.Annotations = UpdateScopeAnnotations(s.Annotations, SecretScope(secret)) return s, nil } // Unseal decrypts and returns the embedded v1.Secret. func (s *SealedSecret) Unseal(codecs runtimeserializer.CodecFactory, privKeys map[string]*rsa.PrivateKey) (*v1.Secret, error) { boolTrue := true smeta := s.GetObjectMeta() // This will fail to decrypt unless the same label was used // during encryption. This check ensures that we can't be // tricked into decrypting a sealed secret into an unexpected // namespace/name. label := labelFor(smeta) var secret v1.Secret if s.Spec.Data == nil { s.Spec.Template.ObjectMeta.DeepCopyInto(&secret.ObjectMeta) secret.Type = s.Spec.Template.Type secret.Immutable = s.Spec.Template.Immutable secret.Data = map[string][]byte{} data := map[string]string{} var errs []error for key, value := range s.Spec.EncryptedData { valueBytes, err := base64.StdEncoding.DecodeString(value) if err != nil { errs = append(errs, multierror.Tag(key, err)) continue } plaintext, err := crypto.HybridDecrypt(rand.Reader, privKeys, valueBytes, label) if err != nil { errs = append(errs, multierror.Tag(key, err)) } secret.Data[key] = plaintext data[key] = string(plaintext) } for key, value := range s.Spec.Template.Data { var plaintext bytes.Buffer template, err := template.New(key).Funcs(sprigFuncMap).Parse(value) if err != nil { errs = append(errs, multierror.Tag(key, err)) continue } err = template.Execute(&plaintext, data) if err != nil { errs = append(errs, multierror.Tag(key, err)) } secret.Data[key] = plaintext.Bytes() } if errs != nil { return nil, multierror.Format(errors.Join(multierror.Uniq(errs)...), multierror.InlineFormatter) } } else if AcceptDeprecatedV1Data { // Support decrypting old secrets for backward compatibility if len(s.Spec.EncryptedData) > 0 { return nil, fmt.Errorf("cannot use the field 'encryptedData' and the deprecated field 'data' at the same time") } plaintext, err := crypto.HybridDecrypt(rand.Reader, privKeys, s.Spec.Data, label) if err != nil { return nil, err } dec := codecs.UniversalDecoder(secret.GroupVersionKind().GroupVersion()) if err = runtime.DecodeInto(dec, plaintext, &secret); err != nil { return nil, err } } else { return nil, fmt.Errorf("using deprecated 'data' field, use 'encryptedData' or flip the feature flag") } // Ensure these are set to what we expect secret.SetNamespace(smeta.GetNamespace()) secret.SetName(smeta.GetName()) gvk := s.GetObjectKind().GroupVersionKind() if anno, ok := s.Spec.Template.Annotations[SealedSecretSkipSetOwnerReferencesAnnotation]; !ok || anno != "true" { // Refer back to owning SealedSecret ownerRefs := []metav1.OwnerReference{ { APIVersion: gvk.GroupVersion().String(), Kind: gvk.Kind, Name: smeta.GetName(), UID: smeta.GetUID(), Controller: &boolTrue, }, } secret.SetOwnerReferences(ownerRefs) } return &secret, nil } ================================================ FILE: pkg/apis/sealedsecrets/v1alpha1/sealedsecret_test.go ================================================ package v1alpha1 import ( "bytes" "crypto/rsa" "encoding/base64" "encoding/json" "io" mathrand "math/rand" "reflect" "strings" "testing" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/kubernetes/scheme" "github.com/bitnami-labs/sealed-secrets/pkg/crypto" // Install standard API types. _ "k8s.io/client-go/kubernetes" ) var _ runtime.Object = &SealedSecret{} var _ metav1.ObjectMetaAccessor = &SealedSecret{} var _ runtime.Object = &SealedSecretList{} var _ metav1.ListMetaAccessor = &SealedSecretList{} func TestSealingScope(t *testing.T) { testCases := []struct { scope SealingScope name string }{ {StrictScope, "strict"}, {NamespaceWideScope, "namespace-wide"}, {ClusterWideScope, "cluster-wide"}, } for _, tc := range testCases { if got, want := tc.scope.String(), tc.name; got != want { t.Errorf("got: %q, want: %q", got, want) } var s SealingScope err := s.Set(tc.name) if err != nil { t.Fatal(err) } if got, want := s, tc.scope; got != want { t.Errorf("got: %d, want: %d", got, want) } } var s SealingScope err := s.Set("") if err != nil { t.Fatal(err) } if got, want := s, StrictScope; got != want { t.Errorf("got: %d, want: %d", got, want) } } func TestEncryptionLabel(t *testing.T) { const ( ns = "myns" name = "myname" ) testCases := []struct { scope SealingScope label string }{ {StrictScope, "myns/myname"}, {NamespaceWideScope, "myns"}, {ClusterWideScope, ""}, } for _, tc := range testCases { if got, want := string(EncryptionLabel(ns, name, tc.scope)), tc.label; got != want { t.Errorf("got: %q, want: %q", got, want) } } } func TestLabel(t *testing.T) { s := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "myname", Namespace: "myns", }, } l := labelFor(&s) if string(l) != "myns/myname" { t.Errorf("Unexpected label: %#v", l) } } func TestClusterWide(t *testing.T) { s := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "myname", Namespace: "myns", Annotations: map[string]string{ SealedSecretClusterWideAnnotation: "true", }, }, } l := labelFor(&s) if string(l) != "" { t.Errorf("Unexpected label: %#v", l) } } func TestNamespaceWide(t *testing.T) { s := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "myname", Namespace: "myns", Annotations: map[string]string{ SealedSecretNamespaceWideAnnotation: "true", }, }, } l := labelFor(&s) if string(l) != "myns" { t.Errorf("Unexpected label: %#v", l) } } func TestClusterAndNamespaceWide(t *testing.T) { s := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "myname", Namespace: "myns", Annotations: map[string]string{ SealedSecretNamespaceWideAnnotation: "true", SealedSecretClusterWideAnnotation: "true", }, }, } l := labelFor(&s) if string(l) != "" { t.Errorf("Unexpected label: %#v", l) } } func TestSerialize(t *testing.T) { s := SealedSecret{ ObjectMeta: metav1.ObjectMeta{ Name: "myname", Namespace: "myns", }, Spec: SealedSecretSpec{ EncryptedData: map[string]string{ "foo": base64.StdEncoding.EncodeToString([]byte("secret1")), "bar": base64.StdEncoding.EncodeToString([]byte("secret2")), }, }, } info, ok := runtime.SerializerInfoForMediaType(scheme.Codecs.SupportedMediaTypes(), runtime.ContentTypeJSON) if !ok { t.Fatalf("binary can't serialize JSON") } enc := scheme.Codecs.EncoderForVersion(info.Serializer, SchemeGroupVersion) buf := bytes.Buffer{} if err := enc.Encode(&s, &buf); err != nil { t.Errorf("Error encoding: %v", err) } t.Logf("text is %s", buf.String()) } // This is omg-not safe for real crypto use! func testRand() io.Reader { return mathrand.New(mathrand.NewSource(42)) } func generateTestKey(t *testing.T, rand io.Reader, bits int) (*rsa.PrivateKey, map[string]*rsa.PrivateKey) { key, err := rsa.GenerateKey(rand, 2048) if err != nil { t.Fatalf("Failed to generate test key: %v", err) } fingerprint, err := crypto.PublicKeyFingerprint(&key.PublicKey) if err != nil { t.Fatalf("Failed to generate fingerprint: %v", err) } keys := map[string]*rsa.PrivateKey{fingerprint: key} return key, keys } func TestSealRoundTrip(t *testing.T) { secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "myname", Namespace: "myns", }, Data: map[string][]byte{ "foo": []byte("bar"), }, } ssecret, codecs, keys := sealSecret(t, &secret, NewSealedSecret) secret2, err := ssecret.Unseal(codecs, keys) if err != nil { t.Fatalf("Unseal returned error: %v", err) } if !reflect.DeepEqual(secret.Data, secret2.Data) { t.Errorf("Unsealed secret != original secret: %v != %v", secret, secret2) } } func TestSealRoundTripStringDataConversion(t *testing.T) { secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "myname", Namespace: "myns", }, Data: map[string][]byte{ "foo": []byte("bar"), "fss": []byte("brr"), }, StringData: map[string]string{ "fss": "baa", }, } unsealed := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "myname", Namespace: "myns", }, Data: map[string][]byte{ "foo": []byte("bar"), "fss": []byte("baa"), }, } ssecret, codecs, keys := sealSecret(t, &secret, NewSealedSecret) secret2, err := ssecret.Unseal(codecs, keys) if err != nil { t.Fatalf("Unseal returned error: %v", err) } if !reflect.DeepEqual(unsealed.Data, secret2.Data) { t.Errorf("Unsealed secret != original secret: %v != %v", unsealed, secret2) } } func TestSealRoundTripWithClusterWide(t *testing.T) { secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "myname", Namespace: "myns", Annotations: map[string]string{ SealedSecretClusterWideAnnotation: "true", }, }, Data: map[string][]byte{ "foo": []byte("bar"), }, } ssecret, codecs, keys := sealSecret(t, &secret, NewSealedSecret) secret2, err := ssecret.Unseal(codecs, keys) if err != nil { t.Fatalf("Unseal returned error: %v", err) } if !reflect.DeepEqual(secret.Data, secret2.Data) { t.Errorf("Unsealed secret != original secret: %v != %v", secret, secret2) } } func TestSealRoundTripWithMisMatchClusterWide(t *testing.T) { secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "myname", Namespace: "myns", Annotations: map[string]string{ SealedSecretClusterWideAnnotation: "true", }, }, Data: map[string][]byte{ "foo": []byte("bar"), }, } ssecret, codecs, keys := sealSecret(t, &secret, NewSealedSecret) ssecret.ObjectMeta.Annotations[SealedSecretClusterWideAnnotation] = "false" _, err := ssecret.Unseal(codecs, keys) if err == nil { t.Fatal("Expecting error: got nil instead") } } func TestSealRoundTripWithNamespaceWide(t *testing.T) { secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "myname", Namespace: "myns", Annotations: map[string]string{ SealedSecretNamespaceWideAnnotation: "true", }, }, Data: map[string][]byte{ "foo": []byte("bar"), }, } ssecret, codecs, keys := sealSecret(t, &secret, NewSealedSecret) secret2, err := ssecret.Unseal(codecs, keys) if err != nil { t.Fatalf("Unseal returned error: %v", err) } if !reflect.DeepEqual(secret.Data, secret2.Data) { t.Errorf("Unsealed secret != original secret: %v != %v", secret, secret2) } } func TestSealRoundTripWithMisMatchNamespaceWide(t *testing.T) { secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "myname", Namespace: "myns", Annotations: map[string]string{ SealedSecretNamespaceWideAnnotation: "true", }, }, Data: map[string][]byte{ "foo": []byte("bar"), }, } ssecret, codecs, keys := sealSecret(t, &secret, NewSealedSecret) ssecret.ObjectMeta.Annotations[SealedSecretNamespaceWideAnnotation] = "false" _, err := ssecret.Unseal(codecs, keys) if err == nil { t.Fatalf("Unseal did not return expected error: %v", err) } } func TestSealRoundTripTemplateData(t *testing.T) { secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "myname", Namespace: "myns", }, Data: map[string][]byte{ "foo": []byte("bar"), "password": []byte("hunter2'\"="), }, } ssecret, codecs, keys := sealSecret(t, &secret, NewSealedSecret) ssecret.Spec.Template.Data = map[string]string{ "bar": `secret {{ index . "foo" }} !`, "password-json": `{{ toJson .password }}`, } secret2, err := ssecret.Unseal(codecs, keys) if err != nil { t.Fatalf("Unseal returned error: %v", err) } if got, want := string(secret2.Data["bar"]), "secret bar !"; got != want { t.Errorf("got: %q, want: %q", got, want) } want, err := json.Marshal(string(secret.Data["password"])) if err != nil { t.Fatalf("json.Marshal returned error: %v", err) } if got := string(secret2.Data["password-json"]); got != string(want) { t.Errorf("got: %q, want: %q", got, want) } } func TestTemplateWithoutEncryptedData(t *testing.T) { sealed := SealedSecret{ Spec: SealedSecretSpec{ Template: SecretTemplateSpec{ Data: map[string]string{"foo": "bar"}, }, }, } unsealed, err := sealed.Unseal(serializer.CodecFactory{}, nil) if err != nil { t.Fatalf("Unseal returned error: %v", err) } if got, want := unsealed.Data, map[string][]byte{"foo": []byte("bar")}; !reflect.DeepEqual(got, want) { t.Errorf("got: %q, want: %q", got, want) } } func TestSkipSetOwnerReference(t *testing.T) { testCases := []struct { sealedSecret SealedSecret skipSetOwnerReference bool secret v1.Secret }{ { sealedSecret: SealedSecret{ Spec: SealedSecretSpec{ Template: SecretTemplateSpec{ Data: map[string]string{"foo": "bar"}, }, }, }, skipSetOwnerReference: true, secret: v1.Secret{ ObjectMeta: metav1.ObjectMeta{}, }, }, { sealedSecret: SealedSecret{ Spec: SealedSecretSpec{ Template: SecretTemplateSpec{ Data: map[string]string{"foo": "bar"}, }, }, }, skipSetOwnerReference: false, secret: v1.Secret{ ObjectMeta: metav1.ObjectMeta{ OwnerReferences: []metav1.OwnerReference{}, }, }, }, } for _, tc := range testCases { if tc.skipSetOwnerReference { if tc.sealedSecret.Spec.Template.Annotations == nil { tc.sealedSecret.Spec.Template.Annotations = make(map[string]string) } tc.sealedSecret.Spec.Template.Annotations[SealedSecretSkipSetOwnerReferencesAnnotation] = "true" } unsealed, err := tc.sealedSecret.Unseal(serializer.CodecFactory{}, nil) if err != nil { t.Fatalf("Unseal returned error: %v", err) } if tc.sealedSecret.Spec.Template.Annotations[SealedSecretSkipSetOwnerReferencesAnnotation] == "true" && len(unsealed.ObjectMeta.OwnerReferences) > 0 { t.Errorf("got: owner, want: no owner") } else if (tc.sealedSecret.Spec.Template.Annotations[SealedSecretSkipSetOwnerReferencesAnnotation] != "true") && len(unsealed.ObjectMeta.OwnerReferences) == 0 { t.Errorf("got: no owner, want: owner") } } } func TestSealMetadataPreservation(t *testing.T) { scheme := runtime.NewScheme() codecs := serializer.NewCodecFactory(scheme) utilruntime.Must(SchemeBuilder.AddToScheme(scheme)) utilruntime.Must(v1.SchemeBuilder.AddToScheme(scheme)) key, _ := generateTestKey(t, testRand(), 2048) testCases := []struct { key string preserved bool }{ {"foo", true}, {"foo.bar.io/foo-bar-baz", true}, {"kubectl.kubernetes.io/last-applied-configuration", false}, {"kubecfg.ksonnet.io/last-applied-configuration", false}, } for _, tc := range testCases { secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "myname", Namespace: "myns", Annotations: map[string]string{tc.key: "test value"}, OwnerReferences: []metav1.OwnerReference{ { APIVersion: "foo/v1", Kind: "Foo", Name: "foo", }, }, }, Data: map[string][]byte{ "foo": []byte("bar"), }, } ssecret, err := NewSealedSecret(codecs, &key.PublicKey, &secret) if err != nil { t.Fatalf("NewSealedSecret returned error: %v", err) } _, got := ssecret.Spec.Template.Annotations[tc.key] if want := tc.preserved; got != want { t.Errorf("key %q: exists: %v, expected to exist: %v", tc.key, got, want) } if got, want := len(ssecret.Spec.Template.OwnerReferences), 0; got != want { t.Errorf("got: %d, want: %d", got, want) } } } func TestUnsealingV1Format(t *testing.T) { secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "myname", Namespace: "myns", Annotations: map[string]string{ SealedSecretClusterWideAnnotation: "true", SealedSecretNamespaceWideAnnotation: "true", }, }, Data: map[string][]byte{ "foo": []byte("bar"), }, } ssecret, codecs, keys := sealSecret(t, &secret, NewSealedSecretV1) t.Run("AcceptDeprecatedV1Data", testWithAcceptDeprecatedV1Data(true, func(t *testing.T) { secret2, err := ssecret.Unseal(codecs, keys) if err != nil { t.Fatalf("Unseal returned error: %v", err) } if !reflect.DeepEqual(secret.Data, secret2.Data) { t.Errorf("Unsealed secret != original secret: %v != %v", secret, secret2) } })) t.Run("RejectDeprecatedV1Data", testWithAcceptDeprecatedV1Data(false, func(t *testing.T) { _, err := ssecret.Unseal(codecs, keys) if needle := "deprecated"; err == nil || !strings.Contains(err.Error(), needle) { t.Fatalf("Expecting error: %v to contain %q", err, needle) } })) } func TestRejectBothEncryptedDataAndDeprecatedV1Data(t *testing.T) { secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "myname", Namespace: "myns", }, StringData: map[string]string{"foo": "bar"}, } sealedSecret, codecs, keys := sealSecret(t, &secret, NewSealedSecret) sealedSecret.Spec.Data = []byte{} t.Run("AcceptDeprecatedV1Data", testWithAcceptDeprecatedV1Data(true, func(t *testing.T) { _, err := sealedSecret.Unseal(codecs, keys) if needle := "at the same time"; err == nil || !strings.Contains(err.Error(), needle) { t.Fatalf("Expecting error: %v to contain %q", err, needle) } })) t.Run("RejectDeprecatedV1Data", testWithAcceptDeprecatedV1Data(false, func(t *testing.T) { _, err := sealedSecret.Unseal(codecs, keys) if needle := "deprecated"; err == nil || !strings.Contains(err.Error(), needle) { t.Fatalf("Expecting error: %v to contain %q", err, needle) } })) } func TestInvalidBase64(t *testing.T) { sealedSecret := &SealedSecret{ ObjectMeta: metav1.ObjectMeta{ Name: "myname", Namespace: "myns", }, Spec: SealedSecretSpec{ EncryptedData: map[string]string{ "foo": "NOTVALIDBASE64", }, }, } scheme := runtime.NewScheme() codecs := serializer.NewCodecFactory(scheme) _, keys := generateTestKey(t, testRand(), 2048) _, err := sealedSecret.Unseal(codecs, keys) if err == nil { t.Fatal("Expecting error: got nil instead") } if !strings.Contains(err.Error(), "foo") { t.Errorf("Expecting error: %q to contain field %q", err, "foo") } if strings.Contains(err.Error(), "decrypt") { t.Errorf("Expecting error: %q to not contain %q (invalid base64 should skip decryption)", err, "decrypt") } } func sealSecret(t *testing.T, secret *v1.Secret, newSealedSecret func(serializer.CodecFactory, *rsa.PublicKey, *v1.Secret) (*SealedSecret, error)) (*SealedSecret, serializer.CodecFactory, map[string]*rsa.PrivateKey) { scheme := runtime.NewScheme() codecs := serializer.NewCodecFactory(scheme) utilruntime.Must(SchemeBuilder.AddToScheme(scheme)) utilruntime.Must(v1.SchemeBuilder.AddToScheme(scheme)) key, keys := generateTestKey(t, testRand(), 2048) sealedSecret, err := newSealedSecret(codecs, &key.PublicKey, secret) if err != nil { t.Fatalf("NewSealedSecret returned error: %v", err) } return sealedSecret, codecs, keys } func testWithAcceptDeprecatedV1Data(acceptDeprecated bool, inner func(t *testing.T)) func(*testing.T) { return func(t *testing.T) { defer func(saved bool) { AcceptDeprecatedV1Data = saved }(AcceptDeprecatedV1Data) AcceptDeprecatedV1Data = acceptDeprecated inner(t) } } ================================================ FILE: pkg/apis/sealedsecrets/v1alpha1/types.go ================================================ package v1alpha1 import ( "encoding/json" apiv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( // SealedSecretName is the name used in SealedSecret CRD. SealedSecretName = "sealed-secret." + GroupName // SealedSecretPlural is the collection plural used with SealedSecret API. SealedSecretPlural = "sealedsecrets" // Annotation namespace prefix. annoNs = "sealedsecrets." + GroupName + "/" // SealedSecretClusterWideAnnotation is the name for the annotation for // setting the secret to be available cluster wide. SealedSecretClusterWideAnnotation = annoNs + "cluster-wide" // SealedSecretNamespaceWideAnnotation is the name for the annotation for // setting the secret to be available namespace wide. SealedSecretNamespaceWideAnnotation = annoNs + "namespace-wide" // SealedSecretManagedAnnotation is the name for the annotation for // flagging existing secrets to be managed by the Sealed Secrets controller. SealedSecretManagedAnnotation = annoNs + "managed" // SealedSecretPatchAnnotation is the name for the annotation for // flagging existing secrets to be patched instead of overwritten by the Sealed Secrets controller. SealedSecretPatchAnnotation = annoNs + "patch" // SealedSecretSkipSetOwnerReferencesAnnotation is the name for the annotation for // flagging the controller not to set owner reference to secret. SealedSecretSkipSetOwnerReferencesAnnotation = annoNs + "skip-set-owner-references" ) // SecretTemplateSpec describes the structure a Secret should have // when created from a template. type SecretTemplateSpec struct { // Standard object's metadata. // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata // +optional // +nullable // +kubebuilder:pruning:PreserveUnknownFields metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` // Used to facilitate programmatic handling of secret data. // +optional Type apiv1.SecretType `json:"type,omitempty" protobuf:"bytes,3,opt,name=type,casttype=SecretType"` // Immutable, if set to true, ensures that data stored in the Secret cannot // be updated (only object metadata can be modified). // If not set to true, the field can be modified at any time. // Defaulted to nil. // +optional Immutable *bool `json:"immutable,omitempty" protobuf:"varint,5,opt,name=immutable"` // Keys that should be templated using decrypted data. // +optional // +nullable Data map[string]string `json:"data,omitempty"` } // SealedSecretSpec is the specification of a SealedSecret. type SealedSecretSpec struct { // Template defines the structure of the Secret that will be // created from this sealed secret. // +optional Template SecretTemplateSpec `json:"template,omitempty"` // Data is deprecated and will be removed eventually. Use per-value EncryptedData instead. Data []byte `json:"data,omitempty"` EncryptedData SealedSecretEncryptedData `json:"encryptedData"` } // +kubebuilder:pruning:PreserveUnknownFields type SealedSecretEncryptedData map[string]string func (s *SealedSecretEncryptedData) UnmarshalJSON(data []byte) error { tmp := map[string]string{} // drop error - likelihood of an error occurring is quite high due to the disabled schema validation, these errors. // would cause the controller to stop processing any SealedSecret. _ = json.Unmarshal(data, &tmp) *s = tmp return nil } // SealedSecretConditionType describes the type of SealedSecret condition. type SealedSecretConditionType string const ( // SealedSecretSynced means the SealedSecret has been decrypted and the Secret has been updated successfully. SealedSecretSynced SealedSecretConditionType = "Synced" ) // SealedSecretCondition describes the state of a sealed secret at a certain point. type SealedSecretCondition struct { // Type of condition for a sealed secret. // Valid value: "Synced" Type SealedSecretConditionType `json:"type" protobuf:"bytes,1,opt,name=type,casttype=DeploymentConditionType"` // Status of the condition for a sealed secret. // Valid values for "Synced": "True", "False", or "Unknown". Status apiv1.ConditionStatus `json:"status" protobuf:"bytes,2,opt,name=status,casttype=k8s.io/api/core/v1.ConditionStatus"` // The last time this condition was updated. LastUpdateTime metav1.Time `json:"lastUpdateTime,omitempty" protobuf:"bytes,6,opt,name=lastUpdateTime"` // Last time the condition transitioned from one status to another. LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty" protobuf:"bytes,7,opt,name=lastTransitionTime"` // The reason for the condition's last transition. Reason string `json:"reason,omitempty" protobuf:"bytes,4,opt,name=reason"` // A human readable message indicating details about the transition. Message string `json:"message,omitempty" protobuf:"bytes,5,opt,name=message"` } // SealedSecretStatus is the most recently observed status of the SealedSecret. type SealedSecretStatus struct { // ObservedGeneration reflects the generation most recently observed by the sealed-secrets controller. // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,3,opt,name=observedGeneration"` // Represents the latest available observations of a sealed secret's current state. // +optional // +patchMergeKey=type // +patchStrategy=merge Conditions []SealedSecretCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,6,rep,name=conditions"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[0].message" // +kubebuilder:printcolumn:name="Synced",type="string",JSONPath=".status.conditions[0].status" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" // +genclient // SealedSecret is the K8s representation of a "sealed Secret" - a // regular k8s Secret that has been sealed (encrypted) using the // controller's key. type SealedSecret struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec SealedSecretSpec `json:"spec"` // +optional Status *SealedSecretStatus `json:"status,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // SealedSecretList represents a list of SealedSecrets. type SealedSecretList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata"` Items []SealedSecret `json:"items"` } // ByCreationTimestamp is used to sort a list of secrets. type ByCreationTimestamp []apiv1.Secret func (s ByCreationTimestamp) Len() int { return len(s) } func (s ByCreationTimestamp) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s ByCreationTimestamp) Less(i, j int) bool { return s[i].GetCreationTimestamp().Unix() < s[j].GetCreationTimestamp().Unix() } ================================================ FILE: pkg/apis/sealedsecrets/v1alpha1/zz_generated.deepcopy.go ================================================ //go:build !ignore_autogenerated // +build !ignore_autogenerated // Code generated by deepcopy-gen. DO NOT EDIT. package v1alpha1 import ( runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in ByCreationTimestamp) DeepCopyInto(out *ByCreationTimestamp) { { in := &in *out = make(ByCreationTimestamp, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } return } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ByCreationTimestamp. func (in ByCreationTimestamp) DeepCopy() ByCreationTimestamp { if in == nil { return nil } out := new(ByCreationTimestamp) in.DeepCopyInto(out) return *out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SealedSecret) DeepCopyInto(out *SealedSecret) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) if in.Status != nil { in, out := &in.Status, &out.Status *out = new(SealedSecretStatus) (*in).DeepCopyInto(*out) } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SealedSecret. func (in *SealedSecret) DeepCopy() *SealedSecret { if in == nil { return nil } out := new(SealedSecret) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *SealedSecret) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SealedSecretCondition) DeepCopyInto(out *SealedSecretCondition) { *out = *in in.LastUpdateTime.DeepCopyInto(&out.LastUpdateTime) in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SealedSecretCondition. func (in *SealedSecretCondition) DeepCopy() *SealedSecretCondition { if in == nil { return nil } out := new(SealedSecretCondition) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in SealedSecretEncryptedData) DeepCopyInto(out *SealedSecretEncryptedData) { { in := &in *out = make(SealedSecretEncryptedData, len(*in)) for key, val := range *in { (*out)[key] = val } return } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SealedSecretEncryptedData. func (in SealedSecretEncryptedData) DeepCopy() SealedSecretEncryptedData { if in == nil { return nil } out := new(SealedSecretEncryptedData) in.DeepCopyInto(out) return *out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SealedSecretList) DeepCopyInto(out *SealedSecretList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]SealedSecret, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SealedSecretList. func (in *SealedSecretList) DeepCopy() *SealedSecretList { if in == nil { return nil } out := new(SealedSecretList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *SealedSecretList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SealedSecretSpec) DeepCopyInto(out *SealedSecretSpec) { *out = *in in.Template.DeepCopyInto(&out.Template) if in.Data != nil { in, out := &in.Data, &out.Data *out = make([]byte, len(*in)) copy(*out, *in) } if in.EncryptedData != nil { in, out := &in.EncryptedData, &out.EncryptedData *out = make(SealedSecretEncryptedData, len(*in)) for key, val := range *in { (*out)[key] = val } } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SealedSecretSpec. func (in *SealedSecretSpec) DeepCopy() *SealedSecretSpec { if in == nil { return nil } out := new(SealedSecretSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SealedSecretStatus) DeepCopyInto(out *SealedSecretStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]SealedSecretCondition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SealedSecretStatus. func (in *SealedSecretStatus) DeepCopy() *SealedSecretStatus { if in == nil { return nil } out := new(SealedSecretStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretTemplateSpec) DeepCopyInto(out *SecretTemplateSpec) { *out = *in in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) if in.Immutable != nil { in, out := &in.Immutable, &out.Immutable *out = new(bool) **out = **in } if in.Data != nil { in, out := &in.Data, &out.Data *out = make(map[string]string, len(*in)) for key, val := range *in { (*out)[key] = val } } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretTemplateSpec. func (in *SecretTemplateSpec) DeepCopy() *SecretTemplateSpec { if in == nil { return nil } out := new(SecretTemplateSpec) in.DeepCopyInto(out) return out } ================================================ FILE: pkg/buildinfo/version.go ================================================ package buildinfo import "runtime/debug" // DefaultVersion is the default version string if it's unset. const DefaultVersion = "UNKNOWN" // FallbackVersion initializes the automatic version detection. func FallbackVersion(v *string, unchanged string) { if *v != unchanged { return } b, ok := debug.ReadBuildInfo() if !ok { return } if modv := b.Main.Version; modv != "(devel)" { *v = modv } } ================================================ FILE: pkg/client/clientset/versioned/clientset.go ================================================ // Code generated by client-gen. DO NOT EDIT. package versioned import ( "fmt" "net/http" bitnamiv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/client/clientset/versioned/typed/sealedsecrets/v1alpha1" discovery "k8s.io/client-go/discovery" rest "k8s.io/client-go/rest" flowcontrol "k8s.io/client-go/util/flowcontrol" ) type Interface interface { Discovery() discovery.DiscoveryInterface BitnamiV1alpha1() bitnamiv1alpha1.BitnamiV1alpha1Interface } // Clientset contains the clients for groups. type Clientset struct { *discovery.DiscoveryClient bitnamiV1alpha1 *bitnamiv1alpha1.BitnamiV1alpha1Client } // BitnamiV1alpha1 retrieves the BitnamiV1alpha1Client func (c *Clientset) BitnamiV1alpha1() bitnamiv1alpha1.BitnamiV1alpha1Interface { return c.bitnamiV1alpha1 } // Discovery retrieves the DiscoveryClient func (c *Clientset) Discovery() discovery.DiscoveryInterface { if c == nil { return nil } return c.DiscoveryClient } // NewForConfig creates a new Clientset for the given config. // If config's RateLimiter is not set and QPS and Burst are acceptable, // NewForConfig will generate a rate-limiter in configShallowCopy. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). func NewForConfig(c *rest.Config) (*Clientset, error) { configShallowCopy := *c if configShallowCopy.UserAgent == "" { configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() } // share the transport between all clients httpClient, err := rest.HTTPClientFor(&configShallowCopy) if err != nil { return nil, err } return NewForConfigAndClient(&configShallowCopy, httpClient) } // NewForConfigAndClient creates a new Clientset for the given config and http client. // Note the http client provided takes precedence over the configured transport values. // If config's RateLimiter is not set and QPS and Burst are acceptable, // NewForConfigAndClient will generate a rate-limiter in configShallowCopy. func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) { configShallowCopy := *c if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { if configShallowCopy.Burst <= 0 { return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") } configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) } var cs Clientset var err error cs.bitnamiV1alpha1, err = bitnamiv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) if err != nil { return nil, err } cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient) if err != nil { return nil, err } return &cs, nil } // NewForConfigOrDie creates a new Clientset for the given config and // panics if there is an error in the config. func NewForConfigOrDie(c *rest.Config) *Clientset { cs, err := NewForConfig(c) if err != nil { panic(err) } return cs } // New creates a new Clientset for the given RESTClient. func New(c rest.Interface) *Clientset { var cs Clientset cs.bitnamiV1alpha1 = bitnamiv1alpha1.New(c) cs.DiscoveryClient = discovery.NewDiscoveryClient(c) return &cs } ================================================ FILE: pkg/client/clientset/versioned/fake/clientset_generated.go ================================================ // Code generated by client-gen. DO NOT EDIT. package fake import ( clientset "github.com/bitnami-labs/sealed-secrets/pkg/client/clientset/versioned" bitnamiv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/client/clientset/versioned/typed/sealedsecrets/v1alpha1" fakebitnamiv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/client/clientset/versioned/typed/sealedsecrets/v1alpha1/fake" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/discovery" fakediscovery "k8s.io/client-go/discovery/fake" "k8s.io/client-go/testing" ) // NewSimpleClientset returns a clientset that will respond with the provided objects. // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, // without applying any validations and/or defaults. It shouldn't be considered a replacement // for a real clientset and is mostly useful in simple unit tests. func NewSimpleClientset(objects ...runtime.Object) *Clientset { o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) for _, obj := range objects { if err := o.Add(obj); err != nil { panic(err) } } cs := &Clientset{tracker: o} cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} cs.AddReactor("*", "*", testing.ObjectReaction(o)) cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { gvr := action.GetResource() ns := action.GetNamespace() watch, err := o.Watch(gvr, ns) if err != nil { return false, nil, err } return true, watch, nil }) return cs } // Clientset implements clientset.Interface. Meant to be embedded into a // struct to get a default implementation. This makes faking out just the method // you want to test easier. type Clientset struct { testing.Fake discovery *fakediscovery.FakeDiscovery tracker testing.ObjectTracker } func (c *Clientset) Discovery() discovery.DiscoveryInterface { return c.discovery } func (c *Clientset) Tracker() testing.ObjectTracker { return c.tracker } var ( _ clientset.Interface = &Clientset{} _ testing.FakeClient = &Clientset{} ) // BitnamiV1alpha1 retrieves the BitnamiV1alpha1Client func (c *Clientset) BitnamiV1alpha1() bitnamiv1alpha1.BitnamiV1alpha1Interface { return &fakebitnamiv1alpha1.FakeBitnamiV1alpha1{Fake: &c.Fake} } ================================================ FILE: pkg/client/clientset/versioned/fake/doc.go ================================================ // Code generated by client-gen. DO NOT EDIT. // This package has the automatically generated fake clientset. package fake ================================================ FILE: pkg/client/clientset/versioned/fake/register.go ================================================ // Code generated by client-gen. DO NOT EDIT. package fake import ( bitnamiv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) var scheme = runtime.NewScheme() var codecs = serializer.NewCodecFactory(scheme) var localSchemeBuilder = runtime.SchemeBuilder{ bitnamiv1alpha1.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition // of clientsets, like in: // // import ( // "k8s.io/client-go/kubernetes" // clientsetscheme "k8s.io/client-go/kubernetes/scheme" // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" // ) // // kclientset, _ := kubernetes.NewForConfig(c) // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) // // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types // correctly. var AddToScheme = localSchemeBuilder.AddToScheme func init() { v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) utilruntime.Must(AddToScheme(scheme)) } ================================================ FILE: pkg/client/clientset/versioned/scheme/doc.go ================================================ // Code generated by client-gen. DO NOT EDIT. // This package contains the scheme of the automatically generated clientset. package scheme ================================================ FILE: pkg/client/clientset/versioned/scheme/register.go ================================================ // Code generated by client-gen. DO NOT EDIT. package scheme import ( bitnamiv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) var Scheme = runtime.NewScheme() var Codecs = serializer.NewCodecFactory(Scheme) var ParameterCodec = runtime.NewParameterCodec(Scheme) var localSchemeBuilder = runtime.SchemeBuilder{ bitnamiv1alpha1.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition // of clientsets, like in: // // import ( // "k8s.io/client-go/kubernetes" // clientsetscheme "k8s.io/client-go/kubernetes/scheme" // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" // ) // // kclientset, _ := kubernetes.NewForConfig(c) // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) // // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types // correctly. var AddToScheme = localSchemeBuilder.AddToScheme func init() { v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) utilruntime.Must(AddToScheme(Scheme)) } ================================================ FILE: pkg/client/clientset/versioned/typed/sealedsecrets/v1alpha1/doc.go ================================================ // Code generated by client-gen. DO NOT EDIT. // This package has the automatically generated typed clients. package v1alpha1 ================================================ FILE: pkg/client/clientset/versioned/typed/sealedsecrets/v1alpha1/fake/doc.go ================================================ // Code generated by client-gen. DO NOT EDIT. // Package fake has the automatically generated clients. package fake ================================================ FILE: pkg/client/clientset/versioned/typed/sealedsecrets/v1alpha1/fake/fake_sealedsecret.go ================================================ // Code generated by client-gen. DO NOT EDIT. package fake import ( "context" v1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" labels "k8s.io/apimachinery/pkg/labels" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" testing "k8s.io/client-go/testing" ) // FakeSealedSecrets implements SealedSecretInterface type FakeSealedSecrets struct { Fake *FakeBitnamiV1alpha1 ns string } var sealedsecretsResource = v1alpha1.SchemeGroupVersion.WithResource("sealedsecrets") var sealedsecretsKind = v1alpha1.SchemeGroupVersion.WithKind("SealedSecret") // Get takes name of the sealedSecret, and returns the corresponding sealedSecret object, and an error if there is any. func (c *FakeSealedSecrets) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.SealedSecret, err error) { obj, err := c.Fake. Invokes(testing.NewGetAction(sealedsecretsResource, c.ns, name), &v1alpha1.SealedSecret{}) if obj == nil { return nil, err } return obj.(*v1alpha1.SealedSecret), err } // List takes label and field selectors, and returns the list of SealedSecrets that match those selectors. func (c *FakeSealedSecrets) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.SealedSecretList, err error) { obj, err := c.Fake. Invokes(testing.NewListAction(sealedsecretsResource, sealedsecretsKind, c.ns, opts), &v1alpha1.SealedSecretList{}) if obj == nil { return nil, err } label, _, _ := testing.ExtractFromListOptions(opts) if label == nil { label = labels.Everything() } list := &v1alpha1.SealedSecretList{ListMeta: obj.(*v1alpha1.SealedSecretList).ListMeta} for _, item := range obj.(*v1alpha1.SealedSecretList).Items { if label.Matches(labels.Set(item.Labels)) { list.Items = append(list.Items, item) } } return list, err } // Watch returns a watch.Interface that watches the requested sealedSecrets. func (c *FakeSealedSecrets) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { return c.Fake. InvokesWatch(testing.NewWatchAction(sealedsecretsResource, c.ns, opts)) } // Create takes the representation of a sealedSecret and creates it. Returns the server's representation of the sealedSecret, and an error, if there is any. func (c *FakeSealedSecrets) Create(ctx context.Context, sealedSecret *v1alpha1.SealedSecret, opts v1.CreateOptions) (result *v1alpha1.SealedSecret, err error) { obj, err := c.Fake. Invokes(testing.NewCreateAction(sealedsecretsResource, c.ns, sealedSecret), &v1alpha1.SealedSecret{}) if obj == nil { return nil, err } return obj.(*v1alpha1.SealedSecret), err } // Update takes the representation of a sealedSecret and updates it. Returns the server's representation of the sealedSecret, and an error, if there is any. func (c *FakeSealedSecrets) Update(ctx context.Context, sealedSecret *v1alpha1.SealedSecret, opts v1.UpdateOptions) (result *v1alpha1.SealedSecret, err error) { obj, err := c.Fake. Invokes(testing.NewUpdateAction(sealedsecretsResource, c.ns, sealedSecret), &v1alpha1.SealedSecret{}) if obj == nil { return nil, err } return obj.(*v1alpha1.SealedSecret), err } // UpdateStatus was generated because the type contains a Status member. // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). func (c *FakeSealedSecrets) UpdateStatus(ctx context.Context, sealedSecret *v1alpha1.SealedSecret, opts v1.UpdateOptions) (*v1alpha1.SealedSecret, error) { obj, err := c.Fake. Invokes(testing.NewUpdateSubresourceAction(sealedsecretsResource, "status", c.ns, sealedSecret), &v1alpha1.SealedSecret{}) if obj == nil { return nil, err } return obj.(*v1alpha1.SealedSecret), err } // Delete takes name of the sealedSecret and deletes it. Returns an error if one occurs. func (c *FakeSealedSecrets) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { _, err := c.Fake. Invokes(testing.NewDeleteActionWithOptions(sealedsecretsResource, c.ns, name, opts), &v1alpha1.SealedSecret{}) return err } // DeleteCollection deletes a collection of objects. func (c *FakeSealedSecrets) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { action := testing.NewDeleteCollectionAction(sealedsecretsResource, c.ns, listOpts) _, err := c.Fake.Invokes(action, &v1alpha1.SealedSecretList{}) return err } // Patch applies the patch and returns the patched sealedSecret. func (c *FakeSealedSecrets) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.SealedSecret, err error) { obj, err := c.Fake. Invokes(testing.NewPatchSubresourceAction(sealedsecretsResource, c.ns, name, pt, data, subresources...), &v1alpha1.SealedSecret{}) if obj == nil { return nil, err } return obj.(*v1alpha1.SealedSecret), err } ================================================ FILE: pkg/client/clientset/versioned/typed/sealedsecrets/v1alpha1/fake/fake_sealedsecrets_client.go ================================================ // Code generated by client-gen. DO NOT EDIT. package fake import ( v1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/client/clientset/versioned/typed/sealedsecrets/v1alpha1" rest "k8s.io/client-go/rest" testing "k8s.io/client-go/testing" ) type FakeBitnamiV1alpha1 struct { *testing.Fake } func (c *FakeBitnamiV1alpha1) SealedSecrets(namespace string) v1alpha1.SealedSecretInterface { return &FakeSealedSecrets{c, namespace} } // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeBitnamiV1alpha1) RESTClient() rest.Interface { var ret *rest.RESTClient return ret } ================================================ FILE: pkg/client/clientset/versioned/typed/sealedsecrets/v1alpha1/generated_expansion.go ================================================ // Code generated by client-gen. DO NOT EDIT. package v1alpha1 type SealedSecretExpansion interface{} ================================================ FILE: pkg/client/clientset/versioned/typed/sealedsecrets/v1alpha1/sealedsecret.go ================================================ // Code generated by client-gen. DO NOT EDIT. package v1alpha1 import ( "context" "time" v1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" scheme "github.com/bitnami-labs/sealed-secrets/pkg/client/clientset/versioned/scheme" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" rest "k8s.io/client-go/rest" ) // SealedSecretsGetter has a method to return a SealedSecretInterface. // A group's client should implement this interface. type SealedSecretsGetter interface { SealedSecrets(namespace string) SealedSecretInterface } // SealedSecretInterface has methods to work with SealedSecret resources. type SealedSecretInterface interface { Create(ctx context.Context, sealedSecret *v1alpha1.SealedSecret, opts v1.CreateOptions) (*v1alpha1.SealedSecret, error) Update(ctx context.Context, sealedSecret *v1alpha1.SealedSecret, opts v1.UpdateOptions) (*v1alpha1.SealedSecret, error) UpdateStatus(ctx context.Context, sealedSecret *v1alpha1.SealedSecret, opts v1.UpdateOptions) (*v1alpha1.SealedSecret, error) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.SealedSecret, error) List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.SealedSecretList, error) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.SealedSecret, err error) SealedSecretExpansion } // sealedSecrets implements SealedSecretInterface type sealedSecrets struct { client rest.Interface ns string } // newSealedSecrets returns a SealedSecrets func newSealedSecrets(c *BitnamiV1alpha1Client, namespace string) *sealedSecrets { return &sealedSecrets{ client: c.RESTClient(), ns: namespace, } } // Get takes name of the sealedSecret, and returns the corresponding sealedSecret object, and an error if there is any. func (c *sealedSecrets) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.SealedSecret, err error) { result = &v1alpha1.SealedSecret{} err = c.client.Get(). Namespace(c.ns). Resource("sealedsecrets"). Name(name). VersionedParams(&options, scheme.ParameterCodec). Do(ctx). Into(result) return } // List takes label and field selectors, and returns the list of SealedSecrets that match those selectors. func (c *sealedSecrets) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.SealedSecretList, err error) { var timeout time.Duration if opts.TimeoutSeconds != nil { timeout = time.Duration(*opts.TimeoutSeconds) * time.Second } result = &v1alpha1.SealedSecretList{} err = c.client.Get(). Namespace(c.ns). Resource("sealedsecrets"). VersionedParams(&opts, scheme.ParameterCodec). Timeout(timeout). Do(ctx). Into(result) return } // Watch returns a watch.Interface that watches the requested sealedSecrets. func (c *sealedSecrets) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { var timeout time.Duration if opts.TimeoutSeconds != nil { timeout = time.Duration(*opts.TimeoutSeconds) * time.Second } opts.Watch = true return c.client.Get(). Namespace(c.ns). Resource("sealedsecrets"). VersionedParams(&opts, scheme.ParameterCodec). Timeout(timeout). Watch(ctx) } // Create takes the representation of a sealedSecret and creates it. Returns the server's representation of the sealedSecret, and an error, if there is any. func (c *sealedSecrets) Create(ctx context.Context, sealedSecret *v1alpha1.SealedSecret, opts v1.CreateOptions) (result *v1alpha1.SealedSecret, err error) { result = &v1alpha1.SealedSecret{} err = c.client.Post(). Namespace(c.ns). Resource("sealedsecrets"). VersionedParams(&opts, scheme.ParameterCodec). Body(sealedSecret). Do(ctx). Into(result) return } // Update takes the representation of a sealedSecret and updates it. Returns the server's representation of the sealedSecret, and an error, if there is any. func (c *sealedSecrets) Update(ctx context.Context, sealedSecret *v1alpha1.SealedSecret, opts v1.UpdateOptions) (result *v1alpha1.SealedSecret, err error) { result = &v1alpha1.SealedSecret{} err = c.client.Put(). Namespace(c.ns). Resource("sealedsecrets"). Name(sealedSecret.Name). VersionedParams(&opts, scheme.ParameterCodec). Body(sealedSecret). Do(ctx). Into(result) return } // UpdateStatus was generated because the type contains a Status member. // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). func (c *sealedSecrets) UpdateStatus(ctx context.Context, sealedSecret *v1alpha1.SealedSecret, opts v1.UpdateOptions) (result *v1alpha1.SealedSecret, err error) { result = &v1alpha1.SealedSecret{} err = c.client.Put(). Namespace(c.ns). Resource("sealedsecrets"). Name(sealedSecret.Name). SubResource("status"). VersionedParams(&opts, scheme.ParameterCodec). Body(sealedSecret). Do(ctx). Into(result) return } // Delete takes name of the sealedSecret and deletes it. Returns an error if one occurs. func (c *sealedSecrets) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { return c.client.Delete(). Namespace(c.ns). Resource("sealedsecrets"). Name(name). Body(&opts). Do(ctx). Error() } // DeleteCollection deletes a collection of objects. func (c *sealedSecrets) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { var timeout time.Duration if listOpts.TimeoutSeconds != nil { timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second } return c.client.Delete(). Namespace(c.ns). Resource("sealedsecrets"). VersionedParams(&listOpts, scheme.ParameterCodec). Timeout(timeout). Body(&opts). Do(ctx). Error() } // Patch applies the patch and returns the patched sealedSecret. func (c *sealedSecrets) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.SealedSecret, err error) { result = &v1alpha1.SealedSecret{} err = c.client.Patch(pt). Namespace(c.ns). Resource("sealedsecrets"). Name(name). SubResource(subresources...). VersionedParams(&opts, scheme.ParameterCodec). Body(data). Do(ctx). Into(result) return } ================================================ FILE: pkg/client/clientset/versioned/typed/sealedsecrets/v1alpha1/sealedsecrets_client.go ================================================ // Code generated by client-gen. DO NOT EDIT. package v1alpha1 import ( "net/http" v1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" "github.com/bitnami-labs/sealed-secrets/pkg/client/clientset/versioned/scheme" rest "k8s.io/client-go/rest" ) type BitnamiV1alpha1Interface interface { RESTClient() rest.Interface SealedSecretsGetter } // BitnamiV1alpha1Client is used to interact with features provided by the bitnami.com group. type BitnamiV1alpha1Client struct { restClient rest.Interface } func (c *BitnamiV1alpha1Client) SealedSecrets(namespace string) SealedSecretInterface { return newSealedSecrets(c, namespace) } // NewForConfig creates a new BitnamiV1alpha1Client for the given config. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). func NewForConfig(c *rest.Config) (*BitnamiV1alpha1Client, error) { config := *c if err := setConfigDefaults(&config); err != nil { return nil, err } httpClient, err := rest.HTTPClientFor(&config) if err != nil { return nil, err } return NewForConfigAndClient(&config, httpClient) } // NewForConfigAndClient creates a new BitnamiV1alpha1Client for the given config and http client. // Note the http client provided takes precedence over the configured transport values. func NewForConfigAndClient(c *rest.Config, h *http.Client) (*BitnamiV1alpha1Client, error) { config := *c if err := setConfigDefaults(&config); err != nil { return nil, err } client, err := rest.RESTClientForConfigAndClient(&config, h) if err != nil { return nil, err } return &BitnamiV1alpha1Client{client}, nil } // NewForConfigOrDie creates a new BitnamiV1alpha1Client for the given config and // panics if there is an error in the config. func NewForConfigOrDie(c *rest.Config) *BitnamiV1alpha1Client { client, err := NewForConfig(c) if err != nil { panic(err) } return client } // New creates a new BitnamiV1alpha1Client for the given RESTClient. func New(c rest.Interface) *BitnamiV1alpha1Client { return &BitnamiV1alpha1Client{c} } func setConfigDefaults(config *rest.Config) error { gv := v1alpha1.SchemeGroupVersion config.GroupVersion = &gv config.APIPath = "/apis" config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() if config.UserAgent == "" { config.UserAgent = rest.DefaultKubernetesUserAgent() } return nil } // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *BitnamiV1alpha1Client) RESTClient() rest.Interface { if c == nil { return nil } return c.restClient } ================================================ FILE: pkg/client/informers/externalversions/factory.go ================================================ // Code generated by informer-gen. DO NOT EDIT. package externalversions import ( reflect "reflect" sync "sync" time "time" versioned "github.com/bitnami-labs/sealed-secrets/pkg/client/clientset/versioned" internalinterfaces "github.com/bitnami-labs/sealed-secrets/pkg/client/informers/externalversions/internalinterfaces" sealedsecrets "github.com/bitnami-labs/sealed-secrets/pkg/client/informers/externalversions/sealedsecrets" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" ) // SharedInformerOption defines the functional option type for SharedInformerFactory. type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory type sharedInformerFactory struct { client versioned.Interface namespace string tweakListOptions internalinterfaces.TweakListOptionsFunc lock sync.Mutex defaultResync time.Duration customResync map[reflect.Type]time.Duration transform cache.TransformFunc informers map[reflect.Type]cache.SharedIndexInformer // startedInformers is used for tracking which informers have been started. // This allows Start() to be called multiple times safely. startedInformers map[reflect.Type]bool // wg tracks how many goroutines were started. wg sync.WaitGroup // shuttingDown is true when Shutdown has been called. It may still be running // because it needs to wait for goroutines. shuttingDown bool } // WithCustomResyncConfig sets a custom resync period for the specified informer types. func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { for k, v := range resyncConfig { factory.customResync[reflect.TypeOf(k)] = v } return factory } } // WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { factory.tweakListOptions = tweakListOptions return factory } } // WithNamespace limits the SharedInformerFactory to the specified namespace. func WithNamespace(namespace string) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { factory.namespace = namespace return factory } } // WithTransform sets a transform on all informers. func WithTransform(transform cache.TransformFunc) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { factory.transform = transform return factory } } // NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { return NewSharedInformerFactoryWithOptions(client, defaultResync) } // NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. // Listers obtained via this SharedInformerFactory will be subject to the same filters // as specified here. // Deprecated: Please use NewSharedInformerFactoryWithOptions instead func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) } // NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { factory := &sharedInformerFactory{ client: client, namespace: v1.NamespaceAll, defaultResync: defaultResync, informers: make(map[reflect.Type]cache.SharedIndexInformer), startedInformers: make(map[reflect.Type]bool), customResync: make(map[reflect.Type]time.Duration), } // Apply all options for _, opt := range options { factory = opt(factory) } return factory } func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { f.lock.Lock() defer f.lock.Unlock() if f.shuttingDown { return } for informerType, informer := range f.informers { if !f.startedInformers[informerType] { f.wg.Add(1) // We need a new variable in each loop iteration, // otherwise the goroutine would use the loop variable // and that keeps changing. informer := informer go func() { defer f.wg.Done() informer.Run(stopCh) }() f.startedInformers[informerType] = true } } } func (f *sharedInformerFactory) Shutdown() { f.lock.Lock() f.shuttingDown = true f.lock.Unlock() // Will return immediately if there is nothing to wait for. f.wg.Wait() } func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { informers := func() map[reflect.Type]cache.SharedIndexInformer { f.lock.Lock() defer f.lock.Unlock() informers := map[reflect.Type]cache.SharedIndexInformer{} for informerType, informer := range f.informers { if f.startedInformers[informerType] { informers[informerType] = informer } } return informers }() res := map[reflect.Type]bool{} for informType, informer := range informers { res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) } return res } // InformerFor returns the SharedIndexInformer for obj using an internal // client. func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { f.lock.Lock() defer f.lock.Unlock() informerType := reflect.TypeOf(obj) informer, exists := f.informers[informerType] if exists { return informer } resyncPeriod, exists := f.customResync[informerType] if !exists { resyncPeriod = f.defaultResync } informer = newFunc(f.client, resyncPeriod) informer.SetTransform(f.transform) f.informers[informerType] = informer return informer } // SharedInformerFactory provides shared informers for resources in all known // API group versions. // // It is typically used like this: // // ctx, cancel := context.Background() // defer cancel() // factory := NewSharedInformerFactory(client, resyncPeriod) // defer factory.WaitForStop() // Returns immediately if nothing was started. // genericInformer := factory.ForResource(resource) // typedInformer := factory.SomeAPIGroup().V1().SomeType() // factory.Start(ctx.Done()) // Start processing these informers. // synced := factory.WaitForCacheSync(ctx.Done()) // for v, ok := range synced { // if !ok { // fmt.Fprintf(os.Stderr, "caches failed to sync: %v", v) // return // } // } // // // Creating informers can also be created after Start, but then // // Start must be called again: // anotherGenericInformer := factory.ForResource(resource) // factory.Start(ctx.Done()) type SharedInformerFactory interface { internalinterfaces.SharedInformerFactory // Start initializes all requested informers. They are handled in goroutines // which run until the stop channel gets closed. Start(stopCh <-chan struct{}) // Shutdown marks a factory as shutting down. At that point no new // informers can be started anymore and Start will return without // doing anything. // // In addition, Shutdown blocks until all goroutines have terminated. For that // to happen, the close channel(s) that they were started with must be closed, // either before Shutdown gets called or while it is waiting. // // Shutdown may be called multiple times, even concurrently. All such calls will // block until all goroutines have terminated. Shutdown() // WaitForCacheSync blocks until all started informers' caches were synced // or the stop channel gets closed. WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool // ForResource gives generic access to a shared informer of the matching type. ForResource(resource schema.GroupVersionResource) (GenericInformer, error) // InformerFor returns the SharedIndexInformer for obj using an internal // client. InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer Bitnami() sealedsecrets.Interface } func (f *sharedInformerFactory) Bitnami() sealedsecrets.Interface { return sealedsecrets.New(f, f.namespace, f.tweakListOptions) } ================================================ FILE: pkg/client/informers/externalversions/generic.go ================================================ // Code generated by informer-gen. DO NOT EDIT. package externalversions import ( "fmt" v1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" ) // GenericInformer is type of SharedIndexInformer which will locate and delegate to other // sharedInformers based on type type GenericInformer interface { Informer() cache.SharedIndexInformer Lister() cache.GenericLister } type genericInformer struct { informer cache.SharedIndexInformer resource schema.GroupResource } // Informer returns the SharedIndexInformer. func (f *genericInformer) Informer() cache.SharedIndexInformer { return f.informer } // Lister returns the GenericLister. func (f *genericInformer) Lister() cache.GenericLister { return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) } // ForResource gives generic access to a shared informer of the matching type // TODO extend this to unknown resources with a client pool func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { switch resource { // Group=bitnami.com, Version=v1alpha1 case v1alpha1.SchemeGroupVersion.WithResource("sealedsecrets"): return &genericInformer{resource: resource.GroupResource(), informer: f.Bitnami().V1alpha1().SealedSecrets().Informer()}, nil } return nil, fmt.Errorf("no informer found for %v", resource) } ================================================ FILE: pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go ================================================ // Code generated by informer-gen. DO NOT EDIT. package internalinterfaces import ( time "time" versioned "github.com/bitnami-labs/sealed-secrets/pkg/client/clientset/versioned" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" cache "k8s.io/client-go/tools/cache" ) // NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer // SharedInformerFactory a small interface to allow for adding an informer without an import cycle type SharedInformerFactory interface { Start(stopCh <-chan struct{}) InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer } // TweakListOptionsFunc is a function that transforms a v1.ListOptions. type TweakListOptionsFunc func(*v1.ListOptions) ================================================ FILE: pkg/client/informers/externalversions/sealedsecrets/interface.go ================================================ // Code generated by informer-gen. DO NOT EDIT. package sealedsecrets import ( internalinterfaces "github.com/bitnami-labs/sealed-secrets/pkg/client/informers/externalversions/internalinterfaces" v1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/client/informers/externalversions/sealedsecrets/v1alpha1" ) // Interface provides access to each of this group's versions. type Interface interface { // V1alpha1 provides access to shared informers for resources in V1alpha1. V1alpha1() v1alpha1.Interface } type group struct { factory internalinterfaces.SharedInformerFactory namespace string tweakListOptions internalinterfaces.TweakListOptionsFunc } // New returns a new Interface. func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } // V1alpha1 returns a new v1alpha1.Interface. func (g *group) V1alpha1() v1alpha1.Interface { return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) } ================================================ FILE: pkg/client/informers/externalversions/sealedsecrets/v1alpha1/interface.go ================================================ // Code generated by informer-gen. DO NOT EDIT. package v1alpha1 import ( internalinterfaces "github.com/bitnami-labs/sealed-secrets/pkg/client/informers/externalversions/internalinterfaces" ) // Interface provides access to all the informers in this group version. type Interface interface { // SealedSecrets returns a SealedSecretInformer. SealedSecrets() SealedSecretInformer } type version struct { factory internalinterfaces.SharedInformerFactory namespace string tweakListOptions internalinterfaces.TweakListOptionsFunc } // New returns a new Interface. func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } // SealedSecrets returns a SealedSecretInformer. func (v *version) SealedSecrets() SealedSecretInformer { return &sealedSecretInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } ================================================ FILE: pkg/client/informers/externalversions/sealedsecrets/v1alpha1/sealedsecret.go ================================================ // Code generated by informer-gen. DO NOT EDIT. package v1alpha1 import ( "context" time "time" sealedsecretsv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" versioned "github.com/bitnami-labs/sealed-secrets/pkg/client/clientset/versioned" internalinterfaces "github.com/bitnami-labs/sealed-secrets/pkg/client/informers/externalversions/internalinterfaces" v1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/client/listers/sealedsecrets/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" watch "k8s.io/apimachinery/pkg/watch" cache "k8s.io/client-go/tools/cache" ) // SealedSecretInformer provides access to a shared informer and lister for // SealedSecrets. type SealedSecretInformer interface { Informer() cache.SharedIndexInformer Lister() v1alpha1.SealedSecretLister } type sealedSecretInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc namespace string } // NewSealedSecretInformer constructs a new informer for SealedSecret type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewSealedSecretInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { return NewFilteredSealedSecretInformer(client, namespace, resyncPeriod, indexers, nil) } // NewFilteredSealedSecretInformer constructs a new informer for SealedSecret type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFilteredSealedSecretInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.BitnamiV1alpha1().SealedSecrets(namespace).List(context.TODO(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.BitnamiV1alpha1().SealedSecrets(namespace).Watch(context.TODO(), options) }, }, &sealedsecretsv1alpha1.SealedSecret{}, resyncPeriod, indexers, ) } func (f *sealedSecretInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { return NewFilteredSealedSecretInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } func (f *sealedSecretInformer) Informer() cache.SharedIndexInformer { return f.factory.InformerFor(&sealedsecretsv1alpha1.SealedSecret{}, f.defaultInformer) } func (f *sealedSecretInformer) Lister() v1alpha1.SealedSecretLister { return v1alpha1.NewSealedSecretLister(f.Informer().GetIndexer()) } ================================================ FILE: pkg/client/listers/sealedsecrets/v1alpha1/expansion_generated.go ================================================ // Code generated by lister-gen. DO NOT EDIT. package v1alpha1 // SealedSecretListerExpansion allows custom methods to be added to // SealedSecretLister. type SealedSecretListerExpansion interface{} // SealedSecretNamespaceListerExpansion allows custom methods to be added to // SealedSecretNamespaceLister. type SealedSecretNamespaceListerExpansion interface{} ================================================ FILE: pkg/client/listers/sealedsecrets/v1alpha1/sealedsecret.go ================================================ // Code generated by lister-gen. DO NOT EDIT. package v1alpha1 import ( v1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/tools/cache" ) // SealedSecretLister helps list SealedSecrets. // All objects returned here must be treated as read-only. type SealedSecretLister interface { // List lists all SealedSecrets in the indexer. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*v1alpha1.SealedSecret, err error) // SealedSecrets returns an object that can list and get SealedSecrets. SealedSecrets(namespace string) SealedSecretNamespaceLister SealedSecretListerExpansion } // sealedSecretLister implements the SealedSecretLister interface. type sealedSecretLister struct { indexer cache.Indexer } // NewSealedSecretLister returns a new SealedSecretLister. func NewSealedSecretLister(indexer cache.Indexer) SealedSecretLister { return &sealedSecretLister{indexer: indexer} } // List lists all SealedSecrets in the indexer. func (s *sealedSecretLister) List(selector labels.Selector) (ret []*v1alpha1.SealedSecret, err error) { err = cache.ListAll(s.indexer, selector, func(m interface{}) { ret = append(ret, m.(*v1alpha1.SealedSecret)) }) return ret, err } // SealedSecrets returns an object that can list and get SealedSecrets. func (s *sealedSecretLister) SealedSecrets(namespace string) SealedSecretNamespaceLister { return sealedSecretNamespaceLister{indexer: s.indexer, namespace: namespace} } // SealedSecretNamespaceLister helps list and get SealedSecrets. // All objects returned here must be treated as read-only. type SealedSecretNamespaceLister interface { // List lists all SealedSecrets in the indexer for a given namespace. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*v1alpha1.SealedSecret, err error) // Get retrieves the SealedSecret from the indexer for a given namespace and name. // Objects returned here must be treated as read-only. Get(name string) (*v1alpha1.SealedSecret, error) SealedSecretNamespaceListerExpansion } // sealedSecretNamespaceLister implements the SealedSecretNamespaceLister // interface. type sealedSecretNamespaceLister struct { indexer cache.Indexer namespace string } // List lists all SealedSecrets in the indexer for a given namespace. func (s sealedSecretNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.SealedSecret, err error) { err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { ret = append(ret, m.(*v1alpha1.SealedSecret)) }) return ret, err } // Get retrieves the SealedSecret from the indexer for a given namespace and name. func (s sealedSecretNamespaceLister) Get(name string) (*v1alpha1.SealedSecret, error) { obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) if err != nil { return nil, err } if !exists { return nil, errors.NewNotFound(v1alpha1.Resource("sealedsecret"), name) } return obj.(*v1alpha1.SealedSecret), nil } ================================================ FILE: pkg/controller/controller.go ================================================ package controller import ( "context" "crypto/rsa" "encoding/json" "errors" "fmt" "log/slog" "reflect" "strings" "time" corev1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" v1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" "k8s.io/klog" "k8s.io/client-go/informers" ssv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" ssclientset "github.com/bitnami-labs/sealed-secrets/pkg/client/clientset/versioned" ssscheme "github.com/bitnami-labs/sealed-secrets/pkg/client/clientset/versioned/scheme" ssv1alpha1client "github.com/bitnami-labs/sealed-secrets/pkg/client/clientset/versioned/typed/sealedsecrets/v1alpha1" ssinformer "github.com/bitnami-labs/sealed-secrets/pkg/client/informers/externalversions" "github.com/bitnami-labs/sealed-secrets/pkg/multidocyaml" ) const ( // SuccessUnsealed is used as part of the Event 'reason' when // a SealedSecret is unsealed successfully. SuccessUnsealed = "Unsealed" // ErrUpdateFailed is used as part of the Event 'reason' when // a SealedSecret fails to update the target Secret for a // non-cryptography reason. Typically this is due to API I/O // or RBAC issues. ErrUpdateFailed = "ErrUpdateFailed" // ErrUnsealFailed is used as part of the Event 'reason' when a // SealedSecret fails the unsealing process. Typically this // is because it is encrypted with the wrong key or has been // renamed from its original namespace/name. ErrUnsealFailed = "ErrUnsealFailed" ) var ( // ErrCast happens when a K8s any type cannot be casted to the expected type. ErrCast = errors.New("cast error") maxRetries = 5 ) // Controller implements the main sealed-secrets-controller loop. type Controller struct { queue workqueue.TypedRateLimitingInterface[string] ssInformer cache.SharedIndexInformer sInformer cache.SharedIndexInformer kInformer cache.SharedIndexInformer sclient v1.SecretsGetter ssclient ssv1alpha1client.SealedSecretsGetter recorder record.EventRecorder keyRegistry *KeyRegistry oldGCBehavior bool // feature flag to revert to old behavior where we delete the secrets instead of relying on owners reference. updateStatus bool // feature flag that enables updating the status subresource. } // NewController returns the main sealed-secrets controller loop. func NewController( clientset kubernetes.Interface, ssclientset ssclientset.Interface, ssinformer ssinformer.SharedInformerFactory, sinformer informers.SharedInformerFactory, kinformer informers.SharedInformerFactory, keyRegistry *KeyRegistry, maxRetriesConfig int, keyOrderPriority string, ) (*Controller, error) { queue := workqueue.NewTypedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[string]()) utilruntime.Must(ssscheme.AddToScheme(scheme.Scheme)) eventBroadcaster := record.NewBroadcaster() eventBroadcaster.StartLogging(func(format string, args ...interface{}) { // Must use Sprintf to ensure slog doesn't interpret args... as key-value pairs slog.Info(fmt.Sprintf(format, args...)) }) eventBroadcaster.StartRecordingToSink(&v1.EventSinkImpl{Interface: clientset.CoreV1().Events("")}) recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "sealed-secrets"}) ssInformer, err := watchSealedSecrets(ssinformer, queue) if err != nil { return nil, err } var sInformer cache.SharedIndexInformer if sinformer != nil { sInformer, err = watchSecrets(sinformer, ssclientset, queue) if err != nil { return nil, err } } var kInformer cache.SharedIndexInformer if kinformer != nil { kInformer, err = watchKeySecrets(kinformer, keyRegistry, keyOrderPriority) if err != nil { return nil, err } } maxRetries = maxRetriesConfig return &Controller{ ssInformer: ssInformer, sInformer: sInformer, kInformer: kInformer, queue: queue, sclient: clientset.CoreV1(), ssclient: ssclientset.BitnamiV1alpha1(), recorder: recorder, keyRegistry: keyRegistry, }, nil } func watchKeySecrets(kinformer informers.SharedInformerFactory, registry *KeyRegistry, keyOrderPriority string) (cache.SharedIndexInformer, error) { kInformer := kinformer.Core().V1().Secrets().Informer() _, err := kInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { err := registryNewKeyWithSecret(obj.(*corev1.Secret), registry, keyOrderPriority) if err != nil { slog.Error("failed to register key", "error", err) return } }, }) if err != nil { return nil, fmt.Errorf("could not add event handler to secrets informer: %w", err) } return kInformer, nil } func watchSealedSecrets(ssinformer ssinformer.SharedInformerFactory, queue workqueue.TypedRateLimitingInterface[string]) (cache.SharedIndexInformer, error) { ssInformer := ssinformer.Bitnami().V1alpha1().SealedSecrets().Informer() _, err := ssInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { key, err := cache.MetaNamespaceKeyFunc(obj) if err == nil { queue.Add(key) } }, UpdateFunc: func(oldObj, newObj interface{}) { key, err := cache.MetaNamespaceKeyFunc(newObj) if err == nil { if sealedSecretChanged(oldObj, newObj) { queue.Add(key) } else { slog.Info("update suppressed, no changes in spec", "sealed-secret", key) } } }, DeleteFunc: func(obj interface{}) { key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) if err == nil { queue.Add(key) } if ssecret, ok := obj.(*ssv1alpha1.SealedSecret); ok { UnregisterCondition(ssecret) } }, }) if err != nil { return nil, fmt.Errorf("could not add event handler to sealed secrets informer: %w", err) } return ssInformer, nil } func sealedSecretChanged(oldObj, newObj interface{}) bool { oldSealedSecret, err := convertSealedSecret(oldObj) if err != nil { return true // any conversion error means we assume it might have changed } newSealedSecret, err := convertSealedSecret(newObj) if err != nil { return true } return !reflect.DeepEqual(oldSealedSecret.Spec, newSealedSecret.Spec) } func watchSecrets(sinformer informers.SharedInformerFactory, ssclientset ssclientset.Interface, queue workqueue.TypedRateLimitingInterface[string]) (cache.SharedIndexInformer, error) { sInformer := sinformer.Core().V1().Secrets().Informer() _, err := sInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ DeleteFunc: func(obj interface{}) { skey, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) if err != nil { slog.Error("failed to fetch Secret key", "error", err) return } ns, name, err := cache.SplitMetaNamespaceKey(skey) if err != nil { slog.Error("failed to get namespace and name from key", "secret", skey, "error", err) return } ssecret, err := ssclientset.BitnamiV1alpha1().SealedSecrets(ns).Get(context.Background(), name, metav1.GetOptions{}) if err != nil { if !k8serrors.IsNotFound(err) { slog.Error("failed to get SealedSecret", "secret", skey, "error", err) } return } if !metav1.IsControlledBy(obj.(*corev1.Secret), ssecret) && !isAnnotatedToBeManaged(obj.(*corev1.Secret)) { return } sskey, err := cache.MetaNamespaceKeyFunc(ssecret) if err != nil { slog.Error("failed to fetch SealedSecret key", "secret", skey, "error", err) return } queue.Add(sskey) }, }) if err != nil { return nil, fmt.Errorf("could not add event handler to secrets informer: %w", err) } return sInformer, nil } // HasSynced returns true once this controller has completed an // initial resource listing. func (c *Controller) HasSynced() bool { var synced bool if c.sInformer == nil { synced = c.ssInformer.HasSynced() } else { synced = c.ssInformer.HasSynced() && c.sInformer.HasSynced() } return synced } // LastSyncResourceVersion is the resource version observed when last // synced with the underlying store. The value returned is not // synchronized with access to the underlying store and is not // thread-safe. func (c *Controller) LastSyncResourceVersion() string { return c.ssInformer.LastSyncResourceVersion() } // Run begins processing items, and will continue until a value is // sent down stopCh. It's an error to call Run more than once. Run // blocks; call via go. func (c *Controller) Run(stopCh <-chan struct{}) { defer utilruntime.HandleCrash() defer c.queue.ShutDown() go c.ssInformer.Run(stopCh) if c.sInformer != nil { go c.sInformer.Run(stopCh) } if c.kInformer != nil { go c.kInformer.Run(stopCh) } if !cache.WaitForCacheSync(stopCh, c.HasSynced) { utilruntime.HandleError(fmt.Errorf("timed out waiting for caches to sync")) return } wait.Until(func() { c.runWorker(context.Background()) }, time.Second, stopCh) slog.Error("Shutting down controller") } func (c *Controller) runWorker(ctx context.Context) { for c.processNextItem(ctx) { // continue looping } } func (c *Controller) processNextItem(ctx context.Context) bool { key, quit := c.queue.Get() if quit { return false } defer c.queue.Done(key) err := c.unseal(ctx, key) if err == nil { // No error, reset the ratelimit counters c.queue.Forget(key) } else if isImmutableError(err) { // Do not retry updating immutable fields of an immutable secret slog.Error(formatImmutableError(key)) c.queue.Forget(key) utilruntime.HandleError(err) } else if c.queue.NumRequeues(key) < maxRetries { slog.Error("Error updating, will retry", "key", key, "error", err) c.queue.AddRateLimited(key) } else { // err != nil and too many retries slog.Error("Error updating, giving up", "key", key, "error", err) c.queue.Forget(key) utilruntime.HandleError(err) } return true } func (c *Controller) unseal(ctx context.Context, key string) (unsealErr error) { unsealRequestsTotal.Inc() obj, exists, err := c.ssInformer.GetIndexer().GetByKey(key) if err != nil { slog.Error("Error fetching object from store", "key", key, "error", err) unsealErrorsTotal.WithLabelValues("fetch", "").Inc() return err } if !exists { // the dependent secret will be GC: by k8s itself, see: // https://kubernetes.io/docs/concepts/workloads/controllers/garbage-collection/#owners-and-dependents // TODO: remove this feature flag in a subsequent release. if c.oldGCBehavior { slog.Info("SealedSecret has gone, deleting Secret", "sealed-secret", key) ns, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { return err } err = c.sclient.Secrets(ns).Delete(ctx, name, metav1.DeleteOptions{}) if err != nil && !k8serrors.IsNotFound(err) { return err } } return nil } ssecret, err := convertSealedSecret(obj) if err != nil { return err } slog.Info("Updating", "key", key) // any exit of this function at this point will cause an update to the status subresource // of the SealedSecret custom resource. The return value of the unseal function is available // to the deferred function body in the unsealErr named return value (even if explicit return // statements are used to return). defer func(ctx context.Context) { if err := c.updateSealedSecretStatus(ctx, ssecret, unsealErr); err != nil { // Non-fatal. Log and continue. slog.Error("Error updating SealedSecret status", "sealed-secret", key, "error", err) unsealErrorsTotal.WithLabelValues("status", ssecret.GetNamespace()).Inc() } else { ObserveCondition(ssecret) } }(ctx) newSecret, err := c.attemptUnseal(ssecret) if err != nil { c.recorder.Eventf(ssecret, corev1.EventTypeWarning, ErrUnsealFailed, "Failed to unseal: %v", err) unsealErrorsTotal.WithLabelValues("unseal", ssecret.GetNamespace()).Inc() return err } secret, err := c.sclient.Secrets(ssecret.GetObjectMeta().GetNamespace()).Get(ctx, newSecret.GetObjectMeta().GetName(), metav1.GetOptions{}) if k8serrors.IsNotFound(err) { secret, err = c.sclient.Secrets(ssecret.GetObjectMeta().GetNamespace()).Create(ctx, newSecret, metav1.CreateOptions{}) } if err != nil { c.recorder.Event(ssecret, corev1.EventTypeWarning, ErrUpdateFailed, err.Error()) unsealErrorsTotal.WithLabelValues("update", ssecret.GetNamespace()).Inc() return err } if !metav1.IsControlledBy(secret, ssecret) && !isAnnotatedToBeManaged(secret) && !isAnnotatedToBePatched(secret) { msg := fmt.Sprintf("Resource %q already exists and is not managed by SealedSecret", secret.Name) c.recorder.Event(ssecret, corev1.EventTypeWarning, ErrUpdateFailed, msg) unsealErrorsTotal.WithLabelValues("unmanaged", ssecret.GetNamespace()).Inc() return fmt.Errorf("failed update: %s", msg) } origSecret := secret secret = secret.DeepCopy() if isAnnotatedToBePatched(secret) { if secret.Data == nil { secret.Data = make(map[string][]byte) } for k, v := range newSecret.Data { secret.Data[k] = v } if secret.ObjectMeta.Labels == nil { secret.ObjectMeta.Labels = make(map[string]string) } for k, v := range newSecret.ObjectMeta.Labels { secret.ObjectMeta.Labels[k] = v } for k, v := range newSecret.ObjectMeta.Annotations { secret.ObjectMeta.Annotations[k] = v } if isAnnotatedToBeManaged(secret) { secret.ObjectMeta.OwnerReferences = newSecret.ObjectMeta.OwnerReferences } } else { secret.Data = newSecret.Data secret.ObjectMeta.Annotations = newSecret.ObjectMeta.Annotations secret.ObjectMeta.Labels = newSecret.ObjectMeta.Labels secret.ObjectMeta.OwnerReferences = newSecret.ObjectMeta.OwnerReferences } secret.Type = newSecret.Type if !apiequality.Semantic.DeepEqual(origSecret, secret) { _, err = c.sclient.Secrets(ssecret.GetObjectMeta().GetNamespace()).Update(ctx, secret, metav1.UpdateOptions{}) if err != nil { var message = err.Error() if isImmutableError(err) { message = formatImmutableError(key) } c.recorder.Event(ssecret, corev1.EventTypeWarning, ErrUpdateFailed, message) unsealErrorsTotal.WithLabelValues("update", ssecret.GetNamespace()).Inc() return err } } c.recorder.Event(ssecret, corev1.EventTypeNormal, SuccessUnsealed, "SealedSecret unsealed successfully") return nil } func convertSealedSecret(obj any) (*ssv1alpha1.SealedSecret, error) { sealedSecret, ok := (obj).(*ssv1alpha1.SealedSecret) if !ok { return nil, fmt.Errorf("%w: failed to cast %v into SealedSecret", ErrCast, obj) } if sealedSecret.APIVersion == "" || sealedSecret.Kind == "" { // https://github.com/operator-framework/operator-sdk/issues/727 gv := schema.GroupVersion{Group: ssv1alpha1.GroupName, Version: "v1alpha1"} gvk := gv.WithKind("SealedSecret") sealedSecret.APIVersion = gvk.GroupVersion().String() sealedSecret.Kind = gvk.Kind } return sealedSecret, nil } func (c *Controller) updateSealedSecretStatus(ctx context.Context, ssecret *ssv1alpha1.SealedSecret, unsealError error) error { if !c.updateStatus { klog.V(2).Infof("not updating status because updateStatus feature flag not turned on") return nil } if ssecret.Status == nil { ssecret.Status = &ssv1alpha1.SealedSecretStatus{} } updatedRequired := updateSealedSecretsStatusConditions(ssecret.Status, unsealError) if updatedRequired || (ssecret.Status.ObservedGeneration != ssecret.ObjectMeta.Generation) { ssecret.Status.ObservedGeneration = ssecret.ObjectMeta.Generation _, err := c.ssclient.SealedSecrets(ssecret.GetObjectMeta().GetNamespace()).UpdateStatus(ctx, ssecret, metav1.UpdateOptions{}) return err } return nil } func updateSealedSecretsStatusConditions(st *ssv1alpha1.SealedSecretStatus, unsealError error) bool { var updateRequired bool cond := func() *ssv1alpha1.SealedSecretCondition { for i := range st.Conditions { if st.Conditions[i].Type == ssv1alpha1.SealedSecretSynced { return &st.Conditions[i] } } st.Conditions = append(st.Conditions, ssv1alpha1.SealedSecretCondition{ Type: ssv1alpha1.SealedSecretSynced, }) return &st.Conditions[len(st.Conditions)-1] }() var status corev1.ConditionStatus if unsealError == nil { status = corev1.ConditionTrue cond.Message = "" } else { status = corev1.ConditionFalse cond.Message = unsealError.Error() } cond.LastUpdateTime = metav1.Now() // Status has changed, update the transition time and signal that an update is required if cond.Status != status { cond.LastTransitionTime = cond.LastUpdateTime cond.Status = status updateRequired = true } return updateRequired } func isAnnotatedToBeManaged(secret *corev1.Secret) bool { return secret.Annotations[ssv1alpha1.SealedSecretManagedAnnotation] == "true" } func isAnnotatedToBePatched(secret *corev1.Secret) bool { return secret.Annotations[ssv1alpha1.SealedSecretPatchAnnotation] == "true" } func isImmutableError(err error) bool { return strings.HasSuffix(err.Error(), "field is immutable when `immutable` is set") } func formatImmutableError(key string) string { return fmt.Sprintf("Error updating %s: the target Secret is immutable. Once a Secret is marked as immutable, it is not possible to revert this change nor to mutate the contents of the data field. You can only delete and recreate the Secret.", key) } // AttemptUnseal tries to unseal a secret. func (c *Controller) AttemptUnseal(content []byte) (bool, error) { if err := multidocyaml.EnsureNotMultiDoc(content); err != nil { return false, err } object, err := runtime.Decode(scheme.Codecs.UniversalDecoder(ssv1alpha1.SchemeGroupVersion), content) if err != nil { return false, err } switch s := object.(type) { case *ssv1alpha1.SealedSecret: if _, err := c.attemptUnseal(s); err != nil { return false, nil } return true, nil default: return false, fmt.Errorf("unexpected resource type: %s", s.GetObjectKind().GroupVersionKind().String()) } } // Rotate takes a sealed secret and returns a sealed secret that has been encrypted // with the latest private key. If the secret is already encrypted with the latest, // returns the input. func (c *Controller) Rotate(content []byte) ([]byte, error) { object, err := runtime.Decode(scheme.Codecs.UniversalDecoder(ssv1alpha1.SchemeGroupVersion), content) if err != nil { return nil, err } switch s := object.(type) { case *ssv1alpha1.SealedSecret: // Verify metainformation is well set up in Template ObjectMeta and ObjectMeta to avoid unconsistences with the scope during the rotate. // This is going to keep the original scope. if !reflect.DeepEqual(s.ObjectMeta, s.Spec.Template.ObjectMeta) { s.ObjectMeta.DeepCopyInto(&s.Spec.Template.ObjectMeta) slog.Warn("Sealed Secret metadata doesn't match. Please align your Sealed Secret metadata") } secret, err := c.attemptUnseal(s) if err != nil { return nil, fmt.Errorf("error decrypting secret. %v", err) } latestPrivKey := c.keyRegistry.latestPrivateKey() resealedSecret, err := ssv1alpha1.NewSealedSecret(scheme.Codecs, &latestPrivKey.PublicKey, secret) if err != nil { return nil, fmt.Errorf("error creating new sealed secret. %v", err) } data, err := json.Marshal(resealedSecret) if err != nil { return nil, fmt.Errorf("error marshalling new secret to json. %v", err) } return data, nil default: return nil, fmt.Errorf("unexpected resource type: %s", s.GetObjectKind().GroupVersionKind().String()) } } func (c *Controller) attemptUnseal(ss *ssv1alpha1.SealedSecret) (*corev1.Secret, error) { return attemptUnseal(ss, c.keyRegistry) } func attemptUnseal(ss *ssv1alpha1.SealedSecret, keyRegistry *KeyRegistry) (*corev1.Secret, error) { privateKeys := map[string]*rsa.PrivateKey{} for k, v := range keyRegistry.keys { privateKeys[k] = v.private } return ss.Unseal(scheme.Codecs, privateKeys) } ================================================ FILE: pkg/controller/controller_test.go ================================================ package controller import ( "context" "crypto/rand" "crypto/rsa" "errors" "fmt" "testing" "time" "encoding/json" ssv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/scheme" ssfake "github.com/bitnami-labs/sealed-secrets/pkg/client/clientset/versioned/fake" ) func TestIsAnnotatedToBePatched(t *testing.T) { tests := []struct { annotations map[string]string want bool }{ {annotations: map[string]string{ssv1alpha1.SealedSecretPatchAnnotation: "true"}, want: true}, {annotations: map[string]string{ssv1alpha1.SealedSecretPatchAnnotation: "TRUE"}, want: false}, {annotations: map[string]string{ssv1alpha1.SealedSecretPatchAnnotation: "false"}, want: false}, {annotations: map[string]string{ssv1alpha1.SealedSecretPatchAnnotation: ""}, want: false}, {annotations: map[string]string{"something": "else"}, want: false}, {annotations: map[string]string{}, want: false}, } for i, tc := range tests { s := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: "test-ns", Name: "test-secret", Annotations: tc.annotations, }, Data: map[string][]byte{ "foo": []byte("bar"), }, } got := isAnnotatedToBePatched(s) if got != tc.want { t.Fatalf("test %d: expected: %v, got: %v", i+1, tc.want, got) } } } func TestIsAnnotatedToBeManaged(t *testing.T) { tests := []struct { annotations map[string]string want bool }{ {annotations: map[string]string{ssv1alpha1.SealedSecretManagedAnnotation: "true"}, want: true}, {annotations: map[string]string{ssv1alpha1.SealedSecretManagedAnnotation: "TRUE"}, want: false}, {annotations: map[string]string{ssv1alpha1.SealedSecretManagedAnnotation: "false"}, want: false}, {annotations: map[string]string{ssv1alpha1.SealedSecretManagedAnnotation: ""}, want: false}, {annotations: map[string]string{"something": "else"}, want: false}, {annotations: map[string]string{}, want: false}, } for i, tc := range tests { s := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: "test-ns", Name: "test-secret", Annotations: tc.annotations, }, Data: map[string][]byte{ "foo": []byte("bar"), }, } got := isAnnotatedToBeManaged(s) if got != tc.want { t.Fatalf("test %d: expected: %v, got: %v", i+1, tc.want, got) } } } func TestConvert2SealedSecretBadType(t *testing.T) { obj := struct{}{} _, got := convertSealedSecret(obj) want := ErrCast if !errors.Is(got, want) { t.Fatalf("got %v want %v", got, want) } } func TestConvert2SealedSecretFills(t *testing.T) { sealedSecret := ssv1alpha1.SealedSecret{} result, err := convertSealedSecret(any(&sealedSecret)) if err != nil { t.Fatalf("unexpected failure converting to a sealed secret: %v", err) } got := fmt.Sprintf("%s %s", result.APIVersion, result.Kind) want := "bitnami.com/v1alpha1 SealedSecret" if got != want { t.Fatalf("got %q want %q", got, want) } } func TestConvert2SealedSecretPassThrough(t *testing.T) { sealedSecret := ssv1alpha1.SealedSecret{} sealedSecret.APIVersion = "bitnami.com/v1alpha1" sealedSecret.Kind = "SealedSecrets" want := &sealedSecret got, err := convertSealedSecret(any(want)) if err != nil { t.Fatalf("unexpected failure converting to a sealed secret: %v", err) } if got != want { t.Fatalf("got %v want %v", got, want) } } func TestDefaultConfigDoesNotSkipRecreate(t *testing.T) { ns := "some-namespace" keyNs := "some-key-namespace" var tweakopts func(*metav1.ListOptions) clientset := fake.NewClientset() ssc := ssfake.NewSimpleClientset() keyRegistry := testKeyRegister(t, context.Background(), clientset, ns) got, err := prepareController(clientset, ns, keyNs, tweakopts, &Flags{SkipRecreate: false}, ssc, keyRegistry) if err != nil { t.Fatalf("err %v want %v", got, nil) } if got == nil { t.Fatalf("ctrl %v want non nil", got) } if got.sInformer == nil { t.Fatalf("sInformer %v want non nil", got.sInformer) } } func TestSkipRecreateConfigDoesSkipIt(t *testing.T) { ns := "some-namespace" keyNs := "some-key-namespace" var tweakopts func(*metav1.ListOptions) clientset := fake.NewClientset() ssc := ssfake.NewSimpleClientset() keyRegistry := testKeyRegister(t, context.Background(), clientset, ns) got, err := prepareController(clientset, ns, keyNs, tweakopts, &Flags{SkipRecreate: true}, ssc, keyRegistry) if err != nil { t.Fatalf("err %v want %v", got, nil) } if got == nil { t.Fatalf("ctrl %v want non nil", got) } if got.sInformer != nil { t.Fatalf("sInformer %v want nil", got.sInformer) } } func TestEmptyStatusSendsUpdate(t *testing.T) { updateRequired := updateSealedSecretsStatusConditions(&ssv1alpha1.SealedSecretStatus{}, nil) if !updateRequired { t.Fatalf("expected status update, but no update was send") } } func TestStatusUpdateSendsUpdate(t *testing.T) { status := &ssv1alpha1.SealedSecretStatus{ Conditions: []ssv1alpha1.SealedSecretCondition{{ Status: "False", Type: ssv1alpha1.SealedSecretSynced, LastUpdateTime: metav1.Now(), }}, } updateRequired := updateSealedSecretsStatusConditions(status, nil) if !updateRequired { t.Fatalf("expected status update, but no update was send") } if status.Conditions[0].LastTransitionTime.IsZero() { t.Fatalf("expected LastTransitionTime is not empty") } if status.Conditions[0].LastUpdateTime.IsZero() { t.Fatalf("expected LastUpdateTime is not empty") } } func TestSameStatusNoUpdate(t *testing.T) { updateRequired := updateSealedSecretsStatusConditions(&ssv1alpha1.SealedSecretStatus{ Conditions: []ssv1alpha1.SealedSecretCondition{{ Type: ssv1alpha1.SealedSecretSynced, Status: "False", }}, }, errors.New("testerror")) if updateRequired { t.Fatalf("expected no status update, but update was send") } } func TestSyncedSecretWithErrorSendsUpdate(t *testing.T) { updateRequired := updateSealedSecretsStatusConditions(&ssv1alpha1.SealedSecretStatus{ Conditions: []ssv1alpha1.SealedSecretCondition{{ Type: ssv1alpha1.SealedSecretSynced, Status: "True", }}, }, errors.New("testerror")) if !updateRequired { t.Fatalf("expected status update, but no update was send") } } func testKeyRegister(t *testing.T, ctx context.Context, clientset kubernetes.Interface, ns string) *KeyRegistry { t.Helper() keyLabel := SealedSecretsKeyLabel prefix := "test-keys" testKeySize := 4096 keyRegistry, err := initKeyRegistry(ctx, clientset, rand.Reader, ns, prefix, keyLabel, testKeySize, "CertNotBefore") if err != nil { t.Fatalf("failed to provision key registry: %v", err) } return keyRegistry } func prettyEncoder(codecs runtimeserializer.CodecFactory, mediaType string, gv runtime.GroupVersioner) (runtime.Encoder, error) { info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), mediaType) if !ok { return nil, fmt.Errorf("binary can't serialize %s", mediaType) } prettyEncoder := info.PrettySerializer if prettyEncoder == nil { prettyEncoder = info.Serializer } enc := codecs.EncoderForVersion(prettyEncoder, gv) return enc, nil } func TestRotate(t *testing.T) { ns := "some-namespace" keyNs := "some-key-namespace" var tweakopts func(*metav1.ListOptions) clientset := fake.NewClientset() ssc := ssfake.NewSimpleClientset() keyRegistry := testKeyRegister(t, context.Background(), clientset, ns) // Add a key to the controller for second test validFor := time.Hour cn := "my-cn" _, err := keyRegistry.generateKey(context.Background(), validFor, cn, "", "") if err != nil { t.Fatal(err) } controller, err := prepareController(clientset, ns, keyNs, tweakopts, &Flags{SkipRecreate: false}, ssc, keyRegistry) if err != nil { t.Fatalf("err %v want %v", err, nil) } if controller == nil { t.Fatalf("ctrl %v want non nil", controller) } if controller.sInformer == nil { t.Fatalf("sInformer %v want non nil", controller.sInformer) } secret := &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "ss", Namespace: "default", }, Data: map[string][]byte{ // dGVtcG9yYWw= is base64 for "temporal" "password": []byte("temporal"), }, } cert, err := controller.keyRegistry.getCert() if err != nil { t.Fatalf("error getting certificate: %v", err) } ssecret, err := ssv1alpha1.NewSealedSecret(scheme.Codecs, cert.PublicKey.(*rsa.PublicKey), secret) if err != nil { t.Fatalf("error creating sealed secrets: %v", err) } prettyEnc, err := prettyEncoder(scheme.Codecs, runtime.ContentTypeYAML, ssv1alpha1.SchemeGroupVersion) if err != nil { t.Fatalf("unexpected pretty encoding: %v", err) } data, err := runtime.Encode(prettyEnc, ssecret) if err != nil { t.Fatalf("unexpected encoding the sealed secret: %v", err) } got, err := controller.Rotate(data) if err != nil { t.Fatalf("unexpected failure converting to a sealed secret: %v", err) } if string(got) == string(data) { t.Fatalf("got %v want %v", string(got), string(data)) } } func TestRotateKeepScope(t *testing.T) { ns := "some-namespace" keyNs := "some-key-namespace" var tweakopts func(*metav1.ListOptions) clientset := fake.NewClientset() ssc := ssfake.NewSimpleClientset() keyRegistry := testKeyRegister(t, context.Background(), clientset, ns) // Add a key to the controller for second test validFor := time.Hour cn := "my-cn" _, err := keyRegistry.generateKey(context.Background(), validFor, cn, "", "") if err != nil { t.Fatal(err) } controller, err := prepareController(clientset, ns, keyNs, tweakopts, &Flags{SkipRecreate: false}, ssc, keyRegistry) if err != nil { t.Fatalf("err %v want %v", err, nil) } if controller == nil { t.Fatalf("ctrl %v want non nil", controller) } if controller.sInformer == nil { t.Fatalf("sInformer %v want non nil", controller.sInformer) } secret := &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ Name: "ss", Namespace: "default", }, Data: map[string][]byte{ // dGVtcG9yYWw= is base64 for "temporal" "password": []byte("temporal"), }, } cert, err := controller.keyRegistry.getCert() if err != nil { t.Fatalf("error getting certificate: %v", err) } ssecret, err := ssv1alpha1.NewSealedSecret(scheme.Codecs, cert.PublicKey.(*rsa.PublicKey), secret) if err != nil { t.Fatalf("error creating sealed secrets: %v", err) } ssecret.Spec.Template.ObjectMeta.Annotations = map[string]string{ssv1alpha1.SealedSecretClusterWideAnnotation: "true"} prettyEnc, err := prettyEncoder(scheme.Codecs, runtime.ContentTypeJSON, ssv1alpha1.SchemeGroupVersion) if err != nil { t.Fatalf("unexpected pretty encoding: %v", err) } data, err := runtime.Encode(prettyEnc, ssecret) if err != nil { t.Fatalf("unexpected encoding the sealed secret: %v", err) } out, err := controller.Rotate(data) if err != nil { t.Fatalf("expected failure is not hit") } s := &ssv1alpha1.SealedSecret{} if err = json.Unmarshal(out, s); err != nil { t.Fatalf("error unmarshalling the rotate sealed secret") } if ssv1alpha1.SecretScope(s) != ssv1alpha1.SecretScope(ssecret) { t.Fatalf("Scope from the original and the rotate sealed secret do not match") } } ================================================ FILE: pkg/controller/funcs.go ================================================ package controller import ( "fmt" "strings" "time" ) // ScheduleJobWithTrigger creates a long-running loop that runs a job after an initialDelay // and then after each period duration. // It returns a trigger function that runs the job early when called. func ScheduleJobWithTrigger(initialDelay, period time.Duration, job func()) func() { trigger := make(chan struct{}) go func() { for { <-trigger job() } }() go func() { time.Sleep(initialDelay) for { trigger <- struct{}{} time.Sleep(period) } }() return func() { trigger <- struct{}{} } } const ( kubeChars = "abcdefghijklmnopqrstuvwxyz0123456789-" // Acceptable characters in k8s resource name maxNameLength = 245 // Max resource name length is 253, leave some room for a suffix ) func validateKeyPrefix(name string) (string, error) { if len(name) > maxNameLength { return "", fmt.Errorf("name is too long, must be shorter than %d, got %d", maxNameLength, len(name)) } for _, char := range name { if !strings.ContainsRune(kubeChars, char) { return "", fmt.Errorf("name contains illegal character %c", char) } } return name, nil } func removeDuplicates(strSlice []string) []string { allKeys := make(map[string]bool) list := []string{} for _, item := range strSlice { if _, value := allKeys[item]; !value { allKeys[item] = true list = append(list, item) } } return list } ================================================ FILE: pkg/controller/keyregistry.go ================================================ package controller import ( "context" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" "log/slog" "sync" "time" "github.com/bitnami-labs/sealed-secrets/pkg/crypto" "k8s.io/client-go/kubernetes" certUtil "k8s.io/client-go/util/cert" ) // A Key holds the cryptographic key pair and some metadata about it. type Key struct { private *rsa.PrivateKey cert *x509.Certificate fingerprint string orderingTime time.Time } // A KeyRegistry manages the key pairs used to (un)seal secrets. type KeyRegistry struct { sync.Mutex client kubernetes.Interface namespace string keyPrefix string keyLabel string keysize int keys map[string]*Key mostRecentKey *Key } // NewKeyRegistry creates a new KeyRegistry. func NewKeyRegistry(client kubernetes.Interface, namespace, keyPrefix, keyLabel string, keysize int) *KeyRegistry { return &KeyRegistry{ client: client, namespace: namespace, keyPrefix: keyPrefix, keysize: keysize, keyLabel: keyLabel, keys: map[string]*Key{}, } } func (kr *KeyRegistry) generateKey(ctx context.Context, validFor time.Duration, cn string, privateKeyAnnotations string, privateKeyLabels string) (string, error) { key, cert, err := generatePrivateKeyAndCert(kr.keysize, validFor, cn) if err != nil { return "", err } certs := []*x509.Certificate{cert} generatedName, err := writeKey(ctx, kr.client, key, certs, kr.namespace, kr.keyLabel, kr.keyPrefix, privateKeyAnnotations, privateKeyLabels) if err != nil { return "", err } // Only store key to local store if write to k8s worked if err := kr.registerNewKey(generatedName, key, cert, time.Now()); err != nil { return "", err } slog.Info("New key written", "namespace", kr.namespace, "name", generatedName) slog.Info("Certificate generated", "certificate", pem.EncodeToMemory(&pem.Block{Type: certUtil.CertificateBlockType, Bytes: cert.Raw})) return generatedName, nil } func (kr *KeyRegistry) registerNewKey(keyName string, privKey *rsa.PrivateKey, cert *x509.Certificate, orderingTime time.Time) error { fingerprint, err := crypto.PublicKeyFingerprint(&privKey.PublicKey) if err != nil { return err } k := &Key{ private: privKey, cert: cert, fingerprint: fingerprint, orderingTime: orderingTime, } kr.keys[k.fingerprint] = k if kr.mostRecentKey == nil || kr.mostRecentKey.orderingTime.Before(orderingTime) { kr.mostRecentKey = k } return nil } func (kr *KeyRegistry) latestPrivateKey() *rsa.PrivateKey { return kr.mostRecentKey.private } // getCert returns the current certificate. This method can be called by another goroutine. func (kr *KeyRegistry) getCert() (*x509.Certificate, error) { kr.Lock() defer kr.Unlock() if kr.mostRecentKey == nil { return nil, fmt.Errorf("key registry has no keys") } return kr.mostRecentKey.cert, nil } ================================================ FILE: pkg/controller/keyregistry_test.go ================================================ package controller import ( "testing" "time" ) func TestRegisterNewKey(t *testing.T) { const keySize = 2048 validFor := time.Hour cn := "my-cn" kr := NewKeyRegistry(nil, "namespace", "prefix", "label", keySize) if kr.mostRecentKey != nil { t.Fatal("this test assumes a new key registry has no keys") } key1, cert1, err := generatePrivateKeyAndCert(keySize, validFor, cn) if err != nil { t.Fatal(err) } t1 := time.Now() key2, cert2, err := generatePrivateKeyAndCert(keySize, validFor, cn) if err != nil { t.Fatal(err) } t2 := time.Now() if err := kr.registerNewKey("k2", key2, cert2, t2); err != nil { t.Fatal(err) } if got, want := kr.mostRecentKey.private, key2; got != want { t.Errorf("got: %v, want: %v", got, want) } // key1 is older, so it shouldn't replace key2 as the mostRecentKey if err := kr.registerNewKey("k1", key1, cert1, t1); err != nil { t.Fatal(err) } if got, want := kr.mostRecentKey.private, key2; got != want { t.Errorf("got: %v, want: %v", got, want) } } ================================================ FILE: pkg/controller/keys.go ================================================ package controller import ( "context" "crypto/rsa" "crypto/x509" "encoding/pem" "errors" "strings" "time" "github.com/bitnami-labs/sealed-secrets/pkg/crypto" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" certUtil "k8s.io/client-go/util/cert" "k8s.io/client-go/util/keyutil" ) // SealedSecretsKeyLabel is that label used to locate active key pairs used to decrypt sealed secrets. const SealedSecretsKeyLabel = "sealedsecrets.bitnami.com/sealed-secrets-key" var ( // ErrPrivateKeyNotRSA is returned when the private key is not a valid RSA key. ErrPrivateKeyNotRSA = errors.New("private key is not an RSA key") ) func generatePrivateKeyAndCert(keySize int, validFor time.Duration, cn string) (*rsa.PrivateKey, *x509.Certificate, error) { return crypto.GeneratePrivateKeyAndCert(keySize, validFor, cn) } func readKey(secret *v1.Secret) (*rsa.PrivateKey, []*x509.Certificate, error) { key, err := keyutil.ParsePrivateKeyPEM(secret.Data[v1.TLSPrivateKeyKey]) if err != nil { return nil, nil, err } switch rsaKey := key.(type) { case *rsa.PrivateKey: certs, err := certUtil.ParseCertsPEM(secret.Data[v1.TLSCertKey]) if err != nil { return nil, nil, err } return rsaKey, certs, nil default: return nil, nil, ErrPrivateKeyNotRSA } } type writeKeyOpt func(*writeKeyOpts) type writeKeyOpts struct{ creationTime metav1.Time } func writeKeyWithCreationTime(t metav1.Time) writeKeyOpt { return func(opts *writeKeyOpts) { opts.creationTime = t } } func writeKey(ctx context.Context, client kubernetes.Interface, key *rsa.PrivateKey, certs []*x509.Certificate, namespace, krLabel, prefix string, additionalAnnotations string, additionalLabels string, optSetters ...writeKeyOpt) (string, error) { var opts writeKeyOpts for _, o := range optSetters { o(&opts) } certbytes := []byte{} for _, cert := range certs { certbytes = append(certbytes, pem.EncodeToMemory(&pem.Block{Type: certUtil.CertificateBlockType, Bytes: cert.Raw})...) } labels := map[string]string{ krLabel: "active", } annotations := map[string]string{} if additionalLabels != "" { for _, label := range removeDuplicates(strings.Split(additionalLabels, ",")) { key := strings.Split(label, "=")[0] value := strings.Split(label, "=")[1] if key != krLabel { labels[key] = value } } } if additionalAnnotations != "" { for _, label := range removeDuplicates(strings.Split(additionalAnnotations, ",")) { key := strings.Split(label, "=")[0] value := strings.Split(label, "=")[1] annotations[key] = value } } secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, GenerateName: prefix, Labels: labels, Annotations: annotations, CreationTimestamp: opts.creationTime, }, Data: map[string][]byte{ v1.TLSPrivateKeyKey: pem.EncodeToMemory(&pem.Block{Type: keyutil.RSAPrivateKeyBlockType, Bytes: x509.MarshalPKCS1PrivateKey(key)}), v1.TLSCertKey: certbytes, }, Type: v1.SecretTypeTLS, } createdSecret, err := client.CoreV1().Secrets(namespace).Create(ctx, &secret, metav1.CreateOptions{}) if err != nil { return "", err } return createdSecret.Name, nil } ================================================ FILE: pkg/controller/keys_test.go ================================================ package controller import ( "context" "crypto/rsa" "crypto/x509" "encoding/pem" "io" mathrand "math/rand" "reflect" "strings" "testing" "time" "github.com/bitnami-labs/sealed-secrets/pkg/crypto" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/fake" ktesting "k8s.io/client-go/testing" certUtil "k8s.io/client-go/util/cert" "k8s.io/client-go/util/keyutil" ) // This is omg-not safe for real crypto use! func testRand() io.Reader { return mathrand.New(mathrand.NewSource(42)) } func signKey(r io.Reader, key *rsa.PrivateKey) (*x509.Certificate, error) { return crypto.SignKey(r, key, time.Hour, "testcn") } func signKeyWithNotBefore(r io.Reader, key *rsa.PrivateKey, notBefore time.Time) (*x509.Certificate, error) { return crypto.SignKeyWithNotBefore(r, key, notBefore, time.Hour, "testcn") } func TestReadKey(t *testing.T) { rand := testRand() key, err := rsa.GenerateKey(rand, 2048) if err != nil { t.Fatalf("Failed to generate test key: %v", err) } cert, err := signKey(rand, key) if err != nil { t.Fatalf("Failed to self-sign key: %v", err) } secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mykey", Namespace: "myns", }, Data: map[string][]byte{ v1.TLSPrivateKeyKey: pem.EncodeToMemory(&pem.Block{Type: keyutil.RSAPrivateKeyBlockType, Bytes: x509.MarshalPKCS1PrivateKey(key)}), v1.TLSCertKey: pem.EncodeToMemory(&pem.Block{Type: certUtil.CertificateBlockType, Bytes: cert.Raw}), }, Type: v1.SecretTypeTLS, } key2, cert2, err := readKey(&secret) if err != nil { t.Errorf("readKey() failed with: %v", err) } if !reflect.DeepEqual(key, key2) { t.Errorf("Extracted key != original key") } if !reflect.DeepEqual(cert, cert2[0]) { t.Errorf("Extracted cert != original cert") } } func TestWriteKey(t *testing.T) { ctx := context.Background() rand := testRand() key, err := rsa.GenerateKey(rand, 2048) if err != nil { t.Fatalf("Failed to generate test key: %v", err) } cert, err := signKey(rand, key) if err != nil { t.Fatalf("signKey failed: %v", err) } client := fake.NewClientset() namespace := "myns" defaultLabel := "default-label" myKey := "mykey" additionalAnnotations := "testAnnotation1=additional.annotation,test.annotation.2=test/2" additionalLabels := "testLabel1=additional.label,test.label.2=test/2" _, err = writeKey(ctx, client, key, []*x509.Certificate{cert}, namespace, defaultLabel, myKey, additionalAnnotations, additionalLabels) if err != nil { t.Errorf("writeKey() failed with: %v", err) } t.Logf("actions: %v", client.Actions()) if a := findAction(client, "create", "secrets"); a == nil { t.Errorf("writeKey didn't create a secret") } else if a.GetNamespace() != namespace { t.Errorf("writeKey() created key in wrong namespace!") } a := findAction(client, "create", "secrets").(ktesting.CreateActionImpl) secret, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(a.Object) generateName := secret["metadata"].(map[string]interface{})["generateName"].(string) if generateName != myKey { t.Errorf("writeKey didn't set the correct name") } labels := secret["metadata"].(map[string]interface{})["labels"] annotations := secret["metadata"].(map[string]interface{})["annotations"] if labels.(map[string]interface{})[defaultLabel] != "active" { t.Errorf("writeKey didn't set default label") } for _, label := range strings.Split(additionalLabels, ",") { labelKey := strings.Split(label, "=")[0] labelValue := strings.Split(label, "=")[1] if labels.(map[string]interface{})[labelKey] != labelValue { t.Errorf("writeKey didn't set label %v to value '%v'", labelKey, labelValue) } } for _, annotation := range strings.Split(additionalAnnotations, ",") { annotationKey := strings.Split(annotation, "=")[0] annotationValue := strings.Split(annotation, "=")[1] if annotations.(map[string]interface{})[annotationKey] != annotationValue { t.Errorf("writeKey didn't set annotation '%v' to value '%v'", annotationKey, annotationValue) } } } ================================================ FILE: pkg/controller/main.go ================================================ package controller import ( "context" "crypto/rand" "crypto/x509" "io" "log/slog" "os" "os/signal" "sort" "strings" "syscall" "time" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/informers" ssv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" "github.com/bitnami-labs/sealed-secrets/pkg/client/clientset/versioned" sealedsecrets "github.com/bitnami-labs/sealed-secrets/pkg/client/clientset/versioned" ssinformers "github.com/bitnami-labs/sealed-secrets/pkg/client/informers/externalversions" ) var ( // Selector used to find existing public/private key pairs on startup. keySelector = fields.OneTermEqualSelector(SealedSecretsKeyLabel, "active") ) // Flags to configure the controller. type Flags struct { KeyPrefix string KeySize int ValidFor time.Duration MyCN string KeyRenewPeriod time.Duration KeyOrderPriority string AcceptV1Data bool KeyCutoffTime string NamespaceAll bool AdditionalNamespaces string LabelSelector string RateLimitPerSecond int RateLimitBurst int OldGCBehavior bool UpdateStatus bool SkipRecreate bool LogInfoToStdout bool LogLevel string LogFormat string PrivateKeyAnnotations string PrivateKeyLabels string MaxRetries int WatchForSecrets bool KubeClientQPS float32 KubeClientBurst int } func initKeyPrefix(keyPrefix string) (string, error) { return validateKeyPrefix(keyPrefix) } func initKeyRegistry(ctx context.Context, client kubernetes.Interface, r io.Reader, namespace, prefix, label string, keysize int, keyOrderPriority string) (*KeyRegistry, error) { slog.Info("Searching for existing private keys") secretList, err := client.CoreV1().Secrets(namespace).List(ctx, metav1.ListOptions{ LabelSelector: keySelector.String(), }) if err != nil { return nil, err } items := secretList.Items s, err := client.CoreV1().Secrets(namespace).Get(ctx, prefix, metav1.GetOptions{}) if !errors.IsNotFound(err) { if err != nil { return nil, err } items = append(items, *s) // TODO(mkm): add the label to the legacy secret to simplify discovery and backups. } keyRegistry := NewKeyRegistry(client, namespace, prefix, label, keysize) sort.Sort(ssv1alpha1.ByCreationTimestamp(items)) for _, secret := range items { err = registryNewKeyWithSecret(&secret, keyRegistry, keyOrderPriority) if err != nil { return nil, err } } return keyRegistry, nil } func registryNewKeyWithSecret(secret *v1.Secret, keyRegistry *KeyRegistry, keyOrderPriority string) error { key, certs, err := readKey(secret) if err != nil { slog.Error("Error reading key", "secret", secret.Name, "error", err) } // Select ordering time based on the keyOrderPriority flag orderingTime := getKeyOrderPriority(keyOrderPriority, certs[0], secret) if err := keyRegistry.registerNewKey(secret.Name, key, certs[0], orderingTime); err != nil { return err } slog.Info("registered private key", "secretname", secret.Name) return nil } func getKeyOrderPriority(keyOrderPriority string, cert *x509.Certificate, secret *v1.Secret) time.Time { switch keyOrderPriority { case "CertNotBefore": return cert.NotBefore case "SecretCreationTimestamp": return secret.GetCreationTimestamp().Time default: slog.Error("Invalid keyOrderPriority. Use CertNotBefore or SecretCreationTimestamp", "keyOrderPriority", keyOrderPriority) } return cert.NotBefore } func myNamespace() string { if ns := os.Getenv("POD_NAMESPACE"); ns != "" { return ns } // Fall back to the namespace associated with the service account token, if available if data, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil { if ns := strings.TrimSpace(string(data)); len(ns) > 0 { return ns } } return metav1.NamespaceDefault } // Initialises the first key and starts the rotation job. returns an early trigger function. // A period of 0 deactivates automatic rotation, but manual rotation (e.g. triggered by SIGUSR1) // is still honoured. func initKeyRenewal(ctx context.Context, registry *KeyRegistry, period, validFor time.Duration, cutoffTime time.Time, cn string, privateKeyAnnotations string, privateKeyLabels string) (func(), error) { // Create a new key if it's the first key, // or if it's older than cutoff time. if len(registry.keys) == 0 || registry.mostRecentKey.orderingTime.Before(cutoffTime) { if _, err := registry.generateKey(ctx, validFor, cn, privateKeyAnnotations, privateKeyLabels); err != nil { return nil, err } } // wrapper function to log error thrown by generateKey function keyGenFunc := func() { if _, err := registry.generateKey(ctx, validFor, cn, privateKeyAnnotations, privateKeyLabels); err != nil { slog.Error("Failed to generate new key", "error", err) } } if period == 0 { return keyGenFunc, nil } // If key rotation is enabled, we'll rotate the key when the most recent // key becomes stale (older than period). mostRecentKeyAge := time.Since(registry.mostRecentKey.orderingTime) initialDelay := period - mostRecentKeyAge if initialDelay < 0 { initialDelay = 0 } return ScheduleJobWithTrigger(initialDelay, period, keyGenFunc), nil } func Main(f *Flags, version string) error { registerMetrics(version) config, err := rest.InClusterConfig() if err != nil { return err } config.QPS = f.KubeClientQPS config.Burst = f.KubeClientBurst clientset, err := kubernetes.NewForConfig(config) if err != nil { return err } ssclientset, err := sealedsecrets.NewForConfig(config) if err != nil { return err } myNs := myNamespace() ctx := context.Background() prefix, err := initKeyPrefix(f.KeyPrefix) if err != nil { return err } keyRegistry, err := initKeyRegistry(ctx, clientset, rand.Reader, myNs, prefix, SealedSecretsKeyLabel, f.KeySize, f.KeyOrderPriority) if err != nil { return err } var ct time.Time if f.KeyCutoffTime != "" { var err error ct, err = time.Parse(time.RFC1123Z, f.KeyCutoffTime) if err != nil { return err } } trigger, err := initKeyRenewal(ctx, keyRegistry, f.KeyRenewPeriod, f.ValidFor, ct, f.MyCN, f.PrivateKeyAnnotations, f.PrivateKeyLabels) if err != nil { return err } initKeyGenSignalListener(trigger) namespace := v1.NamespaceAll if !f.NamespaceAll || f.AdditionalNamespaces != "" { namespace = myNamespace() slog.Info("Starting informer", "namespace", namespace) } var tweakopts func(*metav1.ListOptions) = nil if f.LabelSelector != "" { tweakopts = func(options *metav1.ListOptions) { options.LabelSelector = f.LabelSelector } } controller, err := prepareController(clientset, namespace, myNs, tweakopts, f, ssclientset, keyRegistry) if err != nil { return err } controller.oldGCBehavior = f.OldGCBehavior controller.updateStatus = f.UpdateStatus stop := make(chan struct{}) defer close(stop) go controller.Run(stop) if f.AdditionalNamespaces != "" { addNS := removeDuplicates(strings.Split(f.AdditionalNamespaces, ",")) for _, ns := range addNS { if _, err := clientset.CoreV1().Namespaces().Get(ctx, ns, metav1.GetOptions{}); err != nil { if errors.IsNotFound(err) { slog.Error("namespace doesn't exist", "namespace", ns) continue } return err } if ns != namespace { ctlr, err := prepareController(clientset, ns, myNs, tweakopts, f, ssclientset, keyRegistry) if err != nil { return err } ctlr.oldGCBehavior = f.OldGCBehavior ctlr.updateStatus = f.UpdateStatus slog.Info("Starting informer", "namespace", ns) go ctlr.Run(stop) } } } cp := func() ([]*x509.Certificate, error) { cert, err := keyRegistry.getCert() if err != nil { return nil, err } return []*x509.Certificate{cert}, nil } server := httpserver(cp, controller.AttemptUnseal, controller.Rotate, f.RateLimitBurst, f.RateLimitPerSecond) serverMetrics := httpserverMetrics() sigterm := make(chan os.Signal, 1) signal.Notify(sigterm, syscall.SIGTERM) <-sigterm if err := server.Shutdown(context.Background()); err != nil { return err } if err := serverMetrics.Shutdown(context.Background()); err != nil { return err } return nil } func prepareController( clientset kubernetes.Interface, namespace string, keyNamespace string, tweakopts func(*metav1.ListOptions), f *Flags, ssclientset versioned.Interface, keyRegistry *KeyRegistry, ) (*Controller, error) { kinformer := initSecretInformerFactory(clientset, keyNamespace, func(options *metav1.ListOptions) { options.LabelSelector = keySelector.String() }, f.WatchForSecrets) sinformer := initSecretInformerFactory(clientset, namespace, tweakopts, !f.SkipRecreate) ssinformer := ssinformers.NewFilteredSharedInformerFactory(ssclientset, 0, namespace, tweakopts) controller, err := NewController(clientset, ssclientset, ssinformer, sinformer, kinformer, keyRegistry, f.MaxRetries, f.KeyOrderPriority) return controller, err } func initSecretInformerFactory(clientset kubernetes.Interface, ns string, tweakopts func(*metav1.ListOptions), enabled bool) informers.SharedInformerFactory { if !enabled { return nil } return informers.NewSharedInformerFactoryWithOptions(clientset, 0, informers.WithNamespace(ns), informers.WithTweakListOptions(tweakopts)) } ================================================ FILE: pkg/controller/main_test.go ================================================ package controller import ( "context" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" "testing" "time" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" krand "k8s.io/apimachinery/pkg/util/rand" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" ktesting "k8s.io/client-go/testing" certUtil "k8s.io/client-go/util/cert" "k8s.io/client-go/util/keyutil" ) func findAction(fake *fake.Clientset, verb, resource string) ktesting.Action { for _, a := range fake.Actions() { if a.Matches(verb, resource) { return a } } return nil } func hasAction(fake *fake.Clientset, verb, resource string) bool { return findAction(fake, verb, resource) != nil } // generateNameReactor implements the logic required for the GenerateName field to work when using // the fake client. Add it with client.PrependReactor to your fake client. func generateNameReactor(action ktesting.Action) (handled bool, ret runtime.Object, err error) { s := action.(ktesting.CreateAction).GetObject().(*v1.Secret) if s.Name == "" && s.GenerateName != "" { s.Name = fmt.Sprintf("%s-%s", s.GenerateName, krand.String(16)) } return false, nil, nil } func TestInitKeyRegistry(t *testing.T) { ctx := context.Background() rand := testRand() client := fake.NewClientset() client.PrependReactor("create", "secrets", generateNameReactor) registry, err := initKeyRegistry(ctx, client, rand, "namespace", "prefix", "label", 1024, "CertNotBefore") if err != nil { t.Fatalf("initKeyRegistry() returned err: %v", err) } // Add a key to the controller for second test validFor := time.Hour cn := "my-cn" _, err = registry.generateKey(ctx, validFor, cn, "", "") if err != nil { t.Fatal(err) } if !hasAction(client, "create", "secrets") { t.Fatalf("Error adding initial key to registry") } client.ClearActions() // Due to limitations of the fake client, we cannot test whether initKeyRegistry is able // to pick up existing keys _, err = initKeyRegistry(ctx, client, rand, "namespace", "prefix", "label", 1024, "CertNotBefore") if err != nil { t.Fatalf("initKeyRegistry() returned err: %v", err) } if !hasAction(client, "list", "secrets") { t.Errorf("initKeyRegistry() failed to read existing keys") } } func TestInitKeyRotation(t *testing.T) { ctx := context.Background() rand := testRand() client := fake.NewClientset() client.PrependReactor("create", "secrets", generateNameReactor) registry, err := initKeyRegistry(ctx, client, rand, "namespace", "prefix", "label", 1024, "CertNotBefore") if err != nil { t.Fatalf("initKeyRegistry() returned err: %v", err) } validFor := time.Hour cn := "my-cn" keyGenTrigger, err := initKeyRenewal(ctx, registry, 0, validFor, time.Time{}, cn, "", "") if err != nil { t.Fatalf("initKeyRenewal() returned err: %v", err) } if !hasAction(client, "create", "secrets") { t.Errorf("initKeyRenewal() failed to generate an initial key") } client.ClearActions() // Test the trigger function // Activates trigger and polls client every 50 ms up to 10s for the appropriate action keyGenTrigger() maxWait := 10 * time.Second endTime := time.Now().Add(maxWait) successful := false for time.Now().Before(endTime) { time.Sleep(50 * time.Millisecond) if hasAction(client, "create", "secrets") { successful = true break } } if !successful { t.Errorf("trigger function failed to activate early key generation") } } func TestInitKeyRotationTick(t *testing.T) { ctx := context.Background() rand := testRand() client := fake.NewClientset() client.PrependReactor("create", "secrets", generateNameReactor) registry, err := initKeyRegistry(ctx, client, rand, "namespace", "prefix", "label", 1024, "CertNotBefore") if err != nil { t.Fatalf("initKeyRegistry() returned err: %v", err) } validFor := time.Hour cn := "my-cn" _, err = initKeyRenewal(ctx, registry, 100*time.Millisecond, validFor, time.Time{}, cn, "", "") if err != nil { t.Fatalf("initKeyRenewal() returned err: %v", err) } if !hasAction(client, "create", "secrets") { t.Errorf("initKeyRenewal() failed to generate an initial key") } client.ClearActions() maxWait := 10 * time.Second endTime := time.Now().Add(maxWait) successful := false for time.Now().Before(endTime) { time.Sleep(50 * time.Millisecond) if hasAction(client, "create", "secrets") { successful = true break } } if !successful { t.Errorf("trigger function failed to activate early key generation") } } func TestReuseKey(t *testing.T) { ctx := context.Background() rand := testRand() key, err := rsa.GenerateKey(rand, 2048) if err != nil { t.Fatalf("Failed to generate test key: %v", err) } cert, err := signKey(rand, key) if err != nil { t.Fatalf("signKey failed: %v", err) } client := fake.NewClientset() client.PrependReactor("create", "secrets", generateNameReactor) _, err = writeKey(ctx, client, key, []*x509.Certificate{cert}, "namespace", SealedSecretsKeyLabel, "prefix", "", "") if err != nil { t.Errorf("writeKey() failed with: %v", err) } client.ClearActions() registry, err := initKeyRegistry(ctx, client, rand, "namespace", "prefix", SealedSecretsKeyLabel, 1024, "CertNotBefore") if err != nil { t.Fatalf("initKeyRegistry() returned err: %v", err) } validFor := time.Hour cn := "my-cn" _, err = initKeyRenewal(ctx, registry, 0, validFor, time.Time{}, cn, "", "") if err != nil { t.Fatalf("initKeyRenewal() returned err: %v", err) } if hasAction(client, "create", "secrets") { t.Errorf("initKeyRenewal() should not create a new secret when one already exist and rotation is deactivated") } } func TestRenewStaleKey(t *testing.T) { ctx := context.Background() rand := testRand() key, err := rsa.GenerateKey(rand, 2048) if err != nil { t.Fatalf("Failed to generate test key: %v", err) } // we'll simulate the existence of a secret that is about to expire // by making it old enough so that it's just "staleness" short of using // the full rotation "period". const ( period = 20 * time.Second staleness = 100 * time.Millisecond oldAge = period - staleness ) notBefore := time.Now().Add(-oldAge) cert, err := signKeyWithNotBefore(rand, key, notBefore) if err != nil { t.Fatalf("signKey failed: %v", err) } client := fake.NewClientset() client.PrependReactor("create", "secrets", generateNameReactor) _, err = writeKey(ctx, client, key, []*x509.Certificate{cert}, "namespace", SealedSecretsKeyLabel, "prefix", "", "") if err != nil { t.Errorf("writeKey() failed with: %v", err) } registry, err := initKeyRegistry(ctx, client, rand, "namespace", "prefix", SealedSecretsKeyLabel, 1024, "CertNotBefore") if err != nil { t.Fatalf("initKeyRegistry() returned err: %v", err) } validFor := time.Hour cn := "my-cn" _, err = initKeyRenewal(ctx, registry, period, validFor, time.Time{}, cn, "", "") if err != nil { t.Fatalf("initKeyRenewal() returned err: %v", err) } client.ClearActions() maxWait := 1 * time.Second endTime := time.Now().Add(maxWait) successful := false for time.Now().Before(endTime) { time.Sleep(50 * time.Millisecond) if hasAction(client, "create", "secrets") { successful = true break } } if !successful { t.Errorf("trigger function failed to activate early key generation") } } func TestKeyCutoff(t *testing.T) { ctx := context.Background() rand := testRand() key, err := rsa.GenerateKey(rand, 2048) if err != nil { t.Fatalf("Failed to generate test key: %v", err) } cert, err := signKey(rand, key) if err != nil { t.Fatalf("signKey failed: %v", err) } // we'll simulate the existence of a secret that would be still valid // according to our rotation period, if it were not for it being older than the cutoff date. const ( period = 24 * time.Hour oldAge = 1 * time.Hour ) client := fake.NewClientset() client.PrependReactor("create", "secrets", generateNameReactor) _, err = writeKey(ctx, client, key, []*x509.Certificate{cert}, "namespace", SealedSecretsKeyLabel, "prefix", "", "", writeKeyWithCreationTime(metav1.NewTime(time.Now().Add(-oldAge)))) if err != nil { t.Errorf("writeKey() failed with: %v", err) } registry, err := initKeyRegistry(ctx, client, rand, "namespace", "prefix", SealedSecretsKeyLabel, 1024, "CertNotBefore") if err != nil { t.Fatalf("initKeyRegistry() returned err: %v", err) } client.ClearActions() // by setting cutoff to "now" we effectively force the creation of a new key. validFor := time.Hour cn := "my-cn" _, err = initKeyRenewal(ctx, registry, period, validFor, time.Now(), cn, "", "") if err != nil { t.Fatalf("initKeyRenewal() returned err: %v", err) } if !hasAction(client, "create", "secrets") { t.Errorf("trigger function failed to activate early key generation") } } func writeLegacyKey(ctx context.Context, client kubernetes.Interface, key *rsa.PrivateKey, certs []*x509.Certificate, namespace, name string) (string, error) { certbytes := []byte{} for _, cert := range certs { certbytes = append(certbytes, pem.EncodeToMemory(&pem.Block{Type: certUtil.CertificateBlockType, Bytes: cert.Raw})...) } secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: name, }, Data: map[string][]byte{ v1.TLSPrivateKeyKey: pem.EncodeToMemory(&pem.Block{Type: keyutil.RSAPrivateKeyBlockType, Bytes: x509.MarshalPKCS1PrivateKey(key)}), v1.TLSCertKey: certbytes, }, Type: v1.SecretTypeTLS, } createdSecret, err := client.CoreV1().Secrets(namespace).Create(ctx, &secret, metav1.CreateOptions{}) if err != nil { return "", err } return createdSecret.Name, nil } func TestLegacySecret(t *testing.T) { ctx := context.Background() rand := testRand() key, err := rsa.GenerateKey(rand, 2048) if err != nil { t.Fatalf("Failed to generate test key: %v", err) } cert, err := signKey(rand, key) if err != nil { t.Fatalf("signKey failed: %v", err) } client := fake.NewClientset() client.PrependReactor("create", "secrets", generateNameReactor) _, err = writeLegacyKey(ctx, client, key, []*x509.Certificate{cert}, "namespace", "prefix") if err != nil { t.Errorf("writeKey() failed with: %v", err) } client.ClearActions() registry, err := initKeyRegistry(ctx, client, rand, "namespace", "prefix", SealedSecretsKeyLabel, 1024, "CertNotBefore") if err != nil { t.Fatalf("initKeyRegistry() returned err: %v", err) } validFor := time.Hour cn := "my-cn" _, err = initKeyRenewal(ctx, registry, 0, validFor, time.Time{}, cn, "", "") if err != nil { t.Fatalf("initKeyRenewal() returned err: %v", err) } if hasAction(client, "create", "secrets") { t.Errorf("initKeyRenewal() should not create a new secret when one already exist and rotation is deactivated") } } ================================================ FILE: pkg/controller/metrics.go ================================================ package controller import ( "net/http" "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" v1 "k8s.io/api/core/v1" ) // Define Prometheus Exporter namespace (prefix) for all metric names. const metricNamespace string = "sealed_secrets_controller" const ( labelNamespace = "namespace" labelName = "name" labelCondition = "condition" labelInstance = "ss_app_kubernetes_io_instance" ) var conditionStatusToGaugeValue = map[v1.ConditionStatus]float64{ v1.ConditionFalse: -1, v1.ConditionUnknown: 0, v1.ConditionTrue: 1, } // Define Prometheus metrics to expose. var ( buildInfo prometheus.Gauge // TODO: rename metric, change increment logic, or accept behaviour // when a SealedSecret is deleted the unseal() function is called which is // not technically an 'unseal request'. unsealRequestsTotal = prometheus.NewCounter( prometheus.CounterOpts{ Namespace: metricNamespace, Name: "unseal_requests_total", Help: "Total number of sealed secret unseal requests", }, ) unsealErrorsTotal = prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: metricNamespace, Name: "unseal_errors_total", Help: "Total number of sealed secret unseal errors by reason", }, []string{"reason", "namespace"}, ) conditionInfo = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: metricNamespace, Name: "condition_info", Help: "Current SealedSecret condition status. Values are -1 (false), 0 (unknown or absent), 1 (true)", }, []string{labelNamespace, labelName, labelCondition, labelInstance}, ) httpRequestsTotal = prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: metricNamespace, Name: "http_requests_total", Help: "A counter for requests to the wrapped handler.", }, []string{"path", "code", "method"}, ) httpRequestDurationSeconds = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Namespace: metricNamespace, Name: "http_request_duration_seconds", Help: "A histogram of latencies for requests.", Buckets: prometheus.DefBuckets, }, []string{"path", "method"}, ) ) func registerMetrics(version string) { buildInfo = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: metricNamespace, Name: "build_info", Help: "Build information.", ConstLabels: prometheus.Labels{"revision": version}, }, ) // Register metrics with Prometheus prometheus.MustRegister(buildInfo) prometheus.MustRegister(collectors.NewBuildInfoCollector()) prometheus.MustRegister(unsealRequestsTotal) prometheus.MustRegister(unsealErrorsTotal) prometheus.MustRegister(conditionInfo) prometheus.MustRegister(httpRequestsTotal) prometheus.MustRegister(httpRequestDurationSeconds) } // ObserveCondition sets a `condition_info` Gauge according to a SealedSecret status. func ObserveCondition(ssecret *v1alpha1.SealedSecret) { if ssecret.Status == nil { return } for _, condition := range ssecret.Status.Conditions { conditionInfo.With(prometheus.Labels{ labelNamespace: ssecret.Namespace, labelName: ssecret.Name, labelCondition: string(condition.Type), labelInstance: ssecret.Labels["app.kubernetes.io/instance"], }).Set(conditionStatusToGaugeValue[condition.Status]) } } // UnregisterCondition unregisters Gauges associated to a SealedSecret conditions. func UnregisterCondition(ssecret *v1alpha1.SealedSecret) { if ssecret.Status == nil { return } for _, condition := range ssecret.Status.Conditions { conditionInfo.DeleteLabelValues(ssecret.Namespace, ssecret.Name, string(condition.Type), ssecret.Labels["app.kubernetes.io/instance"]) } } // Instrument HTTP handler. func Instrument(path string, h http.Handler) http.Handler { return promhttp.InstrumentHandlerDuration(httpRequestDurationSeconds.MustCurryWith(prometheus.Labels{"path": path}), promhttp.InstrumentHandlerCounter(httpRequestsTotal.MustCurryWith(prometheus.Labels{"path": path}), h)) } ================================================ FILE: pkg/controller/metrics_test.go ================================================ package controller import ( "testing" ssv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // setupTestMetrics creates a fresh metrics setup for testing func setupTestMetrics() *prometheus.Registry { registry := prometheus.NewRegistry() // Create a new conditionInfo metric for testing testConditionInfo := prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: metricNamespace, Name: "condition_info", Help: "Current SealedSecret condition status. Values are -1 (false), 0 (unknown or absent), 1 (true)", }, []string{labelNamespace, labelName, labelCondition, labelInstance}, ) registry.MustRegister(testConditionInfo) // Replace the global conditionInfo for testing conditionInfo = testConditionInfo return registry } func TestObserveCondition(t *testing.T) { registry := setupTestMetrics() ssecret := &ssv1alpha1.SealedSecret{ ObjectMeta: metav1.ObjectMeta{ Namespace: "test-ns", Name: "test-secret", Labels: map[string]string{ "app.kubernetes.io/instance": "test-instance", }, }, Status: &ssv1alpha1.SealedSecretStatus{ Conditions: []ssv1alpha1.SealedSecretCondition{ { Type: ssv1alpha1.SealedSecretSynced, Status: corev1.ConditionTrue, }, }, }, } ObserveCondition(ssecret) // Verify metric was created metricFamilies, err := registry.Gather() if err != nil { t.Fatalf("Failed to gather metrics: %v", err) } found := false for _, mf := range metricFamilies { if mf.GetName() == "sealed_secrets_controller_condition_info" { for _, metric := range mf.GetMetric() { labels := metric.GetLabel() if getLabel(labels, "namespace") == "test-ns" && getLabel(labels, "name") == "test-secret" && getLabel(labels, "condition") == "Synced" && getLabel(labels, "ss_app_kubernetes_io_instance") == "test-instance" { found = true if metric.GetGauge().GetValue() != 1.0 { t.Errorf("Expected metric value 1.0, got %f", metric.GetGauge().GetValue()) } } } } } if !found { t.Error("Expected metric not found") } } func TestUnregisterCondition(t *testing.T) { registry := setupTestMetrics() ssecret := &ssv1alpha1.SealedSecret{ ObjectMeta: metav1.ObjectMeta{ Namespace: "test-ns", Name: "test-secret", Labels: map[string]string{ "app.kubernetes.io/instance": "test-instance", }, }, Status: &ssv1alpha1.SealedSecretStatus{ Conditions: []ssv1alpha1.SealedSecretCondition{ { Type: ssv1alpha1.SealedSecretSynced, Status: corev1.ConditionTrue, }, }, }, } // First observe the condition to create the metric ObserveCondition(ssecret) // Verify metric exists metricFamilies, err := registry.Gather() if err != nil { t.Fatalf("Failed to gather metrics: %v", err) } metricExists := func() bool { for _, mf := range metricFamilies { if mf.GetName() == "sealed_secrets_controller_condition_info" { for _, metric := range mf.GetMetric() { labels := metric.GetLabel() if getLabel(labels, "namespace") == "test-ns" && getLabel(labels, "name") == "test-secret" && getLabel(labels, "condition") == "Synced" && getLabel(labels, "ss_app_kubernetes_io_instance") == "test-instance" { return true } } } } return false } if !metricExists() { t.Fatal("Metric should exist before unregistering") } // Now unregister the condition UnregisterCondition(ssecret) // Verify metric was removed metricFamilies, err = registry.Gather() if err != nil { t.Fatalf("Failed to gather metrics: %v", err) } if metricExists() { t.Error("Metric should have been removed after unregistering") } } func TestUnregisterConditionWithNilStatus(t *testing.T) { ssecret := &ssv1alpha1.SealedSecret{ ObjectMeta: metav1.ObjectMeta{ Namespace: "test-ns", Name: "test-secret", }, Status: nil, } // Should not panic UnregisterCondition(ssecret) } func TestObserveConditionWithNilStatus(t *testing.T) { ssecret := &ssv1alpha1.SealedSecret{ ObjectMeta: metav1.ObjectMeta{ Namespace: "test-ns", Name: "test-secret", }, Status: nil, } // Should not panic ObserveCondition(ssecret) } func TestUnregisterConditionWithMissingLabel(t *testing.T) { registry := setupTestMetrics() ssecret := &ssv1alpha1.SealedSecret{ ObjectMeta: metav1.ObjectMeta{ Namespace: "test-ns", Name: "test-secret", // Missing app.kubernetes.io/instance label }, Status: &ssv1alpha1.SealedSecretStatus{ Conditions: []ssv1alpha1.SealedSecretCondition{ { Type: ssv1alpha1.SealedSecretSynced, Status: corev1.ConditionTrue, }, }, }, } // First observe the condition to create the metric (with empty instance label) ObserveCondition(ssecret) // Now unregister the condition - should work with empty instance label UnregisterCondition(ssecret) // Verify metric was removed metricFamilies, err := registry.Gather() if err != nil { t.Fatalf("Failed to gather metrics: %v", err) } for _, mf := range metricFamilies { if mf.GetName() == "sealed_secrets_controller_condition_info" { for _, metric := range mf.GetMetric() { labels := metric.GetLabel() if getLabel(labels, "namespace") == "test-ns" && getLabel(labels, "name") == "test-secret" && getLabel(labels, "condition") == "Synced" && getLabel(labels, "ss_app_kubernetes_io_instance") == "" { t.Error("Metric should have been removed after unregistering") } } } } } // Helper function to get label value from metric labels func getLabel(labels []*dto.LabelPair, name string) string { for _, label := range labels { if label.GetName() == name { return label.GetValue() } } return "" } ================================================ FILE: pkg/controller/server.go ================================================ package controller import ( "crypto/x509" "encoding/pem" "io" "log" "log/slog" "net/http" "time" "github.com/prometheus/client_golang/prometheus/promhttp" flag "github.com/spf13/pflag" "github.com/throttled/throttled" "github.com/throttled/throttled/store/memstore" certUtil "k8s.io/client-go/util/cert" ) var ( listenAddr = flag.String("listen-addr", ":8080", "HTTP serving address.") listenMetricsAddr = flag.String("listen-metrics-addr", ":8081", "HTTP metrics serving address.") readTimeout = flag.Duration("read-timeout", 2*time.Minute, "HTTP request timeout.") writeTimeout = flag.Duration("write-timeout", 2*time.Minute, "HTTP response timeout.") ) // Called on every request to /cert. Errors will be logged and return a 500. type certProvider func() ([]*x509.Certificate, error) type secretChecker func([]byte) (bool, error) type secretRotator func([]byte) ([]byte, error) // httpserver starts an HTTP that exposes core functionality like serving the public key // or secret rotation and validation. This endpoint is designed to be accessible by // all users of a given cluster. It must not leak any secret material. // The server is started in the background and a handle to it returned so it can be shut down. func httpserver(cp certProvider, sc secretChecker, sr secretRotator, burst int, rate int) *http.Server { httpRateLimiter := rateLimiter(burst, rate) mux := http.NewServeMux() mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, err := io.WriteString(w, "ok\n") if err != nil { log.Fatal(err) } }) mux.Handle("/v1/verify", Instrument("/v1/verify", httpRateLimiter.RateLimit(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { content, err := io.ReadAll(r.Body) if err != nil { slog.Error("Error handling /v1/verify request", "error", err) w.WriteHeader(http.StatusBadRequest) return } valid, err := sc(content) if err != nil { slog.Error("Error validating secret", "error", err) w.WriteHeader(http.StatusInternalServerError) return } if valid { w.WriteHeader(http.StatusOK) } else { w.WriteHeader(http.StatusConflict) } })))) // TODO(mkm): rename to re-encrypt mux.Handle("/v1/rotate", Instrument("/v1/rotate", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { content, err := io.ReadAll(r.Body) if err != nil { slog.Error("Error handling /v1/rotate request", "error", err) w.WriteHeader(http.StatusBadRequest) return } newSecret, err := sr(content) if err != nil { slog.Error("Error rotating secret", "error", err) w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") _, _ = w.Write(newSecret) }))) mux.Handle("/v1/cert.pem", Instrument("/v1/cert.pem", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { certs, err := cp() if err != nil { slog.Error("cannot get certificates", "error", err) http.Error(w, "cannot get certificate", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/x-pem-file") for _, cert := range certs { _, _ = w.Write(pem.EncodeToMemory(&pem.Block{Type: certUtil.CertificateBlockType, Bytes: cert.Raw})) } }))) server := http.Server{ Addr: *listenAddr, Handler: mux, ReadTimeout: *readTimeout, ReadHeaderTimeout: *readTimeout, WriteTimeout: *writeTimeout, } slog.Info("HTTP server serving", "addr", server.Addr) go func() { err := server.ListenAndServe() slog.Error("HTTP server exiting", "error", err) }() return &server } func httpserverMetrics() *http.Server { mux := http.NewServeMux() mux.Handle("/metrics", promhttp.Handler()) server := http.Server{ Addr: *listenMetricsAddr, Handler: mux, ReadTimeout: *readTimeout, ReadHeaderTimeout: *readTimeout, WriteTimeout: *writeTimeout, } slog.Info("HTTP metrics server serving", "addr", server.Addr) go func() { err := server.ListenAndServe() slog.Error("HTTP metrics server exiting", "error", err) }() return &server } func rateLimiter(burst int, rate int) throttled.HTTPRateLimiter { store, err := memstore.New(65536) if err != nil { log.Fatal(err) } quota := throttled.RateQuota{MaxRate: throttled.PerSec(rate), MaxBurst: burst} rateLimiter, err := throttled.NewGCRARateLimiter(store, quota) if err != nil { log.Fatal(err) } return throttled.HTTPRateLimiter{ RateLimiter: rateLimiter, VaryBy: &throttled.VaryBy{Path: true, Headers: []string{"X-Forwarded-For"}}, } } ================================================ FILE: pkg/controller/server_test.go ================================================ package controller import ( "context" "crypto/x509" "fmt" "io" "net/http" "strings" "sync" "testing" "time" certUtil "k8s.io/client-go/util/cert" ) type testCertStore struct { sync.Mutex cert *x509.Certificate } func (c *testCertStore) getCert() ([]*x509.Certificate, error) { c.Lock() defer c.Unlock() return []*x509.Certificate{c.cert}, nil } func (c *testCertStore) setCert(cert *x509.Certificate) { c.Lock() defer c.Unlock() c.cert = cert } func shutdownServer(server *http.Server, t *testing.T) { err := server.Shutdown(context.Background()) if err != nil { t.Fatal(err) } } func TestHttpCert(t *testing.T) { validFor := time.Hour cn := "my-cn" _, certBefore, err := generatePrivateKeyAndCert(2048, validFor, cn) if err != nil { t.Fatal(err) } _, certAfter, err := generatePrivateKeyAndCert(2048, validFor, cn) if err != nil { t.Fatal(err) } cs := &testCertStore{} server := httpserver(cs.getCert, nil, nil, 2, 2) defer shutdownServer(server, t) hp := *listenAddr if strings.HasPrefix(hp, ":") { hp = fmt.Sprintf("localhost%s", hp) } time.Sleep(1 * time.Second) // TODO(mkm) find a better way, e.g. retries check := func(cert *x509.Certificate) { resp, err := http.Get(fmt.Sprintf("http://%s/v1/cert.pem", hp)) if err != nil { t.Fatal(err) } if got, want := resp.StatusCode, http.StatusOK; got != want { t.Fatalf("got: %v, want: %v", got, want) } defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) } certs, err := certUtil.ParseCertsPEM(b) if err != nil { t.Fatal(err) } if got, want := len(certs), 1; got != want { t.Fatalf("got: %v, want: %v", got, want) } if got, want := certs[0], cert; !got.Equal(want) { t.Fatalf("got: %v, want: %v", got, want) } } cs.setCert(certBefore) check(certBefore) cs.setCert(certAfter) check(certAfter) } ================================================ FILE: pkg/controller/signal_notwin.go ================================================ //go:build !windows // +build !windows package controller import ( "os" "os/signal" "syscall" ) func initKeyGenSignalListener(trigger func()) { sigChannel := make(chan os.Signal, 1) signal.Notify(sigChannel, syscall.SIGUSR1) go func() { for { <-sigChannel trigger() } }() } ================================================ FILE: pkg/controller/signal_windows.go ================================================ package controller func initKeyGenSignalListener(trigger func()) {} ================================================ FILE: pkg/crypto/crypto.go ================================================ package crypto import ( "crypto/aes" "crypto/cipher" "crypto/rsa" "crypto/sha256" "encoding/binary" "errors" "fmt" "io" "golang.org/x/crypto/ssh" ) const ( sessionKeyBytes = 32 ) // ErrTooShort indicates the provided data is too short to be valid. var ErrTooShort = errors.New("SealedSecret data is too short") // PublicKeyFingerprint returns a fingerprint for a public key. func PublicKeyFingerprint(rp *rsa.PublicKey) (string, error) { sp, err := ssh.NewPublicKey(rp) if err != nil { return "", err } return ssh.FingerprintSHA256(sp), nil } // HybridEncrypt performs a regular AES-GCM + RSA-OAEP encryption. // The output byte string is: // // RSA ciphertext length || RSA ciphertext || AES ciphertext func HybridEncrypt(rnd io.Reader, pubKey *rsa.PublicKey, plaintext, label []byte) ([]byte, error) { // Generate a random symmetric key sessionKey := make([]byte, sessionKeyBytes) if _, err := io.ReadFull(rnd, sessionKey); err != nil { return nil, err } block, err := aes.NewCipher(sessionKey) if err != nil { return nil, err } aed, err := cipher.NewGCM(block) if err != nil { return nil, err } // Encrypt symmetric key rsaCiphertext, err := rsa.EncryptOAEP(sha256.New(), rnd, pubKey, sessionKey, label) if err != nil { return nil, err } // First 2 bytes are RSA ciphertext length, so we can separate // all the pieces later. ciphertext := make([]byte, 2) // #nosec G115 binary.BigEndian.PutUint16(ciphertext, uint16(len(rsaCiphertext))) ciphertext = append(ciphertext, rsaCiphertext...) // SessionKey is only used once, so zero nonce is ok zeroNonce := make([]byte, aed.NonceSize()) // Append symmetrically encrypted Secret ciphertext = aed.Seal(ciphertext, zeroNonce, plaintext, nil) return ciphertext, nil } // HybridDecrypt performs a regular AES-GCM + RSA-OAEP decryption. // The private keys map has a fingerprint of each public key as the map key. func HybridDecrypt(rnd io.Reader, privKeys map[string]*rsa.PrivateKey, ciphertext, label []byte) ([]byte, error) { // TODO(mkm): use the key fingerprint encoded in ciphertext (if present) instead of trying all the possible keys for _, privKey := range privKeys { if secret, err := singleDecrypt(rnd, privKey, ciphertext, label); err == nil { return secret, nil } } return nil, fmt.Errorf("no key could decrypt secret") } // singleDecrypt performs a regular AES-GCM + RSA-OAEP decryption. func singleDecrypt(rnd io.Reader, privKey *rsa.PrivateKey, ciphertext, label []byte) ([]byte, error) { if len(ciphertext) < 2 { return nil, ErrTooShort } rsaLen := int(binary.BigEndian.Uint16(ciphertext)) if len(ciphertext) < rsaLen+2 { return nil, ErrTooShort } rsaCiphertext := ciphertext[2 : rsaLen+2] aesCiphertext := ciphertext[rsaLen+2:] sessionKey, err := rsa.DecryptOAEP(sha256.New(), rnd, privKey, rsaCiphertext, label) if err != nil { return nil, err } block, err := aes.NewCipher(sessionKey) if err != nil { return nil, err } aed, err := cipher.NewGCM(block) if err != nil { return nil, err } // Key is only used once, so zero nonce is ok zeroNonce := make([]byte, aed.NonceSize()) plaintext, err := aed.Open(nil, zeroNonce, aesCiphertext, nil) if err != nil { return nil, err } return plaintext, nil } ================================================ FILE: pkg/crypto/keys.go ================================================ package crypto import ( "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "io" "math/big" "time" ) // GeneratePrivateKeyAndCert generates a keypair and signed certificate. func GeneratePrivateKeyAndCert(keySize int, validFor time.Duration, cn string) (*rsa.PrivateKey, *x509.Certificate, error) { r := rand.Reader privKey, err := rsa.GenerateKey(r, keySize) if err != nil { return nil, nil, err } cert, err := SignKey(r, privKey, validFor, cn) if err != nil { return nil, nil, err } return privKey, cert, nil } // SignKey returns a signed certificate. func SignKey(r io.Reader, key *rsa.PrivateKey, validFor time.Duration, cn string) (*x509.Certificate, error) { // TODO: use certificates API to get this signed by the cluster root CA // See https://kubernetes.io/docs/tasks/tls/managing-tls-in-a-cluster/ return SignKeyWithNotBefore(r, key, time.Now(), validFor, cn) } // SignKeyWithNotBefore returns a signed certificate with custom notBefore. func SignKeyWithNotBefore(r io.Reader, key *rsa.PrivateKey, notBefore time.Time, validFor time.Duration, cn string) (*x509.Certificate, error) { // TODO: use certificates API to get this signed by the cluster root CA // See https://kubernetes.io/docs/tasks/tls/managing-tls-in-a-cluster/ serialNo, err := rand.Int(r, new(big.Int).Lsh(big.NewInt(1), 128)) if err != nil { return nil, err } cert := x509.Certificate{ SerialNumber: serialNo, KeyUsage: x509.KeyUsageEncipherOnly, NotBefore: notBefore.UTC(), NotAfter: notBefore.Add(validFor).UTC(), Issuer: pkix.Name{ CommonName: cn, }, Subject: pkix.Name{ CommonName: cn, }, BasicConstraintsValid: true, IsCA: true, } data, err := x509.CreateCertificate(r, &cert, &cert, &key.PublicKey, key) if err != nil { return nil, err } return x509.ParseCertificate(data) } ================================================ FILE: pkg/crypto/keys_test.go ================================================ package crypto import ( "crypto/rsa" "io" mathrand "math/rand" "reflect" "testing" "time" ) // This is omg-not safe for real crypto use! func testRand() io.Reader { return mathrand.New(mathrand.NewSource(42)) } func TestSignKey(t *testing.T) { rand := testRand() key, err := rsa.GenerateKey(rand, 2048) if err != nil { t.Fatalf("Failed to generate test key: %v", err) } cert, err := SignKey(rand, key, time.Hour, "mycn") if err != nil { t.Errorf("signKey() returned error: %v", err) } if !reflect.DeepEqual(cert.PublicKey, &key.PublicKey) { t.Errorf("cert pubkey != original pubkey") } } ================================================ FILE: pkg/flagenv/flagenv.go ================================================ // Package flagenv implements a simple way to expose all your flags as environmental variables. // // Commandline flags have more precedence over environment variables. // In order to use it just call flagenv.SetFlagsFromEnv from an init function or from your main. // // You can call it either before or after your your flag.Parse invocation. // // This example will make it possible to set the default of --my_flag also via the MY_PROG_MY_FLAG // env var: // // var myflag = flag.String("my_flag", "", "some flag") // // func init() { // flagenv.SetFlagsFromEnv("MY_PROG", flag.CommandLine) // } // // func main() { // flags.Parse() // ... // } package flagenv import ( "flag" "fmt" "os" "strings" ) // SetFlagsFromEnv sets flag values from environment, e.g. PREFIX_FOO_BAR set -foo_bar. // It sets only flags that haven't been set explicitly. The defaults are preserved and -help // will still show the defaults provided in the code. func SetFlagsFromEnv(prefix string, fs *flag.FlagSet) { set := map[string]bool{} fs.Visit(func(f *flag.Flag) { set[f.Name] = true }) fs.VisitAll(func(f *flag.Flag) { // ignore flags set from the commandline if set[f.Name] { return } // remove trailing _ to reduce common errors with the prefix, i.e. people setting it to MY_PROG_ cleanPrefix := strings.TrimSuffix(prefix, "_") name := fmt.Sprintf("%s_%s", cleanPrefix, strings.Replace(strings.ToUpper(f.Name), "-", "_", -1)) if e, ok := os.LookupEnv(name); ok { _ = f.Value.Set(e) } }) } ================================================ FILE: pkg/flagenv/flagenv_test.go ================================================ package flagenv_test import ( "flag" "fmt" "os" "testing" "github.com/bitnami-labs/sealed-secrets/pkg/flagenv" ) func TestFlagenv(t *testing.T) { testCases := []struct { set bool val string want string }{ {false, "", "default"}, {true, "bar", "bar"}, {true, "", ""}, } for i, tc := range testCases { t.Run(fmt.Sprint(i), func(t *testing.T) { defer os.Unsetenv("MY_TEST_FOO") if tc.set { os.Setenv("MY_TEST_FOO", tc.val) } fs := flag.NewFlagSet("test", flag.PanicOnError) s := fs.String("foo", "default", "help") flagenv.SetFlagsFromEnv("MY_TEST", fs) _ = fs.Parse(nil) if got, want := *s, tc.want; got != want { t.Errorf("got %q, want %q", got, want) } }) } } ================================================ FILE: pkg/kubeseal/kubeseal.go ================================================ package kubeseal import ( "bytes" "context" "crypto/rand" "crypto/rsa" "encoding/base64" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "reflect" "strings" "time" "k8s.io/apimachinery/pkg/util/yaml" ssv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" "github.com/bitnami-labs/sealed-secrets/pkg/crypto" v1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/util/net" "k8s.io/client-go/kubernetes/scheme" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/rest" "k8s.io/client-go/util/cert" "k8s.io/client-go/util/keyutil" ) type ClientConfig interface { ClientConfig() (*rest.Config, error) Namespace() (string, bool, error) } func ParseKey(r io.Reader) (*rsa.PublicKey, error) { data, err := io.ReadAll(r) if err != nil { return nil, err } certs, err := cert.ParseCertsPEM(data) if err != nil { return nil, err } // ParseCertsPem returns error if len(certs) == 0, but best to be sure... if len(certs) == 0 { return nil, errors.New("failed to read any certificates") } cert, ok := certs[0].PublicKey.(*rsa.PublicKey) if !ok { return nil, fmt.Errorf("expected RSA public key but found %v", certs[0].PublicKey) } if time.Now().After(certs[0].NotAfter) { return nil, fmt.Errorf("failed to encrypt using an expired certificate on %v", certs[0].NotAfter.Format("January 2, 2006")) } return cert, nil } func prettyEncoder(codecs runtimeserializer.CodecFactory, mediaType string, gv runtime.GroupVersioner) (runtime.Encoder, error) { info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), mediaType) if !ok { return nil, fmt.Errorf("binary can't serialize %s", mediaType) } prettyEncoder := info.PrettySerializer if prettyEncoder == nil { prettyEncoder = info.Serializer } enc := codecs.EncoderForVersion(prettyEncoder, gv) return enc, nil } func isFilename(name string) (bool, error) { u, err := url.Parse(name) if err != nil { return false, err } // windows drive letters if s := strings.ToLower(u.Scheme); len(s) == 1 && s[0] >= 'a' && s[0] <= 'z' { return true, nil } return u.Scheme == "", nil } // getServicePortName obtains the SealedSecrets service port name. func getServicePortName(ctx context.Context, client corev1.CoreV1Interface, namespace, serviceName string) (string, error) { service, err := client.Services(namespace).Get(ctx, serviceName, metav1.GetOptions{}) if err != nil { return "", fmt.Errorf("cannot get sealed secret service: %v.\nPlease, use the flag --controller-name and --controller-namespace to set up the name and namespace of the sealed secrets controller", err) } return service.Spec.Ports[0].Name, nil } // openCertLocal opens a cert URI or local filename, by fetching it locally from the client // (as opposed as openCertCluster which fetches it via HTTP but through the k8s API proxy). func openCertLocal(filenameOrURI string) (io.ReadCloser, error) { // detect if a certificate is a local file or an URI. if ok, err := isFilename(filenameOrURI); err != nil { return nil, err } else if ok { // #nosec G304 -- should open user provided file return os.Open(filenameOrURI) } return openCertURI(filenameOrURI) } func openCertURI(uri string) (io.ReadCloser, error) { // support file:// scheme. Note: we're opening the file using os.Open rather // than using the file:// scheme below because there is no point in complicating our lives // and escape the filename properly. t := &http.Transport{Proxy: http.ProxyFromEnvironment} // #nosec: G111 -- we want to allow all files to be opened t.RegisterProtocol("file", http.NewFileTransport(http.Dir("/"))) c := &http.Client{Transport: t} resp, err := c.Get(uri) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("cannot fetch %q: %s", uri, resp.Status) } return resp.Body, nil } // openCertCluster fetches a certificate by performing an HTTP request to the controller // through the k8s API proxy. func openCertCluster(ctx context.Context, c corev1.CoreV1Interface, namespace, name string) (io.ReadCloser, error) { portName, err := getServicePortName(ctx, c, namespace, name) if err != nil { return nil, err } cert, err := c.Services(namespace).ProxyGet("http", name, portName, "/v1/cert.pem", nil).Stream(ctx) if err != nil { return nil, fmt.Errorf("cannot fetch certificate: %v", err) } return cert, nil } func OpenCert(ctx context.Context, clientConfig ClientConfig, controllerNs, controllerName string, certURL string) (io.ReadCloser, error) { if certURL != "" { return openCertLocal(certURL) } conf, err := clientConfig.ClientConfig() if err != nil { return nil, err } conf.AcceptContentTypes = "application/x-pem-file, */*" restClient, err := corev1.NewForConfig(conf) if err != nil { return nil, err } return openCertCluster(ctx, restClient, controllerNs, controllerName) } func readSecrets(r io.Reader) ([]*v1.Secret, error) { decoder := yaml.NewYAMLOrJSONDecoder(r, 4096) var secrets []*v1.Secret empty := v1.Secret{} for { sec := v1.Secret{} err := decoder.Decode(&sec) if reflect.DeepEqual(sec, empty) { if errors.Is(err, io.EOF) { break } else { continue } } secrets = append(secrets, &sec) if err != nil && err != io.EOF { return nil, err } } return secrets, nil } func readSealedSecrets(r io.Reader) ([]*ssv1alpha1.SealedSecret, error) { decoder := yaml.NewYAMLOrJSONDecoder(r, 4096) var secrets []*ssv1alpha1.SealedSecret empty := ssv1alpha1.SealedSecret{} for { sec := ssv1alpha1.SealedSecret{} err := decoder.Decode(&sec) if reflect.DeepEqual(sec, empty) { if errors.Is(err, io.EOF) { break } else { continue } } secrets = append(secrets, &sec) if err != nil && err != io.EOF { return nil, err } } return secrets, nil } // Seal reads a k8s Secret resource parsed from an input reader by a given codec, encrypts all its secrets // with a given public key, using the name and namespace found in the input secret, unless explicitly overridden // by the overrideName and overrideNamespace arguments. func Seal(clientConfig ClientConfig, outputFormat string, in io.Reader, out io.Writer, codecs runtimeserializer.CodecFactory, pubKey *rsa.PublicKey, scope ssv1alpha1.SealingScope, allowEmptyData bool, overrideName, overrideNamespace string) error { secrets, err := readSecrets(in) if err != nil { return err } if len(secrets) == 0 { return fmt.Errorf("no secrets found. Ensure the input is valid and UTF-8 encoded") } for _, secret := range secrets { if len(secret.Data) == 0 && len(secret.StringData) == 0 && !allowEmptyData { return fmt.Errorf("secret.data is empty in input Secret, assuming this is an error and aborting. To work with empty data, --allow-empty-data can be used") } if overrideName != "" { secret.Name = overrideName } if secret.GetName() == "" { return fmt.Errorf("missing metadata.name in input Secret") } if overrideNamespace != "" { secret.Namespace = overrideNamespace } if scope != ssv1alpha1.DefaultScope { secret.Annotations = ssv1alpha1.UpdateScopeAnnotations(secret.Annotations, scope) } if ssv1alpha1.SecretScope(secret) != ssv1alpha1.ClusterWideScope { ns, namespaceSet, _ := clientConfig.Namespace() // Check for namespace mismatch when namespace is explicitly set via command line if namespaceSet && secret.GetNamespace() != "" && secret.GetNamespace() != ns { return fmt.Errorf("namespace mismatch: input secret is in namespace %q but %q was specified", secret.GetNamespace(), ns) } if secret.GetNamespace() == "" { secret.SetNamespace(ns) } } // Strip read-only server-side ObjectMeta (if present) secret.SetSelfLink("") secret.SetUID("") secret.SetResourceVersion("") secret.Generation = 0 secret.SetCreationTimestamp(metav1.Time{}) secret.SetDeletionTimestamp(nil) secret.DeletionGracePeriodSeconds = nil ssecret, err := ssv1alpha1.NewSealedSecret(codecs, pubKey, secret) if err != nil { return err } if err = sealedSecretOutput(out, outputFormat, codecs, ssecret); err != nil { return err } // return nil } return nil } func ValidateSealedSecret(ctx context.Context, clientConfig ClientConfig, controllerNs, controllerName string, in io.Reader) error { conf, err := clientConfig.ClientConfig() if err != nil { return err } restClient, err := corev1.NewForConfig(conf) if err != nil { return err } portName, err := getServicePortName(ctx, restClient, controllerNs, controllerName) if err != nil { return err } req := restClient.RESTClient().Post(). Namespace(controllerNs). Resource("services"). SubResource("proxy"). Name(net.JoinSchemeNamePort("http", controllerName, portName)). Suffix("/v1/verify") secrets, err := readSealedSecrets(in) if err != nil { return fmt.Errorf("unable to decrypt sealed secret") } for _, secret := range secrets { content, err := json.Marshal(secret) if err != nil { return fmt.Errorf("error while marshalling sealed secret: %w", err) } req.Body(content) res := req.Do(ctx) if err := res.Error(); err != nil { if status, ok := err.(*k8serrors.StatusError); ok && status.Status().Code == http.StatusConflict { return fmt.Errorf("unable to decrypt sealed secret: %v", secret.GetName()) } return fmt.Errorf("cannot validate sealed secret: %v", err) } } return nil } func ReEncryptSealedSecret(ctx context.Context, clientConfig ClientConfig, controllerNs, controllerName, outputFormat string, in io.Reader, out io.Writer, codecs runtimeserializer.CodecFactory) error { conf, err := clientConfig.ClientConfig() if err != nil { return err } restClient, err := corev1.NewForConfig(conf) if err != nil { return err } portName, err := getServicePortName(ctx, restClient, controllerNs, controllerName) if err != nil { return err } if err != nil { return err } req := restClient.RESTClient().Post(). Namespace(controllerNs). Resource("services"). SubResource("proxy"). Name(net.JoinSchemeNamePort("http", controllerName, portName)). Suffix("/v1/rotate") secrets, err := readSealedSecrets(in) if err != nil { return err } for _, secret := range secrets { content, err := json.Marshal(secret) if err != nil { return err } req.Body(content) res := req.Do(ctx) if err := res.Error(); err != nil { if status, ok := err.(*k8serrors.StatusError); ok && status.Status().Code == http.StatusConflict { return fmt.Errorf("unable to rotate secret") } return fmt.Errorf("cannot re-encrypt secret: %v", err) } body, err := res.Raw() if err != nil { return err } ssecret := &ssv1alpha1.SealedSecret{} if err = json.Unmarshal(body, ssecret); err != nil { return err } ssecret.SetCreationTimestamp(metav1.Time{}) ssecret.SetDeletionTimestamp(nil) ssecret.Generation = 0 if err = sealedSecretOutput(out, outputFormat, codecs, ssecret); err != nil { return err } } return nil } func resourceOutput(out io.Writer, outputFormat string, codecs runtimeserializer.CodecFactory, gv runtime.GroupVersioner, obj runtime.Object) error { var contentType string switch strings.ToLower(outputFormat) { case "json", "": contentType = runtime.ContentTypeJSON case "yaml": contentType = runtime.ContentTypeYAML fmt.Fprint(out, "---\n") default: return fmt.Errorf("unsupported output format: %s", outputFormat) } prettyEnc, err := prettyEncoder(codecs, contentType, gv) if err != nil { return err } buf, err := runtime.Encode(prettyEnc, obj) if err != nil { return err } _, _ = out.Write(buf) if contentType == runtime.ContentTypeJSON { fmt.Fprint(out, "\n") } return nil } func sealedSecretOutput(out io.Writer, outputFormat string, codecs runtimeserializer.CodecFactory, ssecret *ssv1alpha1.SealedSecret) error { return resourceOutput(out, outputFormat, codecs, ssv1alpha1.SchemeGroupVersion, ssecret) } func decodeSealedSecret(codecs runtimeserializer.CodecFactory, b []byte) (*ssv1alpha1.SealedSecret, error) { var ss ssv1alpha1.SealedSecret if err := runtime.DecodeInto(codecs.UniversalDecoder(), b, &ss); err != nil { return nil, err } return &ss, nil } func SealMergingInto(clientConfig ClientConfig, outputFormat string, in io.Reader, filename string, codecs runtimeserializer.CodecFactory, pubKey *rsa.PublicKey, scope ssv1alpha1.SealingScope, allowEmptyData bool) error { // #nosec G304 -- should open user provided file f, err := os.OpenFile(filename, os.O_RDWR, 0) if err != nil { return err } // #nosec G307 -- we are explicitly managing a potential error from f.Close() at the end of the function defer f.Close() b, err := io.ReadAll(f) if err != nil { return err } orig, err := decodeSealedSecret(codecs, b) if err != nil { return err } var buf bytes.Buffer if err := Seal(clientConfig, outputFormat, in, &buf, codecs, pubKey, scope, allowEmptyData, orig.Name, orig.Namespace); err != nil { return err } update, err := decodeSealedSecret(codecs, buf.Bytes()) if err != nil { return err } // merge encrypted data and metadata for k, v := range update.Spec.EncryptedData { orig.Spec.EncryptedData[k] = v } for k, v := range update.Spec.Template.Annotations { orig.Spec.Template.Annotations[k] = v } for k, v := range update.Spec.Template.Labels { orig.Spec.Template.Labels[k] = v } for k, v := range update.Spec.Template.Data { orig.Spec.Template.Data[k] = v } // updated sealed secret file in-place avoiding clobbering the file upon rendering errors. var out bytes.Buffer if err := sealedSecretOutput(&out, outputFormat, codecs, orig); err != nil { return err } if err := f.Truncate(0); err != nil { return err } if _, err := f.Seek(0, 0); err != nil { return err } if _, err := io.Copy(f, &out); err != nil { return err } // we explicitly call f.Close() to return a potential error when closing the file that wouldn't be returned in the deferred f.Close() if err := f.Close(); err != nil { return err } return nil } func EncryptSecretItem(w io.Writer, secretName, ns string, data []byte, scope ssv1alpha1.SealingScope, pubKey *rsa.PublicKey) error { // TODO(mkm): refactor cluster-wide/namespace-wide to an actual enum so we can have a simple flag // to refer to the scope mode that is not a tuple of booleans. label := ssv1alpha1.EncryptionLabel(ns, secretName, scope) out, err := crypto.HybridEncrypt(rand.Reader, pubKey, data, label) if err != nil { return err } fmt.Fprint(w, base64.StdEncoding.EncodeToString(out)) return nil } // parseFromFile parses a value of the kubectl --from-file flag, which can optionally include an item name // preceding the first equals sign. func ParseFromFile(s string) (string, string) { c := strings.SplitN(s, "=", 2) if len(c) == 1 { return "", c[0] } return c[0], c[1] } func readPrivKeysFromFile(filename string) ([]*rsa.PrivateKey, error) { // #nosec G304 -- should open user provided file b, err := os.ReadFile(filename) if err != nil { return nil, err } res, err := parsePrivKey(b) if err == nil { return []*rsa.PrivateKey{res}, nil } var secrets []*v1.Secret // try to parse it as json/yaml encoded v1.List of secrets var lst v1.List if err = runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), b, &lst); err == nil { for _, r := range lst.Items { s, err := readSecrets(bytes.NewBuffer(r.Raw)) if err != nil { return nil, err } secrets = append(secrets, s...) } } else { // try to parse it as json/yaml encoded secret s, err := readSecrets(bytes.NewBuffer(b)) if err != nil { return nil, err } secrets = append(secrets, s...) } var keys []*rsa.PrivateKey for _, s := range secrets { tlsKey, ok := s.Data["tls.key"] if !ok { return nil, fmt.Errorf("secret must contain a 'tls.data' key") } pk, err := parsePrivKey(tlsKey) if err != nil { return nil, err } keys = append(keys, pk) } return keys, nil } func readPrivKey(filename string) (*rsa.PrivateKey, error) { pks, err := readPrivKeysFromFile(filename) if err != nil { return nil, err } return pks[0], nil } func parsePrivKey(b []byte) (*rsa.PrivateKey, error) { key, err := keyutil.ParsePrivateKeyPEM(b) if err != nil { return nil, err } switch rsaKey := key.(type) { case *rsa.PrivateKey: return rsaKey, nil default: return nil, fmt.Errorf("unexpected private key type %T", key) } } func readPrivKeys(filenames []string) (map[string]*rsa.PrivateKey, error) { res := map[string]*rsa.PrivateKey{} for _, filename := range filenames { pks, err := readPrivKeysFromFile(filename) if err != nil { return nil, err } for _, pk := range pks { fingerprint, err := crypto.PublicKeyFingerprint(&pk.PublicKey) if err != nil { return nil, err } res[fingerprint] = pk } } return res, nil } func UnsealSealedSecret(w io.Writer, in io.Reader, privKeysFilenames []string, outputFormat string, codecs runtimeserializer.CodecFactory) error { privKeys, err := readPrivKeys(privKeysFilenames) if err != nil { return err } b, err := io.ReadAll(in) if err != nil { return err } ss, err := decodeSealedSecret(codecs, b) if err != nil { return err } sec, err := ss.Unseal(codecs, privKeys) if err != nil { return err } return resourceOutput(w, outputFormat, codecs, v1.SchemeGroupVersion, sec) } ================================================ FILE: pkg/kubeseal/kubeseal_test.go ================================================ package kubeseal import ( "bytes" "context" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" "io" "math/big" "net/http" "net/http/httptest" "os" "path/filepath" goruntime "runtime" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" "k8s.io/client-go/util/keyutil" "k8s.io/utils/strings/slices" ssv1alpha1 "github.com/bitnami-labs/sealed-secrets/pkg/apis/sealedsecrets/v1alpha1" "github.com/bitnami-labs/sealed-secrets/pkg/crypto" certUtil "k8s.io/client-go/util/cert" ) const testCert = ` -----BEGIN CERTIFICATE----- MIIErTCCApWgAwIBAgIQBekz48i8NbrzIpIrLMIULTANBgkqhkiG9w0BAQsFADAA MB4XDTE3MDYyMDA0MzI0NVoXDTI3MDYxODA0MzI0NVowADCCAiIwDQYJKoZIhvcN AQEBBQADggIPADCCAgoCggIBAL6ISW4MnHAmC6MdmJOwo9C6YYhKYDwPD2tF+j4p I2duB3y7DLF+zWNHgbUlBZck8CudacJTuxOJFEqr4umqm0f4EGgRPwZgFvFLHKSZ /hxUFnMcGVhY1qsk55peSghPHarOYyBhhHDtCu7qdMu9MqPZB68y16HdPvwWPadI dBKSxDLvwYfjDnG/ZHX9rmlDKej7jPGdvqAY5VJteP30w6YHb1Uc4whppNcDSc2l gOuKAWtQ5WfZbB0NpMhj4framNeXMYwjZytEdC1c/4O45zm5eK4FNPueCfxOlzFQ D3y34OuQlJwlrPE4KmdMHtE1a8x0ihbglInJrtqcXK3vEdUJ2c/BKWgFtPOTz6Du jV4j0OMVVGnk5jUmh+yfbgielIkPcpSTWP1cIPwK3eWbrvMziq6sv0x7QoOD3Pzm GBE8Y9sa5uy+bJZt5MywbamZ3xWaxoQbSN8RPoxRhTe0DEpx6utCXSWpapT7kWZ3 R1PTuVx+Ktyz7MRoDUWvxfpMJ2hsJ71Az0AuUZ4N4fmmGdUcM81GPUOiMZ4uqySQ A2phgikbJaTzcT85RcNFYSi4eKc5mYFNqr5xVa6uHhZ+OGeGy1yyOEWLgIZV3A/8 4eZshOyYtRlZjCkaGZTfXNft+8QJi8rEZRcJtVhqLzezBVRsL7pt6P/mQj4+XHsE VSBrAgMBAAGjIzAhMA4GA1UdDwEB/wQEAwIAATAPBgNVHRMBAf8EBTADAQH/MA0G CSqGSIb3DQEBCwUAA4ICAQCSizqBB3bjHCSGk/8lpqIyHJQR5u4Cf7LRrC9U8mxe pvC3Fx3/RlVe87Y4cUb37xZc/TmB6Bq10Y6R7ydS3oe8PCh4UQRnEfBgtJ6m59ha t3iPX0NdQVYz/D+yEiHjpI7gpyFNuGkd4/78JE51SO4yGYvWk/ChHoMvbLcxzfdK PI2Ymf3MWtGfoF/TQ1jy/Biy+qumDPSz23MynQG39cdUInSK26oemUbTH0koLulN fNl4TwSEdSm2DRl0la+vkrzu7SvF9SJ2ES6wMWVjYiJLNpApjGuF9/ZOFw9DvSSH m+UYXn+IC7rTgvXKvXTlG//z/14Lx0GFIY+ZjdENwLH//orBQLg37TZatKEpaWO6 uRzFUxZVw3ic3RxoHfEbRA9vQlQdKnV+BpZe/Pb08RAh82OZyujqqyK7cPPOW5Vi T9y+NeMwfKH8H4un7mQWkgWFw3LMIspYY5uHWp6jBwU9u/mjoK4+Y219dkaAhAcx D+YIZRXwxc6ehLCavGF2DIepybzDlJbiCe8JxUDsrE/Xkm6x28uq35oZ3UQznubU 7LfAeRSI99sNvFnq0TqhSlp+CUDs8Z1LvDXzAHX4UeZQl4g+H+w1KudCvjO0mPPp R9bIjJLIvp7CQPDkdRzJSjvetrKtI0l97VjsjbRB9v6ZekGY9SFI49KzKUTk8fsF /A== -----END CERTIFICATE----- ` var ( testModulus *big.Int testExponent = 65537 ) func init() { testModulus = new(big.Int) _, err := fmt.Sscan("777304254876434297689544225447769213262492599515515837291621795936355252933930193245809942636192119684040605554803489669141565417296821660595336672178414512660751886699171738066307588619202437848899334837760648051656982184646490661921128886671800776058692981991859399404705935722225294811424879738586269551402668122524371718537515440568440102201259925611463161144897905846190044735554045001999198442528435295995584980713050916813579912296878368079243909549993116827192901474611239264189340401059113919551426849847211275352102674049634252149163111599977742365280992561904350781270344655927564475032580504276518647106167707150111291732645399166011800154961975117045723373023335778593638216165426988399138193230056486079421256484837299169853958601000282124667227789126483641999102102039577368681983584245367307077546423870452524154641890843463963116237003367269116435430641427113406369059991147359641266708862913786891945896441771663010146473536372286482453315017377528517965715554550898957321536181165129538808789201530141159181590893764287807749414277289452691723903046140558704697831351834538780165261072894792900501671534138992265545905216973214953125367388406669893889742303072755608685449114438926280862339744991872488262084141163", testModulus) if err != nil { panic(err) } } func tmpfile(t *testing.T, contents []byte) string { f, err := os.CreateTemp("", "testdata") if err != nil { t.Fatalf("Failed to create tempfile: %v", err) } if _, err := f.Write(contents); err != nil { t.Fatalf("Failed to write to tempfile: %v", err) } if err := f.Close(); err != nil { t.Fatalf("Failed to close tempfile: %v", err) } return f.Name() } func TestParseKey(t *testing.T) { key, err := ParseKey(strings.NewReader(testCert)) if err != nil { t.Fatalf("Failed to parse test key: %v", err) } if key.N.Cmp(testModulus) != 0 { t.Errorf("Unexpected key modulus: %v", key.N) } if key.E != testExponent { t.Errorf("Unexpected key exponent: %v", key.E) } } /* repeated from main here... STARTs */ func testClientConfig() clientcmd.ClientConfig { return &mockClientConfig{namespace: "testns", namespaceSet: false} } /* repeated from main here... ENDs */ func TestOpenCertFile(t *testing.T) { ctx := context.Background() clientConfig := testClientConfig() controllerNs := "default" controllerName := "controller" certFile := tmpfile(t, []byte(testCert)) s := httptest.NewServer(http.FileServer(http.Dir(filepath.Dir(certFile)))) defer s.Close() testCases := []string{ certFile, fmt.Sprintf("%s/%s", s.URL, filepath.Base(certFile)), // This should work on windows but it causes a 500 error in the file handler. TODO: investigate // (&url.URL{Scheme: "file", Path: path.Join("/", filepath.ToSlash(certFile))}).String(), } if goruntime.GOOS != "windows" { testCases = append(testCases, fmt.Sprintf("file://%s", certFile)) } for _, certURL := range testCases { f, err := OpenCert(ctx, clientConfig, controllerNs, controllerName, certURL) if err != nil { t.Fatalf("Error reading test cert file: %v", err) } data, err := io.ReadAll(f) if err != nil { t.Fatalf("Error reading from test cert file: %v", err) } if string(data) != testCert { t.Errorf("Read incorrect data from cert file?!") } } } func TestSealWithMultiDocSecrets(t *testing.T) { key, err := ParseKey(strings.NewReader(testCert)) if err != nil { t.Fatalf("Failed to parse gotSecrets key: %v", err) } testCases := []struct { name string asYaml bool inputSeparator string outputFormat string }{ { name: "multi-doc json", asYaml: false, inputSeparator: "\n", outputFormat: "json", }, { name: "multi-doc yaml", asYaml: true, inputSeparator: "---\n", outputFormat: "yaml", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s1 := mkTestSecret(t, "foo", "1", withSecretName("s1"), asYAML(tc.asYaml)) s2 := mkTestSecret(t, "bar", "2", withSecretName("s2"), asYAML(tc.asYaml)) multiDocYaml := fmt.Sprintf("%s%s%s", s1, tc.inputSeparator, s2) clientConfig := &mockClientConfig{namespace: "testns", namespaceSet: false} outputFormat := tc.outputFormat inbuf := bytes.Buffer{} _, err = bytes.NewBuffer([]byte(multiDocYaml)).WriteTo(&inbuf) if err != nil { t.Fatalf("Error writing to buffer: %v", err) } t.Logf("input is:\n%s", inbuf.String()) outbuf := bytes.Buffer{} if err := Seal(clientConfig, outputFormat, &inbuf, &outbuf, scheme.Codecs, key, ssv1alpha1.NamespaceWideScope, false, "", ""); err != nil { t.Fatalf("seal() returned error: %v", err) } outBytes := outbuf.Bytes() t.Logf("output is:\n%s", outBytes) if tc.asYaml { if !strings.HasPrefix(string(outBytes), "---") { t.Errorf("YAML output should start with ---") } if strings.HasSuffix(string(outBytes), "---\n") { t.Errorf("YAML output should not end with ---") } } decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(outBytes), 4096) var gotSecrets []*ssv1alpha1.SealedSecret for { s := ssv1alpha1.SealedSecret{} err := decoder.Decode(&s) if err != nil { if err == io.EOF { break } t.Fatalf("Failed to parse result: %v", err) } gotSecrets = append(gotSecrets, &s) } if got, want := len(gotSecrets), 2; got != want { t.Errorf("Wrong element output length: got: %d, want: %d", got, want) } for _, gotSecret := range gotSecrets { if got, want := gotSecret.GetNamespace(), "testns"; got != want { t.Errorf("got: %q, want: %q", got, want) } if got, want := gotSecret.GetName(), []string{"s1", "s2"}; !slices.Contains(want, got) { t.Errorf("got: %q, want: %q", got, want) } } }) } } func TestSeal(t *testing.T) { key, err := ParseKey(strings.NewReader(testCert)) if err != nil { t.Fatalf("Failed to parse test key: %v", err) } testCases := []struct { secret v1.Secret scope ssv1alpha1.SealingScope want ssv1alpha1.SealedSecret // partial object }{ { secret: v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "myns", }, Data: map[string][]byte{ "foo": []byte("sekret"), }, StringData: map[string]string{ "foos": "stringsekret", }, }, want: ssv1alpha1.SealedSecret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "myns", }, }, }, { secret: v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", }, Data: map[string][]byte{ "foo": []byte("sekret"), }, }, want: ssv1alpha1.SealedSecret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "default", }, }, }, { secret: v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "", Annotations: map[string]string{ ssv1alpha1.SealedSecretNamespaceWideAnnotation: "true", }, }, Data: map[string][]byte{ "foo": []byte("sekret"), }, }, want: ssv1alpha1.SealedSecret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "default", Annotations: map[string]string{ ssv1alpha1.SealedSecretNamespaceWideAnnotation: "true", }, }, }, }, { secret: v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "", Annotations: map[string]string{ ssv1alpha1.SealedSecretClusterWideAnnotation: "true", }, }, Data: map[string][]byte{ "foo": []byte("sekret"), }, }, want: ssv1alpha1.SealedSecret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "", // <--- we shouldn't force the default namespace for cluster wide secrets ... Annotations: map[string]string{ ssv1alpha1.SealedSecretClusterWideAnnotation: "true", }, }, }, }, { secret: v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "myns", Annotations: map[string]string{ ssv1alpha1.SealedSecretClusterWideAnnotation: "true", }, }, Data: map[string][]byte{ "foo": []byte("sekret"), }, }, want: ssv1alpha1.SealedSecret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "myns", // <--- ... but we should preserve one if specified. Annotations: map[string]string{ ssv1alpha1.SealedSecretClusterWideAnnotation: "true", }, }, }, }, { secret: v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "", }, Data: map[string][]byte{ "foo": []byte("sekret"), }, }, scope: ssv1alpha1.NamespaceWideScope, want: ssv1alpha1.SealedSecret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "default", Annotations: map[string]string{ ssv1alpha1.SealedSecretNamespaceWideAnnotation: "true", }, }, }, }, { secret: v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "", }, Data: map[string][]byte{ "foo": []byte("sekret"), }, }, scope: ssv1alpha1.ClusterWideScope, want: ssv1alpha1.SealedSecret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "", Annotations: map[string]string{ ssv1alpha1.SealedSecretClusterWideAnnotation: "true", }, }, }, }, } for i, tc := range testCases { t.Run(fmt.Sprint(i), func(t *testing.T) { clientConfig := &mockClientConfig{namespace: "testns", namespaceSet: false} // For test cases where the secret has no namespace and we expect it to be filled with "default" if tc.secret.GetNamespace() == "" && tc.want.GetNamespace() == "default" { clientConfig = &mockClientConfig{namespace: "default", namespaceSet: true} } outputFormat := "json" info, ok := runtime.SerializerInfoForMediaType(scheme.Codecs.SupportedMediaTypes(), runtime.ContentTypeJSON) if !ok { t.Fatalf("binary can't serialize JSON") } enc := scheme.Codecs.EncoderForVersion(info.Serializer, v1.SchemeGroupVersion) inbuf := bytes.Buffer{} if err := enc.Encode(&tc.secret, &inbuf); err != nil { t.Fatalf("Error encoding: %v", err) } t.Logf("input is: %s", inbuf.String()) outbuf := bytes.Buffer{} if err := Seal(clientConfig, outputFormat, &inbuf, &outbuf, scheme.Codecs, key, tc.scope, false, "", ""); err != nil { t.Fatalf("seal() returned error: %v", err) } outBytes := outbuf.Bytes() t.Logf("output is %s", outBytes) var result ssv1alpha1.SealedSecret if err = runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), outBytes, &result); err != nil { t.Fatalf("Failed to parse result: %v", err) } smeta := result.GetObjectMeta() if got, want := smeta.GetName(), tc.want.GetName(); got != want { t.Errorf("got: %q, want: %q", got, want) } if got, want := smeta.GetNamespace(), tc.want.GetNamespace(); got != want { t.Errorf("got: %q, want: %q", got, want) } if got, want := smeta.GetAnnotations(), tc.want.GetAnnotations(); !cmp.Equal(got, want, cmpopts.EquateEmpty()) { t.Errorf("got: %q, want: %q", got, want) } for n := range tc.secret.Data { if len(result.Spec.EncryptedData[n]) < 100 { t.Errorf("Encrypted data is implausibly short: %v", result.Spec.EncryptedData[n]) } } for n := range tc.secret.StringData { if len(result.Spec.EncryptedData[n]) < 100 { t.Errorf("Encrypted data is implausibly short: %v", result.Spec.EncryptedData[n]) } } // NB: See sealedsecret_test.go for e2e crypto test }) } } type mkTestSecretOpt func(*mkTestSecretOpts) type mkTestSecretOpts struct { secretName string secretNamespace string asYAML bool } func withSecretName(n string) mkTestSecretOpt { return func(o *mkTestSecretOpts) { o.secretName = n } } func withSecretNamespace(n string) mkTestSecretOpt { return func(o *mkTestSecretOpts) { o.secretNamespace = n } } func asYAML(y bool) mkTestSecretOpt { return func(o *mkTestSecretOpts) { o.asYAML = y } } func mkTestSecret(t *testing.T, key, value string, opts ...mkTestSecretOpt) []byte { o := mkTestSecretOpts{ secretName: "testname", secretNamespace: "testns", } for _, opt := range opts { opt(&o) } secret := v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: o.secretName, Namespace: o.secretNamespace, Annotations: map[string]string{ key: value, // putting secret here just to have a simple way to test annotation merges }, Labels: map[string]string{ key: value, }, }, Data: map[string][]byte{ key: []byte(value), }, } contentType := runtime.ContentTypeJSON if o.asYAML { contentType = runtime.ContentTypeYAML } info, ok := runtime.SerializerInfoForMediaType(scheme.Codecs.SupportedMediaTypes(), contentType) if !ok { t.Fatalf("binary can't serialize JSON") } enc := scheme.Codecs.EncoderForVersion(info.Serializer, v1.SchemeGroupVersion) var inbuf bytes.Buffer if err := enc.Encode(&secret, &inbuf); err != nil { t.Fatalf("Error encoding: %v", err) } return inbuf.Bytes() } func mkTestSealedSecret(t *testing.T, pubKey *rsa.PublicKey, key, value string, opts ...mkTestSecretOpt) []byte { clientConfig := &mockClientConfig{namespace: "testns", namespaceSet: false} outputFormat := "json" inbuf := bytes.NewBuffer(mkTestSecret(t, key, value, opts...)) var outbuf bytes.Buffer if err := Seal(clientConfig, outputFormat, inbuf, &outbuf, scheme.Codecs, pubKey, ssv1alpha1.DefaultScope, false, "", ""); err != nil { t.Fatalf("seal() returned error: %v", err) } return outbuf.Bytes() } // TODO(mkm): rename newTestKeyPair to newTestKeyPairs. func newTestKeyPair(t *testing.T) (*rsa.PublicKey, map[string]*rsa.PrivateKey) { privKey, _, err := crypto.GeneratePrivateKeyAndCert(2048, time.Hour, "testcn") if err != nil { t.Fatal(err) } pubKey := &privKey.PublicKey fp, err := crypto.PublicKeyFingerprint(pubKey) if err != nil { t.Fatal(err) } privKeys := map[string]*rsa.PrivateKey{fp: privKey} return pubKey, privKeys } func TestUnseal(t *testing.T) { pubKey, privKeys := newTestKeyPair(t) pkFile, err := os.CreateTemp("", "") if err != nil { t.Fatal(err) } defer os.RemoveAll(pkFile.Name()) if len(privKeys) != 1 { t.Fatal("assuming only one test key-pair") } for _, key := range privKeys { err := pem.Encode(pkFile, &pem.Block{Type: keyutil.RSAPrivateKeyBlockType, Bytes: x509.MarshalPKCS1PrivateKey(key)}) if err != nil { t.Fatal(err) } } pkFile.Close() const ( secretItemKey = "foo" secretItemValue = "secret1" ) ss := mkTestSealedSecret(t, pubKey, secretItemKey, secretItemValue) var buf bytes.Buffer privKeysList := []string{pkFile.Name()} outputFormat := "json" if err := UnsealSealedSecret(&buf, bytes.NewBuffer(ss), privKeysList, outputFormat, scheme.Codecs); err != nil { t.Fatal(err) } secret, err := readSecrets(&buf) if err != nil { t.Fatal(err) } for _, secret := range secret { if got, want := string(secret.Data[secretItemKey]), secretItemValue; got != want { t.Fatalf("got: %q, want: %q", got, want) } } } func TestUnsealList(t *testing.T) { pubKey, privKeys := newTestKeyPair(t) pkFile, err := os.CreateTemp("", "") if err != nil { t.Fatal(err) } defer os.RemoveAll(pkFile.Name()) // encode a v1.List containing all the privKeys into one file. prettyEnc, err := prettyEncoder(scheme.Codecs, runtime.ContentTypeJSON, v1.SchemeGroupVersion) if err != nil { t.Fatal(err) } var secrets [][]byte for _, key := range privKeys { b := pem.EncodeToMemory(&pem.Block{Type: keyutil.RSAPrivateKeyBlockType, Bytes: x509.MarshalPKCS1PrivateKey(key)}) buf, err := runtime.Encode(prettyEnc, &v1.Secret{Data: map[string][]byte{"tls.key": b}}) if err != nil { t.Fatal(err) } secrets = append(secrets, buf) } lst := &v1.List{} for _, s := range secrets { lst.Items = append(lst.Items, runtime.RawExtension{Raw: s}) } blst, err := runtime.Encode(prettyEnc, lst) if err != nil { t.Fatal(err) } if _, err := pkFile.Write(blst); err != nil { t.Fatal(err) } pkFile.Close() const ( secretItemKey = "foo" secretItemValue = "secret1" ) ss := mkTestSealedSecret(t, pubKey, secretItemKey, secretItemValue) var buf bytes.Buffer privKeysList := []string{pkFile.Name()} outputFormat := "json" if err := UnsealSealedSecret(&buf, bytes.NewBuffer(ss), privKeysList, outputFormat, scheme.Codecs); err != nil { t.Fatal(err) } secret, err := readSecrets(&buf) if err != nil { t.Fatal(err) } for _, secret := range secret { if got, want := string(secret.Data[secretItemKey]), secretItemValue; got != want { t.Fatalf("got: %q, want: %q", got, want) } } } func TestMergeInto(t *testing.T) { clientConfig := &mockClientConfig{namespace: "testns", namespaceSet: false} outputFormat := "json" pubKey, privKeys := newTestKeyPair(t) merge := func(t *testing.T, newSecret, oldSealedSecret []byte) *ssv1alpha1.SealedSecret { f, err := os.CreateTemp("", "*.json") if err != nil { t.Fatal(err) } if _, err := f.Write(oldSealedSecret); err != nil { t.Fatal(err) } f.Close() buf := bytes.NewBuffer(newSecret) if err := SealMergingInto(clientConfig, outputFormat, buf, f.Name(), scheme.Codecs, pubKey, ssv1alpha1.DefaultScope, false); err != nil { t.Fatal(err) } b, err := os.ReadFile(f.Name()) if err != nil { t.Fatal(err) } merged, err := decodeSealedSecret(scheme.Codecs, b) if err != nil { t.Fatal(err) } _, err = merged.Unseal(scheme.Codecs, privKeys) if err != nil { t.Fatal(err) } return merged } t.Run("added", func(t *testing.T) { merged := merge(t, mkTestSecret(t, "foo", "secret1"), mkTestSealedSecret(t, pubKey, "bar", "secret2"), ) checkAdded := func(m map[string]string, old, new string) { if got, want := len(m), 2; got != want { t.Fatalf("got: %d, want: %d", got, want) } if _, ok := m[old]; !ok { t.Fatalf("cannot find expected key") } if _, ok := m[new]; !ok { t.Fatalf("cannot find expected key") } } checkAdded(merged.Spec.EncryptedData, "foo", "bar") checkAdded(merged.Spec.Template.Annotations, "foo", "bar") checkAdded(merged.Spec.Template.Labels, "foo", "bar") }) t.Run("updated", func(t *testing.T) { origSrc := mkTestSealedSecret(t, pubKey, "foo", "secret2") orig, err := decodeSealedSecret(scheme.Codecs, origSrc) if err != nil { t.Fatal(err) } merged := merge(t, mkTestSecret(t, "foo", "secret1"), origSrc, ) checkUpdated := func(before, after map[string]string, key string) { if got, want := len(after), 1; got != want { t.Fatalf("got: %d, want: %d", got, want) } if old, new := before[key], after[key]; old == new { t.Fatalf("expecting %q and %q to be different", old, new) } } checkUpdated(orig.Spec.EncryptedData, merged.Spec.EncryptedData, "foo") checkUpdated(orig.Spec.Template.Annotations, merged.Spec.Template.Annotations, "foo") checkUpdated(orig.Spec.Template.Labels, merged.Spec.Template.Labels, "foo") }) t.Run("bad name", func(t *testing.T) { // should not fail even if input has a bad secret name because the name in existing existing sealed secret // should win (same for namespace). // TODO(mkm): test for case with scope mismatch too. merge(t, mkTestSecret(t, "foo", "secret1", withSecretName("badname"), withSecretNamespace("badns")), mkTestSealedSecret(t, pubKey, "bar", "secret2"), ) }) } // writeTempFile creates a temporary file, writes data into it and closes it. func writeTempFile(b []byte) (string, error) { tmp, err := os.CreateTemp("", "") if err != nil { return "", err } defer tmp.Close() if _, err := tmp.Write(b); err != nil { os.RemoveAll(tmp.Name()) return "", err } return tmp.Name(), nil } // testingKeypairFiles returns a path to a PEM encoded certificate and a PEM encoded private key // along with a function to be called to cleanup those files. func testingKeypairFiles(t *testing.T) (string, string, func()) { _, pk := newTestKeyPairSingle(t) cert, err := crypto.SignKey(rand.Reader, pk, time.Hour, "testcn") if err != nil { t.Fatal(err) } certFile, err := writeTempFile(pem.EncodeToMemory(&pem.Block{Type: certUtil.CertificateBlockType, Bytes: cert.Raw})) if err != nil { t.Fatal(err) } pkPEM, err := keyutil.MarshalPrivateKeyToPEM(pk) if err != nil { t.Fatal(err) } pkFile, err := writeTempFile(pkPEM) if err != nil { t.Fatal(err) } return certFile, pkFile, func() { os.RemoveAll(certFile) os.RemoveAll(pkFile) } } func sealTestItem(certFilename, secretNS, secretName, secretValue string, scope ssv1alpha1.SealingScope) (string, error) { var buf bytes.Buffer ctx := context.Background() clientConfig := testClientConfig() controllerNs := "default" controllerName := "controller" f, err := OpenCert(ctx, clientConfig, controllerNs, controllerName, certFilename) if err != nil { return "", err } defer f.Close() pubKey, err := ParseKey(f) if err != nil { return "", err } if err := EncryptSecretItem(&buf, secretName, secretNS, []byte(secretValue), scope, pubKey); err != nil { return "", err } return buf.String(), nil } func TestRaw(t *testing.T) { certFilename, privKeyFilename, cleanup := testingKeypairFiles(t) defer cleanup() const ( secretNS = "myns" secretName = "mysecret" secretItem = "foo" secretValue = "supersecret" ) testCases := []struct { ns string name string scope ssv1alpha1.SealingScope unsealErr string }{ // strict scope {ns: secretNS, name: secretName}, {ns: "youGiveRest", name: secretName, unsealErr: "no key could decrypt secret"}, {ns: secretNS, name: "aBadName", unsealErr: "no key could decrypt secret"}, // namespace-wide scope {scope: ssv1alpha1.NamespaceWideScope, ns: secretNS, name: secretName}, {scope: ssv1alpha1.NamespaceWideScope, ns: "youGiveRest", unsealErr: "no key could decrypt secret"}, {scope: ssv1alpha1.NamespaceWideScope, ns: "youGiveRest", name: "aBadName", unsealErr: "no key could decrypt secret"}, {scope: ssv1alpha1.NamespaceWideScope, ns: secretNS, name: ""}, {scope: ssv1alpha1.NamespaceWideScope, ns: secretNS, name: "aBadName"}, // cluster-wide scope {scope: ssv1alpha1.ClusterWideScope, ns: secretNS, name: secretName}, {scope: ssv1alpha1.ClusterWideScope, ns: "youGiveRest", name: secretName}, {scope: ssv1alpha1.ClusterWideScope, ns: secretNS, name: ""}, {scope: ssv1alpha1.ClusterWideScope, ns: secretNS, name: "aBadName"}, {scope: ssv1alpha1.ClusterWideScope, ns: "", name: ""}, {scope: ssv1alpha1.ClusterWideScope, ns: "", name: "aBadName"}, } for i, tc := range testCases { // encrypt an item with data from the testCase and put it // in a sealed secret with the metadata from the constants above t.Run(fmt.Sprint(i), func(t *testing.T) { enc, err := sealTestItem(certFilename, tc.ns, tc.name, secretValue, tc.scope) if err != nil { t.Fatal(err) } ss := &ssv1alpha1.SealedSecret{ ObjectMeta: metav1.ObjectMeta{ Namespace: secretNS, Name: secretName, Annotations: map[string]string{ fmt.Sprintf("sealedsecrets.bitnami.com/%s", tc.scope.String()): "true", }, }, Spec: ssv1alpha1.SealedSecretSpec{ EncryptedData: map[string]string{ secretItem: enc, }, }, } privKeys, err := readPrivKeys([]string{privKeyFilename}) if err != nil { t.Fatal(err) } sec, err := ss.Unseal(scheme.Codecs, privKeys) if tc.unsealErr != "" { if got, want := err.Error(), tc.unsealErr; !strings.HasPrefix(got, want) { t.Fatalf("got: %v, want: %v", err, want) } return } if err != nil { t.Fatal(err) } if got, want := string(sec.Data[secretItem]), secretValue; got != want { t.Errorf("got: %q, want: %q", got, want) } }) } } func newTestKeyPairSingle(t *testing.T) (*rsa.PublicKey, *rsa.PrivateKey) { privKey, _, err := crypto.GeneratePrivateKeyAndCert(2048, time.Hour, "testcn") if err != nil { t.Fatal(err) } return &privKey.PublicKey, privKey } func TestReadPrivKeySecret(t *testing.T) { outputFormat := "json" _, pkw := newTestKeyPairSingle(t) b, err := keyutil.MarshalPrivateKeyToPEM(pkw) if err != nil { t.Fatal(err) } sec := &v1.Secret{ Data: map[string][]byte{ "tls.key": b, }, } tmp, err := os.CreateTemp("", "") if err != nil { t.Fatal(err) } // defer os.RemoveAll(tmp.Name()) if err := resourceOutput(tmp, outputFormat, scheme.Codecs, v1.SchemeGroupVersion, sec); err != nil { t.Fatal(err) } tmp.Close() pkr, err := readPrivKey(tmp.Name()) if err != nil { t.Fatal(err) } if got, want := pkr.D.String(), pkw.D.String(); got != want { t.Errorf("got: %q, want: %q", got, want) } } func TestReadPrivKeyPEM(t *testing.T) { _, pkw := newTestKeyPairSingle(t) b, err := keyutil.MarshalPrivateKeyToPEM(pkw) if err != nil { t.Fatal(err) } tmp, err := os.CreateTemp("", "") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmp.Name()) if _, err := tmp.Write(b); err != nil { t.Fatal(err) } tmp.Close() pkr, err := readPrivKey(tmp.Name()) if err != nil { t.Fatal(err) } if got, want := pkr.D.String(), pkw.D.String(); got != want { t.Errorf("got: %q, want: %q", got, want) } } func TestNamespaceMismatchValidation(t *testing.T) { key, err := ParseKey(strings.NewReader(testCert)) if err != nil { t.Fatalf("Failed to parse test key: %v", err) } testCases := []struct { name string secret v1.Secret configNamespace string namespaceSet bool expectedError string }{ { name: "namespace mismatch should fail", secret: v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "secretns", }, Data: map[string][]byte{ "foo": []byte("sekret"), }, }, configNamespace: "flagns", namespaceSet: true, expectedError: "namespace mismatch: input secret is in namespace \"secretns\" but \"flagns\" was specified", }, { name: "matching namespaces should succeed", secret: v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "samens", }, Data: map[string][]byte{ "foo": []byte("sekret"), }, }, configNamespace: "samens", namespaceSet: true, expectedError: "", }, { name: "no namespace flag set should succeed", secret: v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "secretns", }, Data: map[string][]byte{ "foo": []byte("sekret"), }, }, configNamespace: "flagns", namespaceSet: false, expectedError: "", }, { name: "empty secret namespace with flag set should succeed", secret: v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "mysecret", Namespace: "", }, Data: map[string][]byte{ "foo": []byte("sekret"), }, }, configNamespace: "flagns", namespaceSet: true, expectedError: "", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Create a mock client config that returns the test namespace mockClientConfig := &mockClientConfig{ namespace: tc.configNamespace, namespaceSet: tc.namespaceSet, } outputFormat := "json" info, ok := runtime.SerializerInfoForMediaType(scheme.Codecs.SupportedMediaTypes(), runtime.ContentTypeJSON) if !ok { t.Fatalf("binary can't serialize JSON") } enc := scheme.Codecs.EncoderForVersion(info.Serializer, v1.SchemeGroupVersion) inbuf := bytes.Buffer{} if err := enc.Encode(&tc.secret, &inbuf); err != nil { t.Fatalf("Error encoding: %v", err) } outbuf := bytes.Buffer{} err := Seal(mockClientConfig, outputFormat, &inbuf, &outbuf, scheme.Codecs, key, ssv1alpha1.DefaultScope, false, "", "") if tc.expectedError != "" { if err == nil { t.Fatalf("Expected error %q but got nil", tc.expectedError) } if got, want := err.Error(), tc.expectedError; got != want { t.Errorf("got error: %q, want: %q", got, want) } } else { if err != nil { t.Fatalf("Unexpected error: %v", err) } } }) } } // mockClientConfig implements clientcmd.ClientConfig for testing type mockClientConfig struct { namespace string namespaceSet bool } func (m *mockClientConfig) Namespace() (string, bool, error) { return m.namespace, m.namespaceSet, nil } func (m *mockClientConfig) ClientConfig() (*rest.Config, error) { return &rest.Config{}, nil } func (m *mockClientConfig) ConfigAccess() clientcmd.ConfigAccess { return nil } func (m *mockClientConfig) RawConfig() (clientcmdapi.Config, error) { return clientcmdapi.Config{}, nil } ================================================ FILE: pkg/log/log.go ================================================ package log import ( "context" "io" "log/slog" ) // MultiStreamHandler slog handler for directing different type MultiStreamHandler struct { level slog.Level lowHandler slog.Handler highHandler slog.Handler } // New returns new MultiStreamHandler func New(outLow, outHigh io.Writer, format string, opts *slog.HandlerOptions) *MultiStreamHandler { if format == "json" { return &MultiStreamHandler{ level: opts.Level.Level(), lowHandler: slog.NewJSONHandler(outLow, opts), highHandler: slog.NewJSONHandler(outHigh, opts), } } return &MultiStreamHandler{ level: opts.Level.Level(), lowHandler: slog.NewTextHandler(outLow, opts), highHandler: slog.NewTextHandler(outHigh, opts), } } // Enabled check if log level is enabled func (m *MultiStreamHandler) Enabled(ctx context.Context, level slog.Level) bool { return level >= m.level.Level() } // Handle pass to Low or High handlers based on log level func (m *MultiStreamHandler) Handle(ctx context.Context, r slog.Record) error { if r.Level <= slog.LevelInfo.Level() { return m.lowHandler.Handle(ctx, r) } return m.highHandler.Handle(ctx, r) } func (m *MultiStreamHandler) WithAttrs(attrs []slog.Attr) slog.Handler { // Not used within the code panic("Not implemented") } func (m *MultiStreamHandler) WithGroup(string) slog.Handler { // Not used within the code panic("Not implemented") } ================================================ FILE: pkg/multidocyaml/multidocyaml.go ================================================ package multidocyaml import ( "bytes" "fmt" "gopkg.in/yaml.v2" ) func isMultiDocumentYAML(src []byte) bool { dec := yaml.NewDecoder(bytes.NewReader(src)) var dummy struct{} _ = dec.Decode(&dummy) return dec.Decode(&dummy) == nil } // EnsureNotMultiDoc returns an error if the yaml. func EnsureNotMultiDoc(src []byte) error { if isMultiDocumentYAML(src) { return fmt.Errorf("Multistream YAML not supported yet (see https://github.com/bitnami-labs/sealed-secrets/issues/114)") } return nil } ================================================ FILE: pkg/multidocyaml/multidocyaml_test.go ================================================ package multidocyaml import "testing" func TestIsMultiDocumentYAML(t *testing.T) { testCases := []struct { src string ok bool }{ {"foo", false}, {"foo\nbar\n", false}, {"---\nfoo", false}, {"foo\n---\n", true}, {"foo\n ---\n", false}, {"---\nfoo\n---\n", true}, } for _, tc := range testCases { if got, want := isMultiDocumentYAML([]byte(tc.src)), tc.ok; got != want { t.Errorf("got: %v, want: %v (src: %q)", got, want, tc.src) } } } ================================================ FILE: pkg/pflagenv/flagenv.go ================================================ // Package pflagenv implements a simple way to expose all your pflag flags as environmental variables. // // Commandline flags have more precedence over environment variables. // In order to use it just call pflagenv.SetFlagsFromEnv from an init function or from your main. // // You can call it either before or after your your flag.Parse invocation. // // This example will make it possible to set the default of --my_flag also via the MY_PROG_MY_FLAG // env var: // // var myflag = pflag.String("my_flag", "", "some flag") // // func init() { // pflagenv.SetFlagsFromEnv("MY_PROG", pflag.CommandLine) // } // // func main() { // pflag.Parse() // ... // } package pflagenv import ( "fmt" "os" "strings" flag "github.com/spf13/pflag" ) // SetFlagsFromEnv sets flag values from environment, e.g. PREFIX_FOO_BAR set -foo_bar. // It sets only flags that haven't been set explicitly. The defaults are preserved and -help // will still show the defaults provided in the code. func SetFlagsFromEnv(prefix string, fs *flag.FlagSet) { set := map[string]bool{} fs.Visit(func(f *flag.Flag) { set[f.Name] = true }) fs.VisitAll(func(f *flag.Flag) { // ignore flags set from the commandline if set[f.Name] { return } // remove trailing _ to reduce common errors with the prefix, i.e. people setting it to MY_PROG_ cleanPrefix := strings.TrimSuffix(prefix, "_") name := fmt.Sprintf("%s_%s", cleanPrefix, strings.Replace(strings.ToUpper(f.Name), "-", "_", -1)) if e, ok := os.LookupEnv(name); ok { _ = f.Value.Set(e) } }) } ================================================ FILE: pkg/pflagenv/flagenv_test.go ================================================ package pflagenv_test import ( "fmt" "os" "testing" "github.com/bitnami-labs/sealed-secrets/pkg/pflagenv" flag "github.com/spf13/pflag" ) func TestPflagenv(t *testing.T) { testCases := []struct { set bool val string want string }{ {false, "", "default"}, {true, "bar", "bar"}, {true, "", ""}, } for i, tc := range testCases { t.Run(fmt.Sprint(i), func(t *testing.T) { defer os.Unsetenv("MY_TEST_FOO") if tc.set { os.Setenv("MY_TEST_FOO", tc.val) } fs := flag.NewFlagSet("test", flag.PanicOnError) s := fs.String("foo", "default", "help") pflagenv.SetFlagsFromEnv("MY_TEST", fs) _ = fs.Parse(nil) if got, want := *s, tc.want; got != want { t.Errorf("got %q, want %q", got, want) } }) } } ================================================ FILE: schema-v1alpha1.yaml ================================================ openAPIV3Schema: description: |- SealedSecret is the K8s representation of a "sealed Secret" - a regular k8s Secret that has been sealed (encrypted) using the controller's key. properties: apiVersion: description: |- APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: description: |- Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object spec: description: SealedSecretSpec is the specification of a SealedSecret. properties: data: description: Data is deprecated and will be removed eventually. Use per-value EncryptedData instead. format: byte type: string encryptedData: additionalProperties: type: string type: object x-kubernetes-preserve-unknown-fields: true template: description: |- Template defines the structure of the Secret that will be created from this sealed secret. properties: data: additionalProperties: type: string description: Keys that should be templated using decrypted data. nullable: true type: object immutable: description: |- Immutable, if set to true, ensures that data stored in the Secret cannot be updated (only object metadata can be modified). If not set to true, the field can be modified at any time. Defaulted to nil. type: boolean metadata: description: |- Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata nullable: true properties: annotations: additionalProperties: type: string type: object finalizers: items: type: string type: array labels: additionalProperties: type: string type: object name: type: string namespace: type: string type: object x-kubernetes-preserve-unknown-fields: true type: description: Used to facilitate programmatic handling of secret data. type: string type: object required: - encryptedData type: object status: description: SealedSecretStatus is the most recently observed status of the SealedSecret. properties: conditions: description: Represents the latest available observations of a sealed secret's current state. items: description: SealedSecretCondition describes the state of a sealed secret at a certain point. properties: lastTransitionTime: description: Last time the condition transitioned from one status to another. format: date-time type: string lastUpdateTime: description: The last time this condition was updated. format: date-time type: string message: description: A human readable message indicating details about the transition. type: string reason: description: The reason for the condition's last transition. type: string status: description: |- Status of the condition for a sealed secret. Valid values for "Synced": "True", "False", or "Unknown". type: string type: description: |- Type of condition for a sealed secret. Valid value: "Synced" type: string required: - status - type type: object type: array observedGeneration: description: ObservedGeneration reflects the generation most recently observed by the sealed-secrets controller. format: int64 type: integer type: object required: - spec type: object ================================================ FILE: scripts/check-k8s ================================================ #!/bin/bash set -euo pipefail export K8S_CONTEXT="${K8S_CONTEXT}" if kubectl config current-context > /dev/null ;then k8s_current_context=$(kubectl config current-context); if [ "${k8s_current_context}" != "${K8S_CONTEXT}" ]; then \ echo "Expected k8s context '${K8S_CONTEXT}' but got '${k8s_current_context}'"; exit 1; fi else echo "Set up your k8s config for '${K8S_CONTEXT}' (using minikube or kind for example)"; exit 1; fi echo "'${K8S_CONTEXT}' is configured as kubectl's current context" ================================================ FILE: scripts/kubeseal-sudo ================================================ #!/usr/bin/env bash set -o errexit set -o nounset set -o pipefail # Constants RESET='\033[0m' GREEN='\033[38;5;2m' RED='\033[38;5;1m' YELLOW='\033[38;5;3m' # Axiliar functions ######################## # Log message to stderr # Arguments: # $1 - Message to log ######################### log() { printf "%b\n" "${*}" >&2 } ######################## # Log error message # Arguments: # $1 - Message to log ######################### error() { log "${RED}ERROR ${RESET} ==> ${*}" } ######################### # Redirects output to /dev/null unless debug mode is enabled # Globals: # DEBUG_MODE # Arguments: # $@ - Command to execute # Returns: # None ######################### silence() { if ${DEBUG_MODE:-false}; then "$@" else "$@" >/dev/null 2>&1 fi } print_menu() { local script script=$(basename "${BASH_SOURCE[0]}") log "${RED}NAME${RESET}" log " $(basename -s .sh "${BASH_SOURCE[0]}")" log "" log "${RED}SYNOPSIS${RESET}" log " $script [${YELLOW}-h${RESET}] [${YELLOW}-n ${GREEN}\"namespace\"${RESET}] [${YELLOW}-s ${GREEN}\"service_account\"${RESET}]" log "" log "${RED}DESCRIPTION${RESET}" log " Script to run kubeseal using a service account credentials." log "" log " The options are as follow:" log "" log " ${YELLOW}-n, --namespace ${GREEN}[namespace]${RESET} Namespace to use." log " ${YELLOW}-s, --service-account ${GREEN}[service_account]${RESET} ServiceAccount to use." log "" log "${RED}EXAMPLES${RESET}" log " $script --help" log " $script --service-account \"sealed-secrets\"" log " $script --service-account \"sealed-secrets\" --namespace \"kube-system\"" log "" } namespace="default" service_account="" help_menu=0 while [[ "$#" -gt 0 ]]; do case "$1" in -h|--help) help_menu=1 ;; -n|--namespace) shift; namespace="${1:?missing namespace}" ;; -s|--service-account) shift; service_account="${1:?missing service account}" ;; *) error "Invalid command line flag $1" >&2 exit 1 ;; esac shift done if [[ "$help_menu" -eq 1 ]]; then print_menu exit 0 fi if [[ -z "$service_account" ]]; then error "Missing ServiceAccount" exit 1 fi TMPKUBE=$(mktemp) kubectl config view --flatten --minify > "$TMPKUBE" export KUBECONFIG="$TMPKUBE" if ! silence kubectl --kubeconfig "$TMPKUBE" -n "$namespace" get sa "$service_account"; then error "Missing ServiceAccount \"$service_account\" in namespace \"$namespace\"" exit 1 fi sa_secret="$(kubectl --kubeconfig "$TMPKUBE" -n "$namespace" get sa "$service_account" -o jsonpath='{.secrets[0].name}')" sa_token="$(kubectl --kubeconfig "$TMPKUBE" -n "$namespace" get secret "$sa_secret" -o jsonpath='{.data.token}')" silence kubectl --kubeconfig "$TMPKUBE" config set-credentials "kubesudo:$namespace:$service_account" --token="$(echo "$sa_token" | base64 --decode)" silence kubectl --kubeconfig "$TMPKUBE" config set-context "$(kubectl config current-context)" --user="kubesudo:$namespace:$service_account" # We assume the controller is running in the same namespace as the ServiceAccount # and the controller service name is the same used for the ServiceAccount name kubeseal --controller-name="$service_account" --controller-namespace="$namespace" "$@" rm "$TMPKUBE" ================================================ FILE: scripts/release-check ================================================ #!/usr/bin/env bash set -o nounset function docker_tag_exists() { docker pull $1:$2 > /dev/null } function find_release() { curl -v --silent https://github.com/bitnami-labs/sealed-secrets/releases 2>&1 | grep -w kubeseal-$1 > /dev/null echo $? } RELEASE=$(find_release $2) if [ $RELEASE -ne 0 ] ; then if docker_tag_exists $1 $2; then echo 1 else echo 0 fi else echo 0 fi ================================================ FILE: site/.gitignore ================================================ .vagrant .DS_Store .sass-cache _ignore node_modules _site .jekyll .jekyll-metadata .bundle .vscode *.log *.js.map *.css.map .ruby-version public ================================================ FILE: site/README.md ================================================ # Website for [Sealed Secrets](https://sealed-secrets.netlify.app/) ## Deployment The website will be deployed to production each time a new commit gets merged into the main branch. It is deployed using Netlify. ## Requirements This site uses [Hugo](https://github.com/gohugoio/hugo) for rendering. It is recommended you run `hugo` locally to validate your changes render properly. ### Local Hugo Rendering Hugo is available for many platforms. It can be installed using: - Linux: Most native package managers - macOS: `brew install hugo` - Windows: `choco install hugo-extended -confirm` Once installed, you may run the following from the `/site` directory to access a rendered view of the documentation: ```bash cd site hugo server --disableFastRender ``` Access the site at [http://localhost:1313](http://localhost:1313). Press `Ctrl-C` when done viewing. ================================================ FILE: site/archetypes/default.md ================================================ --- title: "{{ replace .Name "-" " " | title }}" date: {{ .Date }} draft: true --- ================================================ FILE: site/config.yaml ================================================ baseURL: "https://sealed-secrets.netlify.app" languageCode: "en-us" title: "Sealed Secrets" theme: "template" outputs: home: [ "HTML", "REDIRECTS" ] disableKinds: - taxonomy - term pygmentsCodefences: true pygmentsStyle: "pygments" markup: highlight: anchorLineNos: false codeFences: true guessSyntax: false hl_Lines: "" lineAnchors: "" lineNoStart: 1 lineNos: false lineNumbersInTable: true noClasses: true style: native tabWidth: 4 menu: docs: - name: Overview url: /docs/ weight: 100 - name: Architecture url: /docs/architecture/ name: Demo url: /docs/demo/ - name: Scope url: /docs/scope/ - name: Update Images url: /docs/contributing/ params: twitter_url: "https://twitter.com/bitnami" github_url: "https://github.com/bitnami-labs/sealed-secrets" slack_url: "https://kubernetes.slack.com/messages/sealed-secrets" github_base_url: "https://github.com/bitnami-labs/sealed-secrets" use_advanced_docs: true docs_right_sidebar: true docs_search: false docs_search_index_name: index_name docs_search_api_key: api_key docs_versioning: true docs_latest: latest docs_versions: - latest mediaTypes: "text/netlify": delimiter: "" outputFormats: REDIRECTS: mediaType: "text/netlify" baseName: "_redirects" ================================================ FILE: site/content/community/_index.html ================================================ --- title: "Community" layout: section ---

Community

Do you want to help us build Sealed Secrets?

Check out GitHub

You can follow the work we do, be part of on-going discussions, and examine our improvement ideas on the GitHub project page.

If you are a newcomer, check out the good first issue label in the repository.

If you are ready to jump in and add code, tests, or help with documentation, follow the guidelines in the contributing documentation.

Join our Slack channel

Join the #sealed-secrets channel on the Kubernetes Slack and talk to us and over 600 other community members.

If you aren't already a member on the Kubernetes Slack workspace, please request an invitation.

We love discussing various Kubernetes Security workflows, patterns, helping with design and issues.

================================================ FILE: site/content/contributors/agarcia-oss.md ================================================ --- first_name: Alfredo last_name: García image: /img/team/agarcia-oss.png github_handle: agarcia-oss --- Maintainer ================================================ FILE: site/content/contributors/alvneiayu.md ================================================ --- first_name: Alvaro last_name: Neira image: /img/team/alvneiayu.png github_handle: alvneiayu --- Maintainer ================================================ FILE: site/content/contributors/index.md ================================================ --- headless: true --- ================================================ FILE: site/content/docs/CONTRIBUTING.md ================================================ # Contributing Leo urna molestie at elementum eu facilisis sed odio. Non nisi est sit amet facilisis. ## Magna etiam tempor orci Congue eu consequat [scope](../scope/) ac felis donec et odio. Elit eget gravida cum sociis. Tempus quam pellentesque nec nam aliquam. Dolor purus non enim praesent elementum facilisis leo. ## Cras semper auctor neque vitae tempus Quis eleifend quam adipiscing vitae proin. Mauris pellentesque pulvinar pellentesque habitant morbi tristique senectus et. ## Ultrices vitae auctor eu augue ut lectus arcu bibendum. Ut aliquam purus sit amet. Id ornare arcu odio ut sem nulla pharetra. Justo nec ultrices dui sapien eget mi proin sed. Nulla malesuada pellentesque elit eget gravida. ### Imperdiet sed euismod ```bash nisi porta ``` ### Egestas pretium aenean ```bash pharetr/magna/ac/placerat/vestibulum ``` ### Vitae proin sagittis nisl 1. Rhoncus mattis rhoncus: - Ante in nibh mauris cursus mattis. - Elementum eu facilisis sed odio morbi quis commodo odio. - Id faucibus nisl tincidunt eget nullam non nisi est sit. - In fermentum posuere urna nec. - Interdum velit laoreet id donec ultrices tincidunt arcu non sodales. ```bash At urna condimentum mattis pellentesque id. Amet venenatis urna cursus eget. ``` 1. Ut tortor pretium viverra suspendisse `potenti`: ```bash Ut tortor pretium viverra suspendisse potenti. ``` 1. Arcu dui vivamus arcu felis bibendum ut `tristique et`: ```bash Ut tellus elementum sagittis vitae et leo duis ut. Urna nunc id cursus metus aliquam. ``` In metus vulputate eu scelerisque felis imperdiet proin fermentum leo. Suscipit adipiscing bibendum est ultricies `integer quis`. Cras pulvinar mattis nunc sed blandit nisl pretium `fusce id velit`. 1. Porta non pulvinar neque laoreet suspendisse interdum consectetur.: ```bash Senectus et netus et malesuada fames ac turpis egestas. ``` Leo urna molestie at elementum eu facilisis sed odio. Non nisi est sit amet facilisis magna etiam tempor orci. ================================================ FILE: site/content/docs/_index.md ================================================ --- version: latest cascade: layout: docs --- # Sealed Secrets documentation Explore the [latest version docs](./latest/) to get started. ================================================ FILE: site/content/docs/img/_index.md ================================================ # Update Images ## Imperdiet sed euismod nisi porta - [Image](placeholder-750x250.png) gestas pretium aenean `plantuml`. - [Image](placeholder-750x250.png) gestas pretium aenean `vestibulum`. - Vitae proin sagittis nisl rhoncus mattis rhoncus. Ante in nibh mauris cursus mattis. Elementum eu facilisis sed odio morbi quis commodo odio. Id faucibus nisl tincidunt eget nullam non nisi est sit. In fermentum posuere urna nec. Interdum velit laoreet id donec ultrices tincidunt arcu non sodales. At urna condimentum mattis pellentesque id. Amet venenatis urna cursus eget. ================================================ FILE: site/content/docs/latest/README.md ================================================ # Sealed Secrets documentation Everything you need to know about Sealed Secrets. > NOTE: we are currently moving our docs to a new website and reviewing our documentation files. During the process, eventual broken links and minor inconsistences may appear. Please, feel free to contact us if you have any questions: file an [issue](https://github.com/bitnami-labs/sealed-secrets/issues), or talk to us on the [#Sealed Secrets slack channel](https://kubernetes.slack.com/messages/sealed-secrets). ## Documentation overview A high-level overview of how it's organized will help Sealed Secrets contributors and users know where to look for certain things. | Section | Description | | --------------------------- | ----------------------------------------------------------------------------------------------------| | [Tutorials](./tutorials/) | Start here if you're new to Sealed Secrets. A hands-on introduction to Seled Secrets for new users. | | [How-to guides](./howto/) | They guide you through the steps involved in addressing key problems and use-cases. | | [Background](./background/) | Dive into the overall architecture and implementation details of Sealed Secrets. | | [Reference](./reference/) | Technical information - developer guides, design proposals, examples, translations, etc. | ================================================ FILE: site/content/docs/latest/_index.md ================================================ --- version: latest cascade: layout: docs --- {{% readfile file="/content/docs/latest/README.md" %}} ================================================ FILE: site/content/docs/latest/background/README.md ================================================ # Sealed Secrets Background Big-picture explanations of higher-level Sealed Secrets concepts. Most useful for building understanding of a particular topic. | Background | Description | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | [Cryptography](./cryptography.md) | Dive into the overall Sealed Secrets Cryptography. | Alternatively, if you have a specific goal, but are already familiar with Sealed Secrets, take a look at our [How-to guides](../howto/README.md). These have more in-depth detail and can be applied to a broader set of features. Take a look at our [Reference section](../reference/README.md) when you need to know design decisions, detailed developer guides, etc. Finally, our [Tutorials section](../tutorials/README.md) contains step-by-step tutorials to help outline what Sealed Secrets is capable of. ================================================ FILE: site/content/docs/latest/background/_index.md ================================================ --- version: latest cascade: layout: docs --- {{% readfile file="/content/docs/latest/background/README.md" %}} ================================================ FILE: site/content/docs/latest/background/cryptography.md ================================================ # Cryptography details of Sealed Secrets - [Protocols and cryptographic tools used](#protocols-and-cryptographic-tools-used) - [Entropy considerations](#entropy-considerations) - [Sealing process](#sealing-process) - [Public/private key pair management](#publicprivate-key-pair-management) - [Secret encryption](#secret-encryption) - [Session key encryption](#session-key-encryption) - [Sealed Secret storage](#sealed-secret-storage) - [Diagram to summarize](#diagram-to-summarize) - [Decryption process](#decryption-process) - [Post-quantum cryptography considerations](#post-quantum-cryptography-considerations) - [Entropy source](#entropy-source) - [Analysis](#analysis) - [Associated documentation](#associated-documentation) - [AES-256-GCM](#aes-256-gcm) - [Analysis](#analysis-1) - [Recommendations](#recommendations) - [Associated documentation](#associated-documentation-1) - [SHA-256](#sha-256) - [Analysis](#analysis-2) - [Recommendations](#recommendations-1) - [Associated documentation](#associated-documentation-2) - [RSA-OAEP](#rsa-oaep) - [Analysis](#analysis-3) - [Recommendations](#recommendations-2) - [Associated documentation](#associated-documentation-3) ## Protocols and cryptographic tools used Sealed Secrets uses the following protocols for the secret management: - **AES-256-GCM** with a randomly generated single-use 32 bytes session key. Since the key is single-use, we do not use any nonce. The key is used to encrypt the secret, ensuring its confidentiality and integrity. - **RSA-OAEP**, with **SHA-256**. It is used to assure the confidentiality of the AES-256-GCM session key, following the *key encapsulation mechanism*. - **X509** certificates are used to manage RSA public keys. This public key contained in the certificate can be used to encrypt AES-256-GCM session key. Certificates generated by the sealed secrets controller are renewed every 30 days and have a validity span of 10 years. ## Entropy considerations The golang API used for the entropy is `crypto/rand`. The following description can be found about the entropy generator used regarding the host system: ``` // On Linux, FreeBSD, Dragonfly and Solaris, Reader uses getrandom(2) if // available, /dev/urandom otherwise. // On OpenBSD and macOS, Reader uses getentropy(2). // On other Unix-like systems, Reader reads from /dev/urandom. // On Windows systems, Reader uses the RtlGenRandom API. // On Wasm, Reader uses the Web Crypto API. ``` Those cryptographic APIs are known to provide a good cryptographic entropy, and are not vulnerable to cryptographic attacks unless the seed is known. For further information about those APIs: - [Linux/FreeBSD/Dragonfly/Solaris](https://linux.die.net/man/4/urandom) - [OpenBSD/macOS](https://www.freebsd.org/cgi/man.cgi?query=getentropy&sektion=3&format=html) - [Windows](https://download.microsoft.com/download/1/c/9/1c9813b8-089c-4fef-b2ad-ad80e79403ba/Whitepaper%20-%20The%20Windows%2010%20random%20number%20generation%20infrastructure.pdf) - [WASM](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues) ## Sealing process ### Public/private key pair management The controller looks for a cluster-wide private/public key pair on startup. If no key pair is found and none is provided manually, the controller generates a new 4096 bit (by default) RSA key pair. In both cases, the key pair is persisted in a regular Secret in the same namespace as the controller. The public key (in the form of a self-signed certificate if it was generated by the controller) should be made publicly available to anyone wanting to use Sealed Secrets with this cluster. > Note that it is possible to use your own X509 certificate with the command bellow: ```shell kubeseal --cert [https:/]/path/to/your-cert.pem ``` The certificate is printed in the controller log at startup and is also available via an HTTP GET request to `/v1/cert.pem` on the controller. ### Secret encryption The secret is encrypted by AES-256-GCM with a randomly-generated single-use 32 byte session key. The result of this operation will be called `AES encrypted data` in the next diagram, and the present step is the `1.`. ### Session key encryption The session key used by AES-256-GCM to encrypt the Secret is encapsulated with the controller's public key using RSA-OAEP with SHA256. The OAEP input content, called `label` in the next diagram, differs depending on the sealed secret controller scope configuration. This algorithm is only used to encrypt the AES session key. - Default scope configuration : `label` is equal to the concatenation of the Secret's namespace and the Secret's name. - Namespace-wide scope configuration : `label` is equal to the Secret's namespace. - Cluster-wide scope configuration : `label` is empty. The result of the RSA-OAEP encryption is called `RSA encrypted data` in the next diagram, and the present step is the `2.`. ### Sealed Secret storage The final Sealed Secret data format is the following (where `||` is the concatenation operator): `size of AES encrypted key (2 bytes) || RSA encrypted data || AES encrypted data` ### Diagram to summarize ``` Secret | │ K_s────────────►│ │ │ K_pub───────►│ │ │ │ 1. label───────►│ 2. │ │ │ ┌──────────────────────┬───────▼───────┬──────▼───────┐ Sealed Secret data = │size of AES encrypted │ RSA encrypted │ AES encrypted│ │key (2 bytes) │ data │ data │ └──────────────────────┴───────────────┴──────────────┘ K_s = 256 bits single-use session key, used by AES-GCM K_pub = Public key from the self-signed certificate, used by RSA-OAEP label = Additional input for RSA-OAEP encryption. Content differs depending on the scope configuration: * Default config : label = Secret's namespace || Secret's name * Namespace-wide : label = Secret's namespace * Cluster-wide : label is empty ``` ### Decryption process The decryption is simply the inversion of the encryption. `Size of AES encrypted key` is read and used to separate `RSA encrypted data` and `AES encrypted data` properly. Then the private key associated with the public key (see Session key encryption) is used with the `label` to decrypt the `RSA encrypted data`, effectively retrieving the AES session key. To end this process, the `AES encrypted data` is decrypted using the AES session key, therefore unsealing the original Secret. # Post-quantum cryptography considerations ## Entropy source ### Analysis Even if QRNG (Quantum Random Number Generator) are considered better than PRNG (Pseudo Random Number Generator) in a quantum cryptography context as well as in a non-quantum context, QRNG relies on a quantum mechanical phenomenon. It requires a physical device, therefore QRNG usage is out of Sealed Secrets scope, which will stay on the `crypto/rand` usage. ### Associated documentation [Combining a quantum random number generator and quantum-resistant algorithms into the GnuGPG open-source software](https://doi.org/10.1515/aot-2020-0021) ## AES-256-GCM ### Analysis AES-256-GCM is quantum resistant. Grover algorithm can reduce the bruteforce of the key from 2²⁵⁶ to 2¹²⁸ which is still considered very secure. Nevertheless, since AES uses unchangeable 128 bits blocks, Grover algorithm can in some cases decrease the complexity of the bruteforce to 2⁶⁴. ### Recommendations AES-256-GCM quantum security is not a concern. Cases with a bruteforce complexity of 2⁶⁴ are unlikely for Sealed Secret considering how AES is used in the project. Even assuming that 2⁶⁴ bruteforce is likely, it can still be considered secure today (but not in the long run). A recommendation is to look for a AES replacement that provide 128 bits post-quantum cryptographic security in any cases, such as ChaCha20-Poly1305. Applying this recommendation is considered low priority. ### Associated documentation [Quantum Security Analysis of AES](https://eprint.iacr.org/2019/272.pdf) [Critics on AES-256-GCM](https://soatok.blog/2020/05/13/why-aes-gcm-sucks/) [Security Analysis of ChaCha20-Poly1305 AEAD](https://www.cryptrec.go.jp/exreport/cryptrec-ex-2601-2016.pdf) ## SHA-256 ### Analysis SHA-256 is quantum resistant. Grover Algorithm can reduce the bruteforce from 2²⁵⁶ to 2¹²⁸ which is considered very secure. It is computationally cheaper to use a non-quantum algorithm to generate a collision than to employ a quantum computer. ### Recommendations No recommendations about SHA-256. ### Associated documentation [Cost analysis of hash collisions: Will quantum computers make SHARCS obsolete?](https://cr.yp.to/hash/collisioncost-20090823.pdf) ## RSA-OAEP ### Analysis RSA-OAEP, as any RSA algorithm, **is not quantum resistant**. Shor algorithm can be used to solve in a reasonable time 3 mathematical problems on which RSA cryptography is based on: integer factorization problem, the discrete logarithm problem and the elliptic-curve discrete logarithm problem. Therefore, RSA-OAEP is easily breakable for an attacker with quantum capability. ### Recommendations Replace RSA whenever feasible. This recommendation must be the highest priority regarding the post-quantum security of Sealed Secrets. There are three serious candidates to use instead of RSA: LMS and XMSS, which are Lattice-based, and McEliece with random Goppa codes, which is code-based and relies on SDP (Syndrome Decoding Problem). Those three algorithms are serious candidates for RSA replacement and the choice must be done carefully, without forgetting to study other algorithms such as NTRU. It is important to qualify this recommendation with a couple of prerequisites: - A standard or clear recommended Public Key Cryptography Algorithm replacement emerges in the industry. - A reliable Go implementation is available in a compatible Open Source license. Without such prerequisites in place, an RSA replacement cannot be commited upon. ### Associated documentation [LMS](https://datatracker.ietf.org/doc/html/rfc8554) [XMSS](https://datatracker.ietf.org/doc/html/rfc8391) [Lattice-based cryptography](https://en.wikipedia.org/wiki/Lattice-based_cryptography) [McEliece](https://ipnpr.jpl.nasa.gov/progress_report2/42-44/44N.PDF) [Syndrome Decoding Problem](https://en.wikipedia.org/wiki/Decoding_methods#Syndrome_decoding) [NIST on post-quantum algorithms](https://csrc.nist.gov/Projects/post-quantum-cryptography/round-3-submissions) [Quantum-Resistant Cryptography](https://arxiv.org/ftp/arxiv/papers/2112/2112.00399.pdf) ================================================ FILE: site/content/docs/latest/howto/README.md ================================================ # How-to guides How-to guides can be thought of as directions that guide the reader through the steps to achieve a specific end. They'll help you achieve a result but may require you to understand and adapt the steps to fit your specific requirements. Here you'll find short answers to "How do I...?" types of questions. | How-to-guides | Get stuff done | | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | [Validate Sealed Secrets](./validate-sealed-secrets.md) | Understand how to validate an existing Sealed Secret. | | Alternatively, our [Tutorials section](../tutorials/README.md) contains step-by-step tutorials to help outline what Sealed Secrets is capable of. Take a look at our [Reference section](../reference/README.md) when you need to know design decisions, detailed developer guides, etc. Finally, for a better understanding of Sealed Secrets architecture, our [Background section](../background/README.md) enables you to expand your knowledge. ================================================ FILE: site/content/docs/latest/howto/_index.md ================================================ --- version: latest cascade: layout: docs --- {{% readfile file="/content/docs/latest/howto/README.md" %}} ================================================ FILE: site/content/docs/latest/howto/validate-sealed-secrets.md ================================================ # How-to Validate existing Sealed Secrets The `validate` Sealed Secrets feature is useful for ensuring the correctness of Sealed Secrets, especially when they need to be shared or used in various Kubernetes environments. By validating Sealed Secrets, you can verify that the encryption and decryption processes are functioning as expected and that the secrets are protected properly. If you want to validate an existing sealed secret, `kubeseal` has the flag `--validate` to help you. Giving a file named `sealed-secrets.yaml` containing the following sealed secret: ```yaml apiVersion: bitnami.com/v1alpha1 kind: SealedSecret metadata: name: mysecret namespace: mynamespace spec: encryptedData: foo: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq..... ``` You can validate if the sealed secret was properly created or not: ```console $ cat sealed-secrets.yaml | kubeseal --validate ``` In case of an invalid sealed secret, `kubeseal` will show: ```console $ cat sealed-secrets.yaml | kubeseal --validate error: unable to decrypt sealed secret ``` ================================================ FILE: site/content/docs/latest/project/.placeholder ================================================ This directory is expected to contain symlinks to certain files of the root project directory. This way, these files will be rendered in Hugo. mklink readme.md ..\..\..\..\..\README.md mklink code-of-conduct.md ..\..\..\..\..\CODE_OF_CONDUCT.md mklink contributing.md ..\..\..\..\..\CONTRIBUTING.md mklink maintainers.md ..\..\..\..\..\MAINTAINERS.md mklink security.md ..\..\..\..\..\SECURITY.md mklink chart-readme.md ..\..\..\..\..\chart\sealed-secrets\README.md ================================================ FILE: site/content/docs/latest/project/_index.md ================================================ --- version: latest cascade: layout: docs --- {{% readfile file="/content/docs/latest/project/readme.md" %}} ================================================ FILE: site/content/docs/latest/reference/README.md ================================================ # Sealed Secrets Reference This section contains technical reference and developer guides for Sealed Secrets. | Reference | Description | | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | [FAQ](./faq.md) | Frequently Asked Questions. Alternatively, our [Tutorials section](../tutorials/README.md) contains step-by-step tutorials to help outline what Sealed Secrets is capable of while helping you achieve specific aims. If you have a specific goal but are already familiar with Sealed Secrets, take a look at our [How-to guides](../howto/README.md). These have more in-depth detail and can be applied to a broader set of features. Finally, for a better understanding of Sealed Secrets architecture, our [Background section](../background/README.md) enables you to expand your knowledge. ================================================ FILE: site/content/docs/latest/reference/_index.md ================================================ --- version: latest cascade: layout: docs --- {{% readfile file="/content/docs/latest/reference/README.md" %}} ================================================ FILE: site/content/docs/latest/reference/faq.md ================================================ # Frequently Asked Questions - [Will you still be able to decrypt if you no longer have access to your cluster?](#will-you-still-be-able-to-decrypt-if-you-no-longer-have-access-to-your-cluster) - [How can I do a backup of my SealedSecrets?](#how-can-i-do-a-backup-of-my-sealedsecrets) - [Can I decrypt my secrets offline with a backup key?](#can-i-decrypt-my-secrets-offline-with-a-backup-key) - [What flags are available for kubeseal?](#what-flags-are-available-for-kubeseal) - [How do I update parts of JSON/YAML/TOML/.. file encrypted with sealed secrets?](#how-do-i-update-parts-of-jsonyamltoml-file-encrypted-with-sealed-secrets) - [Can I bring my own (pre-generated) certificates?](#can-i-bring-my-own-pre-generated-certificates) - [How to use kubeseal if the controller is not running within the `kube-system` namespace?](#how-to-use-kubeseal-if-the-controller-is-not-running-within-the-kube-system-namespace) - [How to verify the images?](#how-to-verify-the-images) - [How to use one controller for a subset of namespaces](#how-to-use-one-controller-for-a-subset-of-namespaces) ## Will you still be able to decrypt if you no longer have access to your cluster? No, the private keys are only stored in the Secret managed by the controller (unless you have some other backup of your k8s objects). There are no backdoors - without that private key used to encrypt a given SealedSecrets, you can't decrypt it. If you can't get to the Secrets with the encryption keys, and you also can't get to the decrypted versions of your Secrets live in the cluster, then you will need to regenerate new passwords for everything, seal them again with a new sealing key, etc. ## How can I do a backup of my SealedSecrets? If you do want to make a backup of the encryption private keys, it's easy to do from an account with suitable access: ```shell kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml >main.key echo "---" >> main.key kubectl get secret -n kube-system sealed-secrets-key -o yaml >>main.key ``` > NOTE: You need the second statement only if you ever installed sealed-secrets older than version 0.9.x on your cluster. > NOTE: This file will contain the controller's public + private keys and should be kept omg-safe! To restore from a backup after some disaster, just put that secrets back before starting the controller - or if the controller was already started, replace the newly-created secrets and restart the controller: ```shell kubectl apply -f main.key kubectl delete pod -n kube-system -l name=sealed-secrets-controller ``` ## Can I decrypt my secrets offline with a backup key? While treating sealed-secrets as long term storage system for secrets is not the recommended use case, some people do have a legitimate requirement for being able to recover secrets when the k8s cluster is down and restoring a backup into a new `SealedSecret` controller deployment is not practical. If you have backed up one or more of your private keys (see previous question), you can use the `kubeseal --recovery-unseal --recovery-private-key file1.key,file2.key,...` command to decrypt a sealed secrets file. ## What flags are available for kubeseal? You can check the flags available using `kubeseal --help`. ## How do I update parts of JSON/YAML/TOML/.. file encrypted with sealed secrets? A kubernetes `Secret` resource contains multiple items, basically a flat map of key/value pairs. SealedSecrets operate at that level, and does not care what you put in the values. In other words it cannot make sense of any structured configuration file you might have put in a secret and thus cannot help you update individual fields in it. Since this is a common problem, especially when dealing with legacy applications, we do offer an [example](docs/examples/config-template) of a possible workaround. ## Can I bring my own (pre-generated) certificates? Yes, you can provide the controller with your own certificates, and it will consume them. Please check [here](docs/bring-your-own-certificates.md) for a workaround. ## How to use kubeseal if the controller is not running within the `kube-system` namespace? If you installed the controller in a different namespace than the default `kube-system`, you need to provide this namespace to the `kubeseal` command line tool. There are two options: 1. You can specify the namespace via the command line option `--controller-namespace `: ```shell kubeseal --controller-namespace sealed-secrets mysealedsecret.json ``` 2. Via the environment variable `SEALED_SECRETS_CONTROLLER_NAMESPACE`: ```shell export SEALED_SECRETS_CONTROLLER_NAMESPACE=sealed-secrets kubeseal mysealedsecret.json ``` ## How to verify the images? Our images are being signed using [cosign](https://github.com/sigstore/cosign). The signatures have been saved in our [GitHub Container Registry](https://ghcr.io/bitnami-labs/sealed-secrets-controller/signs). > Images up to and including v0.20.2 were signed using Cosign v1. Newer images are signed with Cosign v2. It is pretty simple to verify the images: ```console # export the COSIGN_VARIABLE setting up the GitHub container registry signs path $ export COSIGN_REPOSITORY=ghcr.io/bitnami-labs/sealed-secrets-controller/signs # verify the image uploaded in GHCR $ cosign verify --key .github/workflows/cosign.pub ghcr.io/bitnami-labs/sealed-secrets-controller:latest Verification for ghcr.io/bitnami-labs/sealed-secrets-controller:latest -- The following checks were performed on each of these signatures: - The cosign claims were validated - Existence of the claims in the transparency log was verified offline - The signatures were verified against the specified public key ... # verify the image uploaded in Dockerhub $ cosign verify --key .github/workflows/cosign.pub docker.io/bitnami/sealed-secrets-controller:latest Verification for index.docker.io/bitnami/sealed-secrets-controller:latest -- The following checks were performed on each of these signatures: - The cosign claims were validated - Existence of the claims in the transparency log was verified offline - The signatures were verified against the specified public key ... ``` ## How to use one controller for a subset of namespaces If you want to use one controller for more than one namespace, but not all namespaces, you can provide additional namespaces using the command line flag `--additional-namespaces=,,<...>`. Make sure you provide appropriate roles and rolebindings in the target namespaces, so the controller can manage the secrets in there. ================================================ FILE: site/content/docs/latest/tutorials/README.md ================================================ # Sealed Secrets tutorials This section of our documentation contains step-by-step tutorials to help outline what Sealed Secrets is capable of. We hope our tutorials make as few assumptions as possible and are broadly accessible to anyone with an interest in Sealed Secrets. They should also be a good place to start learning about Sealed Secrets, how it works and what it's capable of. | Tutorial | Description | |-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------| | [Getting started](./getting-started.md) | This guide walks you through the process of deploying Sealed Secrets for your cluster and installing an example Sealed Secrets. | | [Sealed Secrets controller installation](./install-sealed-secrets.md) | Here we cover the different alternatives to install the Sealed Secrets controller, with special notes for environments with restricted permissions. | Alternatively, if you have a specific goal, but are already familiar with Sealed Secrets, take a look at our [How-to guides](../howto/README.md). These have more in-depth detail and can be applied to a broader set of features. Take a look at our [Reference section](../reference/README.md) when you need to know design decisions, detailed developer guides, etc. Finally, for a better understanding of Sealed Secrets architecture, our [Background section](../background/README.md) enables you to expand your knowledge. ================================================ FILE: site/content/docs/latest/tutorials/_index.md ================================================ --- version: latest cascade: layout: docs --- {{% readfile file="/content/docs/latest/tutorials/README.md" %}} ================================================ FILE: site/content/docs/latest/tutorials/getting-started.md ================================================ # Get Started with Sealed Secrets ## Table of Contents 1. [Introduction](#introduction) 1. [Pre-requisites](#pre-requisites) 1. [Step 1: Install the Sealed Secrets](#step-1-install-sealed-secrets) 1. [Step 2: Encrypt local secrets into Sealed Secrets](#step-2-encrypt-local-secrets-into-sealed-secrets) ## Introduction [Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets) is commonly used for achieving declarative Kubernetes Secret Management. The project offers a mechanism to encrypt secrets locally. Since the Sealed Secrets are encrypted, they can be safely stored in a code repository. This enables an easy to implement GitOps flow that is very popular among the OSS community. This guide walks you through the process of deploying Sealed Secrets in your cluster and installing an example secret. ## Pre-requisites - Sealed Secrets assumes a working Kubernetes cluster (v1.16+), as well as the [`helm`](https://helm.sh/docs/intro/install/) (v3.1.0+) and [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/) command-line interfaces installed and configured to talk to your Kubernetes cluster. - Sealed Secrets has been tested with Amazon Elastic Kubernetes Service (EKS) Azure Kubernetes Service (AKS), Google Kubernetes Engine (GKE), minikube and Openshift. ## Step 1: Install Sealed Secrets Sealed Secrets is composed of two parts: - A cluster-side controller - A client-side utility: kubeseal ### Installing the sealed-secrets-controller The controller can be deployed using three different methods: direct yaml manifest installation, helm chart or carvel package. #### Sealed Secrets manifest Sealed secrets controller manifests are available from the [releases page](https://github.com/bitnami-labs/sealed-secrets/releases). You can choose the most convenient deployment for your cluster: - `controller.yaml` Is a full manifest description of all the components required for the Sealed Secrets controller to operate. This includes Cluster role permissions and CRD definitions. - `controller-norbac.yaml` Is a restricted version of the manifest descriptor. This version does not include CRDs nor Cluster roles. #### Helm chart The Sealed Secrets [Helm chart](https://helm.sh/) is officially supported and hosted in this GitHub repository. ```shell helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets helm install sealed-secrets-controller sealed-secrets/sealed-secrets \ --set namespace=kube-system \ ``` > The kubeseal CLI assumes that the controller is installed within the `kube-system` namespace by default with a deployment named `sealed-secrets-controller`. The above installation defines the same configuration to avoid unnecessary friction while using kubeseal. #### Carvel package It is also possible to install Sealed Secrets as a [Carvel package](https://carvel.dev/kapp-controller/docs/v0.46.0/packaging/). To do so, you'll need to install `kapp-controller` in the target cluster and then deploy the needed `Package` and `PackageInstall` manifests. ```shell # Deploy kapp-controller kapp deploy -a kc -f https://github.com/vmware-tanzu/carvel-kapp-controller/releases/latest/download/release.yml # Deploy the Sealed Secrets package in the cluster kapp deploy -a sealed-secrets-carvel -f https://raw.githubusercontent.com/bitnami-labs/sealed-secrets/main/carvel/package.yaml Changes Namespace Name Kind Conds. Age Op Op st. Wait to Rs Ri default sealedsecrets.bitnami.com.2.10.0 Package - - create - reconcile - - ... Succeeded kubectl get Package NAME PACKAGEMETADATA NAME VERSION AGE sealedsecrets.bitnami.com.2.10.0 sealedsecrets.bitnami.com 2.10.0 18s ``` Once the Package is available, it'll be necessary to execute the PackageInstall action, following the [carvel documentation](https://carvel.dev/kapp-controller/docs/v0.35.0/packaging-tutorial/#installing-a-package). ### Installing the kubeseal CLI #### Homebrew The `kubeseal` client is available on [homebrew](https://formulae.brew.sh/formula/kubeseal): ```bash brew install kubeseal ``` #### MacPorts The `kubeseal` client is also available on [MacPorts](https://ports.macports.org/port/kubeseal/summary): ```bash port install kubeseal ``` #### Nixpkgs The `kubeseal` client is also available on [Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=kubeseal&from=0&size=50&sort=relevance&type=packages&query=kubeseal): (**DISCLAIMER**: Not maintained by sealed secrets). ```bash nix-env -iA nixpkgs.kubeseal ``` #### Linux The `kubeseal` client can be installed on Linux, using the below commands: ```bash wget https://github.com/bitnami-labs/sealed-secrets/releases/download//kubeseal--linux-amd64.tar.gz tar -xvzf kubeseal--linux-amd64.tar.gz kubeseal sudo install -m 755 kubeseal /usr/local/bin/kubeseal ``` where `release-tag` is the [version tag](https://github.com/bitnami-labs/sealed-secrets/tags) of the kubeseal release you want to use. For example: `v0.21.0`. #### Installation from source If you just want the latest client tool, it can be installed into `$GOPATH/bin` with: ```bash go install github.com/bitnami-labs/sealed-secrets/cmd/kubeseal@main ``` You can specify a release tag or a commit SHA instead of `main`. The `go install` command will place the `kubeseal` binary at `$GOPATH/bin`: ```bash $(go env GOPATH)/bin/kubeseal ``` ## Step 2: Encrypt local secrets into Sealed Secrets ```bash # Create a json/yaml-encoded Secret somehow: # (note use of `--dry-run` - this is just a local file!) echo -n bar | kubectl create secret generic mysecret --dry-run=client --from-file=foo=/dev/stdin -o json >mysecret.json # This is the important bit: kubeseal -f mysecret.json -w mysealedsecret.json # At this point mysealedsecret.json is safe to upload to Github, # post on Twitter, etc. # Eventually: kubectl create -f mysealedsecret.json # Profit! kubectl get secret mysecret ``` > The `SealedSecret` and `Secret` must have **the same namespace and name**. This is a feature to prevent other users on the same cluster from re-using your sealed secrets. ================================================ FILE: site/content/docs/latest/tutorials/install-sealed-secrets.md ================================================ # Sealed Secrets controller installation - [Assumptions and prerequisites](#assumptions-and-prerequisites) - [Installing from Manifests](#installing-from-manifests) - [Installing in a GKE cluster](#installing-in-a-gke-cluster) - [Installing the Helm Chart](#installing-the-helm-chart) - [Installing in an Openshift cluster](#installing-in-an-openshift-cluster) - [Installing the Carvel package](#installing-the-carvel-package) ## Assumptions and prerequisites - You have access to an existing Kubernetes cluster (v1.16+). - You have [`kubectl`](https://kubernetes.io/docs/tasks/tools/) command-line interface installed and configured to talk to your Kubernetes cluster. - For the Helm installation, you have the [`helm`](https://helm.sh/docs/intro/install/) (v3.1.0+) command-line interface installed and configured to talk to your Kubernetes cluster. - For the Carvel installation, you have the [`kapp`](https://carvel.dev/kapp/docs/latest/install/) command-line interface installed and configured to talk to your Kubernetes cluster. The controller can be deployed using three different methods: direct yaml manifest installation, helm chart or carvel package. ## Installing from Manifests Sealed secrets controller manifests are available from the [releases page](https://github.com/bitnami-labs/sealed-secrets/releases). You can choose the most convenient deployment for your cluster: - `controller.yaml` Is a full manifest description of all the components required for the Sealed Secrets controller to operate. This includes Cluster role permissions and CRD definitions. - `controller-norbac.yaml` Is a restricted version of the manifest descriptor. This version does not include CRDs nor Cluster roles. To install the controller simply type: ```console $ kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/{{VERSION}}/controller.yaml role.rbac.authorization.k8s.io/sealed-secrets-service-proxier created rolebinding.rbac.authorization.k8s.io/sealed-secrets-controller created clusterrolebinding.rbac.authorization.k8s.io/sealed-secrets-controller created serviceaccount/sealed-secrets-controller created deployment.apps/sealed-secrets-controller created customresourcedefinition.apiextensions.k8s.io/sealedsecrets.bitnami.com configured rolebinding.rbac.authorization.k8s.io/sealed-secrets-service-proxier created service/sealed-secrets-controller created role.rbac.authorization.k8s.io/sealed-secrets-key-admin created clusterrole.rbac.authorization.k8s.io/secrets-unsealer configured ``` Where `{{VERSION}}` is the Sealed Secrets latest version (i.e `v0.22.0`). Once you deploy the manifest it will create the SealedSecret resource and install the controller into `kube-system` namespace, create a service account and necessary RBAC roles. After a few moments, the controller will start, generate a key pair, and be ready for operation. If it does not, check the controller logs. ### Installing in a GKE cluster Installing the controller on GKE clusters without admin rights might be problematic. For that, a `ClusterRoleBinding` will be needed to deploy the controller in the final command. Replace `{{your-email}}` with a valid email, and then deploy the cluster role binding: ```bash USER_EMAIL={{your-email}} kubectl create clusterrolebinding $USER-cluster-admin-binding --clusterrole=cluster-admin --user=$USER_EMAIL ``` Please refer to the [GKE how-to](../howto/) for additional instructions on that platform. ## Installing the Helm Chart The Sealed Secrets [Helm chart](https://helm.sh/) is officially supported and hosted in this GitHub repository. ```shell helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets helm install sealed-secrets-controller sealed-secrets/sealed-secrets \ --set namespace=kube-system \ ``` > The `kubeseal` CLI assumes that the controller is installed within the `kube-system` namespace by default with a deployment named `sealed-secrets-controller`. The above installation defines the same configuration to avoid unnecessary friction while using kubeseal. ### Installing in an Openshift cluster Openshift installations will require some minor adjustments to comply with the standard Container Security Context restrictions: ```yaml containerSecurityContext: enabled: true readOnlyRootFilesystem: true runAsNonRoot: true runAsUser: null podSecurityContext: ``` ## Installing the Carvel package It is also possible to install Sealed Secrets as a [Carvel package](https://carvel.dev/kapp-controller/docs/v0.46.0/packaging/). To do so, you'll need to install `kapp-controller` in the target cluster and then deploy the needed `Package` and `PackageInstall` manifests. ```console $ kapp deploy -a kc -f https://github.com/vmware-tanzu/carvel-kapp-controller/releases/latest/download/release.yml $ kapp deploy -a sealed-secrets-carvel -f https://raw.githubusercontent.com/bitnami-labs/sealed-secrets/main/carvel/package.yaml Changes Namespace Name Kind Conds. Age Op Op st. Wait to Rs Ri default sealedsecrets.bitnami.com.2.10.0 Package - - create - reconcile - - ... Succeeded $ kubectl get Package NAME PACKAGEMETADATA NAME VERSION AGE sealedsecrets.bitnami.com.2.10.0 sealedsecrets.bitnami.com 2.10.0 18s ``` Once the Package is available, it'll be necessary to execute the PackageInstall action, following the [carvel documentation](https://carvel.dev/kapp-controller/docs/v0.35.0/packaging-tutorial/#installing-a-package). ================================================ FILE: site/content/posts/_index.md ================================================ --- title: "Blog" id: blog url: /blog outputs: ["HTML", "RSS"] layout: listß _build: render: never list: never --- ================================================ FILE: site/content/resources/_index.html ================================================ --- layout: page title: Resources description: Sealed Secrets Resources id: resources ---

Resources

Some useful external resources about Sealed Secrets, such as videos, workshops, and community articles.

Sealed Secrets: Protecting your passwords before they reach Kubernetes

https://docs.bitnami.com/tutorials/sealed-secrets

Tanzu Development Center: Secret Management

https://tanzu.vmware.com/developer/

FluxCd configuration with Sealed Secrets

https://fluxcd.io/docs/guides/sealed-secrets/

================================================ FILE: site/data/docs/latest-toc.yml ================================================ --- toc: - title: About Sealed Secrets subfolderitems: - page: What is Sealed Secrets url: /project/readme - page: Sealed Secrets helm chart url: /project/chart-readme - title: Tutorials subfolderitems: - url: /tutorials/getting-started page: Get Started with Sealed Secrets - url: /tutorials/install-sealed-secrets page: Sealed Secrets controller installation - title: How-to guides subfolderitems: - url: /howto/validate-sealed-secrets page: Validate an existing Sealed Secret - title: Background subfolderitems: - url: /background/cryptography page: Cryptography details - title: Reference subfolderitems: - url: /reference/faq page: FAQ ================================================ FILE: site/data/docs/toc-mapping.yml ================================================ # This file can be used to explicitly map a release to a specific table-of-contents # (TOC). You'll want to use this after any revamps to information architecture, to ensure # that the navigation for older versions still work. latest: latest-toc ================================================ FILE: site/resources/_gen/assets/scss/scss/site.scss_8967e03afb92eb0cac064520bf021ba2.content ================================================ body{font-family:"Metropolis-Light",Helvetica,sans-serif;margin:0px;line-height:1.25}.wrapper{max-width:980px;margin:0px auto;padding:20px}@media only screen and (max-width: 767px){.wrapper{max-width:100%}}@media only screen and (min-width: 1440px){.wrapper.docs{max-width:80%}}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}h1,h2,h3,h4,h5,h6{font-weight:300}h1{font-size:28px}h2{font-size:22px;color:#333}h3{font-size:20px}h4{font-size:18px}li{list-style-type:none;display:inline;padding-right:25px;font-size:14px;line-height:1.7em}li:last-of-type{padding-right:0px}p{line-height:1.7em;font-weight:300;font-size:16px;color:#333}p.intro{font-size:18px}a{font-size:16px;text-decoration:none;color:#0095D3;font-family:"Metropolis-Medium",Helvetica,sans-serif}button{background-color:unset;border:none}.button{color:#0095D3;font-size:12px;font-weight:600;background-color:#fff;border-radius:3px;padding:14px 10px;min-width:200px;text-transform:uppercase;border:1px solid #fff}.button.secondary{background-color:#0091DA;color:#fff}.button.tertiary{border:1px solid #0095D3}.buttons{margin-top:40px}.buttons .button:first-of-type{margin-right:30px}@media only screen and (max-width: 767px){.buttons .button:first-of-type{margin:0px 0px 20px 0px}}.strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.bg-grey{background-color:#F2F2F2}.grid.three{display:grid;grid-template-columns:1fr 1fr 1fr;row-gap:20px;column-gap:20px}@media only screen and (max-width: 767px){.grid.three{grid-template-columns:1fr}}.grid.two{display:grid;grid-template-columns:1fr 1fr}@media only screen and (max-width: 767px){.grid.two{grid-template-columns:1fr}}.rounded-circle{border-radius:50% !important}noscript{background-color:rgba(255,0,0,0.25);display:block;font-size:14px;font-weight:bold;padding:10px;margin:0 20px 20px 0}.filter-blue{filter:brightness(0) saturate(100%) invert(39%) sepia(55%) saturate(4454%) hue-rotate(177deg) brightness(99%) contrast(104%)}@font-face{font-family:"Metropolis-Bold";src:url("/fonts/Metropolis-Bold.eot");src:url("/fonts/Metropolis-Bold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Bold.woff2") format("woff2"),url("/fonts/Metropolis-Bold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-BoldItalic";src:url("/fonts/Metropolis-BoldItalic.eot");src:url("/fonts/Metropolis-BoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-BoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-BoldItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Light";src:url("/fonts/Metropolis-Light.eot");src:url("/fonts/Metropolis-Light.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Light.woff2") format("woff2"),url("/fonts/Metropolis-Light.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-LightItalic";src:url("/fonts/Metropolis-LightItalic.eot");src:url("/fonts/Metropolis-LightItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-LightItalic.woff2") format("woff2"),url("/fonts/Metropolis-LightItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Regular";src:url("/fonts/Metropolis-Regular.eot");src:url("/fonts/Metropolis-Regular.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Regular.woff2") format("woff2"),url("/fonts/Metropolis-Regular.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-RegularItalic";src:url("/fonts/Metropolis-RegularItalic.eot");src:url("/fonts/Metropolis-RegularItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-RegularItalic.woff2") format("woff2"),url("/fonts/Metropolis-RegularItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Medium";src:url("/fonts/Metropolis-Medium.eot");src:url("/fonts/Metropolis-Medium.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Medium.woff2") format("woff2"),url("/fonts/Metropolis-Medium.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-MediumItalic";src:url("/fonts/Metropolis-MediumItalic.eot");src:url("/fonts/Metropolis-MediumItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-MediumItalic.woff2") format("woff2"),url("/fonts/Metropolis-MediumItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBold";src:url("/fonts/Metropolis-SemiBold.eot");src:url("/fonts/Metropolis-SemiBold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBold.woff2") format("woff2"),url("/fonts/Metropolis-SemiBold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBoldItalic";src:url("/fonts/Metropolis-SemiBoldItalic.eot");src:url("/fonts/Metropolis-SemiBoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-SemiBoldItalic.woff") format("woff");font-weight:normal;font-style:normal}header .wrapper{padding:10px 20px;min-height:52px;display:flex;align-items:center;justify-content:space-between}header .desktop-links{padding-left:0px}header a{color:#333;font-family:"Metropolis-Light",Helvetica,sans-serif}header a.active{font-family:"Metropolis-Medium",Helvetica,sans-serif}header li img{vertical-align:bottom;margin-right:10px}header .mobile{display:none}@media only screen and (min-width: 768px) and (max-width: 1279px){header .desktop-links li{padding-right:10px}}@media only screen and (max-width: 767px){header{position:relative}header .expanded-icon{display:none;padding:11px 3px 0px 0px}header .collapsed-icon{padding-top:12px}header .mobile-menu-visible .mobile{display:block}header .mobile-menu-visible .mobile .collapsed-icon{display:none}header .mobile-menu-visible .mobile .expanded-icon{display:block}header .desktop-links{display:none}header .mobile{display:block}header button{float:right}header button:focus{outline:none}header ul{padding-left:0px}header ul li{display:block;margin:20px 0px}header .mobile-menu{position:absolute;background-color:#fff;width:100%;top:70px;left:0px;padding-bottom:20px;display:none;z-index:10}header .mobile-menu .header-links{margin:0px 20px}header .mobile-menu .social{margin:0px 20px;padding-top:20px}header .mobile-menu .social img{vertical-align:middle;padding-right:10px}header .mobile-menu .social a{font-size:14px;padding-right:35px}header .mobile-menu .social a:last-of-type{padding-right:0px}}body{font-family:"Metropolis-Light",Helvetica,sans-serif;margin:0px;line-height:1.25}.wrapper{max-width:980px;margin:0px auto;padding:20px}@media only screen and (max-width: 767px){.wrapper{max-width:100%}}@media only screen and (min-width: 1440px){.wrapper.docs{max-width:80%}}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}h1,h2,h3,h4,h5,h6{font-weight:300}h1{font-size:28px}h2{font-size:22px;color:#333}h3{font-size:20px}h4{font-size:18px}li{list-style-type:none;display:inline;padding-right:25px;font-size:14px;line-height:1.7em}li:last-of-type{padding-right:0px}p{line-height:1.7em;font-weight:300;font-size:16px;color:#333}p.intro{font-size:18px}a{font-size:16px;text-decoration:none;color:#0095D3;font-family:"Metropolis-Medium",Helvetica,sans-serif}button{background-color:unset;border:none}.button{color:#0095D3;font-size:12px;font-weight:600;background-color:#fff;border-radius:3px;padding:14px 10px;min-width:200px;text-transform:uppercase;border:1px solid #fff}.button.secondary{background-color:#0091DA;color:#fff}.button.tertiary{border:1px solid #0095D3}.buttons{margin-top:40px}.buttons .button:first-of-type{margin-right:30px}@media only screen and (max-width: 767px){.buttons .button:first-of-type{margin:0px 0px 20px 0px}}.strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.bg-grey{background-color:#F2F2F2}.grid.three{display:grid;grid-template-columns:1fr 1fr 1fr;row-gap:20px;column-gap:20px}@media only screen and (max-width: 767px){.grid.three{grid-template-columns:1fr}}.grid.two{display:grid;grid-template-columns:1fr 1fr}@media only screen and (max-width: 767px){.grid.two{grid-template-columns:1fr}}.rounded-circle{border-radius:50% !important}noscript{background-color:rgba(255,0,0,0.25);display:block;font-size:14px;font-weight:bold;padding:10px;margin:0 20px 20px 0}.filter-blue{filter:brightness(0) saturate(100%) invert(39%) sepia(55%) saturate(4454%) hue-rotate(177deg) brightness(99%) contrast(104%)}@font-face{font-family:"Metropolis-Bold";src:url("/fonts/Metropolis-Bold.eot");src:url("/fonts/Metropolis-Bold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Bold.woff2") format("woff2"),url("/fonts/Metropolis-Bold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-BoldItalic";src:url("/fonts/Metropolis-BoldItalic.eot");src:url("/fonts/Metropolis-BoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-BoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-BoldItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Light";src:url("/fonts/Metropolis-Light.eot");src:url("/fonts/Metropolis-Light.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Light.woff2") format("woff2"),url("/fonts/Metropolis-Light.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-LightItalic";src:url("/fonts/Metropolis-LightItalic.eot");src:url("/fonts/Metropolis-LightItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-LightItalic.woff2") format("woff2"),url("/fonts/Metropolis-LightItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Regular";src:url("/fonts/Metropolis-Regular.eot");src:url("/fonts/Metropolis-Regular.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Regular.woff2") format("woff2"),url("/fonts/Metropolis-Regular.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-RegularItalic";src:url("/fonts/Metropolis-RegularItalic.eot");src:url("/fonts/Metropolis-RegularItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-RegularItalic.woff2") format("woff2"),url("/fonts/Metropolis-RegularItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Medium";src:url("/fonts/Metropolis-Medium.eot");src:url("/fonts/Metropolis-Medium.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Medium.woff2") format("woff2"),url("/fonts/Metropolis-Medium.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-MediumItalic";src:url("/fonts/Metropolis-MediumItalic.eot");src:url("/fonts/Metropolis-MediumItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-MediumItalic.woff2") format("woff2"),url("/fonts/Metropolis-MediumItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBold";src:url("/fonts/Metropolis-SemiBold.eot");src:url("/fonts/Metropolis-SemiBold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBold.woff2") format("woff2"),url("/fonts/Metropolis-SemiBold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBoldItalic";src:url("/fonts/Metropolis-SemiBoldItalic.eot");src:url("/fonts/Metropolis-SemiBoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-SemiBoldItalic.woff") format("woff");font-weight:normal;font-style:normal}footer .top-links{min-height:52px;display:flex;align-items:center;justify-content:space-between}footer .left-links{padding:0px}footer .left-links li img{vertical-align:bottom;margin-right:10px}footer .left-links li a{color:#333;font-weight:300;font-size:12px;font-family:"Metropolis-Light",Helvetica,sans-serif}footer .left-links .mobile{display:none}footer .right-links p{margin:0px}footer .right-links .copywrite{font-size:12px;padding-right:10px}footer .right-links .copywrite a{font-size:12px;color:#333;font-family:"Metropolis-Light",Helvetica,sans-serif}footer .right-links a{vertical-align:middle}footer .bottom-links{margin:10px 0px 30px 0px;float:right}footer .bottom-links p{font-size:12px}footer .bottom-links a{font-size:12px;font-family:"Metropolis-Light",Helvetica,sans-serif}footer .bottom-links img{max-width:75px;vertical-align:middle;margin-left:30px}@media only screen and (max-width: 767px){footer .footer-links{display:block}footer .footer-links .right-links{display:none}footer .footer-links .left-links{float:none;margin:10px 0px}footer .footer-links .left-links .desktop{display:none}footer .footer-links .left-links .mobile{display:inline}footer .footer-links .left-links .copywrite{display:block;margin-top:20px}footer .bottom-links{margin:10px 0px 20px 0px;float:none}footer .bottom-links img{margin-left:0px;display:block;margin-top:10px}}body{font-family:"Metropolis-Light",Helvetica,sans-serif;margin:0px;line-height:1.25}.wrapper{max-width:980px;margin:0px auto;padding:20px}@media only screen and (max-width: 767px){.wrapper{max-width:100%}}@media only screen and (min-width: 1440px){.wrapper.docs{max-width:80%}}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}h1,h2,h3,h4,h5,h6{font-weight:300}h1{font-size:28px}h2{font-size:22px;color:#333}h3{font-size:20px}h4{font-size:18px}li{list-style-type:none;display:inline;padding-right:25px;font-size:14px;line-height:1.7em}li:last-of-type{padding-right:0px}p{line-height:1.7em;font-weight:300;font-size:16px;color:#333}p.intro{font-size:18px}a{font-size:16px;text-decoration:none;color:#0095D3;font-family:"Metropolis-Medium",Helvetica,sans-serif}button{background-color:unset;border:none}.button{color:#0095D3;font-size:12px;font-weight:600;background-color:#fff;border-radius:3px;padding:14px 10px;min-width:200px;text-transform:uppercase;border:1px solid #fff}.button.secondary{background-color:#0091DA;color:#fff}.button.tertiary{border:1px solid #0095D3}.buttons{margin-top:40px}.buttons .button:first-of-type{margin-right:30px}@media only screen and (max-width: 767px){.buttons .button:first-of-type{margin:0px 0px 20px 0px}}.strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.bg-grey{background-color:#F2F2F2}.grid.three{display:grid;grid-template-columns:1fr 1fr 1fr;row-gap:20px;column-gap:20px}@media only screen and (max-width: 767px){.grid.three{grid-template-columns:1fr}}.grid.two{display:grid;grid-template-columns:1fr 1fr}@media only screen and (max-width: 767px){.grid.two{grid-template-columns:1fr}}.rounded-circle{border-radius:50% !important}noscript{background-color:rgba(255,0,0,0.25);display:block;font-size:14px;font-weight:bold;padding:10px;margin:0 20px 20px 0}.filter-blue{filter:brightness(0) saturate(100%) invert(39%) sepia(55%) saturate(4454%) hue-rotate(177deg) brightness(99%) contrast(104%)}@font-face{font-family:"Metropolis-Bold";src:url("/fonts/Metropolis-Bold.eot");src:url("/fonts/Metropolis-Bold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Bold.woff2") format("woff2"),url("/fonts/Metropolis-Bold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-BoldItalic";src:url("/fonts/Metropolis-BoldItalic.eot");src:url("/fonts/Metropolis-BoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-BoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-BoldItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Light";src:url("/fonts/Metropolis-Light.eot");src:url("/fonts/Metropolis-Light.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Light.woff2") format("woff2"),url("/fonts/Metropolis-Light.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-LightItalic";src:url("/fonts/Metropolis-LightItalic.eot");src:url("/fonts/Metropolis-LightItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-LightItalic.woff2") format("woff2"),url("/fonts/Metropolis-LightItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Regular";src:url("/fonts/Metropolis-Regular.eot");src:url("/fonts/Metropolis-Regular.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Regular.woff2") format("woff2"),url("/fonts/Metropolis-Regular.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-RegularItalic";src:url("/fonts/Metropolis-RegularItalic.eot");src:url("/fonts/Metropolis-RegularItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-RegularItalic.woff2") format("woff2"),url("/fonts/Metropolis-RegularItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-Medium";src:url("/fonts/Metropolis-Medium.eot");src:url("/fonts/Metropolis-Medium.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-Medium.woff2") format("woff2"),url("/fonts/Metropolis-Medium.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-MediumItalic";src:url("/fonts/Metropolis-MediumItalic.eot");src:url("/fonts/Metropolis-MediumItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-MediumItalic.woff2") format("woff2"),url("/fonts/Metropolis-MediumItalic.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBold";src:url("/fonts/Metropolis-SemiBold.eot");src:url("/fonts/Metropolis-SemiBold.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBold.woff2") format("woff2"),url("/fonts/Metropolis-SemiBold.woff") format("woff");font-weight:normal;font-style:normal}@font-face{font-family:"Metropolis-SemiBoldItalic";src:url("/fonts/Metropolis-SemiBoldItalic.eot");src:url("/fonts/Metropolis-SemiBoldItalic.eot?#iefix") format("embedded-opentype"),url("/fonts/Metropolis-SemiBoldItalic.woff2") format("woff2"),url("/fonts/Metropolis-SemiBoldItalic.woff") format("woff");font-weight:normal;font-style:normal}.hero{background-color:#0091DA;color:#fff}.hero .text-block{max-width:550px;padding:0px 0px 10px 0px}.hero .text-block p{margin-bottom:20px;font-size:18px;color:#fff}.hero .text-block h2{font-size:36px}.hero.homepage{background-position:center center;background-repeat:no-repeat;background-size:cover;padding-bottom:80px}.hero.homepage h1{font-size:36px}@media only screen and (max-width: 767px){.hero .text-block{max-width:unset;margin-right:0px}.hero .button{display:block;text-align:center}.hero.homepage{background-image:none}}.grid-container{margin-top:-80px}.grid-container .grid.three{padding-bottom:20px}.grid-container .grid.three .card{position:relative;padding:30px 20px;background-color:#fff;text-align:center;box-shadow:0px 2px 10px rgba(0,0,0,0.2)}.grid-container .grid.three .card h3{color:#333;font-size:22px}.grid-container .grid.three .card p{color:#333}.introduction .grid.two{column-gap:140px;padding:35px 20px}.introduction .grid.two p{margin:0px;font-size:16px}.introduction .grid.two p.strong{color:#333}@media only screen and (max-width: 767px){.introduction{padding:0px 20px}.introduction .col:first-of-type{padding-bottom:50px}}.use-cases .grid{grid-template-columns:220px 1fr;margin-bottom:30px;grid-template-areas:"image text"}.use-cases .grid .image{background-color:#0091DA;text-align:center;display:flex;align-items:center;justify-content:center;grid-area:image}.use-cases .grid .image img{justify-self:center}.use-cases .grid .text{border:1px solid #F2F2F2;padding:30px;grid-area:text}.use-cases .grid .text a.button{display:block;max-width:138px;text-align:center;padding:5px 10px;min-width:unset}.use-cases .grid.image-right{grid-template-columns:1fr 220px;grid-template-areas:"text image"}@media only screen and (max-width: 767px){.use-cases .grid.image-right{grid-template-columns:1fr;grid-template-areas:"image" "text"}}@media only screen and (max-width: 767px){.use-cases .grid{grid-template-columns:1fr;grid-template-rows:minmax(160px, 1fr);grid-template-areas:"image" "text"}}.use-cases h2{color:#111}.use-cases p.strong{color:#1B3951;font-size:16px}.team{background-color:#1D428A}.team h2,.team h3,.team p{color:#fff}.team p{font-size:16px}.team a{color:#fff;font-weight:300;text-decoration:underline}.team .grid.three{row-gap:40px;margin:40px 0px}.team .bio{display:grid;grid-template-columns:120px 1fr;column-gap:20px}.team .bio .image img{height:120px;width:120px}.team .bio .info{align-self:center}.team .bio .info p{margin:0px}.team .bio .info p.name{font-size:16px;font-family:"Metropolis-Medium",Helvetica,sans-serif}.team .bio .info p.position{font-size:14px}.avatar{filter:grayscale(100%);transition:filter 0.6s ease-in-out}.avatar:hover{filter:grayscale(0%)}.hero.subpage-hero{background-position:center center;background-repeat:no-repeat;background-size:cover;padding-bottom:90px}.hero.subpage-hero h1{font-size:46px;text-align:center}@media only screen and (max-width: 767px){.hero.subpage-hero h1{font-size:26px}}.experimental .grid.three .col{padding:0px}.experimental .icon{background-color:#0091DA;padding:25px;min-height:95px;display:flex;align-items:center;justify-content:center}.experimental .content{padding:25px}.experimental .content .example{background-color:#F2F2F2}.blog{padding-bottom:50px}.blog .col{border:1px solid #F2F2F2}.blog .col img{width:100%}.blog .col .content{padding:0px 20px}.blog.landing{background-color:#fff;margin-top:-90px}.blog.landing h3 a{font-size:16px}.blog.landing .pagination{margin:30px auto 50px auto}.blog.landing .pagination ul{padding:0px;text-align:center}.blog.landing .pagination ul li{padding:0px}.blog.landing .pagination ul li a{padding:5px 10px}.blog.landing .pagination ul li a.active{background-color:#F2F2F2;border-radius:50%}.blog.landing .pagination ul li.left-arrow{margin-right:15px}.blog.landing .pagination ul li.right-arrow{margin-left:15px}.blog .blog-post{background-color:#fff;margin:-110px 0px 0px -30px;padding:30px 90px 30px 30px}.blog .blog-post .author{color:#0095D3;margin:0px}.blog .blog-post .date{color:#111;margin:0px;font-weight:600}.blog .blog-post .header,.blog .blog-post h4{color:#111;font-weight:600}.blog .blog-post a{font-size:16px}.blog .blog-post ul{list-style-type:disc;padding-left:20px}.blog .blog-post ul li{list-style-type:unset;display:list-item;margin-bottom:10px;font-size:14px;color:#333;line-height:1.6em;list-style-image:url(/img/arrow.svg)}.blog .blog-post ul li:first-child{margin-top:10px}.blog .blog-post ol li{list-style-type:decimal;display:list-item;margin-bottom:10px;font-size:16px;color:#333}.blog .blog-post ol li:first-child{margin-top:10px}.blog .blog-post code{border:2px solid #EFEFEF;color:#333;padding:2px 8px}.blog .blog-post pre code{display:block;border:15px solid #EFEFEF;padding:15px;margin-bottom:30px;overflow-x:auto}.blog .blog-post img{max-width:100%}.blog .blog-post strong{font-family:"Metropolis-Medium",Helvetica,sans-serif}.getting-started{background-color:#F2F2F2;color:#111}.getting-started p{color:#111;font-size:16px}.getting-started .left-side{width:50%;float:left}.getting-started .right-side{width:25%;float:right}.getting-started h2{font-size:30px;margin-bottom:0px}.getting-started a{display:block;max-width:138px;text-align:center;padding:10px;min-width:unset}.getting-started .button{margin-top:50px;border:1px solid #0095D3}@media only screen and (max-width: 767px){.getting-started .wrapper{padding-bottom:40px}.getting-started .left-side{width:100%;float:none}.getting-started .right-side{width:100%;float:none}.getting-started .button{display:block;text-align:center;max-width:unset;margin-top:20px}}.subpage{background-color:#fff;margin-top:-90px;padding:30px 30px 50px 30px}.subpage .section-header{margin-top:3rem;font-weight:600;font-size:20px}.subpage .embed-responsive{position:relative}.subpage .embed-responsive:before{padding-top:56.25%;display:block;content:""}.subpage .embed-responsive .embed-responsive-item{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.subpage .grid{margin-bottom:20px}.subpage .grid .col{border:1px solid #F2F2F2}.subpage .grid .col .icon{display:flex;align-items:center;justify-content:center;min-height:140px}.subpage .grid .col .content{padding:0px 20px 20px 20px}.subpage .grid .col .content.plugins{padding-top:20px}.subpage .grid .col .content.plugins img{display:block;margin:0px auto 5px auto}.subpage .grid .col .content h3{margin-top:0px;text-align:center}.subpage .grid .col .content h3 a{font-size:20px}.subpage .grid .col .content ul{padding-left:20px}.subpage .grid .col .content ul li{margin-bottom:10px;color:#333;line-height:1.6em;list-style-image:url(/img/arrow.svg)}.docs{background-color:#fff;margin-top:-90px;padding:30px 30px 50px 30px;display:flex}.docs .side-nav{width:25%;float:left;position:relative}.docs .side-nav ul{padding-left:0px;margin-bottom:35px}.docs .side-nav ul li{display:list-item;margin-bottom:15px}.docs .side-nav ul li a{color:#333;font-size:14px}.docs .side-nav ul li a.active{color:#0095D3}.docs .side-nav ul li.heading{color:#111;font-size:14px}.docs .side-nav .dropdown{font-size:14px;font-family:"Metropolis-Medium",Helvetica,sans-serif;margin-bottom:10px}.docs .side-nav .dropdown button{background-image:url(/img/down-arrow.svg);background-repeat:no-repeat;background-position:90% center;border-radius:5px;display:inline;padding:10px 30px 10px 10px;border:1px solid #0095D3;color:#111;cursor:pointer;font-size:14px;font-family:"Metropolis-Medium",Helvetica,sans-serif;margin-bottom:10px}.docs .side-nav .dropdown button:focus{background-color:#F2F2F2}.docs .side-nav .dropdown-menu{position:absolute;border:1px solid #777;border-radius:5px;top:35px;left:0px;background-color:#fff;padding:10px 0;min-width:100px;display:none}.docs .side-nav .dropdown-menu a{display:block;padding:7px 20px}.docs .side-nav .dropdown-menu a:hover{background-color:#F2F2F2}.docs .side-nav .dropdown-menu.dropdown-menu-visible{display:block;z-index:1}.docs .side-nav .form-control{display:block;width:100%;height:40px;padding:.375rem .75rem;font-size:1.125rem;line-height:1.5;color:#333;background-color:#fff;border:1px solid #cecece;background-image:url(/img/search-icon.svg);background-repeat:no-repeat;background-position:95% center;border-radius:5px}.docs .side-nav .form-control:focus{outline:none}.docs .side-nav .form-control::-webkit-search-cancel-button{-webkit-appearance:none}.docs .side-nav .ds-dataset-1{padding:15px 15px 0}.docs .side-nav .ds-dataset-1 a{color:#333;display:inline-block;font-family:"Metropolis-Light",Helvetica,sans-serif;margin-bottom:10px}.docs .side-nav .ds-dataset-1 a div{display:inline}.docs .side-nav .ds-dataset-1 .algolia-docsearch-suggestion--subcategory-inline::after{content:' /'}.docs .side-nav .ds-dataset-1 .algolia-docsearch-suggestion--highlight{background-color:rgba(0,149,211,0.1);color:#1D428A}.docs .side-nav .ds-dataset-1 .algolia-docsearch-suggestion--title{font-family:"Metropolis-Medium",Helvetica,sans-serif}.docs .side-nav .ds-dataset-1 .algolia-docsearch-suggestion--category-header,.docs .side-nav .ds-dataset-1 .algolia-docsearch-suggestion--subcategory-column{display:none}.docs .side-nav .ds-dataset-1 .algolia-docsearch-footer{font-size:14px;text-align:right}.docs .side-nav .ds-dataset-1 .algolia-docsearch-footer a{font-size:14px}.docs .side-nav .ds-dropdown-menu{background-color:#fff;border:1px solid #cecece;border-radius:5px;width:130%}@media only screen and (min-width: 1440px){.docs .side-nav{width:22%}}@media only screen and (min-width: 1280px) and (max-width: 1439px){.docs .side-nav{width:22%}}.docs .docs-content{width:75%;float:right}.docs .docs-content.full{width:100%}.docs .docs-content a{font-size:16px}.docs .docs-content ul{list-style-type:disc;padding-left:20px}.docs .docs-content ul li{list-style-type:unset;display:list-item;margin-bottom:10px;font-size:16px;color:#333;line-height:1.6em;list-style-image:url(/img/arrow.svg)}.docs .docs-content ul li:first-child{margin-top:10px}.docs .docs-content ol li{list-style-type:decimal;display:list-item;margin-bottom:10px;font-size:16px;color:#333}.docs .docs-content ol li:first-child{margin-top:10px}.docs .docs-content code{border:2px solid #EFEFEF;color:#777;padding:2px 8px}.docs .docs-content pre{white-space:pre-wrap}.docs .docs-content pre code{display:block;border:15px solid #EFEFEF;padding:15px;margin-bottom:30px;overflow-x:auto}.docs .docs-content img{max-width:100%}@media only screen and (min-width: 1280px) and (max-width: 1439px){.docs .docs-content{width:58%;padding-right:20px}}@media only screen and (min-width: 1440px){.docs .docs-content{width:75%;padding-right:20px}}.docs .right-nav{width:20%;float:right;margin:-30px -30px 0px 0px}.docs .right-nav .right-nav-content{background-color:#EFEFEF;padding:30px 30px 30px 20px;margin-right:-20px;position:sticky;top:0}.docs .right-nav .buttons{margin-top:0px}.docs .right-nav .buttons li{margin-bottom:0px;padding:8px 0px;display:inline-block}.docs .right-nav .buttons li:first-of-type{border-right:1px solid #ddd;padding-right:5px}.docs .right-nav .buttons li a{text-transform:uppercase;font-size:14px}.docs .right-nav .buttons li a img{vertical-align:middle;width:22px}.docs .right-nav h4{font-size:16px}.docs .right-nav ul{padding-left:0px;margin-bottom:0px}.docs .right-nav ul li{display:block;padding-right:0px;margin-bottom:7px}.docs .right-nav ul li a{font-family:"Metropolis-Light",Helvetica,sans-serif;font-size:14px}.docs .right-nav ul li ul{margin-top:7px;padding-inline-start:14px}.docs .right-nav .sticky{position:fixed;top:0}@media only screen and (max-width: 1279px){.docs .right-nav{display:none}} /*# sourceMappingURL=style.css.map */ ================================================ FILE: site/resources/_gen/assets/scss/scss/site.scss_8967e03afb92eb0cac064520bf021ba2.json ================================================ {"Target":"css/style.css","MediaType":"text/css","Data":{}} ================================================ FILE: site/themes/template/archetypes/default.md ================================================ +++ title = "{{ replace .Name "-" " " | title }}" date = {{ .Date }} +++ ================================================ FILE: site/themes/template/assets/scss/_base.scss ================================================ @import 'variables'; @import 'mixins'; $font-family-base: "Metropolis-Light", Helvetica, sans-serif; $metropolis-light: $font-family-base; $metropolis-light-italic: "Metropolis-LightItalic", Helvetica, sans-serif; $metropolis-regular: "Metropolis-Regular", Helvetica, sans-serif; $metropolis-regular-italic: "Metropolis-RegularItalic", Helvetica, sans-serif; $metropolis-medium: "Metropolis-Medium", Helvetica, sans-serif; $metropolis-medium-italic: "Metropolis-MediumItalic", Helvetica, sans-serif; $metropolis-bold: "Metropolis-Bold", Helvetica, sans-serif; $metropolis-bold-italic: "Metropolis-BoldItalic", Helvetica, sans-serif; $metropolis-semibold: "Metropolis-SemiBold", Helvetica, sans-serif; $metropolis-semibold-italic: "Metropolis-SemiBoldItalic", Helvetica, sans-serif; body { font-family: $font-family-base; margin: 0px; line-height: 1.25; } .wrapper { max-width: 980px; margin: 0px auto; padding: 20px; @include breakpoint(small) { max-width: 100%; } @include breakpoint(medium) { } &.docs { @include breakpoint(extra-large) { max-width: 80%; } } } .clearfix { *zoom: 1; &:before, &:after { display: table; content: ""; line-height: 0; } &:after { clear: both; } } h1, h2, h3, h4, h5, h6 { font-weight: 300; } h1 { font-size: 28px; } h2 { font-size: 22px; color: #333; } h3 { font-size: 20px; } h4 { font-size: 18px; } li { list-style-type: none; display: inline; padding-right: 25px; font-size: 14px; line-height: 1.7em; &:last-of-type { padding-right: 0px; } } p { line-height: 1.7em; font-weight: 300; font-size: 16px; color: $darkgrey; &.intro { font-size: 18px; } } a { font-size: 16px; text-decoration: none; color: $blue; font-family: $metropolis-medium } button { background-color: unset; border: none; } .button { color: $blue; font-size: 12px; font-weight: 600; background-color: $white; border-radius: 3px; padding: 14px 10px; min-width: 200px; text-transform: uppercase; border: 1px solid $white; &.secondary { background-color: $mainblue; color: $white; } &.tertiary { border: 1px solid $blue; } } .buttons { margin-top: 40px; .button:first-of-type { margin-right: 30px; @include breakpoint(small) { margin: 0px 0px 20px 0px; } } } .strong { font-family: $metropolis-medium; } .bg-grey { background-color: $lightgrey; } .grid.three { display: grid; grid-template-columns: 1fr 1fr 1fr; row-gap: 20px; column-gap: 20px; @include breakpoint(small) { grid-template-columns: 1fr; } } .grid.two { display: grid; grid-template-columns: 1fr 1fr; @include breakpoint(small) { grid-template-columns: 1fr; } } .rounded-circle { border-radius: 50% !important; } noscript { background-color: rgba(red, .25); display: block; font-size: 14px; font-weight: bold; padding: 10px; margin: 0 20px 20px 0; } .filter-blue{ filter: brightness(0) saturate(100%) invert(39%) sepia(55%) saturate(4454%) hue-rotate(177deg) brightness(99%) contrast(104%); } // Metropolis @font-face { font-family: "Metropolis-Bold"; src:url("/fonts/Metropolis-Bold.eot"); src:url("/fonts/Metropolis-Bold.eot?#iefix") format("embedded-opentype"), url("/fonts/Metropolis-Bold.woff2") format("woff2"), url("/fonts/Metropolis-Bold.woff") format("woff"); font-weight: normal; font-style: normal; } @font-face { font-family: "Metropolis-BoldItalic"; src:url("/fonts/Metropolis-BoldItalic.eot"); src:url("/fonts/Metropolis-BoldItalic.eot?#iefix") format("embedded-opentype"), url("/fonts/Metropolis-BoldItalic.woff2") format("woff2"), url("/fonts/Metropolis-BoldItalic.woff") format("woff"); font-weight: normal; font-style: normal; } @font-face { font-family: "Metropolis-Light"; src:url("/fonts/Metropolis-Light.eot"); src:url("/fonts/Metropolis-Light.eot?#iefix") format("embedded-opentype"), url("/fonts/Metropolis-Light.woff2") format("woff2"), url("/fonts/Metropolis-Light.woff") format("woff"); font-weight: normal; font-style: normal; } @font-face { font-family: "Metropolis-LightItalic"; src:url("/fonts/Metropolis-LightItalic.eot"); src:url("/fonts/Metropolis-LightItalic.eot?#iefix") format("embedded-opentype"), url("/fonts/Metropolis-LightItalic.woff2") format("woff2"), url("/fonts/Metropolis-LightItalic.woff") format("woff"); font-weight: normal; font-style: normal; } @font-face { font-family: "Metropolis-Regular"; src:url("/fonts/Metropolis-Regular.eot"); src:url("/fonts/Metropolis-Regular.eot?#iefix") format("embedded-opentype"), url("/fonts/Metropolis-Regular.woff2") format("woff2"), url("/fonts/Metropolis-Regular.woff") format("woff"); font-weight: normal; font-style: normal; } @font-face { font-family: "Metropolis-RegularItalic"; src:url("/fonts/Metropolis-RegularItalic.eot"); src:url("/fonts/Metropolis-RegularItalic.eot?#iefix") format("embedded-opentype"), url("/fonts/Metropolis-RegularItalic.woff2") format("woff2"), url("/fonts/Metropolis-RegularItalic.woff") format("woff"); font-weight: normal; font-style: normal; } @font-face { font-family: "Metropolis-Medium"; src:url("/fonts/Metropolis-Medium.eot"); src:url("/fonts/Metropolis-Medium.eot?#iefix") format("embedded-opentype"), url("/fonts/Metropolis-Medium.woff2") format("woff2"), url("/fonts/Metropolis-Medium.woff") format("woff"); font-weight: normal; font-style: normal; } @font-face { font-family: "Metropolis-MediumItalic"; src:url("/fonts/Metropolis-MediumItalic.eot"); src:url("/fonts/Metropolis-MediumItalic.eot?#iefix") format("embedded-opentype"), url("/fonts/Metropolis-MediumItalic.woff2") format("woff2"), url("/fonts/Metropolis-MediumItalic.woff") format("woff"); font-weight: normal; font-style: normal; } @font-face { font-family: "Metropolis-SemiBold"; src:url("/fonts/Metropolis-SemiBold.eot"); src:url("/fonts/Metropolis-SemiBold.eot?#iefix") format("embedded-opentype"), url("/fonts/Metropolis-SemiBold.woff2") format("woff2"), url("/fonts/Metropolis-SemiBold.woff") format("woff"); font-weight: normal; font-style: normal; } @font-face { font-family: "Metropolis-SemiBoldItalic"; src:url("/fonts/Metropolis-SemiBoldItalic.eot"); src:url("/fonts/Metropolis-SemiBoldItalic.eot?#iefix") format("embedded-opentype"), url("/fonts/Metropolis-SemiBoldItalic.woff2") format("woff2"), url("/fonts/Metropolis-SemiBoldItalic.woff") format("woff"); font-weight: normal; font-style: normal; } ================================================ FILE: site/themes/template/assets/scss/_components.scss ================================================ @import 'variables'; @import 'mixins'; /* Homepage Hero */ .hero { background-color: $mainblue; color: $white; .text-block { max-width: 7000px; padding: 0px 0px 10px 0px; p { margin-bottom: 20px; font-size: 18px; color: $white; } h2 { font-size: 36px; } } &.homepage { //background-image: url(/img/hero-image.png); background-position: center center; background-repeat: no-repeat; background-size: cover; padding-bottom: 80px; h1 { font-size: 36px; } } @include breakpoint(small) { .text-block { max-width: unset; margin-right: 0px; } .button { display: block; text-align: center; } &.homepage { background-image: none; } } } .grid-container { margin-top: -80px; .grid.three { padding-bottom: 20px; .card { position: relative; padding: 30px 20px; background-color: $white; text-align: center; box-shadow: 0px 2px 10px rgba(0,0,0,0.2); h3 { color: $darkgrey; font-size: 22px; } p { color: $darkgrey; } } } } .introduction { .grid.two { column-gap: 140px; padding: 35px 20px; p { margin: 0px; font-size: 16px; &.strong { color: $darkgrey; } } } @include breakpoint(small) { padding: 0px 20px; .col:first-of-type { padding-bottom: 50px; } } } .use-cases { .grid { grid-template-columns: 220px 1fr; margin-bottom: 30px; grid-template-areas: "image text"; .image { background-color: $mainblue; text-align: center; display: flex; align-items: center; justify-content: center; grid-area: image; img { justify-self: center; } } .text { border: 1px solid $lightgrey; padding: 30px; grid-area: text; a.button { display: block; max-width: 138px; text-align: center; padding: 5px 10px; min-width: unset; } } &.image-right { grid-template-columns: 1fr 220px; grid-template-areas: "text image"; @include breakpoint(small) { grid-template-columns: 1fr; grid-template-areas: "image" "text"; } } @include breakpoint(small) { grid-template-columns: 1fr; grid-template-rows: minmax(160px, 1fr); grid-template-areas: "image" "text"; } } h2 { color: $black; } p.strong { color: #1B3951; font-size: 16px; } } .team { background-color: $navyblue; h2, h3, p { color: $white; } p { font-size: 16px; } a { color: $white; font-weight: 300; text-decoration: underline; } .grid.three { row-gap: 40px; margin: 40px 0px; } .bio { display: grid; grid-template-columns: 120px 1fr; column-gap: 20px; .image img { height: 120px; width: 120px; } .info { align-self: center; p { margin: 0px; &.name { font-size: 16px; font-family: $metropolis-medium; } &.position { font-size: 14px; } } } } } .avatar { filter: grayscale(100%); transition: filter 0.6s ease-in-out; &:hover { filter: grayscale(0%); } } .hero.subpage-hero { //background-image: url(/img/blog-hero-image.png); background-position: center center; background-repeat: no-repeat; background-size: cover; padding-bottom: 90px; h1 { font-size: 46px; text-align: center; @include breakpoint(small) { font-size: 26px; } } } .experimental { .grid.three .col { padding: 0px; } .icon { background-color: $mainblue; padding: 25px; min-height: 95px; display: flex; align-items: center; justify-content: center; } .content { padding: 25px; .example { background-color: $lightgrey; } } } .blog { padding-bottom: 50px; .col { border: 1px solid $lightgrey; img { width: 100%; } .content { padding: 0px 20px; } } &.landing { background-color: #fff; margin-top: -90px; h3 a { font-size: 16px; } .pagination { margin: 30px auto 50px auto; ul { padding: 0px; text-align: center; li { padding: 0px; a { padding: 5px 10px; &.active { background-color: $lightgrey; border-radius: 50%; } } &.left-arrow { margin-right: 15px; } &.right-arrow { margin-left: 15px; } } } } } .blog-post { background-color: #fff; margin: -110px 0px 0px -30px; padding: 30px 90px 30px 30px; .author { color: $blue; margin: 0px; } .date { color: $black; margin: 0px; font-weight: 600; } .header, h4 { color: $black; font-weight: 600; } a { font-size: 16px; } ul { list-style-type: disc; padding-left: 20px; li { list-style-type: unset; display: list-item; margin-bottom: 10px; font-size: 14px; color: $darkgrey; line-height: 1.6em; list-style-image: url(/img/arrow.svg); &:first-child { margin-top: 10px; } } } ol { li { list-style-type: decimal; display: list-item; margin-bottom: 10px; font-size: 16px; color: $darkgrey; &:first-child { margin-top: 10px; } } } code { border: 2px solid #EFEFEF; color: $darkgrey; padding: 2px 8px; } pre { code { display: block; border: 15px solid #EFEFEF; padding: 15px; margin-bottom: 30px; overflow-x: auto; } } img { max-width: 100%; } strong { font-family: $metropolis-medium; } } } .getting-started { background-color: $lightgrey; color: $black; p { color: $black; font-size: 16px; } .left-side { width: 50%; float: left; } .right-side { width: 25%; float: right; } h2 { font-size: 30px; margin-bottom: 0px; } a { display: block; max-width: 138px; text-align: center; padding: 10px; min-width: unset; } .button { margin-top: 50px; border: 1px solid $blue; } @include breakpoint(small) { .wrapper { padding-bottom: 40px; } .left-side { width: 100%; float: none; } .right-side { width: 100%; float: none; } .button { display: block; text-align: center; max-width: unset; margin-top: 20px; } } } .subpage { background-color: #fff; margin-top: -90px; padding: 30px 30px 50px 30px; .section-header { margin-top: 3rem; font-weight: 600; font-size: 20px; } .embed-responsive { position: relative; &:before { padding-top: 56.25%; display: block; content: ""; } .embed-responsive-item { position: absolute; top: 0; bottom: 0; left: 0; width: 100%; height: 100%; border: 0; } } .grid { margin-bottom: 20px; .col { border: 1px solid #F2F2F2; .icon { display: flex; align-items: center; justify-content: center; min-height: 140px; } .content { padding: 0px 20px 20px 20px; &.plugins { padding-top: 20px; img { display: block; margin: 0px auto 5px auto; } } h3 { margin-top: 0px; text-align: center; a { font-size: 20px; } } ul { padding-left: 20px; li { margin-bottom: 10px; color: $darkgrey; line-height: 1.6em; list-style-image: url(/img/arrow.svg); } } } } } } .docs { background-color: #fff; margin-top: -90px; padding: 30px 30px 50px 30px; display: flex; .side-nav { width: 25%; float: left; position: relative; ul { padding-left: 0px; margin-bottom: 35px; li { display: list-item; margin-bottom: 15px; a { color: $darkgrey; font-size: 14px; &.active { color: $blue; } } &.heading { color: $black; font-size: 14px; } } } .dropdown { font-size: 14px; font-family: $metropolis-medium; margin-bottom: 10px; button { background-image: url(/img/down-arrow.svg); background-repeat: no-repeat; background-position: 90% center; border-radius: 5px; display: inline; padding: 10px 30px 10px 10px; border: 1px solid $blue; color: $black; cursor: pointer; font-size: 14px; font-family: $metropolis-medium; margin-bottom: 10px; &:focus { background-color: $lightgrey; } } } .dropdown-menu { position: absolute; border: 1px solid $grey; border-radius: 5px; top: 35px; left: 0px; background-color: $white; padding: 10px 0; min-width: 100px; display: none; a { display: block; padding: 7px 20px; &:hover { background-color: $lightgrey; } } &.dropdown-menu-visible { display: block; z-index: 1; } } .form-control { display: block; width: 100%; height: 40px; padding: .375rem .75rem; font-size: 1.125rem; line-height: 1.5; color: $darkgrey; background-color: #fff; border: 1px solid #cecece; background-image: url(/img/search-icon.svg); background-repeat: no-repeat; background-position: 95% center; border-radius: 5px; &:focus { outline: none; } &::-webkit-search-cancel-button { -webkit-appearance: none; } } .ds-dataset-1 { padding: 15px 15px 0; a { color: $darkgrey; display: inline-block; font-family: $metropolis-light; margin-bottom: 10px; div { display: inline; } } .algolia-docsearch-suggestion--subcategory-inline { &::after { content: ' /'; } } .algolia-docsearch-suggestion--highlight { background-color: rgba($blue, .1); color: $navyblue; } .algolia-docsearch-suggestion--title { font-family: $metropolis-medium; } .algolia-docsearch-suggestion--category-header, .algolia-docsearch-suggestion--subcategory-column { display: none; } .algolia-docsearch-footer { font-size: 14px; text-align: right; a { font-size: 14px; } } } .ds-dropdown-menu { background-color: #fff; border: 1px solid #cecece; border-radius: 5px; width: 130%; } @include breakpoint(extra-large) { width: 22%; } @include breakpoint(large) { width: 22%; } } .docs-content { width: 75%; float: right; &.full { width: 100%; } a { font-size: 16px; } ul { list-style-type: disc; padding-left: 20px; li { list-style-type: unset; display: list-item; margin-bottom: 10px; font-size: 16px; color: $darkgrey; line-height: 1.6em; list-style-image: url(/img/arrow.svg); &:first-child { margin-top: 10px; } } } ol { li { list-style-type: decimal; display: list-item; margin-bottom: 10px; font-size: 16px; color: $darkgrey; &:first-child { margin-top: 10px; } } } code { border: 2px solid #EFEFEF; color: $grey; padding: 2px 8px; } pre { white-space: pre-wrap; code { display: block; border: 15px solid #EFEFEF; padding: 15px; margin-bottom: 30px; overflow-x: auto; } } img { max-width: 100%; } @include breakpoint(large) { width: 58%; padding-right: 20px; } @include breakpoint(extra-large) { width: 75%; padding-right: 20px; } } .right-nav { width: 20%; float: right; margin: -30px -30px 0px 0px; .right-nav-content { background-color: #EFEFEF; padding: 30px 30px 30px 20px; margin-right: -20px; position: sticky; top: 0; } .buttons { margin-top: 0px; li { margin-bottom: 0px; padding: 8px 0px; display: inline-block; &:first-of-type { border-right: 1px solid #ddd; padding-right: 5px; } a { text-transform: uppercase; font-size: 14px; img { vertical-align: middle; width: 22px; } } } } h4 { font-size: 16px; } ul { padding-left: 0px; margin-bottom: 0px; li { display: block; padding-right: 0px; margin-bottom: 7px; a { font-family: $metropolis-light; font-size: 14px; } ul { margin-top: 7px; padding-inline-start: 14px; } } } .sticky { position: fixed; top: 0; } @include breakpoint(small-medium) { display: none; } } } ================================================ FILE: site/themes/template/assets/scss/_footer.scss ================================================ @import 'variables'; @import 'mixins'; @import 'base'; footer { .top-links { min-height: 52px; display: flex; align-items: center; justify-content: space-between; } .left-links { padding: 0px; li { img { vertical-align: bottom; margin-right: 10px; } a { color: $darkgrey; font-weight: 300; font-size: 12px; font-family: $metropolis-light; } } .mobile { display: none; } } .right-links { p { margin: 0px; } .copywrite { font-size: 12px; padding-right: 10px; a { font-size: 12px; color: $darkgrey; font-family: $metropolis-light; } } a { vertical-align: middle; } } .bottom-links { margin: 10px 0px 30px 0px; p { display: flex; flex-wrap: wrap; justify-content: space-between; font-size: 12px; .ot-sdk-show-settings { cursor: pointer; } } a { font-size: 12px; font-family: $metropolis-light; text-decoration: underline; } img { max-width: 75px; vertical-align: middle; margin-left: 30px; } .footer-logo { height: 3em; } } @include breakpoint(small) { .footer-links { display: block; .right-links { display: none; } .left-links { float: none; margin: 10px 0px; .desktop { display: none; } .mobile { display: inline; } .copywrite { display: block; margin-top:20px; } } } .bottom-links { margin: 10px 0px 20px 0px; float: none; img { margin-left: 0px; display: block; margin-top: 10px; } } } } ================================================ FILE: site/themes/template/assets/scss/_header.scss ================================================ @import 'variables'; @import 'mixins'; @import 'base'; header { .wrapper { padding: 10px 20px; min-height: 52px; display: flex; align-items: center; justify-content: space-between; } .desktop-links { padding-left: 0px; } a { color: $darkgrey; font-family: $metropolis-light; &.active { font-family: $metropolis-medium; } } li img { vertical-align: bottom; margin-right: 10px; } .mobile { display: none; } @include breakpoint(medium) { .desktop-links li { padding-right: 10px; } } @include breakpoint(small) { .expanded-icon { display: none; padding: 11px 3px 0px 0px; } .collapsed-icon { padding-top: 12px; } .mobile-menu-visible { .mobile { display: block; .collapsed-icon { display: none; } .expanded-icon { display: block; } } } position: relative; .desktop-links { display: none; } .mobile { display: block; } button { float: right; &:focus { outline: none; } } ul { padding-left: 0px; li { display: block; margin: 20px 0px; } } .mobile-menu { position: absolute; background-color: #fff; width: 100%; top: 70px; left: 0px; padding-bottom: 20px; display: none; z-index: 10; .header-links { margin: 0px 20px; } .social { margin: 0px 20px; padding-top: 20px; img { vertical-align: middle; padding-right: 10px; } a { font-size: 14px; padding-right: 35px; &:last-of-type { padding-right: 0px; } } } } } } ================================================ FILE: site/themes/template/assets/scss/_mixins.scss ================================================ @mixin breakpoint($point) { $small: 767px; // Up to 767px $medium: 1279px; // Up to 1279px $large: 1439px; // Up to 1439px $extra-large: 1800px; // Up to 1800px @if $point == extra-large { @media only screen and (min-width : $large+1) { @content; } } @else if $point == large { @media only screen and (min-width : $medium+1) and (max-width: $large) { @content; } } @else if $point == medium-large { @media only screen and (min-width: $medium+1) { @content; } } @else if $point == medium { @media only screen and (min-width: $small+1) and (max-width: $medium) { @content; } } @else if $point == small-medium { @media only screen and (max-width: $medium) { @content; } } @else if $point == small { @media only screen and (max-width: $small) { @content; } } } @mixin clearfix { *zoom: 1; &:before, &:after { display: table; content: ""; line-height: 0; } &:after { clear: both; } } ================================================ FILE: site/themes/template/assets/scss/_variables.scss ================================================ $white: #ffffff; $blue: #0095D3; $darkgrey: #333333; $grey: #777777; $lightgrey: #F2F2F2; $darkblue: #002538; $purple: #7F35B2; $black: #111111; $mainblue: #0091DA; $navyblue: #1D428A; ================================================ FILE: site/themes/template/assets/scss/site.scss ================================================ @import 'header'; @import 'footer'; @import 'base'; @import 'variables'; @import 'components'; @import 'mixins'; ================================================ FILE: site/themes/template/layouts/_default/_markup/render-image.html ================================================ {{ $link := .Destination }} {{ if not (strings.HasPrefix $link "http") }} {{ if strings.HasSuffix .Page.Parent.RelPermalink "docs/" }} {{ $link = printf "%s%s" .Page.RelPermalink .Destination }} {{ else }} {{ $link = printf "%s%s" .Page.Parent.RelPermalink .Destination }} {{ end }} {{ end }}

{{ .Text }}

================================================ FILE: site/themes/template/layouts/_default/_markup/render-link.html ================================================ {{ $link := .Destination }} {{ $isRemote := strings.HasPrefix $link "http" }} {{- if not $isRemote -}} {{ $url := urls.Parse .Destination }} {{- if $url.Path -}} {{ $fragment := "" }} {{- with $url.Fragment }}{{ $fragment = printf "#%s" . }}{{ end -}} {{- with .Page.GetPage $url.Path }}{{ $link = printf "%s%s" .RelPermalink $fragment }}{{ end }}{{ end -}} {{- end -}} {{ .Text | safeHTML }} ================================================ FILE: site/themes/template/layouts/_default/baseof.html ================================================ ================================================ FILE: site/themes/template/layouts/_default/docs.html ================================================ {{ define "main" }}

Documentation

{{ partial "docs-sidebar.html" . }}
{{ .Content }}
{{ partial "docs-right-bar.html" . }}
{{ end }} ================================================ FILE: site/themes/template/layouts/_default/list.html ================================================ {{ define "main" }}
{{ if or .Title .Content }}
{{ with .Title }}

{{ . }}

{{ end }} {{ with .Content }}
{{ . }}
{{ end }}
{{ end }} {{ range .Paginator.Pages }} {{ .Render "summary" }} {{ end }}
{{ end }} ================================================ FILE: site/themes/template/layouts/_default/posts.html ================================================ {{ define "main" }}

Blog

{{ range (.Paginator 9).Pages.ByDate }} {{ partial "blog-post-card.html" . }} {{ end }}
{{ partial "pagination.html" . }}
{{ end }} ================================================ FILE: site/themes/template/layouts/_default/search.html ================================================ {{ if .Site.Params.docs_search }}
{{ end }} ================================================ FILE: site/themes/template/layouts/_default/section.html ================================================ {{ define "main" }}
{{ .Content }}
{{ end }} ================================================ FILE: site/themes/template/layouts/_default/single.html ================================================ {{ define "main" }}

Blog

{{ .Title }}

{{ .Params.author }}

{{ dateFormat "Jan 2, 2006" .Date }}

{{ .Content }}

Related Content

{{ $related := (where (.Site.RegularPages.Related .) "Type" "posts") | first 3 }} {{ with $related }} {{ range . }} {{ partial "blog-post-card.html" . }} {{ end }} {{ end }}
{{ end }} ================================================ FILE: site/themes/template/layouts/_default/summary.html ================================================ ================================================ FILE: site/themes/template/layouts/_default/tag.html ================================================ {{ define "main" }}

Blog Posts by {{ .Title }}

{{ range .Pages.ByDate }} {{ partial "blog-post-card.html" . }} {{ end }}
{{ end }} ================================================ FILE: site/themes/template/layouts/_default/versions.html ================================================ {{ if .Site.Params.Use_advanced_docs }} {{ end }} ================================================ FILE: site/themes/template/layouts/index.html ================================================ {{ define "main" }}
{{ partial "hero.html" . }} {{ partial "homepage-grid.html" . }}

Learn More About Sealed Secrets

Learn more about Sealed Secrets and how to create secure Secrets in Kubernetes

Advanced Cryptography with Sealed Secrets

How to apply the best possible encryption to your Sealed Secrets, from using customized Certificates to post-quantum recomendations.

{{ partial "use-cases.html" . }} {{ partial "contributors.html" . }}
{{ end }} ================================================ FILE: site/themes/template/layouts/index.redirects ================================================ {{ $latest := (cond (.Site.Params.docs_versioning) .Site.Params.docs_latest "") }} /docs /docs/{{ $latest }} 301! /docs/latest /docs/{{ $latest }} /docs/latest/* /docs/{{ $latest }}/:splat ================================================ FILE: site/themes/template/layouts/partials/blog-post-card.html ================================================
{{ .Title }}

{{ .Title }}

{{ .Params.Excerpt }}

================================================ FILE: site/themes/template/layouts/partials/contributors.html ================================================

Meet the Sealed Secrets team:

{{ $contributors := .Site.GetPage "/contributors" }} {{ range $contributors.Resources }} {{ end }}

Contributing

Sealed Secrets is released as open-source software and provides community support through our GitHub project page. If you encounter an issue or have a question, feel free to reach out on the GitHub issues page for Sealed Secrets.

The Sealed Secrets project team welcomes contributions from the community — please have a look at our contributing documentation.

================================================ FILE: site/themes/template/layouts/partials/docs-right-bar.html ================================================ {{ if .Site.Params.docs_right_sidebar }}
    {{ if (or .IsNode .IsPage) }} {{ $issueBody := printf "**On Page:** [%s](%s)" .Title .Permalink | htmlEscape }} {{ $issueQuery := (querify "body" $issueBody) }}
  • Report Issues
  • {{ $editQuery := (querify "description" "Signed-off-by: NAME \n\n") }}
  • Edit
  • {{ end }}
{{ if ne .TableOfContents "" }}

On this page:

{{ .TableOfContents }} {{ end }}
{{ end }} ================================================ FILE: site/themes/template/layouts/partials/docs-sidebar.html ================================================
{{ if .Site.Params.use_advanced_docs }} {{ $version := .CurrentSection.Params.version }} {{ .Render "versions" }} {{ .Render "search" }} {{ if $version }} {{ $tocTemplateName := index (index $.Site.Data.docs "toc-mapping") $version }} {{ if not $tocTemplateName }} {{ $tocTemplateName = "default" }} {{ end }} {{ $toc := (index $.Site.Data "docs" $tocTemplateName).toc }} {{ range $toc }}

{{ .title }}

    {{ range .subfolderitems }}
  • {{ $url := (index (print "/docs/" $version .url "/")) }} {{ .page }}
  • {{ end }}
{{ end }} {{ end }} {{ else }}
    {{ $currentPage := . }} {{ range .Site.Menus.docs }}
  • {{ .Name }}
  • {{ end }}
{{ end }}
================================================ FILE: site/themes/template/layouts/partials/footer.html ================================================ ================================================ FILE: site/themes/template/layouts/partials/getting-started.html ================================================ {{ $latest := (cond (.Site.Params.docs_versioning) .Site.Params.docs_latest "") }}

Getting started

Discover how to deploy Sealed Secrets in your cluster, and start managing your Kubernetes Secrets in a secure way!

================================================ FILE: site/themes/template/layouts/partials/header.html ================================================ {{ $latest := (cond (.Site.Params.docs_versioning) .Site.Params.docs_latest "") }}
================================================ FILE: site/themes/template/layouts/partials/hero.html ================================================

"Sealed Secrets" for Kubernetes

Sealed Secrets provides declarative Kubernetes Secret Management in a secure way. Since the Sealed Secrets are encrypted, they can be safely stored in a code repository. This enables an easy to implement GitOps flow that is very popular among the OSS community.

================================================ FILE: site/themes/template/layouts/partials/homepage-grid.html ================================================

On your command line

Sealed Secrets offers a powerful CLI tool (kubeseal) to one-way encrypt your Kubernetes Secret easily.

On your K8S cluster

The Sealed Secrets controller will decrypt any Sealed Secret into its equivalent Kubernetes Secret

On your code repository

Sealed Secrets are safe to store in your local code repository, along with the rest of your configuration.

================================================ FILE: site/themes/template/layouts/partials/pagination.html ================================================ {{ $paginator := .Paginator }} {{ if gt $paginator.TotalPages 1 }} {{ end }} ================================================ FILE: site/themes/template/layouts/partials/use-cases.html ================================================

Features

One-way Encryption

SealedSecrets are a "write only" device. The idea is that the SealedSecret can be decrypted only by the controller running in the target cluster and nobody else (not even the original author) is able to obtain the original Secret from the SealedSecret.

Learn more

Sealing key renewal

Sealing keys are automatically renewed every 30 days. Which means a new sealing key is created and appended to the set of active sealing keys the controller can use to unseal Sealed Secret resources.

Learn more

Sealed Secrets Metrics

The Sealed Secrets Controller running in Kubernetes exposes Prometheus metrics. These metrics enable operators to observe how it is performing. For example how many SealedSecret unseals have been attempted and how many errors may have occured due to RBAC permissions, wrong key, corrupted data, etc.

Learn more

================================================ FILE: site/themes/template/layouts/shortcodes/readfile.html ================================================ {{ .Get "file" | readFile | safeHTML }} ================================================ FILE: site/themes/template/static/fonts/Open Font License.md ================================================ Copyright (c) 2015, Chris Simpson , with Reserved Font Name: "Metropolis". This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL Version 2.0 - 18 March 2012 SIL Open Font License ==================================================== Preamble ---------- The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. Definitions ------------- `"Font Software"` refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. `"Reserved Font Name"` refers to any names specified as such after the copyright statement(s). `"Original Version"` refers to the collection of Font Software components as distributed by the Copyright Holder(s). `"Modified Version"` refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. `"Author"` refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. Permission & Conditions ------------------------ Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1. Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2. Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3. No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4. The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5. The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. Termination ----------- This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. ================================================ FILE: site/themes/template/static/fonts/README.md ================================================ # The Metropolis Typeface The Vision --- To create a modern, geometric typeface. Open sourced, and openly available. Influenced by other popular geometric, minimalist sans-serif typefaces of the new millenium. Designed for optimal readability at small point sizes while beautiful at large point sizes. December 2017 update --- Currently working on greatly improving spacing and kerning of the base typeface. Once this is done, work on other variations (e.g. rounded or slab) can begin in earnest. The License --- Licensed under Open Font License (OFL). Available to anyone and everyone. Contributions welcome. Contact --- Contact me via chris.m.simpson@icloud.com or http://twitter.com/ChrisMSimpson for any questions, requests or improvements (or just submit a pull request). Support --- You can now support work on Metropolis via Patreon at https://www.patreon.com/metropolis. ================================================ FILE: site/themes/template/static/js/main.js ================================================ "use strict"; function mobileNavToggle() { var menu = document.getElementById('mobile-menu').parentElement; menu.classList.toggle('mobile-menu-visible'); } function docsVersionToggle() { var menu = document.getElementById('dropdown-menu'); menu.classList.toggle('dropdown-menu-visible'); } window.onclick = function(event) { var target = event.target, menu = document.getElementById('dropdown-menu') ; if(!target.classList.contains('dropdown-toggle')) { menu.classList.remove('dropdown-menu-visible'); } } ================================================ FILE: vendor_jsonnet/kube-libsonnet/.travis.yml ================================================ language: bash os: - linux services: - docker before_install: # update to docker-ce - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) edge" - sudo apt-get update - sudo apt-get -y install docker-ce script: - make tests ================================================ FILE: vendor_jsonnet/kube-libsonnet/CODEOWNERS ================================================ * @dbarranco @jbianquetti-nami @jjo ================================================ FILE: vendor_jsonnet/kube-libsonnet/LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: vendor_jsonnet/kube-libsonnet/Makefile ================================================ # Originally taken from https://github.com/bitnami-labs/kube-manifests/, # trimmed down to only run lib testing. # # Provides 'test' target. Uses docker. all: @echo make tests tests: make -C tests .PHONY: all tests ================================================ FILE: vendor_jsonnet/kube-libsonnet/README.md ================================================ # kube-libsonnet This repo has been originally populated by the `lib/` folder contents from `https://github.com/bitnami-labs/kube-manifests` as of Mar/2018, aiming to provide a library of `jsonnet` manifests for common Kubernetes objects (such as `Deployment`, `Service`, `Ingress`, etc). Accordingly, above `kube-manifests` has been changed to use this repo as a git submodule, i.e.: $ git submodule add https://github.com/bitnami-labs/kube-libsonnet $ cat .gitmodules [submodule "lib"] path = lib url = https://github.com/bitnami-labs/kube-libsonnet ## Testing Unit and e2e-ish testing at tests/, needs usable `docker-compose` at node, will run a `k3s` "dummy" container to serve Kube API, enough to for `kubecfg validate` against it: make tests If you don't want that full kube-api stack (will then use your "local" kubernetes configured environment), you can run: make -C tests test-srcs test-kube ================================================ FILE: vendor_jsonnet/kube-libsonnet/bitnami.libsonnet ================================================ // Generic stuff is in kube.libsonnet - this file contains // bitnami-specific conventions. local kube = import "kube.libsonnet"; local perCloudSvcAnnotations(cloud, internal, service) = ( { aws: { "service.beta.kubernetes.io/aws-load-balancer-connection-draining-enabled": "true", "service.beta.kubernetes.io/aws-load-balancer-connection-draining-timeout": std.toString(service.target_pod.spec.terminationGracePeriodSeconds), // Use PROXY protocol (nginx supports this too) "service.beta.kubernetes.io/aws-load-balancer-proxy-protocol": "*", // Does LB do NAT or DSR? (OnlyLocal implies DSR) // https://kubernetes.io/docs/tutorials/services/source-ip/ // NB: Don't enable this without modifying set-real-ip-from above! // Not supported on aws in k8s 1.5 - immediate close / serves 503s. //"service.beta.kubernetes.io/external-traffic": "OnlyLocal", }, gke: {}, }[cloud] + if internal then { aws: { "service.beta.kubernetes.io/aws-load-balancer-internal": "0.0.0.0/0", }, gke: { "cloud.google.com/load-balancer-type": "internal", }, }[cloud] else {} ); local perCloudSvcSpec(cloud) = ( { aws: {}, // Required to get real src IP address, which also allows proper // ingress.kubernetes.io/whitelist-source-range matching gke: { externalTrafficPolicy: "Local" }, }[cloud] ); { ElbService(name, cloud, internal): kube.Service(name) { local service = self, metadata+: { annotations+: perCloudSvcAnnotations(cloud, internal, service), }, spec+: { type: "LoadBalancer" } + perCloudSvcSpec(cloud), }, Ingress(name): kube.Ingress(name) { local ing = self, host:: error "host required", target_svc:: error "target_svc required", // Default to single-service - override if you want something else. paths:: [{ path: "/", backend: ing.target_svc.name_port }], secretName:: "%s-cert" % [ing.metadata.name], // cert_provider can either be: // - "kcm": DEPRECATED (will be removed in T26526) uses old kube-cert-manager via route53 for ACME dns-01 challenge // - "cm-dns": cert-manager using route53 for ACME dns-01 challenge // - "cm-http": cert-manager using ACME http, requires public ingress (kube-lego already replaced by cert-manager) cert_provider:: "cm-dns", kcm_metadata:: { annotations+: { "stable.k8s.psg.io/kcm.provider": "route53", "stable.k8s.psg.io/kcm.email": "sre@bitnami.com", }, labels+: { "stable.k8s.psg.io/kcm.class": "default", }, }, cm_dns_metadata:: { annotations+: { "certmanager.k8s.io/cluster-issuer": "letsencrypt-prod-dns", "certmanager.k8s.io/acme-challenge-type": "dns01", "certmanager.k8s.io/acme-dns01-provider": "default", }, }, cm_http_metadata:: { annotations+: { "certmanager.k8s.io/cluster-issuer": "letsencrypt-prod-http", }, }, metadata+: { kcm: ing.kcm_metadata, "cm-dns": ing.cm_dns_metadata, "cm-http": ing.cm_http_metadata, }[ing.cert_provider], spec+: { tls: [ { hosts: std.set([r.host for r in ing.spec.rules]), secretName: ing.secretName, }, ], rules: [ { host: ing.host, http: { paths: ing.paths, }, }, ], }, }, PromScrape(port): { local scrape = self, prom_path:: "/metrics", metadata+: { annotations+: { "prometheus.io/scrape": "true", "prometheus.io/port": std.toString(port), "prometheus.io/path": scrape.prom_path, }, }, }, PodZoneAntiAffinityAnnotation(pod): { podAntiAffinity: { preferredDuringSchedulingIgnoredDuringExecution: [ { weight: 50, podAffinityTerm: { labelSelector: { matchLabels: pod.metadata.labels }, topologyKey: "failure-domain.beta.kubernetes.io/zone", }, }, { weight: 100, podAffinityTerm: { labelSelector: { matchLabels: pod.metadata.labels }, topologyKey: "kubernetes.io/hostname", }, }, ], }, }, } ================================================ FILE: vendor_jsonnet/kube-libsonnet/examples/guestbook/guestbook.jsonnet ================================================ // Copyright 2017 The kubecfg authors // // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Simple to demonstrate kubecfg using kube-libsonnet // This should not necessarily be considered a model jsonnet example // to build upon. // This is a simple port to jsonnet of the standard guestbook example // https://github.com/kubernetes/kubernetes/tree/master/examples/guestbook // // ``` // kubecfg update guestbook.jsonnet // // # poke at // - $(minikube service frontend), etc // - kubectl proxy # then visit http://localhost:8001/api/v1/namespaces/default/services/frontend/proxy/ // kubecfg delete guestbook.jsonnet // ``` local kube = import "lib/kube.libsonnet"; { frontend_deployment: kube.Deployment("frontend") { spec+: { local my_spec = self, replicas: 3, template+: { spec+: { containers_+: { gb_fe: kube.Container("gb-frontend") { image: "gcr.io/google-samples/gb-frontend:v4", resources: { requests: { cpu: "100m", memory: "100Mi" } }, env_+: { GET_HOSTS_FROM: "dns", NUMBER_REPLICAS: my_spec.replicas, }, ports_+: { http: { containerPort: 80 } }, }}}}}}, frontend_service: kube.Service("frontend") { target_pod: $.frontend_deployment.spec.template, // spec+: { type: "LoadBalancer" }, }, redis_master_deployment: kube.Deployment("redis-master") { spec+: { template+: { spec+: { containers_+: { redis_master: kube.Container("redis-master") { image: "gcr.io/google_containers/redis:e2e", resources: { requests: { cpu: "100m", memory: "100Mi" } }, ports_+: { redis: { containerPort: 6379 }, }}}}}}}, redis_master_service: kube.Service("redis-master") { target_pod: $.redis_master_deployment.spec.template, }, redis_slave_deployment: kube.Deployment("redis-slave") { spec+: { replicas: 2, template+: { spec+: { containers_+: { redis_slave: kube.Container("redist-slave") { image: "gcr.io/google_samples/gb-redisslave:v1", resources: { requests: { cpu: "100m", memory: "100Mi" }, }, env_: { GET_HOSTS_FROM: "dns", }, ports_+: { redis: { containerPort: 6379 }, }}}}}}}, redis_slave_service: kube.Service("redis-slave") { target_pod: $.redis_slave_deployment.spec.template, }, } ================================================ FILE: vendor_jsonnet/kube-libsonnet/examples/wordpress/backend.jsonnet ================================================ local kube = import "lib/kube.libsonnet"; local labels = { tier: "backend", }; { backend: { secret: kube.Secret("mariadb") { metadata+: { labels+: labels, }, data_+: { "database_name": "webserver_db", "database_user": "webserver_user", "database_password": "webserver_db_password", "replication_user": "replica_user", "replication_password": "replica_password", "root_user": "root_user", "root_password": "root_password" }}, master: { local masterLabels = labels + { component: "master", }, statefulset: kube.StatefulSet("mariadb-master") { metadata+: { labels+: masterLabels, }, spec+: { template+: { spec+: { securityContext: { runAsUser: 1001, fsGroup: 1001, }, containers_+: { default: kube.Container("mariadb") { image: "bitnami/mariadb", ports_+: { mysql: { containerPort: 3306 } }, env_+: { MARIADB_REPLICATION_MODE: "master", MARIADB_REPLICATION_USER: kube.SecretKeyRef($.backend.secret, "replication_user"), MARIADB_REPLICATION_PASSWORD: kube.SecretKeyRef($.backend.secret, "replication_password"), MARIADB_ROOT_USER: kube.SecretKeyRef($.backend.secret, "root_user"), MARIADB_ROOT_PASSWORD: kube.SecretKeyRef($.backend.secret, "root_password"), MARIADB_USER: kube.SecretKeyRef($.backend.secret, "database_user"), MARIADB_DATABASE: kube.SecretKeyRef($.backend.secret, "database_name"), MARIADB_PASSWORD: kube.SecretKeyRef($.backend.secret, "database_password"), }, livenessProbe: { initialDelaySeconds: 40, exec: { command: [ "sh", "-c", "exec mysqladmin status -u$MARIADB_ROOT_USER -p$MARIADB_ROOT_PASSWORD", ]}}, readinessProbe: self.livenessProbe { initialDelaySeconds: 30, }, volumeMounts_+: { "mariadb-data": { "mountPath": "/bitnami/mariadb", }}}, metrics: kube.Container("metrics") { image: "prom/mysqld-exporter:v0.10.0", command: [ "sh", "-c", "DATA_SOURCE_NAME=\"$MARIADB_ROOT_USER:$MARIADB_ROOT_PASSWORD@(localhost:3306)/\" exec /bin/mysqld_exporter", ], ports_+: { metrics: { containerPort: 9104 } }, env_+: { MARIADB_ROOT_USER: kube.SecretKeyRef($.backend.secret, "root_user"), MARIADB_ROOT_PASSWORD: kube.SecretKeyRef($.backend.secret, "root_password"), }, livenessProbe: { initialDelaySeconds: 15, timeoutSeconds: 1, httpGet: { path: "/metrics", port: 9104, }}, readinessProbe: self.livenessProbe { initialDelaySeconds: 5, timeoutSeconds: 1, }}}}}, volumeClaimTemplates_+: { "mariadb-data": { storage: "10Gi", metadata+: { labels+: masterLabels, }}}}}, service: kube.Service("mariadb-master") { metadata+: { labels+: masterLabels, annotations+: { "prometheus.io/scrape": "true", "prometheus.io/port": "9104", }}, target_pod: $.backend.master.statefulset.spec.template, spec+: { ports: [ { name: "mariadb", port: 3306, targetPort: $.backend.master.statefulset.spec.template.spec.containers[0].ports[0].containerPort, }, { name: "metrics", port: 9104, targetPort: $.backend.master.statefulset.spec.template.spec.containers[1].ports[0].containerPort, }]}}}, slave: { local slaveLabels = labels + { component: "slave", }, statefulset: kube.StatefulSet("mariadb-slave") { metadata+: { labels+: slaveLabels, }, spec+: { template+: { spec+: { securityContext: { runAsUser: 1001, fsGroup: 1001, }, containers_+: { default: kube.Container("mariadb") { image: "bitnami/mariadb", ports_+: { mysql: { containerPort: 3306 } }, env_+: { MARIADB_REPLICATION_MODE: "slave", MARIADB_REPLICATION_USER: kube.SecretKeyRef($.backend.secret, "replication_user"), MARIADB_REPLICATION_PASSWORD: kube.SecretKeyRef($.backend.secret, "replication_password"), MARIADB_MASTER_HOST: $.backend.master.service.metadata.name, MARIADB_MASTER_ROOT_USER: kube.SecretKeyRef($.backend.secret, "root_user"), MARIADB_MASTER_ROOT_PASSWORD: kube.SecretKeyRef($.backend.secret, "root_password"), }, livenessProbe: { initialDelaySeconds: 40, exec: { command: [ "sh", "-c", "exec mysqladmin status -u$MARIADB_MASTER_ROOT_USER -p$MARIADB_MASTER_ROOT_PASSWORD", ]}}, readinessProbe: self.livenessProbe { initialDelaySeconds: 30, }, volumeMounts_+: { "mariadb-data": { "mountPath": "/bitnami/mariadb", }}}, metrics: kube.Container("metrics") { image: "prom/mysqld-exporter:v0.10.0", command: [ "sh", "-c", "DATA_SOURCE_NAME=\"$MARIADB_MASTER_ROOT_USER:$MARIADB_MASTER_ROOT_PASSWORD@(localhost:3306)/\" exec /bin/mysqld_exporter", ], ports_+: { metrics: { containerPort: 9104 } }, env_+: { MARIADB_MASTER_ROOT_USER: kube.SecretKeyRef($.backend.secret, "root_user"), MARIADB_MASTER_ROOT_PASSWORD: kube.SecretKeyRef($.backend.secret, "root_password"), }, livenessProbe: { initialDelaySeconds: 15, timeoutSeconds: 1, httpGet: { path: "/metrics", port: 9104, }}, readinessProbe: self.livenessProbe { initialDelaySeconds: 5, timeoutSeconds: 5, }}}}}, volumeClaimTemplates_+: { "mariadb-data": { storage: "10Gi", metadata+: { labels+: slaveLabels, }}}}}, service: kube.Service("mariadb-slave") { metadata+: { labels+: slaveLabels, annotations+: { "prometheus.io/scrape": "true", "prometheus.io/port": "9104", }}, target_pod: $.backend.slave.statefulset.spec.template, spec+: { ports: [ { name: "mariadb", port: 3306, targetPort: $.backend.slave.statefulset.spec.template.spec.containers[0].ports[0].containerPort, }, { name: "metrics", port: 9104, targetPort: $.backend.slave.statefulset.spec.template.spec.containers[1].ports[0].containerPort, }]}}}}} ================================================ FILE: vendor_jsonnet/kube-libsonnet/examples/wordpress/frontend.jsonnet ================================================ local kube = import "lib/kube.libsonnet"; local be = import "backend.jsonnet"; local labels = { tier: "frontend", }; { frontend: { pvc: kube.PersistentVolumeClaim("wordpress") { metadata+: { labels+: labels, }, storage:: "10Gi", }, configmap: kube.ConfigMap("wordpress") { metadata+: { labels+: labels, }, data: { "admin_first_name": "Admin", "admin_last_name": "User", "blog_name": "Kubernetes blog!", }}, secret: kube.Secret("wordpress") { metadata+: { labels+: labels, }, data_+: { "user": "user", "password": "bitnami", "mail": "user@example.com", }}, deployment: kube.Deployment("wordpress") { metadata+: { labels+: labels, }, spec+: { template+: { spec+: { containers_+: { default: kube.Container("wordpress") { image: "bitnami/wordpress", ports_+: { http: { containerPort: 80 } }, env_+: { MARIADB_HOST: be.backend.master.service.metadata.name, WORDPRESS_DATABASE_USER: kube.SecretKeyRef(be.backend.secret, "database_user"), WORDPRESS_DATABASE_NAME: kube.SecretKeyRef(be.backend.secret, "database_name"), WORDPRESS_DATABASE_PASSWORD: kube.SecretKeyRef(be.backend.secret, "database_password"), WORDPRESS_USERNAME: kube.SecretKeyRef($.frontend.secret, "user"), WORDPRESS_EMAIL: kube.SecretKeyRef($.frontend.secret, "mail"), WORDPRESS_PASSWORD: kube.SecretKeyRef($.frontend.secret, "password"), WORDPRESS_BLOG_NAME: kube.ConfigMapRef($.frontend.configmap, "blog_name"), WORDPRESS_FIRST_NAME: kube.ConfigMapRef($.frontend.configmap, "admin_first_name"), WORDPRESS_LAST_NAME: kube.ConfigMapRef($.frontend.configmap, "admin_last_name"), }, livenessProbe: { initialDelaySeconds: 120, httpGet: { path: "/wp-login.php", port: 80 }}, readinessProbe: self.livenessProbe { initialDelaySeconds: 60, }, volumeMounts_+: { "wordpress-data": { "mountPath": "/bitnami", }}}}, volumes_+: { "wordpress-data": { "persistentVolumeClaim": { "claimName": "wordpress", }}}}}}}, service: kube.Service("wordpress") { metadata+: { labels+: labels, }, target_pod: $.frontend.deployment.spec.template, }}} ================================================ FILE: vendor_jsonnet/kube-libsonnet/examples/wordpress/wordpress.jsonnet ================================================ // Copyright (c) 2018 Bitnami // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ``` // kubecfg update wordpress.jsonnet // // kubecfg delete wordpress.jsonnet // ``` local kube = import "lib/kube.libsonnet"; local fe = import "frontend.jsonnet"; local be = import "backend.jsonnet"; local findObjs(top) = std.flattenArrays([ if (std.objectHas(v, "apiVersion") && std.objectHas(v, "kind")) then [v] else findObjs(v) for v in kube.objectValues(top) ]); kube.List() { items_+: { frontend: fe, backend: be, }, items: findObjs(self.items_), } ================================================ FILE: vendor_jsonnet/kube-libsonnet/kube.libsonnet ================================================ // Generic library of Kubernetes objects (https://github.com/bitnami-labs/kube-libsonnet) // // Objects in this file follow the regular Kubernetes API object // schema with two exceptions: // // ## Optional helpers // // A few objects have defaults or additional "helper" hidden // (double-colon) fields that will help with common situations. For // example, `Service.target_pod` generates suitable `selector` and // `ports` blocks for the common case of a single-pod/single-port // service. If for some reason you don't want the helper, just // provide explicit values for the regular Kubernetes fields that the // helper *would* have generated, and the helper logic will be // ignored. // // ## The Underscore Convention: // // Various constructs in the Kubernetes API use JSON arrays to // represent unordered sets or named key/value maps. This is // particularly annoying with jsonnet since we want to use jsonnet's // powerful object merge operation with these constructs. // // To combat this, this library attempts to provide more "jsonnet // native" variants of these arrays in alternative hidden fields that // end with an underscore. For example, the `env_` block in // `Container`: // ``` // kube.Container("foo") { // env_: { FOO: "bar" }, // } // ``` // ... produces the expected `container.env` JSON array: // ``` // { // "env": [ // { "name": "FOO", "value": "bar" } // ] // } // ``` // // If you are confused by the underscore versions, or don't want them // in your situation then just ignore them and set the regular // non-underscore field as usual. // // // ## TODO // // TODO: Expand this to include all API objects. // // Should probably fill out all the defaults here too, so jsonnet can // reference them. In addition, jsonnet validation is more useful // (client-side, and gives better line information). { // resource contructors will use kinds/versions/fields compatible at least with version: minKubeVersion: { major: 1, minor: 9, version: "%s.%s" % [self.major, self.minor], }, // Returns array of values from given object. Does not include hidden fields. objectValues(o):: [o[field] for field in std.objectFields(o)], // Returns array of [key, value] pairs from given object. Does not include hidden fields. objectItems(o):: [[k, o[k]] for k in std.objectFields(o)], // Replace all occurrences of `_` with `-`. hyphenate(s):: std.join("-", std.split(s, "_")), // Convert an octal (as a string) to number, parseOctal(s):: ( local len = std.length(s); local leading = std.substr(s, 0, len - 1); local last = std.parseInt(std.substr(s, len - 1, 1)); assert last < 8 : "found '%s' digit >= 8" % [last]; last + (if len > 1 then 8 * $.parseOctal(leading) else 0) ), // Convert {foo: {a: b}} to [{name: foo, a: b}] mapToNamedList(o):: [{ name: $.hyphenate(n) } + o[n] for n in std.objectFields(o)], // Return object containing only these fields elements filterMapByFields(o, fields): { [field]: o[field] for field in std.setInter(std.objectFields(o), fields) }, // Convert from SI unit suffixes to regular number siToNum(n):: ( local convert = if std.endsWith(n, "m") then [1, 0.001] else if std.endsWith(n, "K") then [1, 1e3] else if std.endsWith(n, "M") then [1, 1e6] else if std.endsWith(n, "G") then [1, 1e9] else if std.endsWith(n, "T") then [1, 1e12] else if std.endsWith(n, "P") then [1, 1e15] else if std.endsWith(n, "E") then [1, 1e18] else if std.endsWith(n, "Ki") then [2, std.pow(2, 10)] else if std.endsWith(n, "Mi") then [2, std.pow(2, 20)] else if std.endsWith(n, "Gi") then [2, std.pow(2, 30)] else if std.endsWith(n, "Ti") then [2, std.pow(2, 40)] else if std.endsWith(n, "Pi") then [2, std.pow(2, 50)] else if std.endsWith(n, "Ei") then [2, std.pow(2, 60)] else error "Unknown numerical suffix in " + n; local n_len = std.length(n); std.parseInt(std.substr(n, 0, n_len - convert[0])) * convert[1] ), local remap(v, start, end, newstart) = if v >= start && v <= end then v - start + newstart else v, local remapChar(c, start, end, newstart) = std.char(remap( std.codepoint(c), std.codepoint(start), std.codepoint(end), std.codepoint(newstart) )), toLower(s):: ( std.join("", [remapChar(c, "A", "Z", "a") for c in std.stringChars(s)]) ), toUpper(s):: ( std.join("", [remapChar(c, "a", "z", "A") for c in std.stringChars(s)]) ), _Object(apiVersion, kind, name):: { local this = self, apiVersion: apiVersion, kind: kind, metadata: { name: name, labels: { name: std.join("-", std.split(this.metadata.name, ":")) }, annotations: {}, }, }, List(): { apiVersion: "v1", kind: "List", items_:: {}, items: $.objectValues(self.items_), }, Namespace(name): $._Object("v1", "Namespace", name) { }, Endpoints(name): $._Object("v1", "Endpoints", name) { Ip(addr):: { ip: addr }, Port(p):: { port: p }, subsets: [], }, Service(name): $._Object("v1", "Service", name) { local service = self, target_pod:: error "service target_pod required", port:: self.target_pod.spec.containers[0].ports[0].containerPort, // Helpers that format host:port in various ways host:: "%s.%s.svc" % [self.metadata.name, self.metadata.namespace], host_colon_port:: "%s:%s" % [self.host, self.spec.ports[0].port], http_url:: "http://%s/" % self.host_colon_port, proxy_urlpath:: "/api/v1/proxy/namespaces/%s/services/%s/" % [ self.metadata.namespace, self.metadata.name, ], // Useful in Ingress rules name_port:: { serviceName: service.metadata.name, servicePort: service.spec.ports[0].port, }, spec: { selector: service.target_pod.metadata.labels, ports: [ { port: service.port, targetPort: service.target_pod.spec.containers[0].ports[0].containerPort, }, ], type: "ClusterIP", }, }, PersistentVolume(name): $._Object("v1", "PersistentVolume", name) { spec: {}, }, // TODO: This is a terrible name PersistentVolumeClaimVolume(pvc): { persistentVolumeClaim: { claimName: pvc.metadata.name }, }, StorageClass(name): $._Object("storage.k8s.io/v1beta1", "StorageClass", name) { provisioner: error "provisioner required", }, PersistentVolumeClaim(name): $._Object("v1", "PersistentVolumeClaim", name) { local pvc = self, storageClass:: null, storage:: error "storage required", metadata+: if pvc.storageClass != null then { annotations+: { "volume.beta.kubernetes.io/storage-class": pvc.storageClass, }, } else {}, spec: { resources: { requests: { storage: pvc.storage, }, }, accessModes: ["ReadWriteOnce"], [if pvc.storageClass != null then "storageClassName"]: pvc.storageClass, }, }, Container(name): { name: name, image: error "container image value required", imagePullPolicy: if std.endsWith(self.image, ":latest") then "Always" else "IfNotPresent", envList(map):: [ if std.type(map[x]) == "object" then { name: x, valueFrom: map[x] } else { name: x, value: map[x] } for x in std.objectFields(map) ], env_:: {}, env: self.envList(self.env_), args_:: {}, args: ["--%s=%s" % kv for kv in $.objectItems(self.args_)], ports_:: {}, ports: $.mapToNamedList(self.ports_), volumeMounts_:: {}, volumeMounts: $.mapToNamedList(self.volumeMounts_), stdin: false, tty: false, assert !self.tty || self.stdin : "tty=true requires stdin=true", }, PodDisruptionBudget(name): $._Object("policy/v1beta1", "PodDisruptionBudget", name) { local this = self, target_pod:: error "target_pod required", spec: { minAvailable: 1, selector: { matchLabels: this.target_pod.metadata.labels, }, }, }, Pod(name): $._Object("v1", "Pod", name) { spec: $.PodSpec, }, PodSpec: { // The 'first' container is used in various defaults in k8s. local container_names = std.objectFields(self.containers_), default_container:: if std.length(container_names) > 1 then "default" else container_names[0], containers_:: {}, local container_names_ordered = [self.default_container] + [n for n in container_names if n != self.default_container], containers: [{ name: $.hyphenate(name) } + self.containers_[name] for name in container_names_ordered if self.containers_[name] != null], // Note initContainers are inherently ordered, and using this // named object will lose that ordering. If order matters, then // manipulate `initContainers` directly (perhaps // appending/prepending to `super.initContainers` to mix+match // both approaches) initContainers_:: {}, initContainers: [{ name: $.hyphenate(name) } + self.initContainers_[name] for name in std.objectFields(self.initContainers_) if self.initContainers_[name] != null], volumes_:: {}, volumes: $.mapToNamedList(self.volumes_), imagePullSecrets: [], terminationGracePeriodSeconds: 30, assert std.length(self.containers) > 0 : "must have at least one container", // Return an array of pod's ports numbers ports(proto):: [ p.containerPort for p in std.flattenArrays([ c.ports for c in self.containers ]) if ( (!(std.objectHas(p, "protocol")) && proto == "TCP") || ((std.objectHas(p, "protocol")) && p.protocol == proto) ) ], }, EmptyDirVolume(): { emptyDir: {}, }, HostPathVolume(path, type=""): { hostPath: { path: path, type: type }, }, GitRepoVolume(repository, revision): { gitRepo: { repository: repository, // "master" is possible, but should be avoided for production revision: revision, }, }, SecretVolume(secret): { secret: { secretName: secret.metadata.name }, }, ConfigMapVolume(configmap): { configMap: { name: configmap.metadata.name }, }, ConfigMap(name): $._Object("v1", "ConfigMap", name) { data: {}, // I keep thinking data values can be any JSON type. This check // will remind me that they must be strings :( local nonstrings = [ k for k in std.objectFields(self.data) if std.type(self.data[k]) != "string" ], assert std.length(nonstrings) == 0 : "data contains non-string values: %s" % [nonstrings], }, // subtype of EnvVarSource ConfigMapRef(configmap, key): { assert std.objectHas(configmap.data, key) : "%s not in configmap.data" % [key], configMapKeyRef: { name: configmap.metadata.name, key: key, }, }, Secret(name): $._Object("v1", "Secret", name) { local secret = self, type: "Opaque", data_:: {}, data: { [k]: std.base64(secret.data_[k]) for k in std.objectFields(secret.data_) }, }, // subtype of EnvVarSource SecretKeyRef(secret, key): { assert std.objectHas(secret.data, key) : "%s not in secret.data" % [key], secretKeyRef: { name: secret.metadata.name, key: key, }, }, // subtype of EnvVarSource FieldRef(key): { fieldRef: { apiVersion: "v1", fieldPath: key, }, }, // subtype of EnvVarSource ResourceFieldRef(key, divisor="1"): { resourceFieldRef: { resource: key, divisor: std.toString(divisor), }, }, Deployment(name): $._Object("apps/v1", "Deployment", name) { local deployment = self, spec: { template: { spec: $.PodSpec, metadata: { labels: deployment.metadata.labels, annotations: {}, }, }, selector: { matchLabels: deployment.spec.template.metadata.labels, }, strategy: { type: "RollingUpdate", local pvcs = [ v for v in deployment.spec.template.spec.volumes if std.objectHas(v, "persistentVolumeClaim") ], local is_stateless = std.length(pvcs) == 0, // Apps trying to maintain a majority quorum or similar will // want to tune these carefully. // NB: Upstream default is surge=1 unavail=1 rollingUpdate: if is_stateless then { maxSurge: "25%", // rounds up maxUnavailable: "25%", // rounds down } else { // Poor-man's StatelessSet. Useful mostly with replicas=1. maxSurge: 0, maxUnavailable: 1, }, }, // NB: Upstream default is 0 minReadySeconds: 30, // NB: Regular k8s default is to keep all revisions revisionHistoryLimit: 10, replicas: 1, }, }, CrossVersionObjectReference(target): { apiVersion: target.apiVersion, kind: target.kind, name: target.metadata.name, }, HorizontalPodAutoscaler(name): $._Object("autoscaling/v1", "HorizontalPodAutoscaler", name) { local hpa = self, target:: error "target required", spec: { scaleTargetRef: $.CrossVersionObjectReference(hpa.target), minReplicas: hpa.target.spec.replicas, maxReplicas: error "maxReplicas required", assert self.maxReplicas >= self.minReplicas, }, }, StatefulSet(name): $._Object("apps/v1", "StatefulSet", name) { local sset = self, spec: { serviceName: name, updateStrategy: { type: "RollingUpdate", rollingUpdate: { partition: 0, }, }, template: { spec: $.PodSpec, metadata: { labels: sset.metadata.labels, annotations: {}, }, }, selector: { matchLabels: sset.spec.template.metadata.labels, }, volumeClaimTemplates_:: {}, volumeClaimTemplates: [ // StatefulSet is overly fussy about "changes" (even when // they're no-ops). // In particular annotations={} is apparently a "change", // since the comparison is ignorant of defaults. std.prune($.PersistentVolumeClaim($.hyphenate(kv[0])) + { apiVersion:: null, kind:: null } + kv[1]) for kv in $.objectItems(self.volumeClaimTemplates_) ], replicas: 1, assert self.replicas >= 1, }, }, Job(name): $._Object("batch/v1", "Job", name) { local job = self, spec: $.JobSpec { template+: { metadata+: { labels: job.metadata.labels, }, }, }, }, // NB: kubernetes >= 1.8.x has batch/v1beta1 (olders were batch/v2alpha1) CronJob(name): $._Object("batch/v1beta1", "CronJob", name) { local cronjob = self, spec: { jobTemplate: { spec: $.JobSpec { template+: { metadata+: { labels: cronjob.metadata.labels, }, }, }, }, schedule: error "Need to provide spec.schedule", successfulJobsHistoryLimit: 10, failedJobsHistoryLimit: 20, // NB: upstream concurrencyPolicy default is "Allow" concurrencyPolicy: "Forbid", }, }, JobSpec: { local this = self, template: { spec: $.PodSpec { restartPolicy: "OnFailure", }, }, completions: 1, parallelism: 1, }, DaemonSet(name): $._Object("apps/v1", "DaemonSet", name) { local ds = self, spec: { updateStrategy: { type: "RollingUpdate", rollingUpdate: { maxUnavailable: 1, }, }, template: { metadata: { labels: ds.metadata.labels, annotations: {}, }, spec: $.PodSpec, }, selector: { matchLabels: ds.spec.template.metadata.labels, }, }, }, Ingress(name): $._Object("extensions/v1beta1", "Ingress", name) { spec: {}, local rel_paths = [ p.path for r in self.spec.rules for p in r.http.paths if !std.startsWith(p.path, "/") ], assert std.length(rel_paths) == 0 : "paths must be absolute: " + rel_paths, }, ThirdPartyResource(name): $._Object("extensions/v1beta1", "ThirdPartyResource", name) { versions_:: [], versions: [{ name: n } for n in self.versions_], }, CustomResourceDefinition(group, version, kind): { local this = self, apiVersion: "apiextensions.k8s.io/v1beta1", kind: "CustomResourceDefinition", metadata+: { name: this.spec.names.plural + "." + this.spec.group, }, spec: { scope: "Namespaced", group: group, version: version, names: { kind: kind, singular: $.toLower(self.kind), plural: self.singular + "s", listKind: self.kind + "List", }, }, }, ServiceAccount(name): $._Object("v1", "ServiceAccount", name) { }, Role(name): $._Object("rbac.authorization.k8s.io/v1", "Role", name) { rules: [], }, ClusterRole(name): $.Role(name) { kind: "ClusterRole", }, Group(name): { kind: "Group", name: name, apiGroup: "rbac.authorization.k8s.io", }, User(name): { kind: "User", name: name, apiGroup: "rbac.authorization.k8s.io", }, RoleBinding(name): $._Object("rbac.authorization.k8s.io/v1", "RoleBinding", name) { local rb = self, subjects_:: [], subjects: [{ kind: o.kind, namespace: o.metadata.namespace, name: o.metadata.name, } for o in self.subjects_], roleRef_:: error "roleRef is required", roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: rb.roleRef_.kind, name: rb.roleRef_.metadata.name, }, }, ClusterRoleBinding(name): $.RoleBinding(name) { kind: "ClusterRoleBinding", }, // NB: datalines_ can be used to reduce boilerplate importstr as: // kubectl get secret ... -ojson mysec | kubeseal | jq -r .spec.data > mysec-ssdata.txt // datalines_: importstr "mysec-ssddata.txt" SealedSecret(name): $._Object("bitnami.com/v1alpha1", "SealedSecret", name) { spec: { data: if self.datalines_ != "" then std.join("", std.split(self.datalines_, "\n")) else error "data or datalines_ required (output from: kubeseal | jq -r .spec.data)", datalines_:: "", }, assert std.base64Decode(self.spec.data) != "", }, // NB: helper method to access several Kubernetes objects podRef, // used below to extract its labels podRef(obj):: ({ Pod: obj, Deployment: obj.spec.template, StatefulSet: obj.spec.template, DaemonSet: obj.spec.template, Job: obj.spec.template, CronJob: obj.spec.jobTemplate.spec.template, }[obj.kind]), // NB: return a { podSelector: ... } ready to use for e.g. NSPs (see below) // pod labels can be optionally filtered by their label name 2nd array arg podLabelsSelector(obj, filter=null):: { podSelector: std.prune({ matchLabels: if filter != null then $.filterMapByFields($.podRef(obj).metadata.labels, filter) else $.podRef(obj).metadata.labels, }), }, // NB: Returns an array as [{ port: num, protocol: "PROTO" }, {...}, ... ] // Need to split TCP, UDP logic to be able to dedup each set of protocol ports podsPorts(obj_list):: std.flattenArrays([ [ { port: port, protocol: protocol } for port in std.set( std.flattenArrays([$.podRef(obj).spec.ports(protocol) for obj in obj_list]) ) ] for protocol in ["TCP", "UDP"] ]), // NB: most of the "helper" stuff comes from above (podLabelsSelector, podsPorts), // NetworkPolicy returned object will have "Ingress", "Egress" policyTypes auto-set // based on populated spec.ingress or spec.egress // See tests/test-simple-validate.jsonnet for example(s). NetworkPolicy(name): $._Object("networking.k8s.io/v1", "NetworkPolicy", name) { local networkpolicy = self, spec: { policyTypes: std.prune([ if networkpolicy.spec.ingress != [] then "Ingress" else null, if networkpolicy.spec.egress != [] then "Egress" else null, ]), ingress: $.objectValues(self.ingress_), ingress_:: {}, egress: $.objectValues(self.egress_), egress_:: {}, podSelector: {}, }, }, } ================================================ FILE: vendor_jsonnet/kube-libsonnet/tests/Dockerfile ================================================ FROM bitnami/minideb:buster LABEL org.opencontainers.image.authors="sre@bitnami.com" ARG jsonnet_version=0.14.0 ARG kubectl_version=v1.13.0 ARG kubecfg_version=v0.12.0 RUN install_packages jq make curl ca-certificates RUN adduser --home /home/user --disabled-password --gecos User user RUN curl -sLo /tmp/jsonnet-v${jsonnet_version}.tar.gz https://github.com/google/jsonnet/releases/download/v${jsonnet_version}/jsonnet-bin-v${jsonnet_version}-linux.tar.gz RUN tar -zxf /tmp/jsonnet-v${jsonnet_version}.tar.gz -C /tmp && mv /tmp/jsonnet /tmp/jsonnetfmt /usr/local/bin RUN curl -sLo /usr/local/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/${kubectl_version}/bin/linux/amd64/kubectl RUN chmod +x /usr/local/bin/kubectl RUN curl -sLo /usr/local/bin/kubecfg https://github.com/bitnami/kubecfg/releases/download/${kubecfg_version}/kubecfg-linux-amd64 RUN chmod +x /usr/local/bin/kubecfg USER user WORKDIR /home/user CMD ["/bin/bash", "-l"] ================================================ FILE: vendor_jsonnet/kube-libsonnet/tests/Makefile ================================================ SHELL=/bin/bash JSONNET_FMT=--indent 2 --string-style d --comment-style s --no-pad-arrays --pad-objects --pretty-field-names ALL_JSONNET=$(wildcard *.jsonnet) UNITTEST_JSONNET=$(wildcard unittest*.jsonnet) LIB_JSONNET=$(wildcard ../*.libsonnet) ALL_K8S_VALIDATE_JSONNET=$(wildcard *-validate.jsonnet) PHONY_GOLDEN=$(patsubst %.jsonnet,golden/%.json,$(ALL_JSONNET)) PHONY_DIFF=$(patsubst %.jsonnet,%.diff,$(ALL_JSONNET)) PHONY_PARSE=$(patsubst %.jsonnet,%.parse,$(ALL_JSONNET)) ## These need to be in-sync with docker-compose.yaml DOCKER_E2E=e2e-test TMP_RANCHER=./tmp-rancher PROJECT=kubelibsonnet tests: docker-compose-tests docker-compose-tests: install -d $(TMP_RANCHER) $(TMP_RANCHER)/etc && touch $(TMP_RANCHER)/etc/k3s.yaml USERID=$$(id -u) docker-compose -p $(PROJECT) up -d rc=$$(timeout 60s docker wait $(DOCKER_E2E)) || rc=255 ;\ test $$rc -ne 0 && docker logs k3s-api;\ docker logs $(DOCKER_E2E);\ docker-compose -p $(PROJECT) down;\ exit $$rc rm -rf ./$(TMP_RANCHER) test-srcs: unittests lint parse diff test-kube: validate # NB: unittest jsonnet files are also covered by parse and diff targets, # called out here for convenience unittests: jsonnet $(UNITTEST_JSONNET) lint: @set -e; errs=0; \ for f in $(ALL_JSONNET) $(LIB_JSONNET); do \ if ! jsonnetfmt --test $(JSONNET_FMT) -- $$f; then \ echo "FAILED lint: $$f" >&2; \ errs=$$(( $$errs + 1 )); \ fi; \ done; \ if [ $$errs -gt 0 ]; then \ echo "NOTE: if the 'lint' target fails, run:"; \ echo " $(MAKE) fix-lint lint"; \ exit 1; \ fi parse: $(PHONY_PARSE) diff: diff-help $(PHONY_DIFF) validate: timeout 10 kubectl api-versions > /dev/null \ || { echo "WARNING: no usable runtime kube context, skipping."; exit 0 ;} \ && kubectl version --short && kubecfg version && kubecfg validate --ignore-unknown=false $(ALL_K8S_VALIDATE_JSONNET) %.diff: %.jsonnet diff -u golden/$(*).json <(jsonnet $(<)) %.parse: %.jsonnet jsonnet $(<) > /dev/null golden/%.json: %.jsonnet jsonnet $(<) > $(@) diff-help: @echo "NOTE: if the 'diff' target fails, review output and run:" @echo " $(MAKE) gen-golden diff" @echo fix-lint: @set -e; \ for f in $(ALL_JSONNET) $(LIB_JSONNET); do \ echo jsonnetfmt -i $(JSONNET_FMT) -- $$f; \ jsonnetfmt -i $(JSONNET_FMT) -- $$f; \ done gen-golden: $(PHONY_GOLDEN) .PHONY: unittests lint parse validate diff %.parse %.diff golden/%.json diff-help fix-lint gen-golden ================================================ FILE: vendor_jsonnet/kube-libsonnet/tests/docker-compose.yaml ================================================ version: "3" services: kube-api: image: rancher/k3s:${K3S_VERSION} command: server --disable-agent container_name: k3s-api volumes: - ./tmp-rancher:/.kube - ./tmp-rancher:/.rancher - ./tmp-rancher/etc:/etc/rancher/k3s expose: - 6443 user: "${USERID}" environment: - USER=nobody - HOME=/ e2e-test: build: . container_name: e2e-test links: - "kube-api:kube-api" depends_on: - kube-api volumes: - ./tmp-rancher:/tmp/rancher - ..:/work working_dir: /work environment: - HOME=/ user: "${USERID}" command: - bash - -c - | echo "INFO: Starting tests: unit, lint ..." make -C tests test-srcs export KUBECONFIG=/tmp/kubeconfig echo "INFO: Waiting for kube-api to be available ..." until kubectl get nodes; do sleep 1 # Found that k3s releases create k3s.yaml under diff paths, # redirecting stderr just to avoid red-herrings errors sed -e s/localhost/kube-api/ -e s/127.0.0.1/kube-api/ \ /tmp/rancher/k3s.yaml /tmp/rancher/etc/k3s.yaml \ > $$KUBECONFIG 2>/dev/null done echo "INFO: Starting tests: validate ..." set -x make -C tests test-kube ================================================ FILE: vendor_jsonnet/kube-libsonnet/tests/golden/test-sealedsecrets-datalines.json ================================================ { "apiVersion": "v1", "items": [ { "apiVersion": "bitnami.com/v1alpha1", "kind": "SealedSecret", "metadata": { "annotations": { }, "labels": { "name": "foo" }, "name": "foo" }, "spec": { "data": "AgCz+HLrnNCZoDCOcCbMgKp//+Tfz5ecNcVGZOnLT/M/RP5E7u6ifHtm3GfG8qJKcRK9v6ZBYOIeDqenlSNMpRmigXH0p2ThMfmBcPIYfLvqOcwfwssfOS4jp02qsAjSfaAXuL2Mze15OnXImiPHsoi9BIrJnyhRObBeRMnJ4xMUAFPih1CfqY3nl7FQhH6+4t3Wh52JY3VmDWYYGH5Kc5JyS40PodCUEgId1kJKtChUPQ0JsTgj6W8ymyJ0bC8jyzrZHD0gLo26T+7mNiJyUEU9qj7p8YhZj3v6VcEv4DKQVVvkGk94svTAdnSQrh5cNOaZrp6eTJqIs+PcrelTVitoZzZsnIc+ze6aPjGhft752xa9CDmuCkQ9XkWfWpGcGrEv1jmIPkJYIjPIKgP6CS5eU+eBZNIkNpIof2jiYdqqP59BhoBwVkC/Rm1GfgOdLxO55sicTwSmRA+/pkE6hopK327uwxWmtGZ5/kUuW6nZ4shTNTsr1n1skZJvw6UfwkAgIYuDctHqw+y7eSdDM2gggLM72TgBEf5111LnIj2rroQu5bR6XVVpMy6QFhE9LLAC3kcs/BqPh5A7qq/ffZrFhWpSIS7voavtdqn6XhdJ8TsJ7Wbkhjxf1S/l/YqS6B1ZlJFlrdcNA6BHqzyjncmrD030YfwNqYUl2Itxok7S6DaImzBkxV5Uqd18EiuVcbVL3Ic1I3B4pviBAo0eG5nn4/uZCjuBKu6muLtR5GFMblbqV23MiB8Q3jtODvVw6SbFfNQx836RsJAFSmE+axSGvZPq4zDXGUYekOHZ+ov6Gd2Czv99Cf67r3ogJIzDR1uVRZH78bYrjEAkRGpKETUfLFfRyRZzWW/K5cKEAoDttzHI5grlgm44k5RQeaoargmyVjuEgjEZ2yZnl5Z7BX3YSazcLfK2t/wwGNHMMgedf3SeGETIx552CehRw7+A1+0q4iKQ9z13dPpjAYNzmLd1bGY4ORQeEf4Duk6NTXXHfpyRSq/CNK5BPhKnTn1OEnUQ+ttD/ZjNkx2OMFsX7zfMfk0RRRnKlWpanYOtTinx5WbpIYOKZ4Labb7+CnuHMgTZoNTXZe2DnNFqWIgm47U=" } } ], "kind": "List" } ================================================ FILE: vendor_jsonnet/kube-libsonnet/tests/golden/test-sealedsecrets.json ================================================ { "apiVersion": "v1", "items": [ { "apiVersion": "bitnami.com/v1alpha1", "kind": "SealedSecret", "metadata": { "annotations": { }, "labels": { "name": "foo" }, "name": "foo" }, "spec": { "data": "dGVzdAo=" } } ], "kind": "List" } ================================================ FILE: vendor_jsonnet/kube-libsonnet/tests/golden/test-simple-validate.json ================================================ { "apiVersion": "v1", "items": [ { "apiVersion": "v1", "data": { "foo_key": "bar_val" }, "kind": "ConfigMap", "metadata": { "annotations": { }, "labels": { "name": "foo-config" }, "name": "foo-config", "namespace": "foons" } }, { "apiVersion": "batch/v1beta1", "kind": "CronJob", "metadata": { "annotations": { }, "labels": { "name": "foo-cronjob" }, "name": "foo-cronjob", "namespace": "foons" }, "spec": { "concurrencyPolicy": "Forbid", "failedJobsHistoryLimit": 20, "jobTemplate": { "spec": { "completions": 1, "parallelism": 1, "template": { "metadata": { "labels": { "name": "foo-cronjob" } }, "spec": { "containers": [ { "args": [ ], "env": [ ], "image": "busybox", "imagePullPolicy": "IfNotPresent", "name": "foo", "ports": [ ], "stdin": false, "tty": false, "volumeMounts": [ ] } ], "imagePullSecrets": [ ], "initContainers": [ ], "restartPolicy": "OnFailure", "terminationGracePeriodSeconds": 30, "volumes": [ ] } } } }, "schedule": "0 * * * *", "successfulJobsHistoryLimit": 10 } }, { "apiVersion": "apps/v1", "kind": "Deployment", "metadata": { "annotations": { }, "labels": { "name": "foo-deploy" }, "name": "foo-deploy", "namespace": "foons" }, "spec": { "minReadySeconds": 30, "replicas": 1, "revisionHistoryLimit": 10, "selector": { "matchLabels": { "name": "foo-deploy" } }, "strategy": { "rollingUpdate": { "maxSurge": "25%", "maxUnavailable": "25%" }, "type": "RollingUpdate" }, "template": { "metadata": { "annotations": { }, "labels": { "name": "foo-deploy" } }, "spec": { "containers": [ { "args": [ ], "env": [ { "name": "my_secret", "valueFrom": { "secretKeyRef": { "key": "sec_key", "name": "foo-secret" } } } ], "image": "nginx:1.12", "imagePullPolicy": "IfNotPresent", "name": "foo", "ports": [ { "containerPort": 80, "name": "http" }, { "containerPort": 888, "name": "udp-port", "protocol": "UDP" } ], "stdin": false, "tty": false, "volumeMounts": [ { "mountPath": "/config", "name": "config-vol" } ] } ], "imagePullSecrets": [ ], "initContainers": [ ], "serviceAccountName": "foo-sa", "terminationGracePeriodSeconds": 30, "volumes": [ { "configMap": { "name": "foo-config" }, "name": "config-vol" } ] } } } }, { "apiVersion": "apps/v1", "kind": "DaemonSet", "metadata": { "annotations": { }, "labels": { "name": "foo-ds" }, "name": "foo-ds", "namespace": "foons" }, "spec": { "selector": { "matchLabels": { "name": "foo-ds" } }, "template": { "metadata": { "annotations": { }, "labels": { "name": "foo-ds" } }, "spec": { "containers": [ { "args": [ ], "env": [ { "name": "my_secret", "valueFrom": { "secretKeyRef": { "key": "sec_key", "name": "foo-secret" } } } ], "image": "nginx:1.12", "imagePullPolicy": "IfNotPresent", "name": "foo", "ports": [ { "containerPort": 80, "name": "http" }, { "containerPort": 888, "name": "udp-port", "protocol": "UDP" } ], "stdin": false, "tty": false, "volumeMounts": [ { "mountPath": "/config", "name": "config-vol" } ] } ], "imagePullSecrets": [ ], "initContainers": [ ], "terminationGracePeriodSeconds": 30, "volumes": [ { "configMap": { "name": "foo-config" }, "name": "config-vol" } ] } }, "updateStrategy": { "rollingUpdate": { "maxUnavailable": 1 }, "type": "RollingUpdate" } } }, { "apiVersion": "extensions/v1beta1", "kind": "Ingress", "metadata": { "annotations": { "certmanager.k8s.io/acme-challenge-type": "dns01", "certmanager.k8s.io/acme-dns01-provider": "default", "certmanager.k8s.io/cluster-issuer": "letsencrypt-prod-dns" }, "labels": { "name": "foo-ingress" }, "name": "foo-ingress", "namespace": "foons" }, "spec": { "rules": [ { "host": "foo.g.dev.bitnami.net", "http": { "paths": [ { "backend": { "serviceName": "foo-svc", "servicePort": 80 }, "path": "/" } ] } } ], "tls": [ { "hosts": [ "foo.g.dev.bitnami.net" ], "secretName": "foo-ingress-cert" } ] } }, { "apiVersion": "batch/v1", "kind": "Job", "metadata": { "annotations": { }, "labels": { "name": "foo-job" }, "name": "foo-job", "namespace": "foons" }, "spec": { "completions": 1, "parallelism": 1, "template": { "metadata": { "labels": { "name": "foo-job" } }, "spec": { "containers": [ { "args": [ ], "env": [ ], "image": "busybox", "imagePullPolicy": "IfNotPresent", "name": "foo", "ports": [ ], "stdin": false, "tty": false, "volumeMounts": [ ] } ], "imagePullSecrets": [ ], "initContainers": [ ], "restartPolicy": "OnFailure", "terminationGracePeriodSeconds": 30, "volumes": [ ] } } } }, { "apiVersion": "v1", "kind": "Namespace", "metadata": { "annotations": { }, "labels": { "name": "foons" }, "name": "foons" } }, { "apiVersion": "networking.k8s.io/v1", "kind": "NetworkPolicy", "metadata": { "annotations": { }, "labels": { "name": "foo-nsp-pods" }, "name": "foo-nsp-pods", "namespace": "foons" }, "spec": { "egress": [ { "ports": [ { "port": 53, "protocol": "UDP" } ], "to": [ { "namespaceSelector": { "matchLabels": { "name": "kube-system" } } } ] }, { "ports": [ { "port": 80, "protocol": "TCP" }, { "port": 888, "protocol": "UDP" } ], "to": [ { "podSelector": { "matchLabels": { "name": "foo-sts" } } } ] } ], "ingress": [ { "from": [ { "podSelector": { "matchLabels": { "name": "foo-job" } } }, { "podSelector": { "matchLabels": { "name": "foo-cronjob" } } }, { "namespaceSelector": { "matchLabels": { "name": "nginx-ingress" } } } ], "ports": [ { "port": 80, "protocol": "TCP" }, { "port": 888, "protocol": "UDP" } ] } ], "podSelector": { "matchLabels": { "name": "foo-deploy" } }, "policyTypes": [ "Ingress", "Egress" ] } }, { "apiVersion": "v1", "kind": "Pod", "metadata": { "annotations": { }, "labels": { "name": "foo-pod" }, "name": "foo-pod", "namespace": "foons" }, "spec": { "containers": [ { "args": [ ], "env": [ { "name": "my_secret", "valueFrom": { "secretKeyRef": { "key": "sec_key", "name": "foo-secret" } } } ], "image": "nginx:1.12", "imagePullPolicy": "IfNotPresent", "name": "foo", "ports": [ { "containerPort": 80, "name": "http" }, { "containerPort": 888, "name": "udp-port", "protocol": "UDP" } ], "stdin": false, "tty": false, "volumeMounts": [ { "mountPath": "/config", "name": "config-vol" } ] } ], "imagePullSecrets": [ ], "initContainers": [ ], "terminationGracePeriodSeconds": 30, "volumes": [ { "configMap": { "name": "foo-config" }, "name": "config-vol" } ] } }, { "apiVersion": "rbac.authorization.k8s.io/v1", "kind": "Role", "metadata": { "annotations": { }, "labels": { "name": "foo-role" }, "name": "foo-role", "namespace": "foons" }, "rules": [ { "apiGroups": [ "" ], "resources": [ "pods", "secrets", "configmaps", "persistentvolumeclaims" ], "verbs": [ "get" ] }, { "apiGroups": [ "" ], "resources": [ "pods" ], "verbs": [ "patch" ] } ] }, { "apiVersion": "rbac.authorization.k8s.io/v1", "kind": "RoleBinding", "metadata": { "annotations": { }, "labels": { "name": "foo-rolebinding" }, "name": "foo-rolebinding", "namespace": "foons" }, "roleRef": { "apiGroup": "rbac.authorization.k8s.io", "kind": "Role", "name": "foo-role" }, "subjects": [ { "kind": "ServiceAccount", "name": "foo-sa", "namespace": "foons" } ] }, { "apiVersion": "v1", "kind": "ServiceAccount", "metadata": { "annotations": { }, "labels": { "name": "foo-sa" }, "name": "foo-sa", "namespace": "foons" } }, { "apiVersion": "v1", "data": { "sec_key": "c2VjcmV0Cg==" }, "kind": "Secret", "metadata": { "annotations": { }, "labels": { "name": "foo-secret" }, "name": "foo-secret", "namespace": "foons" }, "type": "Opaque" }, { "apiVersion": "v1", "kind": "Service", "metadata": { "annotations": { }, "labels": { "name": "foo-svc" }, "name": "foo-svc", "namespace": "foons" }, "spec": { "ports": [ { "port": 80, "targetPort": 80 } ], "selector": { "name": "foo-deploy" }, "type": "ClusterIP" } }, { "apiVersion": "apps/v1", "kind": "StatefulSet", "metadata": { "annotations": { }, "labels": { "name": "foo-sts" }, "name": "foo-sts", "namespace": "foons" }, "spec": { "replicas": 1, "selector": { "matchLabels": { "name": "foo-sts" } }, "serviceName": "foo-sts", "template": { "metadata": { "annotations": { }, "labels": { "name": "foo-sts" } }, "spec": { "containers": [ { "args": [ ], "env": [ { "name": "my_secret", "valueFrom": { "secretKeyRef": { "key": "sec_key", "name": "foo-secret" } } } ], "image": "nginx:1.12", "imagePullPolicy": "IfNotPresent", "name": "foo", "ports": [ { "containerPort": 80, "name": "http" }, { "containerPort": 888, "name": "udp-port", "protocol": "UDP" } ], "stdin": false, "tty": false, "volumeMounts": [ { "mountPath": "/config", "name": "config-vol" }, { "mountPath": "/foo/data", "name": "datadir" } ] } ], "imagePullSecrets": [ ], "initContainers": [ ], "serviceAccountName": "foo-sa", "terminationGracePeriodSeconds": 30, "volumes": [ { "configMap": { "name": "foo-config" }, "name": "config-vol" } ] } }, "updateStrategy": { "rollingUpdate": { "partition": 0 }, "type": "RollingUpdate" }, "volumeClaimTemplates": [ { "metadata": { "labels": { "name": "datadir" }, "name": "datadir", "namespace": "foons" }, "spec": { "accessModes": [ "ReadWriteOnce" ], "resources": { "requests": { "storage": "10Gi" } } } } ] } } ], "kind": "List" } ================================================ FILE: vendor_jsonnet/kube-libsonnet/tests/golden/unittests.json ================================================ true ================================================ FILE: vendor_jsonnet/kube-libsonnet/tests/test-sealedsecrets-datalines.jsonnet ================================================ local kube = import "../kube.libsonnet"; local stack = { sealedsecret: kube.SealedSecret("foo") { spec+: { datalines_: importstr "test-sealedsecrets-datalines.txt", }, }, }; kube.List() { items_+: stack, } ================================================ FILE: vendor_jsonnet/kube-libsonnet/tests/test-sealedsecrets-datalines.txt ================================================ AgCz+HLrnNCZoDCOcCbMgKp//+Tfz5ecNcVGZOnLT/M/RP5E7u6ifHtm3GfG8qJKcRK9v6ZBYOIeDqenlSNMpRmigXH0p2ThMfmBcPIYfLvqOcwfwssfOS4jp02qsAjSfaAXuL2Mze15OnXImiPHsoi9BIrJnyhRObBeRMnJ4xMUAFPih1CfqY3nl7FQhH6+4t3Wh52JY3VmDWYYGH5Kc5JyS40PodCUEgId1kJKtChUPQ0JsTgj6W8ymyJ0bC8jyzrZHD0gLo26T+7mNiJyUEU9qj7p8YhZj3v6VcEv4DKQVVvkGk94svTAdnSQrh5cNOaZrp6eTJqIs+PcrelTVitoZzZsnIc+ze6aPjGhft752xa9CDmuCkQ9XkWfWpGcGrEv1jmIPkJYIjPIKgP6CS5eU+eBZNIkNpIof2jiYdqqP59BhoBwVkC/Rm1GfgOdLxO55sicTwSmRA+/pkE6hopK327uwxWmtGZ5/kUuW6nZ4shTNTsr1n1skZJvw6UfwkAgIYuDctHqw+y7eSdDM2gggLM72TgBEf5111LnIj2rroQu5bR6XVVpMy6QFhE9LLAC3kcs/BqPh5A7qq/ffZrFhWpSIS7voavtdqn6XhdJ8TsJ7Wbkhjxf1S/l/YqS6B1ZlJFlrdcNA6BHqzyjncmrD030YfwNqYUl2Itxok7S6DaImzBkxV5Uqd18EiuVcbVL3Ic1I3B4pviBAo0eG5nn4/uZCjuBKu6muLtR5GFMblbqV23MiB8Q3jtODvVw6SbFfNQx836RsJAFSmE+axSGvZPq4zDXGUYekOHZ+ov6Gd2Czv99Cf67r3ogJIzDR1uVRZH78bYrjEAkRGpKETUfLFfRyRZzWW/K5cKEAoDttzHI5grlgm44k5RQeaoargmyVjuEgjEZ2yZnl5Z7BX3YSazcLfK2t/wwGNHMMgedf3SeGETIx552CehRw7+A1+0q4iKQ9z13dPpjAYNzmLd1bGY4ORQeEf4Duk6NTXXHfpyRSq/CNK5BPhKnTn1OEnUQ+ttD/ZjNkx2OMFsX7zfMfk0RRRnKlWpanYOtTinx5WbpIYOKZ4Labb7+CnuHMgTZoNTXZe2DnNFqWIgm47U= ================================================ FILE: vendor_jsonnet/kube-libsonnet/tests/test-sealedsecrets.jsonnet ================================================ local kube = import "../kube.libsonnet"; local stack = { sealedsecret: kube.SealedSecret("foo") { spec+: { data: "dGVzdAo=", }, }, }; kube.List() { items_+: stack, } ================================================ FILE: vendor_jsonnet/kube-libsonnet/tests/test-simple-validate.jsonnet ================================================ local bitnami = import "../bitnami.libsonnet"; local kube = import "../kube.libsonnet"; local stack = { namespace:: "foons", name:: "foo", ns: kube.Namespace($.namespace), sa: kube.ServiceAccount($.name + "-sa") { metadata+: { namespace: $.namespace }, }, role: kube.Role($.name + "-role") { metadata+: { namespace: $.namespace }, rules: [{ apiGroups: [""], resources: ["pods", "secrets", "configmaps", "persistentvolumeclaims"], verbs: ["get"], }, { apiGroups: [""], resources: ["pods"], verbs: ["patch"], }], }, rolebinding: kube.RoleBinding($.name + "-rolebinding") { metadata+: { namespace: $.namespace }, roleRef_: $.role, subjects_+: [$.sa], }, config: kube.ConfigMap($.name + "-config") { metadata+: { namespace: $.namespace }, data: { foo_key: "bar_val", }, }, secret: kube.Secret($.name + "-secret") { metadata+: { namespace: $.namespace }, data: { sec_key: "c2VjcmV0Cg==", }, }, // NB: making up an Ingress pointing to $.deploy Pod service: kube.Service($.name + "-svc") { metadata+: { namespace: $.namespace }, target_pod: $.deploy.spec.template, }, ingress: bitnami.Ingress($.name + "-ingress") { metadata+: { namespace: $.namespace }, host: "foo.g.dev.bitnami.net", target_svc: $.service, }, // NB: just a simple example pod pod: kube.Pod($.name + "-pod") { metadata+: { namespace: $.namespace }, spec+: { containers_+: { foo_cont: kube.Container($.name) { image: "nginx:1.12", env_+: { my_secret: kube.SecretKeyRef($.secret, "sec_key"), }, ports_+: { http: { containerPort: 80 }, udp_port: { containerPort: 888, protocol: "UDP" }, }, volumeMounts_+: { config_vol: { mountPath: "/config" }, }, }, }, volumes_+: { config_vol: kube.ConfigMapVolume($.config), }, }, }, // NB: all object below needing to spec a Pod will just // use above particular pod manifest just for convenience deploy: kube.Deployment($.name + "-deploy") { metadata+: { namespace: $.namespace }, spec+: { template+: { spec+: $.pod.spec { serviceAccountName: $.sa.metadata.name, }, }, }, }, sts: kube.StatefulSet($.name + "-sts") { metadata+: { namespace: $.namespace }, spec+: { template+: { spec+: $.pod.spec { serviceAccountName: $.sa.metadata.name, containers_+: { foo_cont+: { volumeMounts_+: { datadir: { mountPath: "/foo/data" }, }, }, }, }, }, volumeClaimTemplates_+: { datadir: kube.PersistentVolumeClaim("datadir") { metadata+: { namespace: $.namespace }, storage: "10Gi", }, }, }, }, ds: kube.DaemonSet($.name + "-ds") { metadata+: { namespace: $.namespace }, spec+: { template+: { spec: $.pod.spec, }, }, }, job: kube.Job($.name + "-job") { metadata+: { namespace: $.namespace }, spec+: { template+: { spec+: { containers_+: { foo_cont: kube.Container($.name) { image: "busybox", }, }, }, }, }, }, cronjob: kube.CronJob($.name + "-cronjob") { metadata+: { namespace: $.namespace }, spec+: { jobTemplate+: { spec+: { template+: { spec+: { containers_+: { foo_cont: kube.Container($.name) { image: "busybox", }, }, }, }, }, }, schedule: "0 * * * *", }, }, // NB: create NSP from $.deploy Pod ref nsp_pods: kube.NetworkPolicy($.name + "-nsp-pods") { metadata+: { namespace: $.namespace }, // NB: $.deploy has unique "foo-deploy" label (as well as other // podLabelsSelector() arg) spec+: kube.podLabelsSelector($.deploy) { // NB: making up $.deploy needing to get reached by $job, $.cronjob // and nginx-ingress-controller (running in its own NS named "nginx-ingress" ingress_: { from_jobs_and_ingressctl: { from: [ kube.podLabelsSelector($.job), kube.podLabelsSelector($.cronjob), { namespaceSelector: { matchLabels: { name: "nginx-ingress" } } }, ], ports: kube.podsPorts([$.deploy]), }, }, // NB: making up $.deploy needing to connect to $.sts, and // "kube-system" NS for DNS services egress_: { to_sts: { to: [ kube.podLabelsSelector($.sts), ], ports: kube.podsPorts([$.sts]), }, to_kube_dns: { to: [ { namespaceSelector: { matchLabels: { name: "kube-system" } } }, ], ports: [{ port: 53, protocol: "UDP" }], }, }, }, }, }; kube.List() { items_+: stack, } ================================================ FILE: vendor_jsonnet/kube-libsonnet/tests/unittests.jsonnet ================================================ local kube = import "../kube.libsonnet"; local an_obj = kube._Object("v1", "Gentle", "foo"); local a_pod = kube.Pod("foo") { metadata+: { labels+: { foo: "bar", bar: "qxx" } }, spec+: { containers_+: { foo: kube.Container("foo") { image: "nginx", ports_: { http: { containerPort: 8080 }, https: { containerPort: 8443 }, udp: { containerPort: 5353, protocol: "UDP" }, }, }, }, }, }; local a_deploy = kube.Deployment("foo") { spec+: { template+: { metadata+: a_pod.metadata, spec+: a_pod.spec } }, }; // Basic unittesting for methods that are not exercised by the other e2e-ish tests std.assertEqual(kube.objectValues({ a: 1, b: 2 }), [1, 2]) && std.assertEqual(kube.objectItems({ a: 1, b: 2 }), [["a", 1], ["b", 2]]) && std.assertEqual(kube.hyphenate("foo_bar_baz"), ("foo-bar-baz")) && std.assertEqual(kube.mapToNamedList({ foo: { a: "b" } }), [{ name: "foo", a: "b" }]) && std.assertEqual(kube.filterMapByFields({ a: 1, b: 2, c: 3 }, ["a", "c", "d"]), { a: 1, c: 3 }) && std.assertEqual(kube.parseOctal("755"), 493) && std.assertEqual(kube.siToNum("42G"), 42 * 1e9) && std.assertEqual(kube.siToNum("42Gi"), 42 * std.pow(2, 30)) && std.assertEqual(kube.toUpper("ForTy 2"), "FORTY 2") && std.assertEqual(kube.toLower("ForTy 2"), "forty 2") && std.assertEqual(an_obj, { apiVersion: "v1", kind: "Gentle", metadata: { name: "foo", labels: { name: "foo" }, annotations: {} }, }) && std.assertEqual( [kube.podRef(a_deploy).spec.ports("TCP"), kube.podRef(a_deploy).spec.ports("UDP")], [[8080, 8443], [5353]] ) && std.assertEqual( // latest kubecfg produces stable output from maps hashes, so below shouldn't be flaky kube.podsPorts([a_deploy]), [ { port: 8080, protocol: "TCP" }, { port: 8443, protocol: "TCP" }, { port: 5353, protocol: "UDP" }, ] ) && std.assertEqual( kube.podLabelsSelector(a_deploy), { podSelector: { matchLabels: { name: "foo", foo: "bar", bar: "qxx" } } } ) ================================================ FILE: versions.env ================================================ GO_VERSION=1.26.1 GO_VERSION_LIST="[\"$GO_VERSION\"]"