├── .dockerignore ├── .eslintrc ├── .github ├── dependabot.yml └── workflows │ ├── build-container.yaml │ ├── nightly.yaml │ ├── stale.yml │ └── workflow.yml ├── .gitignore ├── .npmrc ├── .versionrc.js ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── api.md ├── architecture.png ├── bin └── daemon.js ├── charts └── kubernetes-external-secrets │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── crds │ └── kubernetes-client.io_externalsecrets_crd.yaml │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── pdb.yaml │ ├── rbac.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── servicemonitor.yaml │ └── values.yaml ├── config ├── akeyless-config.js ├── alicloud-config.js ├── aws-config.js ├── azure-config.js ├── environment.js ├── gcp-config.js ├── ibmcloud-config.js └── index.js ├── docs ├── artifacthub-repo.yml ├── index.yaml ├── kubernetes-external-secrets-1.0.1.tgz ├── kubernetes-external-secrets-1.1.0.tgz ├── kubernetes-external-secrets-2.0.0.tgz ├── kubernetes-external-secrets-2.1.0.tgz ├── kubernetes-external-secrets-2.2.0.tgz ├── kubernetes-external-secrets-2.3.0.tgz ├── kubernetes-external-secrets-3.0.0.tgz ├── kubernetes-external-secrets-3.1.0.tgz ├── kubernetes-external-secrets-3.2.0.tgz ├── kubernetes-external-secrets-3.3.0.tgz ├── kubernetes-external-secrets-4.0.0.tgz ├── kubernetes-external-secrets-4.1.0.tgz ├── kubernetes-external-secrets-4.2.0.tgz ├── kubernetes-external-secrets-5.0.0.tgz ├── kubernetes-external-secrets-5.1.0.tgz ├── kubernetes-external-secrets-5.2.0.tgz ├── kubernetes-external-secrets-6.0.0.tgz ├── kubernetes-external-secrets-6.1.0.tgz ├── kubernetes-external-secrets-6.2.0.tgz ├── kubernetes-external-secrets-6.3.0.tgz ├── kubernetes-external-secrets-6.4.0.tgz ├── kubernetes-external-secrets-7.0.0.tgz ├── kubernetes-external-secrets-7.0.1.tgz ├── kubernetes-external-secrets-7.1.0.tgz ├── kubernetes-external-secrets-7.2.0.tgz ├── kubernetes-external-secrets-7.2.1.tgz ├── kubernetes-external-secrets-8.0.0.tgz ├── kubernetes-external-secrets-8.0.1.tgz ├── kubernetes-external-secrets-8.0.2.tgz ├── kubernetes-external-secrets-8.1.0.tgz ├── kubernetes-external-secrets-8.1.1.tgz ├── kubernetes-external-secrets-8.1.2.tgz ├── kubernetes-external-secrets-8.1.3.tgz ├── kubernetes-external-secrets-8.2.0.tgz ├── kubernetes-external-secrets-8.2.1.tgz ├── kubernetes-external-secrets-8.2.2.tgz ├── kubernetes-external-secrets-8.2.3.tgz ├── kubernetes-external-secrets-8.3.0.tgz ├── kubernetes-external-secrets-8.3.1.tgz ├── kubernetes-external-secrets-8.3.2.tgz ├── kubernetes-external-secrets-8.4.0.tgz ├── kubernetes-external-secrets-8.5.0.tgz ├── kubernetes-external-secrets-8.5.1.tgz ├── kubernetes-external-secrets-8.5.2.tgz ├── kubernetes-external-secrets-8.5.3.tgz ├── kubernetes-external-secrets-8.5.4.tgz └── kubernetes-external-secrets-8.5.5.tgz ├── e2e ├── Dockerfile ├── README.md ├── kind.yaml ├── localstack.deployment.yaml ├── run-e2e-suite.sh └── tests │ ├── crd.test.js │ ├── framework.js │ ├── secrets-manager.test.js │ ├── setup.test.js │ └── ssm.test.js ├── examples ├── akeyless-example.yaml ├── alicloud-secretsmanager.yaml ├── aws-secretsmanager.yaml ├── aws-ssm-path.yaml ├── aws-ssm.yaml ├── azure-keyvault.yaml ├── data-from-example.yml ├── dockerconfig-example.yml ├── gcp-secrets-manager.yml ├── ibmcloud-secrets-manager.yaml ├── template-advanced.yml ├── template-metadata.yml ├── tls-example.yml ├── vault-kv1.yaml └── vault.yml ├── lib ├── backends │ ├── abstract-backend.js │ ├── abstract-backend.test.js │ ├── akeyless-backend.js │ ├── akeyless-backend.test.js │ ├── alicloud-secrets-manager-backend.js │ ├── alicloud-secrets-manager-backend.test.js │ ├── azure-keyvault-backend.js │ ├── azure-keyvault-backend.test.js │ ├── gcp-secrets-manager-backend.js │ ├── gcp-secrets-manager-backend.test.js │ ├── ibmcloud-secrets-manager-backend.js │ ├── ibmcloud-secrets-manager-backend.test.js │ ├── kv-backend.js │ ├── kv-backend.test.js │ ├── secrets-manager-backend.js │ ├── secrets-manager-backend.test.js │ ├── system-manager-backend.js │ ├── system-manager-backend.test.js │ ├── vault-backend.js │ └── vault-backend.test.js ├── daemon.js ├── daemon.test.js ├── external-secret.js ├── external-secret.test.js ├── metrics-server.js ├── metrics-server.test.js ├── metrics.js ├── metrics.test.js ├── poller-factory.js ├── poller.js ├── poller.test.js └── utils.js ├── package-lock.json ├── package.json └── renovate.json /.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | architecture.png 3 | charts/**/.helmignore 4 | charts/**/Chart.yaml 5 | charts/**/README.md 6 | charts/**/templates/ 7 | charts/**/values.yaml 8 | coverage 9 | Dockerfile 10 | node_modules 11 | release.sh 12 | .git 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "security" 4 | ], 5 | "extends": [ 6 | "standard", 7 | "plugin:security/recommended" 8 | ], 9 | "rules": { 10 | "strict": 0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | -------------------------------------------------------------------------------- /.github/workflows/build-container.yaml: -------------------------------------------------------------------------------- 1 | name: docker 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | tags: 8 | - '*.*.*' 9 | pull_request: 10 | 11 | env: 12 | # We can't run a step 'if secrets.GHCR_USERNAME != ""' but we can run a step 13 | # 'if env.GHCR_USERNAME' != ""', so we copy these to test whether credentials 14 | # are available before trying to run steps that need them. Like PRs from forks! 15 | GHCR_USERNAME: ${{ secrets.GHCR_USERNAME }} 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | 24 | - name: Set up QEMU 25 | uses: docker/setup-qemu-action@v2 26 | 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v2 29 | 30 | - name: Image name 31 | id: image_name 32 | uses: actions/github-script@v4.1 33 | with: 34 | github-token: ${{secrets.GITHUB_TOKEN}} 35 | script: | 36 | const res = await github.repos.get(context.repo); 37 | 38 | let imageName = 'kes-dev'; 39 | 40 | if (res.data.default_branch === context.ref.replace('refs/heads/', '') || context.ref.startsWith('refs/tags/')) { 41 | imageName = 'kubernetes-external-secrets'; 42 | } 43 | 44 | core.setOutput('image', `ghcr.io/external-secrets/${imageName}`); 45 | 46 | - name: Docker meta 47 | id: docker_meta 48 | uses: docker/metadata-action@v4 49 | with: 50 | images: ${{ steps.image_name.outputs.image }} 51 | tags: | 52 | type=ref,event=tag 53 | type=ref,event=pr 54 | type=edge,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }},latest=false 55 | 56 | - name: Login to Docker 57 | uses: docker/login-action@v2 58 | if: env.GHCR_USERNAME != '' && steps.docker_meta.outputs.version != '' 59 | with: 60 | registry: ghcr.io 61 | username: ${{ secrets.GHCR_USERNAME }} 62 | password: ${{ secrets.GHCR_TOKEN }} 63 | 64 | - name: Build and push 65 | uses: docker/build-push-action@v3 66 | with: 67 | context: . 68 | platforms: linux/amd64,linux/arm64,linux/arm/v7 69 | push: ${{ env.GHCR_USERNAME != '' && steps.docker_meta.outputs.version != ''}} 70 | tags: ${{ steps.docker_meta.outputs.tags }} 71 | labels: ${{ steps.docker_meta.outputs.labels }} 72 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yaml: -------------------------------------------------------------------------------- 1 | name: nightly 2 | 3 | on: 4 | schedule: 5 | # At 03:07 6 | - cron: '7 3 * * *' 7 | 8 | env: 9 | # We can't run a step 'if secrets.GHCR_USERNAME != ""' but we can run a step 10 | # 'if env.GHCR_USERNAME' != ""', so we copy these to test whether credentials 11 | # are available before trying to run steps that need them. Like PRs from forks! 12 | GHCR_USERNAME: ${{ secrets.GHCR_USERNAME }} 13 | IMAGE_NAME: ghcr.io/external-secrets/kubernetes-external-secrets 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v2 24 | 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v2 27 | 28 | - name: Docker meta 29 | id: docker_meta 30 | uses: docker/metadata-action@v4 31 | with: 32 | images: ${{ env.IMAGE_NAME }} 33 | tags: | 34 | type=schedule,pattern=nightly 35 | 36 | - name: Login to Docker 37 | uses: docker/login-action@v2 38 | if: env.GHCR_USERNAME != '' 39 | with: 40 | registry: ghcr.io 41 | username: ${{ secrets.GHCR_USERNAME }} 42 | password: ${{ secrets.GHCR_TOKEN }} 43 | 44 | - name: Build nightly 45 | uses: docker/build-push-action@v3 46 | with: 47 | context: . 48 | platforms: linux/amd64,linux/arm64,linux/arm/v7 49 | push: ${{ env.GHCR_USERNAME != '' }} 50 | tags: ${{ steps.docker_meta.outputs.tags }} 51 | labels: ${{ steps.docker_meta.outputs.labels }} 52 | 53 | scan: 54 | needs: build 55 | runs-on: ubuntu-latest 56 | steps: 57 | - name: Checkout 58 | uses: actions/checkout@v3 59 | 60 | - name: Run Trivy vulnerability scanner 61 | uses: aquasecurity/trivy-action@master 62 | with: 63 | image-ref: ${{ env.IMAGE_NAME }}:nightly 64 | format: 'template' 65 | ignore-unfixed: true 66 | severity: HIGH,CRITICAL 67 | template: '@/contrib/sarif.tpl' 68 | output: 'trivy-results.sarif' 69 | 70 | - name: Upload Trivy scan results to GitHub Security tab 71 | uses: github/codeql-action/upload-sarif@v2 72 | with: 73 | sarif_file: 'trivy-results.sarif' 74 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PR' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v5 11 | with: 12 | repo-token: ${{ secrets.GITHUB_TOKEN }} 13 | stale-issue-message: 'This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 30 days.' 14 | stale-pr-message: 'This pr is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 30 days.' 15 | close-issue-message: 'This issue was closed because it has been stalled for 30 days with no activity.' 16 | days-before-stale: 90 17 | days-before-close: 30 18 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | name: Node 14 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Setup node 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 14 19 | - run: npm install 20 | - run: npm test 21 | test-e2e: 22 | runs-on: ubuntu-latest 23 | name: E2E 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: versions 27 | run: | 28 | kind version 29 | kubectl version --client 30 | helm version --client 31 | - run: ./e2e/run-e2e-suite.sh 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Yarn Integrity file 52 | .yarn-integrity 53 | 54 | # dotenv environment variables file 55 | .env 56 | 57 | # next.js build output 58 | .next 59 | 60 | # e2e test stuff 61 | e2e/**/.kubeconfig 62 | 63 | # IDE 64 | .idea/ 65 | .vscode/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.versionrc.js: -------------------------------------------------------------------------------- 1 | const chart = { 2 | filename: "charts/kubernetes-external-secrets/Chart.yaml", 3 | updater: { 4 | readVersion(contents) { 5 | const [match] = contents.match(/appVersion: [a-zA-Z0-9\.]*/); 6 | return match.split(": ")[1]; 7 | }, 8 | 9 | writeVersion: (contents, version) => 10 | contents 11 | .replace(/appVersion: [a-zA-Z0-9\.]*/, `appVersion: ${version}`) 12 | .replace(/version: [a-zA-Z0-9\.]*/, `version: ${version}`), 13 | }, 14 | }; 15 | 16 | const values = { 17 | filename: "charts/kubernetes-external-secrets/values.yaml", 18 | updater: { 19 | readVersion(contents) { 20 | const [match] = contents.match(/tag: [a-zA-Z0-9\.]*/); 21 | return match.split(": ")[1]; 22 | }, 23 | 24 | writeVersion: (contents, version) => 25 | contents.replace(/tag: [a-zA-Z0-9\.]*/, `tag: ${version}`), 26 | }, 27 | }; 28 | 29 | const readme = { 30 | filename: "charts/kubernetes-external-secrets/README.md", 31 | updater: { 32 | readVersion(contents) { 33 | const [match] = contents.match( 34 | /kubernetes-external-secrets\sImage\stag\s+\|\s`([0-9]+.[0-9]+.[0-9]+)`/ 35 | ); 36 | return match.split("`")[1]; 37 | }, 38 | 39 | writeVersion: (contents, version) => 40 | contents.replace( 41 | /(kubernetes-external-secrets\sImage\stag\s+\|\s)`([0-9]+.[0-9]+.[0-9]+)`/, 42 | `$1\`${version}\`` 43 | ), 44 | }, 45 | }; 46 | 47 | module.exports = { 48 | scripts: { 49 | "prechangelog": "(cd charts/kubernetes-external-secrets && helm package . && helm repo index --merge ../../docs/index.yaml ./ && mv *.tgz ../../docs && mv index.yaml ../../docs && git add ../../docs)" 50 | }, 51 | bumpFiles: ['package.json', 'package-lock.json', chart, values, readme], 52 | }; 53 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, 8 | body size, disability, ethnicity, sex characteristics, gender identity and 9 | expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity and 11 | orientation. 12 | 13 | ## Our Standards 14 | 15 | Examples of behavior that contributes to creating a positive environment 16 | include: 17 | 18 | * Using welcoming and inclusive language 19 | * Being respectful of differing viewpoints and experiences 20 | * Gracefully accepting constructive criticism 21 | * Focusing on what is best for the community 22 | * Showing empathy towards other community members 23 | 24 | Examples of unacceptable behavior by participants include: 25 | 26 | * The use of sexualized language or imagery and unwelcome sexual attention or 27 | advances 28 | * Trolling, insulting/derogatory comments, and personal or political attacks 29 | * Public or private harassment 30 | * Publishing others' private information, such as a physical or electronic 31 | address, without explicit permission 32 | * Other conduct which could reasonably be considered inappropriate in a 33 | professional setting 34 | 35 | ## Our Responsibilities 36 | 37 | Project maintainers are responsible for clarifying the standards of acceptable 38 | behavior and are expected to take appropriate and fair corrective action in 39 | response to any instances of unacceptable behavior. 40 | 41 | Project maintainers have the right and responsibility to remove, edit, or 42 | reject comments, commits, code, wiki edits, issues, and other contributions 43 | that are not aligned to this Code of Conduct, or to ban temporarily or 44 | permanently any contributor for other behaviors that they deem inappropriate, 45 | threatening, offensive, or harmful. 46 | 47 | ## Scope 48 | 49 | This Code of Conduct applies both within project spaces and in public spaces 50 | when an individual is representing the project or its community. Examples of 51 | representing a project or community include using an official project e-mail 52 | address, posting via an official social media account, or acting as an 53 | appointed representative at an online or offline event. Representation of a 54 | project may be further defined and clarified by project maintainers. 55 | 56 | ## Enforcement 57 | 58 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 59 | reported by contacting the GoDaddy OSPO (Open Source Policy Office) at 60 | oss@godaddy.com. All complaints will be reviewed and investigated and will 61 | result in a response that is deemed necessary and appropriate to the 62 | circumstances. The project team is obligated to maintain confidentiality with 63 | regard to the reporter of an incident. Further details of specific enforcement 64 | policies may be posted separately. 65 | 66 | Project maintainers who do not follow or enforce the Code of Conduct in good 67 | faith may face temporary or permanent repercussions as determined by other 68 | members of the project's leadership. 69 | 70 | ## Attribution 71 | 72 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 73 | version 1.4, available at 74 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 75 | 76 | For answers to common questions about this code of conduct, see 77 | https://www.contributor-covenant.org/faq 78 | 79 | [homepage]: https://www.contributor-covenant.org 80 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to kubernetes-external-secrets 2 | 3 | Thanks for taking the time to contribute! 4 | 5 | ## Contributing an Issue 6 | 7 | We will try to respond to every issue. The issues that get the 8 | quickest response are the ones that are easiest to respond to. The 9 | issues that are easiest to respond to usually include the 10 | following: 11 | 12 | * For unexpected behavior or bugs, include sufficient details about 13 | your cluster and workload so that other contributors can try to 14 | reproduce the issue. 15 | * For requests for help, explain what outcome you're trying to achieve 16 | (*e.g.*, "access secret data in AWS Secret Manager from a `Pod`") 17 | in addition to how you're trying to achieve it (*e.g.*, "I installed 18 | the Helm Chart on my cluster with these options and created an 19 | `ExternalSecret`"). 20 | * Relevant logs (*e.g.*, warnings and errors) from the external 21 | secrets controller. 22 | * For feature requests, a description of the impact the feature will 23 | have on your project or cluster management experience. If possible, 24 | include a link to the open source repository for your project. 25 | 26 | ## Contributing a Pull Request 27 | 28 | The most useful PRs provide the following: 29 | 30 | 1. Rationale for the PR. This can be a link to a supporting issue, or 31 | if it's a short PR a brief explanation in the PR. 32 | 1. Changes that pass all testing (*i.e.*, your PR will not break 33 | main). See the Development section in the 34 | [README.md](./README.md#development) for more details. 35 | 1. A small set of changes. It's OK to split up the full implementation 36 | of a large new feature over multiple PRs that each add a small bit 37 | of functionality. 38 | 39 | ## Maintainers 40 | 41 | ### Publishing a new release 42 | 43 | Use `npm run release` to start the release process and follow the 44 | instructions printed to the console. 45 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine3.14 2 | 3 | ENV NODE_ENV production 4 | ENV NPM_CONFIG_LOGLEVEL warn 5 | 6 | # Setup source directory 7 | WORKDIR /app 8 | COPY package*.json ./ 9 | RUN npm ci --production 10 | 11 | # Copy app to source directory 12 | COPY . . 13 | 14 | # Change back to the "node" user; using its UID for PodSecurityPolicy "non-root" compatibility 15 | USER 1000 16 | CMD ["npm", "start"] 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 GoDaddy Operating Company, LLC. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | - [Security Policy](#security-policy) 4 | - [Reporting security problems](#reporting-security-problems) 5 | - [Vulnerability Management Plans](#vulnerability-management-plans) 6 | - [Critical Updates And Security Notices](#critical-updates-and-security-notices) 7 | 8 | 9 | ## Reporting security problems 10 | 11 | **DO NOT CREATE AN ISSUE** to report a security problem. Instead, please 12 | send an email to contact@external-secrets.io 13 | 14 | 15 | ## Vulnerability Management Plans 16 | 17 | ### Critical Updates And Security Notices 18 | 19 | We learn about critical software updates and security threats from these sources 20 | 21 | 1. GitHub Security Alerts 22 | 2. [Dependabot](https://dependabot.com/) Dependency Updates 23 | -------------------------------------------------------------------------------- /api.md: -------------------------------------------------------------------------------- 1 | # Kubernetes External Secrets Storage Frontends 2 | 3 | Kubernetes External Secrets supports several "frontends" for storing 4 | secret data and presenting it to applications: 5 | 6 | * [Secret frontend](#secret-frontend) 7 | * [Volume frontend](#volume-frontend) 8 | 9 | ## Secret frontend 10 | 11 | TODO. 12 | 13 | ## Volume frontend 14 | 15 | The volume frontend writes secret data to volumes included in `Pod` 16 | specs. The volume frontend implements a behavior analogous to the 17 | secret volume type. You specify `ExternalSecret` objects to use as 18 | volumes and the External Secret controller creates files containing 19 | secret data. 20 | 21 | To emulate an "externalSecret" volume type, you configure which 22 | volumes the External Secret controller writes data to by adding the 23 | `externalsecrets.kubernetes-client.io/volumes` annotation to a `Pod` 24 | manifest". With the manifest below, the External Secret controller 25 | should: 26 | 27 | * create password and username files in the db-secrets volume; 28 | * fetch the value of db/password from AWS Secrets Manager and write 29 | that value to password file in the db-secrets volume; 30 | * fetch the value of db/username from AWS Secrets Manager and write 31 | that value to the username file in the db-secrets volume; 32 | * create a key file in the client-secrets volume; and 33 | * fetch the value of api/key from AWS Secrets Manager and write that 34 | values to the api file in the client-secrets volume. 35 | 36 | ```yaml 37 | apiVersion: v1 38 | kind: Pod 39 | metadata: 40 | generateName: pod-example- 41 | annotations: 42 | externalsecrets.kubernetes-client.io/volumes: | 43 | - name: "db-secrets" 44 | externalSecret: 45 | externalSecretName: "db-secrets" 46 | - name: "client-secrets" 47 | externalSecret: 48 | externalSecretName: "client-secrets" 49 | spec: 50 | containers: 51 | - image: busybox 52 | name: busybox 53 | volumeMounts: 54 | - mountPath: /db-secrets 55 | name: db-secrets 56 | - mountPath: /client-secrets 57 | name: client-secrets 58 | volumes: 59 | - name: db-secrets 60 | emptyDir: 61 | medium: Memory 62 | - name: client-secrets 63 | emptyDir: 64 | medium: Memory 65 | --- 66 | apiVersion: 'kubernetes-client.io/v1' 67 | kind: ExternalSecret 68 | metadata: 69 | name: db-secrets 70 | spec: 71 | backendType: secretsManager 72 | data: 73 | - key: db/password 74 | name: password 75 | - key: db/username 76 | name: username 77 | --- 78 | apiVersion: 'kubernetes-client.io/v1' 79 | kind: ExternalSecret 80 | metadata: 81 | name: client-secrets 82 | spec: 83 | backendType: secretsManager 84 | data: 85 | - key: api/key 86 | name: key 87 | ``` 88 | 89 | The value of `externalsecrets.kubernetes-client.io/volumes` is a JSON or 90 | YAML serialized array of volume configuration objects: 91 | 92 | |Property|Type|Description| 93 | |--------|----|-----------| 94 | |`name`|string|Name of volume to write secret data to| 95 | |`externalSecretName`|string|Name of ExternalSecret to get secret data from| 96 | 97 | You can configure any [type of 98 | volume](https://kubernetes.io/docs/concepts/storage/volumes/#types-of-volumes) 99 | to hold secret data. To avoid storing secret data on disk, 100 | use an 101 | [`emptyDir`](https://kubernetes.io/docs/concepts/storage/volumes/#emptydir) 102 | volume and set `emptyDir.medium` field to `"Memory"`. 103 | -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/architecture.png -------------------------------------------------------------------------------- /bin/daemon.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | // make-promises-safe installs an process.on('unhandledRejection') handler 6 | // that prints the stacktrace and exits the process 7 | // with an exit code of 1, just like any uncaught exception. 8 | require('make-promises-safe') 9 | 10 | const Prometheus = require('prom-client') 11 | const Daemon = require('../lib/daemon') 12 | const MetricsServer = require('../lib/metrics-server') 13 | const Metrics = require('../lib/metrics') 14 | const { getExternalSecretEvents } = require('../lib/external-secret') 15 | const PollerFactory = require('../lib/poller-factory') 16 | 17 | const { 18 | backends, 19 | kubeClient, 20 | customResourceManifest, 21 | logger, 22 | metricsPort, 23 | pollerIntervalMilliseconds, 24 | pollingDisabled, 25 | rolePermittedAnnotation, 26 | namingPermittedAnnotation, 27 | enforceNamespaceAnnotation, 28 | watchTimeout, 29 | watchedNamespaces, 30 | instanceId 31 | } = require('../config') 32 | 33 | async function main () { 34 | logger.info('loading kube specs') 35 | await kubeClient.loadSpec() 36 | logger.info('successfully loaded kube specs') 37 | 38 | kubeClient.addCustomResourceDefinition(customResourceManifest) 39 | 40 | try { 41 | logger.info('verifiying CRD is installed') 42 | await kubeClient 43 | .apis[customResourceManifest.spec.group] 44 | .v1[customResourceManifest.spec.names.plural].get() 45 | } catch (err) { 46 | logger.error('CRD installation check failed, statusCode: %s', err.statusCode) 47 | process.exit(1) 48 | } 49 | 50 | const externalSecretEvents = getExternalSecretEvents({ 51 | kubeClient, 52 | watchedNamespaces, 53 | customResourceManifest, 54 | logger, 55 | watchTimeout 56 | }) 57 | 58 | const registry = Prometheus.register 59 | Prometheus.collectDefaultMetrics({ register: registry }) 60 | const metrics = new Metrics({ registry }) 61 | 62 | const pollerFactory = new PollerFactory({ 63 | backends, 64 | kubeClient, 65 | metrics, 66 | pollerIntervalMilliseconds, 67 | rolePermittedAnnotation, 68 | namingPermittedAnnotation, 69 | enforceNamespaceAnnotation, 70 | customResourceManifest, 71 | pollingDisabled, 72 | logger 73 | }) 74 | 75 | const daemon = new Daemon({ 76 | externalSecretEvents, 77 | logger, 78 | pollerFactory, 79 | instanceId 80 | }) 81 | 82 | const metricsServer = new MetricsServer({ 83 | port: metricsPort, 84 | registry, 85 | logger 86 | }) 87 | 88 | logger.info('starting app') 89 | daemon.start() 90 | metricsServer.start() 91 | logger.info('successfully started app') 92 | } 93 | 94 | main() 95 | -------------------------------------------------------------------------------- /charts/kubernetes-external-secrets/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /charts/kubernetes-external-secrets/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: kubernetes-external-secrets 3 | version: 8.5.5 4 | appVersion: 8.5.5 5 | description: "Deprecated: Kubernetes External Secrets CustomResourceDefinition" 6 | keywords: 7 | - kubernetes-external-secrets 8 | - secrets 9 | home: https://github.com/external-secrets/kubernetes-external-secrets 10 | sources: 11 | - https://github.com/external-secrets/kubernetes-external-secrets 12 | maintainers: 13 | - name: external-secrets 14 | url: https://github.com/external-secrets 15 | deprecated: true 16 | -------------------------------------------------------------------------------- /charts/kubernetes-external-secrets/crds/kubernetes-client.io_externalsecrets_crd.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: externalsecrets.kubernetes-client.io 6 | annotations: 7 | # used in e2e testing 8 | app.kubernetes.io/managed-by: helm 9 | spec: 10 | group: kubernetes-client.io 11 | scope: Namespaced 12 | 13 | versions: 14 | - name: v1 15 | served: true 16 | storage: true 17 | subresources: 18 | status: {} 19 | schema: 20 | openAPIV3Schema: 21 | required: 22 | - spec 23 | type: object 24 | properties: 25 | spec: 26 | type: object 27 | properties: 28 | controllerId: 29 | description: The ID of controller instance that manages this ExternalSecret. 30 | This is needed in case there is more than a KES controller instances within the cluster. 31 | type: string 32 | type: 33 | type: string 34 | description: >- 35 | DEPRECATED: Use spec.template.type 36 | template: 37 | description: Template which will be deep merged without mutating 38 | any existing fields. into generated secret, can be used to 39 | set for example annotations or type on the generated secret 40 | type: object 41 | x-kubernetes-preserve-unknown-fields: true 42 | backendType: 43 | description: >- 44 | Determines which backend to use for fetching secrets 45 | type: string 46 | enum: 47 | - secretsManager 48 | - systemManager 49 | - vault 50 | - azureKeyVault 51 | - gcpSecretsManager 52 | - alicloudSecretsManager 53 | - ibmcloudSecretsManager 54 | - akeyless 55 | vaultRole: 56 | description: >- 57 | Used by: vault 58 | type: string 59 | vaultMountPoint: 60 | description: >- 61 | Used by: vault 62 | type: string 63 | kvVersion: 64 | description: Vault K/V version either 1 or 2, default = 2 65 | type: integer 66 | minimum: 1 67 | maximum: 2 68 | keyVaultName: 69 | description: >- 70 | Used by: azureKeyVault 71 | type: string 72 | dataFrom: 73 | type: array 74 | items: 75 | type: string 76 | dataFromWithOptions: 77 | type: array 78 | items: 79 | type: object 80 | properties: 81 | key: 82 | description: Secret key in backend 83 | type: string 84 | isBinary: 85 | description: >- 86 | Whether the backend secret shall be treated as binary data 87 | represented by a base64-encoded string. You must set this to true 88 | for any base64-encoded binary data in the backend - to ensure it 89 | is not encoded in base64 again. Default is false. 90 | type: boolean 91 | versionStage: 92 | description: >- 93 | Used by: alicloudSecretsManager, secretsManager 94 | type: string 95 | versionId: 96 | description: >- 97 | Used by: secretsManager 98 | type: string 99 | required: 100 | - key 101 | data: 102 | type: array 103 | items: 104 | type: object 105 | properties: 106 | key: 107 | description: Secret key in backend 108 | type: string 109 | name: 110 | description: Name set for this key in the generated secret 111 | type: string 112 | property: 113 | description: Property to extract if secret in backend is a JSON object 114 | type: string 115 | isBinary: 116 | description: >- 117 | Whether the backend secret shall be treated as binary data 118 | represented by a base64-encoded string. You must set this to true 119 | for any base64-encoded binary data in the backend - to ensure it 120 | is not encoded in base64 again. Default is false. 121 | type: boolean 122 | path: 123 | description: >- 124 | Path from SSM to scrape secrets 125 | This will fetch all secrets and use the key from the secret as variable name 126 | type: string 127 | recursive: 128 | description: Allow to recurse thru all child keys on a given path, default false 129 | type: boolean 130 | secretType: 131 | description: >- 132 | Used by: ibmcloudSecretsManager 133 | Type of secret - one of username_password, iam_credentials or arbitrary 134 | type: string 135 | version: 136 | description: >- 137 | Used by: gcpSecretsManager 138 | type: string 139 | x-kubernetes-int-or-string: true 140 | versionStage: 141 | description: >- 142 | Used by: alicloudSecretsManager, secretsManager 143 | type: string 144 | versionId: 145 | description: >- 146 | Used by: secretsManager 147 | type: string 148 | oneOf: 149 | - required: 150 | - key 151 | - name 152 | - required: 153 | - path 154 | roleArn: 155 | type: string 156 | description: >- 157 | Used by: alicloudSecretsManager, secretsManager, systemManager 158 | region: 159 | type: string 160 | description: >- 161 | Used by: secretsManager, systemManager 162 | projectId: 163 | type: string 164 | description: >- 165 | Used by: gcpSecretsManager 166 | keyByName: 167 | type: boolean 168 | description: >- 169 | Whether to interpret the key as a secret name (if true) or ID (the default). 170 | Used by: ibmcloudSecretsManager 171 | oneOf: 172 | - properties: 173 | backendType: 174 | enum: 175 | - secretsManager 176 | - systemManager 177 | - properties: 178 | backendType: 179 | enum: 180 | - vault 181 | - properties: 182 | backendType: 183 | enum: 184 | - azureKeyVault 185 | required: 186 | - keyVaultName 187 | - properties: 188 | backendType: 189 | enum: 190 | - gcpSecretsManager 191 | - properties: 192 | backendType: 193 | enum: 194 | - alicloudSecretsManager 195 | - properties: 196 | backendType: 197 | enum: 198 | - ibmcloudSecretsManager 199 | - properties: 200 | backendType: 201 | enum: 202 | - akeyless 203 | anyOf: 204 | - required: 205 | - data 206 | - required: 207 | - dataFrom 208 | - required: 209 | - dataFromWithOptions 210 | status: 211 | type: object 212 | properties: 213 | lastSync: 214 | type: string 215 | status: 216 | type: string 217 | observedGeneration: 218 | type: number 219 | additionalPrinterColumns: 220 | - jsonPath: .status.lastSync 221 | name: Last Sync 222 | type: date 223 | - jsonPath: .status.status 224 | name: status 225 | type: string 226 | - jsonPath: .metadata.creationTimestamp 227 | name: Age 228 | type: date 229 | 230 | names: 231 | shortNames: 232 | - es 233 | kind: ExternalSecret 234 | plural: externalsecrets 235 | singular: externalsecret 236 | -------------------------------------------------------------------------------- /charts/kubernetes-external-secrets/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | The kubernetes external secrets has been installed. Check its status by running: 2 | kubectl --namespace {{ .Release.Namespace }} get pods -l "app.kubernetes.io/name={{ include "kubernetes-external-secrets.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" 3 | 4 | Visit https://github.com/external-secrets/kubernetes-external-secrets for instructions on how to use kubernetes external secrets 5 | -------------------------------------------------------------------------------- /charts/kubernetes-external-secrets/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "kubernetes-external-secrets.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | */}} 13 | {{- define "kubernetes-external-secrets.fullname" -}} 14 | {{- if .Values.fullnameOverride -}} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 16 | {{- else -}} 17 | {{- $name := default .Chart.Name .Values.nameOverride -}} 18 | {{- if contains $name .Release.Name -}} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 20 | {{- else -}} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 22 | {{- end -}} 23 | {{- end -}} 24 | {{- end -}} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "kubernetes-external-secrets.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 31 | {{- end -}} 32 | 33 | {{/* 34 | Create the name of the service account to use 35 | */}} 36 | {{- define "kubernetes-external-secrets.serviceAccountName" -}} 37 | {{- if .Values.serviceAccount.create -}} 38 | {{ default (include "kubernetes-external-secrets.fullname" .) .Values.serviceAccount.name }} 39 | {{- else -}} 40 | {{ default "default" .Values.serviceAccount.name }} 41 | {{- end -}} 42 | {{- end -}} 43 | -------------------------------------------------------------------------------- /charts/kubernetes-external-secrets/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "kubernetes-external-secrets.fullname" . }} 5 | namespace: {{ .Release.Namespace | quote }} 6 | labels: 7 | app.kubernetes.io/name: {{ include "kubernetes-external-secrets.name" . }} 8 | helm.sh/chart: {{ include "kubernetes-external-secrets.chart" . }} 9 | app.kubernetes.io/instance: {{ .Release.Name }} 10 | app.kubernetes.io/managed-by: {{ .Release.Service }} 11 | {{- if .Values.deploymentLabels }} 12 | {{- toYaml .Values.deploymentLabels | nindent 4 }} 13 | {{- end }} 14 | spec: 15 | replicas: {{ .Values.replicaCount }} 16 | selector: 17 | matchLabels: 18 | app.kubernetes.io/name: {{ include "kubernetes-external-secrets.name" . }} 19 | app.kubernetes.io/instance: {{ .Release.Name }} 20 | template: 21 | metadata: 22 | labels: 23 | app.kubernetes.io/name: {{ include "kubernetes-external-secrets.name" . }} 24 | app.kubernetes.io/instance: {{ .Release.Name }} 25 | {{- if .Values.podLabels }} 26 | {{- toYaml .Values.podLabels | nindent 8 }} 27 | {{- end }} 28 | {{- if .Values.podAnnotations }} 29 | annotations: 30 | {{- toYaml .Values.podAnnotations | nindent 8 }} 31 | {{- end }} 32 | spec: 33 | serviceAccountName: {{ template "kubernetes-external-secrets.serviceAccountName" . }} 34 | {{- if .Values.imagePullSecrets }} 35 | imagePullSecrets: 36 | {{- toYaml .Values.imagePullSecrets | nindent 8 }} 37 | {{- end }} 38 | {{- if .Values.deploymentInitContainers }} 39 | {{- toYaml .Values.deploymentInitContainers | nindent 6 }} 40 | {{- end }} 41 | containers: 42 | - name: {{ .Chart.Name }} 43 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 44 | ports: 45 | - name: prometheus 46 | containerPort: {{ .Values.env.METRICS_PORT }} 47 | imagePullPolicy: {{ .Values.image.pullPolicy }} 48 | resources: 49 | {{- toYaml .Values.resources | nindent 12 }} 50 | env: 51 | {{- range $name, $value := .Values.env }} 52 | {{- if not (empty $value) }} 53 | - name: {{ $name | quote }} 54 | value: {{ $value | quote }} 55 | {{- end }} 56 | {{- end }} 57 | # Params for env vars populated from k8s secrets 58 | {{- range $key, $value := .Values.envVarsFromSecret }} 59 | - name: {{ $key }} 60 | valueFrom: 61 | secretKeyRef: 62 | name: {{ $value.secretKeyRef | quote }} 63 | key: {{ $value.key | quote }} 64 | {{- end }} 65 | {{- range $key, $value := .Values.envVarsFromConfigMap }} 66 | - name: {{ $key }} 67 | valueFrom: 68 | configMapKeyRef: 69 | name: {{ $value.configMapKeyRef | quote }} 70 | key: {{ $value.key | quote }} 71 | {{- end }} 72 | {{- if .Values.envFrom }} 73 | envFrom: 74 | {{- .Values.envFrom | toYaml | nindent 12 }} 75 | {{- end }} 76 | {{- if or .Values.filesFromSecret .Values.extraVolumeMounts }} 77 | volumeMounts: 78 | {{- if .Values.extraVolumeMounts }} 79 | {{- toYaml .Values.extraVolumeMounts | nindent 12 }} 80 | {{- end }} 81 | {{- with .Values.filesFromSecret }} 82 | {{- range $key, $value := . }} 83 | - name: {{ $key }} 84 | mountPath: {{ $value.mountPath }} 85 | readOnly: true 86 | {{- end }} 87 | {{- end }} 88 | {{- end }} 89 | {{- if .Values.containerSecurityContext }} 90 | securityContext: 91 | {{- toYaml .Values.containerSecurityContext | nindent 12 }} 92 | {{- end }} 93 | {{- with .Values.dnsConfig }} 94 | dnsConfig: 95 | {{- toYaml . | nindent 8 }} 96 | {{- end }} 97 | {{- with .Values.securityContext }} 98 | securityContext: 99 | {{- toYaml . | nindent 8 }} 100 | {{- end }} 101 | {{- if .Values.priorityClassName }} 102 | priorityClassName: {{ .Values.priorityClassName }} 103 | {{- end }} 104 | {{- with .Values.nodeSelector }} 105 | nodeSelector: 106 | {{- toYaml . | nindent 8 }} 107 | {{- end }} 108 | {{- with .Values.affinity }} 109 | affinity: 110 | {{- toYaml . | nindent 8 }} 111 | {{- end }} 112 | {{- with .Values.tolerations }} 113 | tolerations: 114 | {{- toYaml . | nindent 8 }} 115 | {{- end }} 116 | {{- if or .Values.filesFromSecret .Values.extraVolumes }} 117 | volumes: 118 | {{- if .Values.extraVolumes }} 119 | {{- toYaml .Values.extraVolumes | nindent 8 }} 120 | {{- end }} 121 | {{- with .Values.filesFromSecret }} 122 | {{- range $key, $value := . }} 123 | - name: {{ $key }} 124 | secret: 125 | secretName: {{ $value.secret }} 126 | {{- end }} 127 | {{- end }} 128 | {{- end }} 129 | -------------------------------------------------------------------------------- /charts/kubernetes-external-secrets/templates/pdb.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.podDisruptionBudget -}} 2 | apiVersion: policy/v1beta1 3 | kind: PodDisruptionBudget 4 | metadata: 5 | name: {{ template "kubernetes-external-secrets.fullname" . }} 6 | namespace: {{ .Release.Namespace | quote }} 7 | labels: 8 | app.kubernetes.io/name: {{ template "kubernetes-external-secrets.name" . }} 9 | helm.sh/chart: {{ include "kubernetes-external-secrets.chart" . }} 10 | app.kubernetes.io/instance: "{{ .Release.Name }}" 11 | app.kubernetes.io/managed-by: "{{ .Release.Service }}" 12 | spec: 13 | selector: 14 | matchLabels: 15 | app.kubernetes.io/name: {{ include "kubernetes-external-secrets.name" . }} 16 | {{ toYaml .Values.podDisruptionBudget | indent 2 }} 17 | {{- end -}} 18 | -------------------------------------------------------------------------------- /charts/kubernetes-external-secrets/templates/rbac.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | {{- if semverCompare ">=1.17.0-0" .Capabilities.KubeVersion.GitVersion -}} 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | {{- else -}} 5 | apiVersion: rbac.authorization.k8s.io/v1beta1 6 | {{- end }} 7 | kind: ClusterRole 8 | metadata: 9 | name: {{ include "kubernetes-external-secrets.fullname" . }} 10 | labels: 11 | app.kubernetes.io/name: {{ include "kubernetes-external-secrets.name" . }} 12 | helm.sh/chart: {{ include "kubernetes-external-secrets.chart" . }} 13 | app.kubernetes.io/instance: {{ .Release.Name }} 14 | app.kubernetes.io/managed-by: {{ .Release.Service }} 15 | rules: 16 | - apiGroups: [""] 17 | resources: ["secrets"] 18 | verbs: ["create", "update", "get"] 19 | - apiGroups: [""] 20 | resources: ["namespaces"] 21 | verbs: ["get", "watch", "list"] 22 | - apiGroups: ["apiextensions.k8s.io"] 23 | resources: ["customresourcedefinitions"] 24 | resourceNames: ["externalsecrets.kubernetes-client.io"] 25 | verbs: ["get", "update"] 26 | - apiGroups: ["kubernetes-client.io"] 27 | resources: ["externalsecrets"] 28 | verbs: ["get", "watch", "list"] 29 | - apiGroups: ["kubernetes-client.io"] 30 | resources: ["externalsecrets/status"] 31 | verbs: ["get", "update"] 32 | {{- if .Values.customClusterRoles }} 33 | {{- toYaml .Values.customClusterRoles | nindent 2 }} 34 | {{- end }} 35 | --- 36 | {{ if semverCompare ">=1.17.0-0" .Capabilities.KubeVersion.GitVersion -}} 37 | apiVersion: rbac.authorization.k8s.io/v1 38 | {{- else -}} 39 | apiVersion: rbac.authorization.k8s.io/v1beta1 40 | {{- end }} 41 | kind: ClusterRoleBinding 42 | metadata: 43 | name: {{ include "kubernetes-external-secrets.fullname" . }} 44 | labels: 45 | app.kubernetes.io/name: {{ include "kubernetes-external-secrets.name" . }} 46 | helm.sh/chart: {{ include "kubernetes-external-secrets.chart" . }} 47 | app.kubernetes.io/instance: {{ .Release.Name }} 48 | app.kubernetes.io/managed-by: {{ .Release.Service }} 49 | roleRef: 50 | apiGroup: rbac.authorization.k8s.io 51 | kind: ClusterRole 52 | name: {{ template "kubernetes-external-secrets.fullname" . }} 53 | subjects: 54 | - name: {{ template "kubernetes-external-secrets.serviceAccountName" . }} 55 | namespace: {{ .Release.Namespace | quote }} 56 | kind: ServiceAccount 57 | --- 58 | {{ if semverCompare ">=1.17.0-0" .Capabilities.KubeVersion.GitVersion -}} 59 | apiVersion: rbac.authorization.k8s.io/v1 60 | {{- else -}} 61 | apiVersion: rbac.authorization.k8s.io/v1beta1 62 | {{- end }} 63 | kind: ClusterRoleBinding 64 | metadata: 65 | name: {{ include "kubernetes-external-secrets.fullname" . }}-auth 66 | labels: 67 | app.kubernetes.io/name: {{ include "kubernetes-external-secrets.name" . }} 68 | helm.sh/chart: {{ include "kubernetes-external-secrets.chart" . }} 69 | app.kubernetes.io/instance: {{ .Release.Name }} 70 | app.kubernetes.io/managed-by: {{ .Release.Service }} 71 | roleRef: 72 | apiGroup: rbac.authorization.k8s.io 73 | kind: ClusterRole 74 | name: system:auth-delegator 75 | subjects: 76 | - name: {{ template "kubernetes-external-secrets.serviceAccountName" . }} 77 | namespace: {{ .Release.Namespace | quote }} 78 | kind: ServiceAccount 79 | {{- end -}} 80 | -------------------------------------------------------------------------------- /charts/kubernetes-external-secrets/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "kubernetes-external-secrets.fullname" . }} 5 | namespace: {{ .Release.Namespace | quote }} 6 | labels: 7 | app.kubernetes.io/name: {{ include "kubernetes-external-secrets.name" . }} 8 | helm.sh/chart: {{ include "kubernetes-external-secrets.chart" . }} 9 | app.kubernetes.io/instance: {{ .Release.Name }} 10 | app.kubernetes.io/managed-by: {{ .Release.Service }} 11 | spec: 12 | selector: 13 | app.kubernetes.io/name: {{ include "kubernetes-external-secrets.name" . }} 14 | ports: 15 | - protocol: TCP 16 | port: {{ .Values.env.METRICS_PORT }} 17 | name: prometheus 18 | targetPort: prometheus 19 | -------------------------------------------------------------------------------- /charts/kubernetes-external-secrets/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "kubernetes-external-secrets.serviceAccountName" . }} 6 | namespace: {{ .Release.Namespace | quote }} 7 | {{- if .Values.serviceAccount.annotations }} 8 | annotations: {{ toYaml .Values.serviceAccount.annotations | nindent 4 }} 9 | {{- end }} 10 | labels: 11 | app.kubernetes.io/name: {{ include "kubernetes-external-secrets.name" . }} 12 | helm.sh/chart: {{ include "kubernetes-external-secrets.chart" . }} 13 | app.kubernetes.io/instance: {{ .Release.Name }} 14 | app.kubernetes.io/managed-by: {{ .Release.Service }} 15 | {{- end -}} 16 | -------------------------------------------------------------------------------- /charts/kubernetes-external-secrets/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceMonitor.enabled }} 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | name: {{ include "kubernetes-external-secrets.fullname" . }} 6 | {{- if .Values.serviceMonitor.namespace }} 7 | namespace: {{ .Values.serviceMonitor.namespace }} 8 | {{- end }} 9 | labels: 10 | app.kubernetes.io/name: {{ include "kubernetes-external-secrets.name" . }} 11 | helm.sh/chart: {{ include "kubernetes-external-secrets.chart" . }} 12 | app.kubernetes.io/instance: {{ .Release.Name }} 13 | app.kubernetes.io/managed-by: {{ .Release.Service }} 14 | spec: 15 | endpoints: 16 | - interval: {{ .Values.serviceMonitor.interval }} 17 | port: prometheus 18 | path: /metrics 19 | namespaceSelector: 20 | matchNames: 21 | - {{ .Release.Namespace }} 22 | selector: 23 | matchLabels: 24 | app.kubernetes.io/name: {{ include "kubernetes-external-secrets.name" . }} 25 | {{- end }} 26 | -------------------------------------------------------------------------------- /charts/kubernetes-external-secrets/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for kubernetes-external-secrets. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | # Environment variables to set on deployment pod 6 | env: 7 | AWS_REGION: us-west-2 8 | POLLER_INTERVAL_MILLISECONDS: 10000 # Caution, setting this frequency may incur additional charges on some platforms 9 | WATCH_TIMEOUT: 60000 10 | WATCHED_NAMESPACES: "" # Comma separated list of namespaces, empty or unset means ALL namespaces. 11 | LOG_LEVEL: info 12 | LOG_MESSAGE_KEY: "msg" 13 | 14 | #Akeyless rest-v2 endpoint 15 | AKEYLESS_API_ENDPOINT: https://api.akeyless.io 16 | AKEYLESS_ACCESS_ID: 17 | #AKEYLESS_ACCESS_TYPE can be one of the following: aws_iam/azure_ad/gcp/access_key 18 | AKEYLESS_ACCESS_TYPE: 19 | #AKEYLESS_ACCESS_TYPE_PARAM can be one of the following: gcp-audience/azure-obj-id/access-key 20 | #AKEYLESS_ACCESS_TYPE_PARAM: 21 | 22 | 23 | # Print logs level as string ("info") rather than integer (30) 24 | # USE_HUMAN_READABLE_LOG_LEVELS: true 25 | METRICS_PORT: 3001 26 | VAULT_ADDR: http://127.0.0.1:8200 27 | # Set a role to be used when assuming roles specified in external secret (AWS only) 28 | # AWS_INTERMEDIATE_ROLE_ARN: 29 | # GOOGLE_APPLICATION_CREDENTIALS: /app/gcp-creds/gcp-creds.json 30 | # Use custom endpoints for FIPS compliance 31 | # AWS_STS_ENDPOINT: https://sts-fips.us-east-1.amazonaws.com 32 | # AWS_SSM_ENDPOINT: http://ssm-fips.us-east-1.amazonaws.com 33 | # AWS_SM_ENDPOINT: http://secretsmanager-fips.us-east-1.amazonaws.com 34 | 35 | # Use Azure Environment-oriented KeyVault endpoints 36 | # AZURE_ENVIRONMENT: AzureUSGovernment 37 | # AZURE_KEY_VAULT_DNS_SUFFIX: vault.usgovcloudapi.net 38 | 39 | # Create environment variables from existing k8s secrets 40 | envVarsFromSecret: {} 41 | # AWS_ACCESS_KEY_ID: 42 | # secretKeyRef: aws-credentials 43 | # key: id 44 | # AWS_SECRET_ACCESS_KEY: 45 | # secretKeyRef: aws-credentials 46 | # key: key 47 | # ALICLOUD_ENDPOINT: 48 | # secretKeyRef: alicloud-credentials 49 | # key: endpoint 50 | # ALICLOUD_ACCESS_KEY_ID: 51 | # secretKeyRef: alicloud-credentials 52 | # key: id 53 | # ALICLOUD_ACCESS_KEY_SECRET: 54 | # secretKeyRef: alicloud-credentials 55 | # key: secret 56 | # AZURE_TENANT_ID: 57 | # secretKeyRef: azure-credentials 58 | # key: tenantid 59 | # AZURE_CLIENT_ID: 60 | # secretKeyRef: azure-credentials 61 | # key: clientid 62 | # AZURE_CLIENT_SECRET: 63 | # secretKeyRef: azure-credentials 64 | # key: clientsecret 65 | 66 | # Create environment variables from existing k8s secrets 67 | envVarsFromConfigMap: {} 68 | # AWS_ACCESS_KEY_ID: 69 | # configMapKeyRef: aws-credentials 70 | # key: id 71 | # AWS_SECRET_ACCESS_KEY: 72 | # configMapKeyRef: aws-credentials 73 | # key: key 74 | # ALICLOUD_ENDPOINT: 75 | # configMapKeyRef: alicloud-credentials 76 | # key: endpoint 77 | # ALICLOUD_ACCESS_KEY_ID: 78 | # configMapKeyRef: alicloud-credentials 79 | # key: id 80 | # ALICLOUD_ACCESS_KEY_SECRET: 81 | # configMapKeyRef: alicloud-credentials 82 | # key: secret 83 | # AZURE_TENANT_ID: 84 | # configMapKeyRef: azure-credentials 85 | # key: tenantid 86 | # AZURE_CLIENT_ID: 87 | # configMapKeyRef: azure-credentials 88 | # key: clientid 89 | # AZURE_CLIENT_SECRET: 90 | # configMapKeyRef: azure-credentials 91 | # key: clientsecret 92 | 93 | 94 | # List of sources to populate environment variables in the container. 95 | # The keys defined within a source must be a C_IDENTIFIER. All invalid keys 96 | # will be reported as an event when the container is starting. When a key 97 | # exists in multiple sources, the value associated with the last source will 98 | # take precedence. Values defined by an Env with a duplicate key will take precedence. 99 | # https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#configure-all-key-value-pairs-in-a-configmap-as-container-environment-variables 100 | envFrom: {} 101 | # - configMapRef: 102 | # name: special-config 103 | # - secretRef: 104 | # name: special-config 105 | 106 | 107 | # Create files from existing k8s secrets 108 | # filesFromSecret: 109 | # gcp-creds: 110 | # secret: gcp-creds 111 | # mountPath: /app/gcp-creds 112 | 113 | rbac: 114 | # Specifies whether RBAC resources should be created 115 | create: true 116 | 117 | serviceAccount: 118 | # Specifies whether a service account should be created 119 | create: true 120 | # Specifies annotations for this service account 121 | annotations: {} 122 | # The name of the service account to use. 123 | # If not set and create is true, a name is generated using the fullname template 124 | name: 125 | 126 | # Using multiple replicas is not recommended as there is no coordination between replicas. 127 | # Replicas will try to create and update secrets concurrently which might lead to race conditions. 128 | replicaCount: 1 129 | 130 | image: 131 | repository: ghcr.io/external-secrets/kubernetes-external-secrets 132 | tag: 8.5.5 133 | pullPolicy: IfNotPresent 134 | 135 | imagePullSecrets: [] 136 | 137 | nameOverride: "" 138 | fullnameOverride: "" 139 | 140 | # All label values must be strings 141 | deploymentLabels: {} 142 | 143 | podAnnotations: {} 144 | podLabels: {} 145 | 146 | priorityClassName: "" 147 | 148 | dnsConfig: {} 149 | 150 | securityContext: 151 | runAsNonRoot: true 152 | # Required for use of IRSA, see https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts-technical-overview.html 153 | # fsGroup: 65534 154 | 155 | # A security context defines privilege and access control settings for a Pod or Container. 156 | # ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ 157 | containerSecurityContext: {} 158 | # allowPrivilegeEscalation: false 159 | # privileged: false 160 | 161 | resources: 162 | {} 163 | # We usually recommend not to specify default resources and to leave this as a conscious 164 | # choice for the user. This also increases chances charts run on environments with little 165 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 166 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 167 | # limits: 168 | # cpu: 100m 169 | # memory: 128Mi 170 | # requests: 171 | # cpu: 100m 172 | # memory: 128Mi 173 | 174 | nodeSelector: {} 175 | 176 | tolerations: [] 177 | 178 | affinity: {} 179 | 180 | podDisruptionBudget: {} 181 | 182 | serviceMonitor: 183 | enabled: false 184 | interval: "30s" 185 | namespace: 186 | 187 | deploymentInitContainers: {} 188 | 189 | # Add in additional named volumes and volume mounts to the deployment 190 | # 191 | extraVolumes: [] 192 | # - name: namedVolume 193 | # emptyDir: {} 194 | # 195 | extraVolumeMounts: [] 196 | # - name: namedVolume 197 | # mountPath: /usr/path 198 | # readOnly: false 199 | 200 | # Add additional RBAC rules to the ClusterRole granted to the service account 201 | customClusterRoles: {} 202 | -------------------------------------------------------------------------------- /config/akeyless-config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const akeyless = require('akeyless') 3 | const AkeylessClient = new akeyless.ApiClient() 4 | AkeylessClient.basePath = process.env.AKEYLESS_API_ENDPOINT || 'https://api.akeyless.io' 5 | 6 | // Akeyless expects the following four environment variables: 7 | // - AKEYLESS_API_ENDPOINT: api-gw endpoint URL http(s)://api.akeyless.io 8 | // - AKEYLESS_ACCESS_ID: The access ID 9 | // - AKEYLESS_ACCESS_TYPE: The access type 10 | // - AKEYLESS_ACCESS_TYPE_PARAM: AZURE_OBJ_ID OR GCP_AUDIENCE OR ACCESS_KEY 11 | 12 | const client = new akeyless.V2Api(AkeylessClient) 13 | module.exports = { 14 | credential: { 15 | accessTypeParam: process.env.AKEYLESS_ACCESS_TYPE_PARAM, 16 | accessId: process.env.AKEYLESS_ACCESS_ID, 17 | accessType: process.env.AKEYLESS_ACCESS_TYPE, 18 | client: client 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /config/alicloud-config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Alibaba Cloud expects the following four environment variables: 4 | // - ALICLOUD_ENDPOINT: endpoint URL http(s)://kms.{regionID}.aliyuncs.com or http(s)://kms-vpc.{regionID}.aliyuncs.com 5 | // - ALICLOUD_ACCESS_KEY_ID: The access key ID 6 | // - ALICLOUD_ACCESS_KEY_SECRET: The access key secret 7 | 8 | module.exports = { 9 | credential: { 10 | accessKeyId: process.env.ALICLOUD_ACCESS_KEY_ID, 11 | accessKeySecret: process.env.ALICLOUD_ACCESS_KEY_SECRET, 12 | endpoint: process.env.ALICLOUD_ENDPOINT, 13 | type: 'access_key' 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /config/aws-config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-disable no-process-env */ 4 | const AWS = require('aws-sdk') 5 | const clonedeep = require('lodash/cloneDeep') 6 | const merge = require('lodash/merge') 7 | const proxy = require('proxy-agent') 8 | 9 | if (process.env.HTTP_PROXY !== '') { 10 | AWS.config.update({ 11 | httpOptions: { 12 | agent: proxy(process.env.HTTP_PROXY) 13 | } 14 | }) 15 | } 16 | 17 | const localstack = process.env.LOCALSTACK || 0 18 | 19 | const intermediateRole = process.env.AWS_INTERMEDIATE_ROLE_ARN || 0 20 | 21 | const stsEndpoint = process.env.AWS_STS_ENDPOINT || 0 22 | const ssmEndpoint = process.env.AWS_SSM_ENDPOINT || 0 23 | const smEndpoint = process.env.AWS_SM_ENDPOINT || 0 24 | 25 | let secretsManagerConfig = {} 26 | let systemManagerConfig = {} 27 | let stsConfig = { 28 | region: process.env.AWS_REGION || 'us-west-2', 29 | stsRegionalEndpoints: process.env.AWS_STS_ENDPOINT_TYPE || 'regional' 30 | } 31 | 32 | if (smEndpoint) { 33 | secretsManagerConfig.endpoint = smEndpoint 34 | } 35 | 36 | if (ssmEndpoint) { 37 | systemManagerConfig.endpoint = ssmEndpoint 38 | } 39 | 40 | if (stsEndpoint) { 41 | stsConfig.endpoint = stsEndpoint 42 | } 43 | 44 | if (localstack) { 45 | secretsManagerConfig = { 46 | endpoint: process.env.LOCALSTACK_SM_URL || 'http://localhost:4566', 47 | region: process.env.AWS_REGION || 'us-west-2' 48 | } 49 | systemManagerConfig = { 50 | endpoint: process.env.LOCALSTACK_SSM_URL || 'http://localhost:4566', 51 | region: process.env.AWS_REGION || 'us-west-2' 52 | } 53 | stsConfig = { 54 | endpoint: process.env.LOCALSTACK_STS_URL || 'http://localhost:4566', 55 | region: process.env.AWS_REGION || 'us-west-2' 56 | } 57 | } 58 | 59 | const intermediateCredentials = intermediateRole ? new AWS.ChainableTemporaryCredentials({ 60 | params: { 61 | RoleArn: intermediateRole 62 | }, 63 | stsConfig 64 | }) : null 65 | 66 | module.exports = { 67 | secretsManagerFactory: (opts = {}) => { 68 | if (localstack) { 69 | opts = merge(clonedeep(opts), secretsManagerConfig) 70 | } 71 | return new AWS.SecretsManager(opts) 72 | }, 73 | systemManagerFactory: (opts = {}) => { 74 | if (localstack) { 75 | opts = merge(clonedeep(opts), systemManagerConfig) 76 | } 77 | return new AWS.SSM(opts) 78 | }, 79 | assumeRole: (assumeRoleOpts) => { 80 | return new AWS.ChainableTemporaryCredentials({ 81 | params: assumeRoleOpts, 82 | masterCredentials: intermediateCredentials, 83 | stsConfig 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /config/azure-config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { DefaultAzureCredential, AzureAuthorityHosts } = require('@azure/identity') 4 | // DefaultAzureCredential expects the following three environment variables: 5 | // - AZURE_TENANT_ID: The tenant ID in Azure Active Directory 6 | // - AZURE_CLIENT_ID: The application (client) ID registered in the AAD tenant 7 | // - AZURE_CLIENT_SECRET: The client secret for the registered application 8 | // An optional environment variable AZURE_ENVIRONMENT may be provided to specify cloud environment 9 | 10 | const authorityHostMap = new Map() 11 | authorityHostMap.set('AzureCloud', AzureAuthorityHosts.AzurePublicCloud) 12 | authorityHostMap.set('AzureChinaCloud', AzureAuthorityHosts.AzureChina) 13 | authorityHostMap.set('AzureGermanCloud', AzureAuthorityHosts.AzureGermany) 14 | authorityHostMap.set('AzureUSGovernmentCloud', AzureAuthorityHosts.AzureGovernment) 15 | 16 | module.exports = { 17 | azureKeyVault: () => { 18 | const env = process.env.AZURE_ENVIRONMENT || 'AzureCloud' 19 | const host = authorityHostMap.get(env) 20 | const credential = new DefaultAzureCredential({ authorityHost: host }) 21 | return credential 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-disable no-process-env */ 4 | 5 | const environment = process.env.NODE_ENV 6 | ? process.env.NODE_ENV.toLowerCase() : 'development' 7 | 8 | // Validate environment 9 | const validEnvironments = new Set(['development', 'test', 'production']) 10 | if (!validEnvironments.has(environment)) { 11 | throw new Error(`Invalid environment: ${environment}`) 12 | } 13 | 14 | // Load env file only when development env 15 | if (environment === 'development') { 16 | require('dotenv').config() 17 | } 18 | 19 | // The name of this KES instance which used to scope access of ExternalSecrets. 20 | // This is needed in case there is more than a KES controller instances within the cluster. 21 | const instanceId = process.env.INSTANCE_ID || '' 22 | 23 | const vaultEndpoint = process.env.VAULT_ADDR || 'http://127.0.0.1:8200' 24 | // Grab the vault namespace from the environment 25 | const vaultNamespace = process.env.VAULT_NAMESPACE || null 26 | const vaultTokenRenewThreshold = process.env.VAULT_TOKEN_RENEW_THRESHOLD || null 27 | const defaultVaultMountPoint = process.env.DEFAULT_VAULT_MOUNT_POINT || null 28 | const defaultVaultRole = process.env.DEFAULT_VAULT_ROLE || null 29 | 30 | const pollerIntervalMilliseconds = process.env.POLLER_INTERVAL_MILLISECONDS 31 | ? Number(process.env.POLLER_INTERVAL_MILLISECONDS) : 10000 32 | 33 | const logLevel = process.env.LOG_LEVEL || 'info' 34 | const os = require('os') 35 | const logBasePidKey = process.env.LOG_BASE_PID_KEY || 'pid' 36 | const logBaseHostnameKey = process.env.LOG_BASE_HOSTNAME_KEY || 'hostname' 37 | const logBase = { [logBasePidKey]: process.pid, [logBaseHostnameKey]: os.hostname() } 38 | const useHumanReadableLogLevels = 'USE_HUMAN_READABLE_LOG_LEVELS' in process.env 39 | const logMessageKey = process.env.LOG_MESSAGE_KEY || 'msg' 40 | 41 | const pollingDisabled = 'DISABLE_POLLING' in process.env 42 | 43 | const rolePermittedAnnotation = process.env.ROLE_PERMITTED_ANNOTATION || 'iam.amazonaws.com/permitted' 44 | const namingPermittedAnnotation = process.env.NAMING_PERMITTED_ANNOTATION || 'externalsecrets.kubernetes-client.io/permitted-key-name' 45 | const enforceNamespaceAnnotation = 'ENFORCE_NAMESPACE_ANNOTATIONS' in process.env || false 46 | 47 | const metricsPort = process.env.METRICS_PORT || 3001 48 | 49 | const watchTimeout = process.env.WATCH_TIMEOUT ? parseInt(process.env.WATCH_TIMEOUT) : 60000 50 | 51 | // A comma-separated list of watched namespaces. If set, only ExternalSecrets in those namespaces will be handled. 52 | let watchedNamespaces = process.env.WATCHED_NAMESPACES || '' 53 | 54 | // Return an array after splitting the watched namespaces string and clean up user input. 55 | watchedNamespaces = watchedNamespaces 56 | .split(',') 57 | // Remove any extra spaces. 58 | .map(namespace => { return namespace.trim() }) 59 | // Remove empty values (in case there is a tailing comma). 60 | .filter(namespace => namespace) 61 | 62 | module.exports = { 63 | instanceId, 64 | vaultEndpoint, 65 | vaultNamespace, 66 | vaultTokenRenewThreshold, 67 | defaultVaultMountPoint, 68 | defaultVaultRole, 69 | environment, 70 | pollerIntervalMilliseconds, 71 | metricsPort, 72 | rolePermittedAnnotation, 73 | namingPermittedAnnotation, 74 | enforceNamespaceAnnotation, 75 | pollingDisabled, 76 | logLevel, 77 | logBase, 78 | useHumanReadableLogLevels, 79 | logMessageKey, 80 | watchTimeout, 81 | watchedNamespaces 82 | } 83 | -------------------------------------------------------------------------------- /config/gcp-config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { SecretManagerServiceClient } = require('@google-cloud/secret-manager') 4 | 5 | // First, ADC checks to see if the environment variable GOOGLE_APPLICATION_CREDENTIALS is set. 6 | // If the variable is set, ADC uses the service account file that the variable points to 7 | // If the environment variable isn't set, ADC uses the default service account that the Kubernetes Engine 8 | // provides, for applications that run on those services 9 | 10 | module.exports = { 11 | gcpSecretsManager: () => { 12 | const client = new SecretManagerServiceClient() 13 | return client 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /config/ibmcloud-config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // IBM Cloud automatically picks up the following credentials so they don't have to be passed in the config 4 | // - SECRETS_MANAGER_API_AUTH_TYPE=iam 5 | // - SECRETS_MANAGER_API_APIKEY= 6 | // - SECRETS_MANAGER_API_ENDPOINT= endpoint URL https://{instance-id}.{region}.secrets-manager.appdomain.cloud 7 | 8 | module.exports = { 9 | credential: { 10 | apikey: process.env.IBM_CLOUD_SECRETS_MANAGER_API_APIKEY, 11 | endpoint: process.env.IBM_CLOUD_SECRETS_MANAGER_API_ENDPOINT, 12 | type: process.env.IBM_CLOUD_SECRETS_MANAGER_API_AUTH_TYPE 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const vault = require('node-vault') 4 | const kube = require('kubernetes-client') 5 | const KubeRequest = require('kubernetes-client/backends/request') 6 | const pino = require('pino') 7 | const yaml = require('js-yaml') 8 | const fs = require('fs') 9 | const path = require('path') 10 | 11 | const awsConfig = require('./aws-config') 12 | const azureConfig = require('./azure-config') 13 | const alicloudConfig = require('./alicloud-config') 14 | const gcpConfig = require('./gcp-config') 15 | const ibmcloudConfig = require('./ibmcloud-config') 16 | const akeylessConfig = require('./akeyless-config') 17 | const envConfig = require('./environment') 18 | const SecretsManagerBackend = require('../lib/backends/secrets-manager-backend') 19 | const SystemManagerBackend = require('../lib/backends/system-manager-backend') 20 | const VaultBackend = require('../lib/backends/vault-backend') 21 | const AzureKeyVaultBackend = require('../lib/backends/azure-keyvault-backend') 22 | const GCPSecretsManagerBackend = require('../lib/backends/gcp-secrets-manager-backend') 23 | const AliCloudSecretsManagerBackend = require('../lib/backends/alicloud-secrets-manager-backend') 24 | const IbmCloudSecretsManagerBackend = require('../lib/backends/ibmcloud-secrets-manager-backend') 25 | const AkeylessBackend = require('../lib/backends/akeyless-backend') 26 | 27 | // Get document, or throw exception on error 28 | // eslint-disable-next-line security/detect-non-literal-fs-filename 29 | const customResourceManifest = yaml.safeLoad(fs.readFileSync(path.resolve(__dirname, '../charts/kubernetes-external-secrets/crds/kubernetes-client.io_externalsecrets_crd.yaml'), 'utf8')) 30 | 31 | const kubeconfig = new kube.KubeConfig() 32 | kubeconfig.loadFromDefault() 33 | const kubeBackend = new KubeRequest({ kubeconfig }) 34 | const kubeClient = new kube.Client({ backend: kubeBackend }) 35 | 36 | const logger = pino({ 37 | serializers: { 38 | err: pino.stdSerializers.err 39 | }, 40 | redact: ['err.options.headers', 'err.options.json.jwt'], 41 | messageKey: envConfig.logMessageKey || 'msg', 42 | level: envConfig.logLevel, 43 | base: envConfig.logBase, 44 | formatters: { 45 | level (label, number) { 46 | return { level: envConfig.useHumanReadableLogLevels ? label : number } 47 | } 48 | }, 49 | nestedKey: 'payload', 50 | timestamp: () => `,"message_time":"${new Date(Date.now()).toISOString()}"` 51 | }) 52 | 53 | const secretsManagerBackend = new SecretsManagerBackend({ 54 | clientFactory: awsConfig.secretsManagerFactory, 55 | assumeRole: awsConfig.assumeRole, 56 | logger 57 | }) 58 | const systemManagerBackend = new SystemManagerBackend({ 59 | clientFactory: awsConfig.systemManagerFactory, 60 | assumeRole: awsConfig.assumeRole, 61 | logger 62 | }) 63 | const vaultOptions = { 64 | apiVersion: 'v1', 65 | endpoint: envConfig.vaultEndpoint, 66 | requestOptions: { 67 | // When running vault in HA mode, you must follow redirects on PUT/POST/DELETE 68 | // See: https://github.com/kr1sp1n/node-vault/issues/23 69 | followAllRedirects: true 70 | } 71 | } 72 | // Include the Vault Namespace header if we have provided it as an env var. 73 | // See: https://github.com/kr1sp1n/node-vault/pull/137#issuecomment-585309687 74 | if (envConfig.vaultNamespace) { 75 | vaultOptions.requestOptions.headers = { 76 | 'X-VAULT-NAMESPACE': envConfig.vaultNamespace 77 | } 78 | } 79 | const vaultFactory = () => vault(vaultOptions) 80 | 81 | // The Vault token is renewed only during polling, not asynchronously. The default tokenRenewThreshold 82 | // is three times larger than the pollerInterval so that the token is renewed before it 83 | // expires and with at least one remaining poll opportunty to retry renewal if it fails. 84 | const vaultTokenRenewThreshold = envConfig.vaultTokenRenewThreshold 85 | ? Number(envConfig.vaultTokenRenewThreshold) : 3 * envConfig.pollerIntervalMilliseconds / 1000 86 | 87 | const vaultBackend = new VaultBackend({ 88 | vaultFactory: vaultFactory, 89 | tokenRenewThreshold: vaultTokenRenewThreshold, 90 | logger: logger, 91 | defaultVaultMountPoint: envConfig.defaultVaultMountPoint, 92 | defaultVaultRole: envConfig.defaultVaultRole 93 | }) 94 | const azureKeyVaultBackend = new AzureKeyVaultBackend({ 95 | credential: azureConfig.azureKeyVault(), 96 | logger 97 | }) 98 | const gcpSecretsManagerBackend = new GCPSecretsManagerBackend({ 99 | client: gcpConfig.gcpSecretsManager(), 100 | logger 101 | }) 102 | const alicloudSecretsManagerBackend = new AliCloudSecretsManagerBackend({ 103 | credential: alicloudConfig.credential, 104 | logger 105 | }) 106 | const ibmcloudSecretsManagerBackend = new IbmCloudSecretsManagerBackend({ 107 | credential: ibmcloudConfig.credential, 108 | logger 109 | }) 110 | const akeylessBackend = new AkeylessBackend({ 111 | credential: akeylessConfig.credential, 112 | logger 113 | }) 114 | 115 | const backends = { 116 | // when adding a new backend, make sure to change the CRD property too 117 | secretsManager: secretsManagerBackend, 118 | systemManager: systemManagerBackend, 119 | vault: vaultBackend, 120 | azureKeyVault: azureKeyVaultBackend, 121 | gcpSecretsManager: gcpSecretsManagerBackend, 122 | alicloudSecretsManager: alicloudSecretsManagerBackend, 123 | ibmcloudSecretsManager: ibmcloudSecretsManagerBackend, 124 | akeyless: akeylessBackend 125 | } 126 | 127 | // backwards compatibility 128 | backends.secretManager = secretsManagerBackend 129 | 130 | module.exports = { 131 | awsConfig, 132 | backends, 133 | customResourceManifest, 134 | ...envConfig, 135 | kubeClient, 136 | logger 137 | } 138 | -------------------------------------------------------------------------------- /docs/artifacthub-repo.yml: -------------------------------------------------------------------------------- 1 | # Artifact Hub repository metadata file 2 | repositoryID: 87cd5c7d-6937-4bfc-9ea7-b4a16cc8a31c 3 | owners: 4 | - name: Flydiverny 5 | email: markus@maga.se 6 | -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-1.0.1.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-1.0.1.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-1.1.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-1.1.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-2.0.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-2.0.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-2.1.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-2.1.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-2.2.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-2.2.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-2.3.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-2.3.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-3.0.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-3.0.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-3.1.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-3.1.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-3.2.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-3.2.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-3.3.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-3.3.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-4.0.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-4.0.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-4.1.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-4.1.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-4.2.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-4.2.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-5.0.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-5.0.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-5.1.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-5.1.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-5.2.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-5.2.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-6.0.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-6.0.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-6.1.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-6.1.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-6.2.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-6.2.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-6.3.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-6.3.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-6.4.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-6.4.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-7.0.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-7.0.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-7.0.1.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-7.0.1.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-7.1.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-7.1.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-7.2.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-7.2.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-7.2.1.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-7.2.1.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.0.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.0.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.0.1.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.0.1.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.0.2.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.0.2.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.1.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.1.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.1.1.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.1.1.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.1.2.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.1.2.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.1.3.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.1.3.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.2.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.2.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.2.1.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.2.1.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.2.2.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.2.2.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.2.3.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.2.3.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.3.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.3.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.3.1.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.3.1.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.3.2.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.3.2.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.4.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.4.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.5.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.5.0.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.5.1.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.5.1.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.5.2.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.5.2.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.5.3.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.5.3.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.5.4.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.5.4.tgz -------------------------------------------------------------------------------- /docs/kubernetes-external-secrets-8.5.5.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/external-secrets/kubernetes-external-secrets/eeafe4d73ad460bda93b6166100d456e6a9d8b02/docs/kubernetes-external-secrets-8.5.5.tgz -------------------------------------------------------------------------------- /e2e/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine3.14 2 | 3 | # Setup source directory 4 | RUN mkdir /app 5 | WORKDIR /app 6 | COPY package.json package-lock.json /app/ 7 | RUN npm ci 8 | 9 | # Copy app to source directory 10 | COPY . /app 11 | 12 | CMD ["/app/node_modules/.bin/mocha", "--timeout", "10000", "/app/e2e/tests/*.test.js"] 13 | -------------------------------------------------------------------------------- /e2e/README.md: -------------------------------------------------------------------------------- 1 | # e2e tests 2 | 3 | ## Running e2e tests 4 | 5 | Prerequisites: 6 | * docker 7 | * kind 8 | * helm 9 | * kubectl 10 | 11 | Run them from the root of the repository `npm run test-e2e`. 12 | 13 | 14 | ## Developing e2e tests 15 | 16 | To better understand how they are being run take a look at `run-e2e-suite.sh`. 17 | 18 | 1. Prepare the environment 19 | 20 | ``` 21 | kind create cluster \ 22 | --name es-dev-cluster \ 23 | --config ./kind.yaml \ 24 | --image "kindest/node:v1.16.15" 25 | 26 | export KUBECONFIG="$(kind get kubeconfig-path --name="es-dev-cluster")" 27 | 28 | # build & load images 29 | docker build -t external-secrets:test -f ../Dockerfile ../ 30 | kind load docker-image --name="es-dev-cluster" external-secrets:test 31 | 32 | # prep localstack 33 | kubectl apply -f ./localstack.deployment.yaml 34 | 35 | # deploy external secrets 36 | helm template e2e ../charts/kubernetes-external-secrets \ 37 | --set image.repository=external-secrets \ 38 | --set image.tag=test \ 39 | --set env.LOG_LEVEL=debug \ 40 | --set env.LOCALSTACK=true \ 41 | --set env.LOCALSTACK_SSM_URL=http://ssm \ 42 | --set env.LOCALSTACK_SM_URL=http://secretsmanager \ 43 | --set env.AWS_ACCESS_KEY_ID=foobar \ 44 | --set env.AWS_SECRET_ACCESS_KEY=foobar \ 45 | --set env.AWS_REGION=us-east-1 \ 46 | --set env.POLLER_INTERVAL_MILLISECONDS=1000 \ 47 | --set env.LOCALSTACK_STS_URL=http://sts | kubectl apply -f - 48 | 49 | # prep e2e test 50 | kubectl create serviceaccount external-secrets-e2e || true 51 | kubectl create clusterrolebinding permissive-binding \ 52 | --clusterrole=cluster-admin \ 53 | --user=admin \ 54 | --user=kubelet \ 55 | --serviceaccount=default:external-secrets-e2e || true 56 | 57 | # make sure that everything is running 58 | kubectl rollout status deploy/localstack 59 | kubectl rollout status deploy/release-name-kubernetes-external-secrets 60 | ``` 61 | 62 | 2. build image & deploy to start the e2e test 63 | 64 | ``` 65 | docker build -t external-secrets-e2e:test -f Dockerfile ../ 66 | kind load docker-image --name="es-dev-cluster" external-secrets-e2e:test 67 | kubectl run \ 68 | --rm \ 69 | --attach \ 70 | --restart=Never \ 71 | --env="LOCALSTACK=true" \ 72 | --env="LOCALSTACK_SSM_URL=http://ssm" \ 73 | --env="LOCALSTACK_SM_URL=http://secretsmanager" \ 74 | --env="AWS_ACCESS_KEY_ID=foobar" \ 75 | --env="AWS_SECRET_ACCESS_KEY=foobar" \ 76 | --env="AWS_REGION=us-east-1" \ 77 | --env="LOCALSTACK_STS_URL=http://sts" \ 78 | --overrides='{ "apiVersion": "v1", "spec":{"serviceAccountName": "external-secrets-e2e"}}' \ 79 | e2e --image=external-secrets-e2e:test 80 | `` 81 | -------------------------------------------------------------------------------- /e2e/kind.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | networking: 4 | apiServerPort: 6443 5 | kubeadmConfigPatches: 6 | - | 7 | apiVersion: kubelet.config.k8s.io/v1beta1 8 | kind: KubeletConfiguration 9 | metadata: 10 | name: config 11 | # this is only relevant for btrfs uses 12 | # https://github.com/kubernetes/kubernetes/issues/80633#issuecomment-550994513 13 | featureGates: 14 | LocalStorageCapacityIsolation: false 15 | nodes: 16 | - role: control-plane 17 | - role: worker 18 | - role: worker 19 | -------------------------------------------------------------------------------- /e2e/localstack.deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: localstack 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: localstack 9 | replicas: 1 10 | template: 11 | metadata: 12 | labels: 13 | app: localstack 14 | spec: 15 | containers: 16 | - name: localstack 17 | image: localstack/localstack:0.10.5 18 | resources: 19 | limits: 20 | cpu: 300m 21 | memory: 500Mi 22 | livenessProbe: 23 | tcpSocket: 24 | port: 4100 25 | initialDelaySeconds: 30 26 | periodSeconds: 15 27 | readinessProbe: 28 | tcpSocket: 29 | port: 4100 30 | initialDelaySeconds: 30 31 | periodSeconds: 15 32 | ports: 33 | - containerPort: 4100 34 | name: ssm 35 | - containerPort: 4101 36 | name: secretsmanager 37 | - containerPort: 4102 38 | name: sts 39 | - containerPort: 32000 40 | name: ui 41 | env: 42 | - name: SERVICES 43 | value: "ssm:4100,secretsmanager:4101,sts:4102" 44 | - name: PORT_WEB_UI 45 | value: "32000" 46 | --- 47 | apiVersion: v1 48 | kind: Service 49 | metadata: 50 | name: ssm 51 | spec: 52 | # selector tells Kubernetes what Deployment this Service 53 | # belongs to 54 | selector: 55 | app: localstack 56 | ports: 57 | - port: 80 58 | targetPort: ssm 59 | --- 60 | apiVersion: v1 61 | kind: Service 62 | metadata: 63 | name: secretsmanager 64 | spec: 65 | # selector tells Kubernetes what Deployment this Service 66 | # belongs to 67 | selector: 68 | app: localstack 69 | ports: 70 | - port: 80 71 | targetPort: secretsmanager 72 | --- 73 | apiVersion: v1 74 | kind: Service 75 | metadata: 76 | name: sts 77 | spec: 78 | # selector tells Kubernetes what Deployment this Service 79 | # belongs to 80 | selector: 81 | app: localstack 82 | ports: 83 | - port: 80 84 | targetPort: sts 85 | --- 86 | apiVersion: v1 87 | kind: Service 88 | metadata: 89 | name: localstack 90 | spec: 91 | # selector tells Kubernetes what Deployment this Service 92 | # belongs to 93 | type: NodePort 94 | selector: 95 | app: localstack 96 | ports: 97 | - nodePort: 32000 98 | port: 80 99 | targetPort: ui 100 | 101 | --- 102 | -------------------------------------------------------------------------------- /e2e/run-e2e-suite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2018 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 18 | KIND_LOGGING="" 19 | if ! [ -z "$DEBUG" ]; then 20 | set -x 21 | KIND_LOGGING="--verbosity=4" 22 | kind version 23 | kubectl version --client 24 | helm version --client 25 | fi 26 | 27 | set -o errexit 28 | set -o nounset 29 | set -o pipefail 30 | 31 | RED='\e[35m' 32 | NC='\e[0m' 33 | BGREEN='\e[32m' 34 | 35 | K8S_VERSION=${K8S_VERSION:-v1.16.15} 36 | KIND_CLUSTER_NAME="external-secrets-dev" 37 | REGISTRY=external-secrets 38 | 39 | export KUBECONFIG="$(pwd)/e2e/.kubeconfig" 40 | 41 | kind --version || $(echo -e "${RED}Please install kind before running e2e tests${NC}";exit 1) 42 | echo -e "${BGREEN}[dev-env] creating Kubernetes cluster with kind${NC}" 43 | 44 | kind create cluster \ 45 | ${KIND_LOGGING} \ 46 | --name ${KIND_CLUSTER_NAME} \ 47 | --config "${DIR}/kind.yaml" \ 48 | --image "kindest/node:${K8S_VERSION}" 49 | 50 | echo -e "${BGREEN}building external-secrets images${NC}" 51 | docker build -t external-secrets:test -f "$DIR/../Dockerfile" "$DIR/../" 52 | docker build -t external-secrets-e2e:test -f "$DIR/Dockerfile" "$DIR/../" 53 | kind load docker-image --name="${KIND_CLUSTER_NAME}" external-secrets-e2e:test 54 | kind load docker-image --name="${KIND_CLUSTER_NAME}" external-secrets:test 55 | 56 | function cleanup { 57 | set +e 58 | kubectl delete pod e2e 2>/dev/null 59 | kubectl delete crd/externalsecrets.kubernetes-client.io 2>/dev/null 60 | kubectl delete -f "${DIR}/localstack.deployment.yaml" 2>/dev/null 61 | kind delete cluster \ 62 | ${KIND_LOGGING} \ 63 | --name ${KIND_CLUSTER_NAME} 64 | 65 | } 66 | trap cleanup EXIT 67 | 68 | kubectl apply -f ${DIR}/localstack.deployment.yaml 69 | 70 | CHART_DIR="$(dirname "$DIR")/charts/kubernetes-external-secrets" 71 | 72 | helm install e2e ${CHART_DIR} \ 73 | --set image.repository=external-secrets \ 74 | --set image.tag=test \ 75 | --set env.LOG_LEVEL=debug \ 76 | --set env.LOCALSTACK=true \ 77 | --set env.LOCALSTACK_SSM_URL=http://ssm \ 78 | --set env.LOCALSTACK_SM_URL=http://secretsmanager \ 79 | --set env.AWS_ACCESS_KEY_ID=foobar \ 80 | --set env.AWS_SECRET_ACCESS_KEY=foobar \ 81 | --set env.AWS_REGION=us-east-1 \ 82 | --set env.POLLER_INTERVAL_MILLISECONDS=1000 \ 83 | --set env.LOCALSTACK_STS_URL=http://sts 84 | 85 | echo -e "${BGREEN}Granting permissions to external-secrets e2e service account...${NC}" 86 | kubectl create serviceaccount external-secrets-e2e || true 87 | kubectl create clusterrolebinding permissive-binding \ 88 | --clusterrole=cluster-admin \ 89 | --user=admin \ 90 | --user=kubelet \ 91 | --serviceaccount=default:external-secrets-e2e || true 92 | 93 | until kubectl get secret | grep -q ^external-secrets-e2e-token; do \ 94 | echo -e "waiting for api token"; \ 95 | sleep 3; \ 96 | done 97 | 98 | echo -e "${BGREEN}Starting external-secrets e2e tests...${NC}" 99 | kubectl rollout status deploy/localstack 100 | kubectl rollout status deploy/e2e-kubernetes-external-secrets 101 | 102 | kubectl run \ 103 | --attach \ 104 | --restart=Never \ 105 | --env="LOCALSTACK=true" \ 106 | --env="LOCALSTACK_SSM_URL=http://ssm" \ 107 | --env="LOCALSTACK_SM_URL=http://secretsmanager" \ 108 | --env="AWS_ACCESS_KEY_ID=foobar" \ 109 | --env="AWS_SECRET_ACCESS_KEY=foobar" \ 110 | --env="AWS_REGION=us-east-1" \ 111 | --env="LOCALSTACK_STS_URL=http://sts" \ 112 | --overrides='{ "apiVersion": "v1", "spec":{"serviceAccountName": "external-secrets-e2e"}}' \ 113 | e2e --image=external-secrets-e2e:test 114 | -------------------------------------------------------------------------------- /e2e/tests/crd.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const { expect } = require('chai') 5 | 6 | const { 7 | kubeClient, 8 | customResourceManifest 9 | } = require('../../config') 10 | 11 | const { 12 | uuid 13 | } = require('./framework.js') 14 | 15 | describe('CRD', () => { 16 | it('ensure CRD is managed correctly', async () => { 17 | const res = await kubeClient 18 | .apis['apiextensions.k8s.io'] 19 | .v1 20 | .customresourcedefinitions(customResourceManifest.metadata.name) 21 | .get() 22 | 23 | const managedBy = 'helm' 24 | expect(res).to.not.equal(undefined) 25 | expect(res.statusCode).to.equal(200) 26 | expect(res.body.metadata.annotations['app.kubernetes.io/managed-by']).to.equal(managedBy) 27 | }) 28 | 29 | it('should reject invalid ExternalSecret manifests', async () => { 30 | return kubeClient 31 | .apis[customResourceManifest.spec.group] 32 | .v1.namespaces('default')[customResourceManifest.spec.names.plural] 33 | .post({ 34 | body: { 35 | apiVersion: 'kubernetes-client.io/v1', 36 | kind: 'ExternalSecret', 37 | metadata: { 38 | name: `e2e-test-validation-${uuid}` 39 | }, 40 | secretDescriptor: { 41 | backendType: 'systemManager', 42 | data: [ 43 | { 44 | key: `/e2e/${uuid}/name`, 45 | name: 'name' 46 | } 47 | ] 48 | } 49 | } 50 | }) 51 | .then(() => { throw new Error('was not supposed to succeed') }) 52 | .catch((err) => expect(err).to.match(/spec: Required value/)) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /e2e/tests/framework.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { 4 | kubeClient 5 | } = require('../../config') 6 | 7 | /** 8 | * "delays" the async execution 9 | * @param {Number} ms - number of milliseconds to wait 10 | */ 11 | async function delay (ms) { 12 | return new Promise(resolve => setTimeout(resolve, ms)) 13 | } 14 | 15 | /** 16 | * generate a uuid for this e2e run 17 | * taken from https://gist.github.com/6174/6062387 18 | */ 19 | const uuid = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) 20 | 21 | /** 22 | * wait for a secret to appear in a given namespace 23 | * this function polls the apiserver for updates in 100ms intervals (max 3s) 24 | * @param {String} ns - namespace 25 | * @param {String} name - secret name 26 | * @return {Secret|undefined} 27 | */ 28 | const waitForSecret = async (ns, name) => { 29 | for (let i = 0; i <= 30; i++) { 30 | try { 31 | const secret = await kubeClient 32 | .api.v1 33 | .namespaces(ns) 34 | .secrets(name).get() 35 | return secret 36 | } catch (e) { 37 | await delay(100) 38 | } 39 | } 40 | } 41 | 42 | module.exports = { 43 | uuid, 44 | delay, 45 | waitForSecret 46 | } 47 | -------------------------------------------------------------------------------- /e2e/tests/secrets-manager.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const util = require('util') 5 | const { expect } = require('chai') 6 | 7 | const { 8 | kubeClient, 9 | customResourceManifest, 10 | awsConfig 11 | } = require('../../config') 12 | const { 13 | waitForSecret, 14 | uuid, 15 | delay 16 | } = require('./framework.js') 17 | 18 | const secretsmanager = awsConfig.secretsManagerFactory() 19 | const createSecret = util.promisify(secretsmanager.createSecret).bind(secretsmanager) 20 | const putSecretValue = util.promisify(secretsmanager.putSecretValue).bind(secretsmanager) 21 | 22 | describe('secretsmanager', async () => { 23 | it('should pull existing secret from secretsmanager and create a secret with its values', async () => { 24 | let result = await createSecret({ 25 | Name: `e2e/${uuid}/credentials`, 26 | SecretString: '{"username":"foo","password":"bar"}' 27 | }).catch(err => { 28 | expect(err).to.equal(null) 29 | }) 30 | 31 | result = await kubeClient 32 | .apis[customResourceManifest.spec.group] 33 | .v1.namespaces('default')[customResourceManifest.spec.names.plural] 34 | .post({ 35 | body: { 36 | apiVersion: 'kubernetes-client.io/v1', 37 | kind: 'ExternalSecret', 38 | metadata: { 39 | name: `e2e-secretmanager-${uuid}` 40 | }, 41 | spec: { 42 | backendType: 'secretsManager', 43 | data: [ 44 | { 45 | key: `e2e/${uuid}/credentials`, 46 | property: 'password', 47 | name: 'password' 48 | }, 49 | { 50 | key: `e2e/${uuid}/credentials`, 51 | property: 'username', 52 | name: 'username' 53 | } 54 | ] 55 | } 56 | } 57 | }) 58 | 59 | expect(result).to.not.equal(undefined) 60 | expect(result.statusCode).to.equal(201) 61 | 62 | let secret = await waitForSecret('default', `e2e-secretmanager-${uuid}`) 63 | expect(secret).to.not.equal(undefined) 64 | expect(secret.body.data.username).to.equal('Zm9v') 65 | expect(secret.body.data.password).to.equal('YmFy') 66 | 67 | // update the secret value 68 | result = await putSecretValue({ 69 | SecretId: `e2e/${uuid}/credentials`, 70 | SecretString: '{"username":"your mom","password":"1234"}' 71 | }).catch(err => { 72 | expect(err).to.equal(null) 73 | }) 74 | await delay(2000) 75 | secret = await waitForSecret('default', `e2e-secretmanager-${uuid}`) 76 | expect(secret.body.data.username).to.equal('eW91ciBtb20=') 77 | expect(secret.body.data.password).to.equal('MTIzNA==') 78 | }) 79 | 80 | it('should pull existing secret from secretsmanager and create a secret using templating', async () => { 81 | let result = await createSecret({ 82 | Name: `e2e/${uuid}/template`, 83 | SecretString: '{"secretData":"foo123"}' 84 | }).catch(err => { 85 | expect(err).to.equal(null) 86 | }) 87 | 88 | result = await kubeClient 89 | .apis[customResourceManifest.spec.group] 90 | .v1.namespaces('default')[customResourceManifest.spec.names.plural] 91 | .post({ 92 | body: { 93 | apiVersion: 'kubernetes-client.io/v1', 94 | kind: 'ExternalSecret', 95 | metadata: { 96 | name: `e2e-secretmanager-template-${uuid}` 97 | }, 98 | spec: { 99 | template: { 100 | metadata: { 101 | labels: { 102 | secretLabel: '<%= "Hello".concat(data.secretData) %>' 103 | } 104 | } 105 | }, 106 | backendType: 'secretsManager', 107 | data: [ 108 | { 109 | key: `e2e/${uuid}/template`, 110 | property: 'secretData', 111 | name: 'secretData' 112 | } 113 | ] 114 | } 115 | } 116 | }) 117 | 118 | expect(result).to.not.equal(undefined) 119 | expect(result.statusCode).to.equal(201) 120 | 121 | const secret = await waitForSecret('default', `e2e-secretmanager-template-${uuid}`) 122 | expect(secret).to.not.equal(undefined) 123 | expect(secret.body.data.secretData).to.equal('Zm9vMTIz') // foo123 is base64 Zm9vMTIz 124 | expect(secret.body.metadata.labels.secretLabel).to.equal('Hellofoo123') 125 | }) 126 | 127 | it('should pull TLS secret from secretsmanager - type', async () => { 128 | let result = await createSecret({ 129 | Name: `e2e/${uuid}/tls/cert`, 130 | SecretString: '{"crt":"foo","key":"bar"}' 131 | }).catch(err => { 132 | expect(err).to.equal(null) 133 | }) 134 | 135 | result = await kubeClient 136 | .apis[customResourceManifest.spec.group] 137 | .v1.namespaces('default')[customResourceManifest.spec.names.plural] 138 | .post({ 139 | body: { 140 | apiVersion: 'kubernetes-client.io/v1', 141 | kind: 'ExternalSecret', 142 | metadata: { 143 | name: `e2e-secretmanager-tls-${uuid}` 144 | }, 145 | spec: { 146 | backendType: 'secretsManager', 147 | type: 'kubernetes.io/tls', 148 | data: [ 149 | { 150 | key: `e2e/${uuid}/tls/cert`, 151 | property: 'crt', 152 | name: 'tls.crt' 153 | }, 154 | { 155 | key: `e2e/${uuid}/tls/cert`, 156 | property: 'key', 157 | name: 'tls.key' 158 | } 159 | ] 160 | } 161 | } 162 | }) 163 | 164 | expect(result).to.not.equal(undefined) 165 | expect(result.statusCode).to.equal(201) 166 | 167 | const secret = await waitForSecret('default', `e2e-secretmanager-tls-${uuid}`) 168 | expect(secret).to.not.equal(undefined) 169 | expect(secret.body.data['tls.crt']).to.equal('Zm9v') 170 | expect(secret.body.data['tls.key']).to.equal('YmFy') 171 | expect(secret.body.type).to.equal('kubernetes.io/tls') 172 | }) 173 | 174 | it('should pull TLS secret from secretsmanager - template', async () => { 175 | let result = await createSecret({ 176 | Name: `e2e/${uuid}/tls/cert-template`, 177 | SecretString: '{"crt":"foo","key":"bar"}' 178 | }).catch(err => { 179 | expect(err).to.equal(null) 180 | }) 181 | 182 | result = await kubeClient 183 | .apis[customResourceManifest.spec.group] 184 | .v1.namespaces('default')[customResourceManifest.spec.names.plural] 185 | .post({ 186 | body: { 187 | apiVersion: 'kubernetes-client.io/v1', 188 | kind: 'ExternalSecret', 189 | metadata: { 190 | name: `e2e-secretmanager-tls-template-${uuid}` 191 | }, 192 | spec: { 193 | backendType: 'secretsManager', 194 | template: { 195 | type: 'kubernetes.io/tls' 196 | }, 197 | data: [ 198 | { 199 | key: `e2e/${uuid}/tls/cert-template`, 200 | property: 'crt', 201 | name: 'tls.crt' 202 | }, 203 | { 204 | key: `e2e/${uuid}/tls/cert-template`, 205 | property: 'key', 206 | name: 'tls.key' 207 | } 208 | ] 209 | } 210 | } 211 | }) 212 | 213 | expect(result).to.not.equal(undefined) 214 | expect(result.statusCode).to.equal(201) 215 | 216 | const secret = await waitForSecret('default', `e2e-secretmanager-tls-template-${uuid}`) 217 | expect(secret).to.not.equal(undefined) 218 | expect(secret.body.data['tls.crt']).to.equal('Zm9v') 219 | expect(secret.body.data['tls.key']).to.equal('YmFy') 220 | expect(secret.body.type).to.equal('kubernetes.io/tls') 221 | }) 222 | 223 | it('should pull existing secret from secretsmanager in the correct region', async () => { 224 | const smEU = awsConfig.secretsManagerFactory({ 225 | region: 'eu-west-1' 226 | }) 227 | const createSecret = util.promisify(smEU.createSecret).bind(smEU) 228 | const putSecretValue = util.promisify(smEU.putSecretValue).bind(smEU) 229 | 230 | let result = await createSecret({ 231 | Name: `e2e/${uuid}/x-region-credentials`, 232 | SecretString: '{"username":"foo","password":"bar"}' 233 | }).catch(err => { 234 | expect(err).to.equal(null) 235 | }) 236 | 237 | result = await kubeClient 238 | .apis[customResourceManifest.spec.group] 239 | .v1.namespaces('default')[customResourceManifest.spec.names.plural] 240 | .post({ 241 | body: { 242 | apiVersion: 'kubernetes-client.io/v1', 243 | kind: 'ExternalSecret', 244 | metadata: { 245 | name: `e2e-secretmanager-x-region-${uuid}` 246 | }, 247 | spec: { 248 | backendType: 'secretsManager', 249 | region: 'eu-west-1', 250 | data: [ 251 | { 252 | key: `e2e/${uuid}/x-region-credentials`, 253 | property: 'password', 254 | name: 'password' 255 | }, 256 | { 257 | key: `e2e/${uuid}/x-region-credentials`, 258 | property: 'username', 259 | name: 'username' 260 | } 261 | ] 262 | } 263 | } 264 | }) 265 | 266 | expect(result).to.not.equal(undefined) 267 | expect(result.statusCode).to.equal(201) 268 | 269 | let secret = await waitForSecret('default', `e2e-secretmanager-x-region-${uuid}`) 270 | expect(secret).to.not.equal(undefined) 271 | expect(secret.body.data.username).to.equal('Zm9v') 272 | expect(secret.body.data.password).to.equal('YmFy') 273 | 274 | // update the secret value 275 | result = await putSecretValue({ 276 | SecretId: `e2e/${uuid}/x-region-credentials`, 277 | SecretString: '{"username":"your mom","password":"1234"}' 278 | }).catch(err => { 279 | expect(err).to.equal(null) 280 | }) 281 | await delay(2000) 282 | secret = await waitForSecret('default', `e2e-secretmanager-x-region-${uuid}`) 283 | expect(secret.body.data.username).to.equal('eW91ciBtb20=') 284 | expect(secret.body.data.password).to.equal('MTIzNA==') 285 | }) 286 | 287 | describe('permitted annotation', async () => { 288 | beforeEach(async () => { 289 | await kubeClient.api.v1.namespaces('default').patch({ 290 | body: { 291 | metadata: { 292 | annotations: { 293 | 'iam.amazonaws.com/permitted': '^(foo|bar)' 294 | } 295 | } 296 | } 297 | }) 298 | }) 299 | 300 | afterEach(async () => { 301 | await kubeClient.api.v1.namespaces('default').patch({ 302 | body: { 303 | metadata: { 304 | annotations: { 305 | 'iam.amazonaws.com/permitted': '.*', 306 | 'externalsecrets.kubernetes-client.io/permitted-key-name': '.*' 307 | } 308 | } 309 | } 310 | }) 311 | }) 312 | 313 | describe('assuming role', async () => { 314 | it('should not pull from secretsmanager', async () => { 315 | let result = await createSecret({ 316 | Name: `e2e/${uuid}/tls/permitted`, 317 | SecretString: '{"crt":"foo","key":"bar"}' 318 | }).catch(err => { 319 | expect(err).to.equal(null) 320 | }) 321 | 322 | result = await kubeClient 323 | .apis[customResourceManifest.spec.group] 324 | .v1.namespaces('default')[customResourceManifest.spec.names.plural] 325 | .post({ 326 | body: { 327 | apiVersion: 'kubernetes-client.io/v1', 328 | kind: 'ExternalSecret', 329 | metadata: { 330 | name: `e2e-secretmanager-permitted-tls-${uuid}` 331 | }, 332 | spec: { 333 | backendType: 'secretsManager', 334 | type: 'kubernetes.io/tls', 335 | // this should not be allowed 336 | roleArn: 'let-me-be-root', 337 | data: [ 338 | { 339 | key: `e2e/${uuid}/tls/permitted`, 340 | property: 'crt', 341 | name: 'tls.crt' 342 | }, 343 | { 344 | key: `e2e/${uuid}/tls/permitted`, 345 | property: 'key', 346 | name: 'tls.key' 347 | } 348 | ] 349 | } 350 | } 351 | }) 352 | 353 | expect(result).to.not.equal(undefined) 354 | expect(result.statusCode).to.equal(201) 355 | 356 | const secret = await waitForSecret('default', `e2e-secretmanager-permitted-tls-${uuid}`) 357 | expect(secret).to.equal(undefined) 358 | 359 | result = await kubeClient 360 | .apis[customResourceManifest.spec.group] 361 | .v1.namespaces('default') 362 | .externalsecrets(`e2e-secretmanager-permitted-tls-${uuid}`) 363 | .get() 364 | expect(result).to.not.equal(undefined) 365 | expect(result.body.status.status).to.contain('namespace does not allow to assume role let-me-be-root') 366 | }) 367 | }) 368 | 369 | describe('enforcing naming convention', async () => { 370 | it('should not pull from secretsmanager', async () => { 371 | await kubeClient.api.v1.namespaces('default').patch({ 372 | body: { 373 | metadata: { 374 | annotations: { 375 | 'iam.amazonaws.com/permitted': '.*', 376 | 'externalsecrets.kubernetes-client.io/permitted-key-name': '/permitted/path/.*' 377 | } 378 | } 379 | } 380 | }) 381 | 382 | let result = await createSecret({ 383 | Name: `e2e/${uuid}/another_credentials`, 384 | SecretString: '{"username":"foo","password":"bar"}' 385 | }).catch(err => { 386 | expect(err).to.equal(null) 387 | }) 388 | 389 | result = await kubeClient 390 | .apis[customResourceManifest.spec.group] 391 | .v1.namespaces('default')[customResourceManifest.spec.names.plural] 392 | .post({ 393 | body: { 394 | apiVersion: 'kubernetes-client.io/v1', 395 | kind: 'ExternalSecret', 396 | metadata: { 397 | name: `e2e-secretmanager-permitted-key-${uuid}` 398 | }, 399 | spec: { 400 | backendType: 'secretsManager', 401 | data: [ 402 | { 403 | key: `e2e/${uuid}/another_credentials`, 404 | property: 'password', 405 | name: 'password' 406 | }, 407 | { 408 | key: `e2e/${uuid}/another_credentials`, 409 | property: 'username', 410 | name: 'username' 411 | } 412 | ] 413 | } 414 | } 415 | }) 416 | 417 | expect(result).to.not.equal(undefined) 418 | expect(result.statusCode).to.equal(201) 419 | 420 | const secret = await waitForSecret('default', `e2e-secretmanager-permitted-key-${uuid}`) 421 | expect(secret).to.equal(undefined) 422 | 423 | result = await kubeClient 424 | .apis[customResourceManifest.spec.group] 425 | .v1.namespaces('default') 426 | .externalsecrets(`e2e-secretmanager-permitted-key-${uuid}`) 427 | .get() 428 | expect(result).to.not.equal(undefined) 429 | expect(result.body.status.status).to.contain(`key name e2e/${uuid}/another_credentials does not match naming convention /permitted/path/.*`) 430 | }) 431 | }) 432 | }) 433 | }) 434 | -------------------------------------------------------------------------------- /e2e/tests/setup.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const { 5 | kubeClient 6 | } = require('../../config') 7 | 8 | before(async () => { 9 | await kubeClient.loadSpec() 10 | }) 11 | -------------------------------------------------------------------------------- /e2e/tests/ssm.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const util = require('util') 5 | const { expect } = require('chai') 6 | 7 | const { 8 | kubeClient, 9 | customResourceManifest, 10 | awsConfig 11 | } = require('../../config') 12 | const { waitForSecret, uuid } = require('./framework.js') 13 | 14 | const ssm = awsConfig.systemManagerFactory() 15 | const putParameter = util.promisify(ssm.putParameter).bind(ssm) 16 | 17 | describe('ssm', async () => { 18 | it('should pull existing secret from ssm and create a secret from it', async () => { 19 | let result = await putParameter({ 20 | Name: `/e2e/${uuid}/name`, 21 | Type: 'String', 22 | Value: 'foo' 23 | }).catch(err => { 24 | expect(err).to.equal(null) 25 | }) 26 | 27 | result = await kubeClient 28 | .apis[customResourceManifest.spec.group] 29 | .v1.namespaces('default')[customResourceManifest.spec.names.plural] 30 | .post({ 31 | body: { 32 | apiVersion: 'kubernetes-client.io/v1', 33 | kind: 'ExternalSecret', 34 | metadata: { 35 | name: `e2e-ssm-${uuid}` 36 | }, 37 | spec: { 38 | backendType: 'systemManager', 39 | data: [ 40 | { 41 | key: `/e2e/${uuid}/name`, 42 | name: 'name' 43 | } 44 | ] 45 | } 46 | } 47 | }) 48 | 49 | expect(result).to.not.equal(undefined) 50 | expect(result.statusCode).to.equal(201) 51 | 52 | const secret = await waitForSecret('default', `e2e-ssm-${uuid}`) 53 | expect(secret.body.data.name).to.equal('Zm9v') 54 | }) 55 | 56 | it('should pull existing secrets from ssm path and create a secret from it', async () => { 57 | const name1 = await putParameter({ 58 | Name: `/e2e/${uuid}-names/name1`, 59 | Type: 'String', 60 | Value: 'foo' 61 | }).catch(err => { 62 | expect(err).to.equal(null) 63 | }) 64 | 65 | const name2 = await putParameter({ 66 | Name: `/e2e/${uuid}-names/name2`, 67 | Type: 'String', 68 | Value: 'bar' 69 | }).catch(err => { 70 | expect(err).to.equal(null) 71 | }) 72 | 73 | const result = await kubeClient 74 | .apis[customResourceManifest.spec.group] 75 | .v1.namespaces('default')[customResourceManifest.spec.names.plural] 76 | .post({ 77 | body: { 78 | apiVersion: 'kubernetes-client.io/v1', 79 | kind: 'ExternalSecret', 80 | metadata: { 81 | name: `e2e-ssm-${uuid}-names` 82 | }, 83 | spec: { 84 | backendType: 'systemManager', 85 | data: [ 86 | { 87 | path: `/e2e/${uuid}-names` 88 | } 89 | ] 90 | } 91 | } 92 | }) 93 | 94 | expect(name1).to.not.equal(undefined) 95 | expect(name2).to.not.equal(undefined) 96 | expect(result).to.not.equal(undefined) 97 | expect(result.statusCode).to.equal(201) 98 | 99 | const secret = await waitForSecret('default', `e2e-ssm-${uuid}-names`) 100 | expect(secret.body.data.name1).to.equal('Zm9v') // Expect base64 foo 101 | expect(secret.body.data.name2).to.equal('YmFy') // Expect base64 bar 102 | }) 103 | 104 | it('should pull existing secret from ssm in a different region', async () => { 105 | const ssmEU = awsConfig.systemManagerFactory({ 106 | region: 'eu-west-1' 107 | }) 108 | const putParameter = util.promisify(ssmEU.putParameter).bind(ssmEU) 109 | 110 | let result = await putParameter({ 111 | Name: `/e2e/${uuid}/x-region`, 112 | Type: 'String', 113 | Value: 'foo' 114 | }).catch(err => { 115 | expect(err).to.equal(null) 116 | }) 117 | 118 | result = await kubeClient 119 | .apis[customResourceManifest.spec.group] 120 | .v1.namespaces('default')[customResourceManifest.spec.names.plural] 121 | .post({ 122 | body: { 123 | apiVersion: 'kubernetes-client.io/v1', 124 | kind: 'ExternalSecret', 125 | metadata: { 126 | name: `e2e-ssm-xregion-${uuid}` 127 | }, 128 | spec: { 129 | backendType: 'systemManager', 130 | region: 'eu-west-1', 131 | data: [ 132 | { 133 | key: `/e2e/${uuid}/x-region`, 134 | name: 'name' 135 | } 136 | ] 137 | } 138 | } 139 | }) 140 | 141 | expect(result).to.not.equal(undefined) 142 | expect(result.statusCode).to.equal(201) 143 | 144 | const secret = await waitForSecret('default', `e2e-ssm-xregion-${uuid}`) 145 | expect(secret.body.data.name).to.equal('Zm9v') 146 | }) 147 | 148 | describe('permitted annotation', async () => { 149 | beforeEach(async () => { 150 | await kubeClient.api.v1.namespaces('default').patch({ 151 | body: { 152 | metadata: { 153 | annotations: { 154 | 'iam.amazonaws.com/permitted': '^(foo|bar)' 155 | } 156 | } 157 | } 158 | }) 159 | }) 160 | 161 | afterEach(async () => { 162 | await kubeClient.api.v1.namespaces('default').patch({ 163 | body: { 164 | metadata: { 165 | annotations: { 166 | 'iam.amazonaws.com/permitted': '.*' 167 | } 168 | } 169 | } 170 | }) 171 | }) 172 | 173 | it('should not pull from ssm', async () => { 174 | let result = await putParameter({ 175 | Name: `/e2e/permitted/${uuid}`, 176 | Type: 'String', 177 | Value: 'foo' 178 | }).catch(err => { 179 | expect(err).to.equal(null) 180 | }) 181 | 182 | result = await kubeClient 183 | .apis[customResourceManifest.spec.group] 184 | .v1.namespaces('default')[customResourceManifest.spec.names.plural] 185 | .post({ 186 | body: { 187 | apiVersion: 'kubernetes-client.io/v1', 188 | kind: 'ExternalSecret', 189 | metadata: { 190 | name: `e2e-ssm-permitted-${uuid}` 191 | }, 192 | spec: { 193 | backendType: 'systemManager', 194 | roleArn: 'let-me-be-root', 195 | data: [ 196 | { 197 | key: `/e2e/permitted/${uuid}`, 198 | name: 'name' 199 | } 200 | ] 201 | } 202 | } 203 | }) 204 | 205 | expect(result).to.not.equal(undefined) 206 | expect(result.statusCode).to.equal(201) 207 | 208 | const secret = await waitForSecret('default', `e2e-ssm-permitted-${uuid}`) 209 | expect(secret).to.equal(undefined) 210 | 211 | result = await kubeClient 212 | .apis[customResourceManifest.spec.group] 213 | .v1.namespaces('default') 214 | .externalsecrets(`e2e-ssm-permitted-${uuid}`) 215 | .get() 216 | expect(result).to.not.equal(undefined) 217 | expect(result.body.status.status).to.contain('namespace does not allow to assume role let-me-be-root') 218 | }) 219 | }) 220 | }) 221 | -------------------------------------------------------------------------------- /examples/akeyless-example.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 'kubernetes-client.io/v1' 2 | kind: ExternalSecret 3 | metadata: 4 | name: hello-secret 5 | spec: 6 | backendType: akeyless 7 | data: 8 | - key: secret-name 9 | name: creds 10 | 11 | --- 12 | 13 | apiVersion: 'kubernetes-client.io/v1' 14 | kind: ExternalSecret 15 | metadata: 16 | name: hello-dynamic-secret 17 | spec: 18 | backendType: akeyless 19 | data: 20 | - key: dynamic-secret-name 21 | name: creds 22 | -------------------------------------------------------------------------------- /examples/alicloud-secretsmanager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kubernetes-client.io/v1 2 | kind: ExternalSecret 3 | metadata: 4 | name: alicloud-secretsmanager 5 | spec: 6 | backendType: alicloudSecretsManager 7 | # optional: specify role to assume using provided access key ID and access key secret when retrieving the data 8 | roleArn: acs:ram::{UID}:role/demo 9 | data: 10 | - key: hello-credentials1 11 | name: password 12 | - key: hello-credentials2 13 | name: username 14 | # Version Stage in Alibaba Cloud KMS Secrets Manager. Optional, default value is ACSCurrent 15 | versionStage: ACSCurrent 16 | -------------------------------------------------------------------------------- /examples/aws-secretsmanager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kubernetes-client.io/v1 2 | kind: ExternalSecret 3 | metadata: 4 | name: aws-secretsmanager 5 | spec: 6 | backendType: secretsManager 7 | # optional: specify role to assume when retrieving the data 8 | roleArn: arn:aws:iam::123412341234:role/let-other-account-access-secrets 9 | # optional: specify region of the secret 10 | region: eu-west-1 11 | data: 12 | - key: demo-service/credentials 13 | name: password 14 | property: password 15 | - key: demo-service/credentials 16 | name: username 17 | property: username 18 | -------------------------------------------------------------------------------- /examples/aws-ssm-path.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kubernetes-client.io/v1 2 | kind: ExternalSecret 3 | metadata: 4 | name: aws-ssm-path 5 | spec: 6 | backendType: systemManager 7 | # optional: specify role to assume when retrieving the data 8 | roleArn: arn:aws:iam::123456789012:role/test-role 9 | # optional: specify region 10 | region: us-east-1 11 | data: 12 | - key: /foo/name 13 | name: fooName 14 | - path: /extra-people/ 15 | recursive: false 16 | -------------------------------------------------------------------------------- /examples/aws-ssm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kubernetes-client.io/v1 2 | kind: ExternalSecret 3 | metadata: 4 | name: aws-ssm 5 | spec: 6 | backendType: systemManager 7 | # optional: specify role to assume when retrieving the data 8 | roleArn: arn:aws:iam::123456789012:role/test-role 9 | # optional: specify region 10 | region: us-west-2 11 | data: 12 | # Can either be key+name or all keys from a given path or even both 13 | # Order below is important. Values are fetched from SSM in the same order you put them here (top to bottom) 14 | # This means that if a given key is found duplicate, the last value found has precedence 15 | - key: /foo/name 16 | name: variable-name 17 | - path: /bar/ 18 | # optional: choose whether to scrape all child paths or not. Default is false 19 | recursive: false 20 | -------------------------------------------------------------------------------- /examples/azure-keyvault.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kubernetes-client.io/v1 2 | kind: ExternalSecret 3 | metadata: 4 | name: azure-keyvault 5 | spec: 6 | backendType: azureKeyVault 7 | keyVaultName: hello-world 8 | data: 9 | - key: hello-service/credentials 10 | name: password 11 | property: value 12 | -------------------------------------------------------------------------------- /examples/data-from-example.yml: -------------------------------------------------------------------------------- 1 | apiVersion: kubernetes-client.io/v1 2 | kind: ExternalSecret 3 | metadata: 4 | name: data-from-example 5 | spec: 6 | backendType: systemManager 7 | dataFrom: 8 | - /foo/name1 9 | -------------------------------------------------------------------------------- /examples/dockerconfig-example.yml: -------------------------------------------------------------------------------- 1 | apiVersion: kubernetes-client.io/v1 2 | kind: ExternalSecret 3 | metadata: 4 | name: dockerconfig-example 5 | spec: 6 | backendType: secretsManager 7 | template: 8 | type: kubernetes.io/dockerconfigjson 9 | data: 10 | - key: /development/dockerhub 11 | name: .dockerconfigjson 12 | -------------------------------------------------------------------------------- /examples/gcp-secrets-manager.yml: -------------------------------------------------------------------------------- 1 | apiVersion: kubernetes-client.io/v1 2 | kind: ExternalSecret 3 | metadata: 4 | name: gcp-secrets-manager 5 | spec: 6 | backendType: gcpSecretsManager 7 | # Project to use for GCP Secrets Manager (use the service account project by default) 8 | projectId: hello-service-project-id 9 | data: 10 | # Key in GCP Secrets Manager (without projet and version) 11 | - key: hello-service-password 12 | # Key to use in Kubernetes secret (not the secret name, who is determined by metadata.name) 13 | name: password 14 | # If the secret is a valid JSON, try to get this property 15 | property: value 16 | # Version of the secret (default: 'latest') 17 | version: 1 18 | # If the secret is already encoded in base64, then sends it unchanged (default: false) 19 | isBinary: false 20 | -------------------------------------------------------------------------------- /examples/ibmcloud-secrets-manager.yaml: -------------------------------------------------------------------------------- 1 | # Secret Manager secret type: username_password, arbitrary, kev, or iam_credentials 2 | apiVersion: kubernetes-client.io/v1 3 | kind: ExternalSecret 4 | metadata: 5 | name: ibmcloud-secrets-manager 6 | spec: 7 | backendType: ibmcloudSecretsManager 8 | # optional: true to key secrets by name instead of by ID 9 | keyByName: true 10 | data: 11 | # get username and password as json of secret type: username_password 12 | - key: my-creds 13 | name: username_password 14 | secretType: username_password 15 | # get just the password of secret type: username_password 16 | - key: my-creds 17 | name: username 18 | property: password 19 | secretType: username_password 20 | # get just the value of a specific key of secret type: keyvalue 21 | - key: my-creds 22 | name: key 23 | property: payload.key 24 | secretType: kv 25 | # get value of secret type: arbitrary 26 | - key: my-creds 27 | name: test 28 | property: payload 29 | secretType: arbitrary 30 | --- 31 | # Secret Manager secret type: imported_cert 32 | apiVersion: kubernetes-client.io/v1 33 | kind: ExternalSecret 34 | metadata: 35 | name: ibmcloud-secrets-manager 36 | spec: 37 | backendType: ibmcloudSecretsManager 38 | template: 39 | type: kubernetes.io/tls 40 | # optional: true to key secrets by name instead of by ID 41 | keyByName: true 42 | data: 43 | # get certificate secret type: imported_cert 44 | - key: my-creds 45 | name: tls.crt 46 | property: certificate 47 | secretType: imported_cert 48 | # get private key from secret type: imported_cert 49 | - key: my-creds 50 | name: tls.key 51 | property: private_key 52 | secretType: imported_cert 53 | -------------------------------------------------------------------------------- /examples/template-advanced.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 'kubernetes-client.io/v1' 2 | kind: ExternalSecret 3 | metadata: 4 | name: template-advanced 5 | spec: 6 | backendType: vault 7 | vaultMountPoint: my-kubernetes-vault-mount-point 8 | vaultRole: my-vault-role 9 | kvVersion: 2 10 | data: 11 | - key: kv/data/test/secret1 12 | name: s1 13 | - key: kv/data/test/secret2 14 | name: s2 15 | template: 16 | metadata: 17 | labels: 18 | world: <% let content = JSON.parse(data.s1) %><%= content.f2.f22 %> 19 | stringData: 20 | file.yaml: | 21 | <%= yaml.dump(JSON.parse(data.s1)) %> 22 | <% let s2 = JSON.parse(data.s2) %><% s2.arr.forEach((e, i) => { %>arr_<%= i %>: <%= e %> 23 | <% }) %> 24 | -------------------------------------------------------------------------------- /examples/template-metadata.yml: -------------------------------------------------------------------------------- 1 | apiVersion: kubernetes-client.io/v1 2 | kind: ExternalSecret 3 | metadata: 4 | name: template-metadata 5 | spec: 6 | template: 7 | metadata: 8 | annotations: 9 | external-secret: 'Yes please!' 10 | backendType: secretsManager 11 | data: 12 | - key: hello-service/password 13 | name: password 14 | dataFrom: 15 | - hello-service/secret-envs 16 | -------------------------------------------------------------------------------- /examples/tls-example.yml: -------------------------------------------------------------------------------- 1 | apiVersion: kubernetes-client.io/v1 2 | kind: ExternalSecret 3 | metadata: 4 | name: tls-example 5 | spec: 6 | backendType: secretsManager 7 | template: 8 | type: kubernetes.io/tls 9 | data: 10 | - key: /development/certificate 11 | property: crt 12 | name: tls.crt 13 | - key: /development/certificate 14 | property: key 15 | name: tls.key 16 | -------------------------------------------------------------------------------- /examples/vault-kv1.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 'kubernetes-client.io/v1' 2 | kind: ExternalSecret 3 | metadata: 4 | name: vault-kv1 5 | spec: 6 | backendType: vault 7 | vaultMountPoint: my-kubernetes-vault-mount-point 8 | vaultRole: my-vault-role 9 | kvVersion: 1 10 | data: 11 | - name: username 12 | key: kv/database 13 | property: db-username 14 | - name: password 15 | key: kv/database 16 | property: db-password 17 | -------------------------------------------------------------------------------- /examples/vault.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 'kubernetes-client.io/v1' 2 | kind: ExternalSecret 3 | metadata: 4 | name: vault 5 | spec: 6 | backendType: vault 7 | vaultMountPoint: my-kubernetes-vault-mount-point 8 | vaultRole: my-vault-role 9 | kvVersion: 2 # defaults to 2 10 | data: 11 | - name: password 12 | key: secret/data/hello-service/password 13 | property: password 14 | - name: cert.p12 15 | key: secret/data/hello-service/certificates 16 | property: cert.p12 17 | isBinary: true # defaults to false 18 | -------------------------------------------------------------------------------- /lib/backends/abstract-backend.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** Abstract backend class. */ 4 | class AbstractBackend { 5 | /** 6 | * Fetch Kubernetes secret manifest data. 7 | */ 8 | getSecretManifestData () { 9 | throw new Error('getSecretManifestData not implemented') 10 | } 11 | } 12 | 13 | module.exports = AbstractBackend 14 | -------------------------------------------------------------------------------- /lib/backends/abstract-backend.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const { expect } = require('chai') 5 | 6 | const AbstractBackend = require('./abstract-backend') 7 | 8 | describe('AbstractBackend', () => { 9 | let abstractBackend 10 | 11 | beforeEach(() => { 12 | abstractBackend = new AbstractBackend() 13 | }) 14 | 15 | describe('getSecretManifestData', () => { 16 | it('throws an error', () => { 17 | let error 18 | 19 | try { 20 | abstractBackend.getSecretManifestData() 21 | } catch (err) { 22 | error = err 23 | } 24 | 25 | expect(error).to.not.equal(undefined) 26 | expect(error.message).equals('getSecretManifestData not implemented') 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /lib/backends/akeyless-backend.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const akeyless = require('akeyless') 3 | const akeylessCloud = require('akeyless-cloud-id') 4 | const KVBackend = require('./kv-backend') 5 | 6 | /** Akeyless Secrets Manager backend class. */ 7 | class AkeylessBackend extends KVBackend { 8 | /** 9 | * Create Akeyless backend. 10 | * @param {Object} credential - Credentials for authenticating with Akeyless Vault. 11 | * @param {Object} logger - Logger for logging stuff. 12 | */ 13 | constructor ({ credential, logger }) { 14 | super({ logger }) 15 | this._credential = credential 16 | } 17 | 18 | _getCloudId () { 19 | return new Promise((resolve, reject) => { 20 | akeylessCloud.getCloudId(this._credential.accessType, this._credential.accessTypeParam, (err, res) => { 21 | if (err) { 22 | reject(err) 23 | } else { 24 | resolve(res) 25 | } 26 | }) 27 | }) 28 | } 29 | 30 | async _getSecret (key) { 31 | const api = this._credential.client 32 | const cloudId = await this._getCloudId() 33 | const opts = { 'access-id': this._credential.accessId, 'access-type': this._credential.accessType, 'access-key': this._credential.accessTypeParam, 'cloud-id': cloudId } 34 | 35 | const authResult = await api.auth(akeyless.Auth.constructFromObject(opts)) 36 | const token = authResult.token 37 | 38 | const dataType = await api.describeItem(akeyless.DescribeItem.constructFromObject({ 39 | name: key, 40 | token: token 41 | })) 42 | if (dataType.item_type === 'DYNAMIC_SECRET') { 43 | const data = await api.getDynamicSecretValue(akeyless.GetDynamicSecretValue.constructFromObject({ 44 | name: key, 45 | token: token 46 | })) 47 | return JSON.stringify(data) 48 | } 49 | if (dataType.item_type === 'STATIC_SECRET') { 50 | const staticSecretParams = akeyless.GetSecretValue.constructFromObject({ 51 | names: [key], 52 | token: token 53 | }) 54 | const data = await api.getSecretValue(staticSecretParams) 55 | const secretValue = JSON.stringify(data[key]) 56 | return JSON.parse(secretValue) 57 | } else { 58 | throw new Error('Invalid secret type' + dataType.item_type) 59 | } 60 | } 61 | 62 | /** 63 | * Get secret value from Akeyless Vault. 64 | * @param {string} key - Key the full name (path/name) of the stored secret at Akeyless. 65 | * @returns {Promise} Promise object representing secret property value. 66 | */ 67 | async _get ({ key }) { 68 | this._logger.info(`fetching secret ${key} from akeyless`) 69 | const secret = await this._getSecret(key) 70 | return secret 71 | } 72 | } 73 | 74 | module.exports = AkeylessBackend 75 | -------------------------------------------------------------------------------- /lib/backends/akeyless-backend.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const { expect } = require('chai') 5 | const sinon = require('sinon') 6 | 7 | const AkeylessBackend = require('./akeyless-backend') 8 | 9 | describe('AkeylessBackend', () => { 10 | let loggerMock 11 | let clientMock 12 | let akeylessBackend 13 | 14 | const secret = 'fakeSecretValue' 15 | const key = 'secret_name' 16 | 17 | beforeEach(() => { 18 | loggerMock = sinon.mock() 19 | loggerMock.info = sinon.stub() 20 | clientMock = sinon.mock() 21 | clientMock.getSecretValue = sinon.stub().returns({ [key]: secret }) 22 | clientMock.getDynamicSecretValue = sinon.stub().returns(secret) 23 | clientMock.auth = sinon.stub().returns('token') 24 | clientMock.describeItem = sinon.stub().returns({ item_type: 'STATIC_SECRET' }) 25 | 26 | akeylessBackend = new AkeylessBackend({ 27 | credential: { endpoint: 'https//sampleendpoint', accessType: 'access_key', client: clientMock }, 28 | logger: loggerMock 29 | }) 30 | }) 31 | 32 | describe('_get', () => { 33 | it('returns secret property value', async () => { 34 | const specOptions = {} 35 | const keyOptions = {} 36 | const secretPropertyValue = await akeylessBackend._get({ 37 | key: key, 38 | specOptions, 39 | keyOptions 40 | }) 41 | expect(secretPropertyValue).equals(secret) 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /lib/backends/alicloud-secrets-manager-backend.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const KVBackend = require('./kv-backend') 4 | const { default: Client, GetSecretValueRequest } = require('@alicloud/kms20160120') 5 | /** Secrets Manager backend class. */ 6 | class AliCloudSecretsManagerBackend extends KVBackend { 7 | /** 8 | * Create Secrets manager backend. 9 | * @param {Object} logger - Logger for logging stuff. 10 | * @param {Object} credential - Secrets manager credential. 11 | */ 12 | constructor ({ logger, credential }) { 13 | super({ logger }) 14 | this._credential = credential 15 | } 16 | 17 | _getClient ({ specOptions: { roleArn } }) { 18 | const config = { 19 | endpoint: this._credential.endpoint, 20 | accessKeyId: this._credential.accessKeyId, 21 | accessKeySecret: this._credential.accessKeySecret, 22 | type: this._credential.type 23 | } 24 | if (roleArn) { 25 | config.type = 'ram_role_arn' 26 | config.roleArn = roleArn 27 | } 28 | return new Client(config) 29 | } 30 | 31 | /** 32 | * Get secret property value from Alibaba Cloud KMS Secrets Manager. 33 | * @param {string} key - Key used to store secret property value in Alibaba Cloud KMS Secrets Manager. 34 | * @returns {Promise} Promise object representing secret property value. 35 | */ 36 | 37 | async _get ({ key, specOptions: { roleArn }, keyOptions: { versionStage } }) { 38 | this._logger.info(`fetching secret ${key} on version stage ${versionStage} from AliCloud Secret Manager using role ${roleArn}`) 39 | const getSecretValueRequest = new GetSecretValueRequest({ 40 | secretName: key, 41 | versionStage: versionStage 42 | }) 43 | 44 | const client = this._getClient({ specOptions: { roleArn } }) 45 | const value = await client.getSecretValue(getSecretValueRequest) 46 | 47 | return value.secretData.toString('utf-8') 48 | } 49 | } 50 | 51 | module.exports = AliCloudSecretsManagerBackend 52 | -------------------------------------------------------------------------------- /lib/backends/alicloud-secrets-manager-backend.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const { expect } = require('chai') 5 | const sinon = require('sinon') 6 | 7 | const AliCloudSecretsManagerBackend = require('./alicloud-secrets-manager-backend') 8 | 9 | describe('AliCloudSecretsManagerBackend', () => { 10 | let loggerMock 11 | let clientMock 12 | let aliCloudSecretsManagerBackend 13 | 14 | const password = 'fakeSecretPropertyValue' 15 | const secret = { 16 | secretData: password 17 | } 18 | const key = 'password' 19 | 20 | beforeEach(() => { 21 | loggerMock = sinon.mock() 22 | loggerMock.info = sinon.stub() 23 | clientMock = sinon.mock() 24 | clientMock.getSecretValue = sinon.stub().returns(secret) 25 | 26 | aliCloudSecretsManagerBackend = new AliCloudSecretsManagerBackend({ 27 | credential: null, 28 | logger: loggerMock 29 | }) 30 | aliCloudSecretsManagerBackend._getClient = sinon.stub().returns(clientMock) 31 | }) 32 | 33 | describe('_get', () => { 34 | it('returns secret property value', async () => { 35 | const specOptions = {} 36 | const keyOptions = {} 37 | const secretPropertyValue = await aliCloudSecretsManagerBackend._get({ 38 | key: key, 39 | specOptions, 40 | keyOptions 41 | }) 42 | expect(secretPropertyValue).equals(password) 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /lib/backends/azure-keyvault-backend.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { SecretClient } = require('@azure/keyvault-secrets') 4 | 5 | const KVBackend = require('./kv-backend') 6 | 7 | /** Secrets Manager backend class. */ 8 | class AzureKeyVaultBackend extends KVBackend { 9 | /** 10 | * Create Key Vault backend. 11 | * @param {Object} credential - Credentials for authenticating with Azure Key Vault. 12 | * @param {Object} logger - Logger for logging stuff. 13 | */ 14 | constructor ({ credential, logger }) { 15 | super({ logger }) 16 | this._credential = credential 17 | this._endpointSuffix = process.env.AZURE_KEY_VAULT_DNS_SUFFIX || 'vault.azure.net' 18 | } 19 | 20 | _keyvaultClient ({ keyVaultName }) { 21 | const url = `https://${keyVaultName}.${this._endpointSuffix}` 22 | const client = new SecretClient(url, this._credential) 23 | return client 24 | } 25 | 26 | /** 27 | * Get secret property value from Azure Key Vault. 28 | * @param {string} key - Key used to store secret property value in Azure Key Vault. 29 | * @param {string} specOptions.keyVaultName - Name of the azure key vault 30 | * @returns {Promise} Promise object representing secret property value. 31 | */ 32 | 33 | async _get ({ key, specOptions: { keyVaultName } }) { 34 | const client = this._keyvaultClient({ keyVaultName }) 35 | this._logger.info(`fetching secret ${key} from Azure KeyVault ${keyVaultName}`) 36 | const secret = await client.getSecret(key) 37 | return secret.value 38 | } 39 | } 40 | 41 | module.exports = AzureKeyVaultBackend 42 | -------------------------------------------------------------------------------- /lib/backends/azure-keyvault-backend.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const { expect } = require('chai') 5 | const sinon = require('sinon') 6 | 7 | const AzureKeyVaultBackend = require('./azure-keyvault-backend') 8 | 9 | describe('AzureKeyVaultBackend', () => { 10 | let credentialMock 11 | let loggerMock 12 | let credentialFactoryMock 13 | let clientMock 14 | let azureKeyVaultBackend 15 | const secret = 'fakeSecretPropertyValue' 16 | const key = 'password' 17 | const keyVaultName = 'vault_name' 18 | const quotedSecretValueAsBase64 = Buffer.from(secret).toString('base64') 19 | 20 | const azureSecret = { 21 | properties: {}, 22 | value: secret, 23 | name: key 24 | } 25 | 26 | beforeEach(() => { 27 | credentialMock = sinon.mock() 28 | loggerMock = sinon.mock() 29 | credentialFactoryMock = sinon.fake.returns(credentialMock) 30 | clientMock = sinon.mock() 31 | clientMock.getSecret = sinon.stub().returns(azureSecret) 32 | loggerMock.info = sinon.stub() 33 | 34 | azureKeyVaultBackend = new AzureKeyVaultBackend({ 35 | credential: credentialFactoryMock, 36 | logger: loggerMock 37 | }) 38 | azureKeyVaultBackend._keyvaultClient = sinon.stub().returns(clientMock) 39 | }) 40 | 41 | describe('_get', () => { 42 | it('returns secret property value', async () => { 43 | const secretPropertyValue = await azureKeyVaultBackend._get({ 44 | key: key, 45 | specOptions: { 46 | keyVaultName: keyVaultName 47 | } 48 | }) 49 | expect(secretPropertyValue).equals(secret) 50 | }) 51 | }) 52 | 53 | describe('getSecretManifestData', () => { 54 | it('returns secret property value', async () => { 55 | const returnedData = await azureKeyVaultBackend.getSecretManifestData({ 56 | spec: { 57 | backendType: 'vault', 58 | keyVaultName: keyVaultName, 59 | data: [{ 60 | key: key, 61 | name: 'name-in-k8s' 62 | }] 63 | } 64 | }) 65 | 66 | // First, we get the client... 67 | sinon.assert.calledWith(azureKeyVaultBackend._keyvaultClient, { keyVaultName }) 68 | 69 | // ... then we fetch the secret ... 70 | sinon.assert.calledWith(clientMock.getSecret, key) 71 | 72 | // ... and expect to get the full proper value 73 | expect(returnedData['name-in-k8s']).equals(quotedSecretValueAsBase64) 74 | }) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /lib/backends/gcp-secrets-manager-backend.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const KVBackend = require('./kv-backend') 4 | 5 | /** GCP Secrets Manager backend class. */ 6 | class GCPSecretsManagerBackend extends KVBackend { 7 | /** 8 | * Create Secrets manager backend. 9 | * @param {Object} logger - Logger for logging stuff. 10 | * @param {Object} client - Secrets manager client. 11 | */ 12 | constructor ({ logger, client }) { 13 | super({ logger }) 14 | this._client = client 15 | } 16 | 17 | /** 18 | * Gets the project id from auth object from the GCP Secret Manager Client 19 | */ 20 | _getProjectId () { 21 | return this._client.auth.getProjectId() 22 | } 23 | 24 | /** 25 | * Get secret property value from GCP Secrets Manager. 26 | * @param {string} key - Key used to store secret property value in GCP Secrets Manager. 27 | * @param {string} specOptions.projectId - Id of the gcp project, if not passed, this will be fetched from the client auth 28 | * @param {string} keyOptions.version - If version is passed then fetch that version, else fetch the latest version 29 | * @returns {Promise} Promise object representing secret property value. 30 | */ 31 | async _get ({ key, keyOptions, specOptions: { projectId } }) { 32 | if (!projectId) { 33 | // get the project id from client 34 | projectId = await this._getProjectId() 35 | } 36 | 37 | let secretVersion = 'latest' 38 | if (keyOptions && keyOptions.version) { 39 | secretVersion = keyOptions.version 40 | } 41 | 42 | this._logger.info(`fetching secret ${key} from GCP Secret for project ${projectId} with version ${secretVersion}`) 43 | 44 | const [version] = await this._client.accessSecretVersion({ 45 | name: 'projects/' + projectId + '/secrets/' + key + '/versions/' + secretVersion 46 | }) 47 | return version.payload.data.toString('utf8') 48 | } 49 | } 50 | 51 | module.exports = GCPSecretsManagerBackend 52 | -------------------------------------------------------------------------------- /lib/backends/gcp-secrets-manager-backend.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const { expect } = require('chai') 5 | const sinon = require('sinon') 6 | 7 | const GCPSecretsManagerBackend = require('./gcp-secrets-manager-backend') 8 | 9 | describe('GCPSecretsManagerBackend', () => { 10 | let loggerMock 11 | let clientMock 12 | let gcpSecretsManagerBackend 13 | const key = 'password' 14 | const version = [{ name: 'projects/111122223333/secrets/password/versions/1', payload: { data: Buffer.from('test', 'utf8') } }, null, null] 15 | const secret = 'test' 16 | 17 | beforeEach(() => { 18 | loggerMock = sinon.mock() 19 | loggerMock.info = sinon.stub() 20 | clientMock = sinon.mock() 21 | clientMock.accessSecretVersion = sinon.stub().returns(version) 22 | 23 | gcpSecretsManagerBackend = new GCPSecretsManagerBackend({ 24 | logger: loggerMock, 25 | client: clientMock 26 | }) 27 | 28 | gcpSecretsManagerBackend._getProjectId = sinon.stub().returns('111122223333') 29 | }) 30 | 31 | describe('_get', () => { 32 | it('returns secret property value', async () => { 33 | const secretPropertyValue = await gcpSecretsManagerBackend._get({ 34 | key: key, 35 | keyOptions: { version: 1 }, 36 | specOptions: { 37 | projectId: '111122223333' 38 | } 39 | }) 40 | expect(secretPropertyValue).equals(secret) 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /lib/backends/ibmcloud-secrets-manager-backend.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const SecretsManager = require('@ibm-cloud/secrets-manager/secrets-manager/v1') 4 | const { getAuthenticatorFromEnvironment, IamAuthenticator } = require('@ibm-cloud/secrets-manager/auth') 5 | 6 | const KVBackend = require('./kv-backend') 7 | 8 | /** Secrets Manager backend class. */ 9 | class IbmCloudSecretsManagerBackend extends KVBackend { 10 | /** 11 | * Create Key Vault backend. 12 | * @param {Object} credential - Credentials for authenticating with IBM Secrets Manager. 13 | * @param {Object} logger - Logger for logging stuff. 14 | */ 15 | constructor ({ credential, logger }) { 16 | super({ logger }) 17 | this._credential = credential 18 | } 19 | 20 | _secretsManagerClient () { 21 | let authenticator 22 | if (process.env.IBM_CLOUD_SECRETS_MANAGER_API_AUTH_TYPE) { 23 | authenticator = getAuthenticatorFromEnvironment('IBM_CLOUD_SECRETS_MANAGER_API') 24 | } else { 25 | authenticator = new IamAuthenticator({ 26 | apikey: this._credential.apikey 27 | }) 28 | } 29 | const client = new SecretsManager({ 30 | authenticator: authenticator, 31 | serviceUrl: this._credential.endpoint 32 | }) 33 | return client 34 | } 35 | 36 | /** 37 | * Get secret_data property value from IBM Cloud Secrets Manager 38 | * @param {string} key - Key used to store secret property value. 39 | * @param {object} specOptions.keyByName - Interpret key as secret names if true, as id otherwise 40 | * @param {string} keyOptions.secretType - Type of secret - one of username_password, iam_credentials or arbitrary 41 | * @returns {Promise} Promise object representing secret property value. 42 | */ 43 | async _get ({ key, specOptions: { keyByName }, keyOptions: { secretType } }) { 44 | const client = this._secretsManagerClient() 45 | let id = key 46 | keyByName = keyByName === true 47 | this._logger.info(`fetching ${secretType} secret ${id}${keyByName ? ' by name' : ''} from IBM Cloud Secrets Manager ${this._credential.endpoint}`) 48 | 49 | if (keyByName) { 50 | const secrets = await client.listAllSecrets({ search: key }) 51 | const filtered = secrets.result.resources.filter((s) => (s.name === key && s.secret_type === secretType)) 52 | if (filtered.length === 1) { 53 | id = filtered[0].id 54 | } else if (filtered.length === 0) { 55 | throw new Error(`No ${secretType} secret named ${key}`) 56 | } else { 57 | throw new Error(`Multiple ${secretType} secrets named ${key}`) 58 | } 59 | } 60 | 61 | const secret = await client.getSecret({ 62 | secretType: secretType, 63 | id 64 | }) 65 | if (secretType === 'iam_credentials') { 66 | return JSON.stringify(secret.result.resources[0].api_key) 67 | } else { 68 | return JSON.stringify(secret.result.resources[0].secret_data) 69 | } 70 | } 71 | } 72 | 73 | module.exports = IbmCloudSecretsManagerBackend 74 | -------------------------------------------------------------------------------- /lib/backends/ibmcloud-secrets-manager-backend.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const { expect } = require('chai') 5 | const sinon = require('sinon') 6 | 7 | const IbmCloudSecretsManagerBackend = require('./ibmcloud-secrets-manager-backend') 8 | 9 | // In the unit test suite, these tests mock calls to IBM Secrets Manager, but mocking can be disabled during development to validate actual operation. 10 | // To diable mocking and enable real calls to an instance of Secrets Manager: 11 | // 12 | // 1. Set the three credential environment variables: 13 | // SECRETS_MANAGER_API_AUTH_TYPE=iam 14 | // SECRETS_MANAGER_API_ENDPOINT=https://{instance-id}.{region}.secrets-manager.appdomain.cloud 15 | // SECRETS_MANAGER_API_APIKEY={API key with Read+ReadSecrets access to the instance} 16 | // 17 | // 2. Add the three secrets described in the data object below to Secrets Manager. 18 | // When you add the IAM secret, be sure that "Reuse IAM credentials until lease expires" is checked. 19 | // 20 | // 3. Set the following three environment variables to the IDs of those secrets: 21 | // IBM_CLOUD_SECRETS_MANAGER_TEST_CREDS_ID 22 | // IBM_CLOUD_SECRETS_MANAGER_TEST_SECRET_ID 23 | // IBM_CLOUD_SECRETS_MANAGER_TEST_IAM_ID 24 | // 25 | // 4. Set the following environment variable to the API key generated as part of the IAM credential: 26 | // IBM_CLOUD_SECRETS_MANAGER_TEST_IAM_APIKEY 27 | // 28 | // Note: In the Secrets Manager UI, you can select "Show snippet" from the secret's overflow menu to show a curl command that will retrieve the value. 29 | // Or you can use the "ibmcloud sm secret" CLI command to handle authentication for you. 30 | // 31 | // You can switch back to mocking simply by unsetting SECRETS_MANAGER_API_AUTH_TYPE. 32 | // This makes it easy to switch back and forth between the two modes when writing new tests. 33 | 34 | const endpoint = process.env.IBM_CLOUD_SECRETS_MANAGER_API_ENDPOINT || 'https://fake.secrets-manager.appdomain.cloud' 35 | 36 | const data = { 37 | creds: { 38 | id: process.env.IBM_CLOUD_SECRETS_MANAGER_TEST_CREDS_ID || 'id1', 39 | name: 'test-creds', 40 | secretType: 'username_password', 41 | username: 'johndoe', 42 | password: 'p@ssw0rd' 43 | }, 44 | secret: { 45 | id: process.env.IBM_CLOUD_SECRETS_MANAGER_TEST_SECRET_ID || 'id2', 46 | name: 'test-secret', 47 | secretType: 'arbitrary', 48 | payload: 's3cr3t' 49 | }, 50 | iam: { 51 | id: process.env.IBM_CLOUD_SECRETS_MANAGER_TEST_IAM_ID || 'id3', 52 | name: 'test-iam', 53 | secretType: 'iam_credentials', 54 | apiKey: process.env.IBM_CLOUD_SECRETS_MANAGER_TEST_IAM_APIKEY || 'key' 55 | } 56 | } 57 | 58 | describe('IbmCloudSecretsManagerBackend', () => { 59 | const mock = !process.env.IBM_CLOUD_SECRETS_MANAGER_API_AUTH_TYPE 60 | let loggerMock 61 | let ibmCloudSecretsManagerBackend 62 | 63 | beforeEach(() => { 64 | if (mock) { 65 | process.env.IBM_CLOUD_SECRETS_MANAGER_API_AUTH_TYPE = 'noauth' 66 | } 67 | 68 | loggerMock = { 69 | info: sinon.stub() 70 | } 71 | 72 | ibmCloudSecretsManagerBackend = new IbmCloudSecretsManagerBackend({ 73 | credential: { endpoint }, 74 | logger: loggerMock 75 | }) 76 | }) 77 | 78 | afterEach(() => { 79 | if (mock) { 80 | delete process.env.IBM_CLOUD_SECRETS_MANAGER_API_AUTH_TYPE 81 | ibmCloudSecretsManagerBackend._secretsManagerClient.restore() 82 | } 83 | }) 84 | 85 | function mockClient ({ list = [], get = {} }) { 86 | if (mock) { 87 | const client = { 88 | listAllSecrets: sinon.stub().resolves({ result: { resources: list } }), 89 | getSecret: sinon.stub().resolves({ result: { resources: [get] } }) 90 | } 91 | sinon.stub(ibmCloudSecretsManagerBackend, '_secretsManagerClient').returns(client) 92 | } 93 | } 94 | 95 | describe('_get', () => { 96 | describe('with default spec options', () => { 97 | it('returns a username_password secret', async () => { 98 | const { id, secretType, username, password } = data.creds 99 | mockClient({ get: { secret_data: { password, username } } }) 100 | 101 | const secretPropertyValue = await ibmCloudSecretsManagerBackend._get({ 102 | key: id, 103 | specOptions: {}, 104 | keyOptions: { secretType } 105 | }) 106 | expect(secretPropertyValue).equals('{"password":"p@ssw0rd","username":"johndoe"}') 107 | }) 108 | 109 | it('returns an arbitrary secret', async () => { 110 | const { id, secretType, payload } = data.secret 111 | mockClient({ get: { secret_data: { payload } } }) 112 | 113 | const secretPropertyValue = await ibmCloudSecretsManagerBackend._get({ 114 | key: id, 115 | specOptions: {}, 116 | keyOptions: { secretType } 117 | }) 118 | expect(secretPropertyValue).equals('{"payload":"s3cr3t"}') 119 | }) 120 | 121 | it('returns an API key from an iam_credentials secret', async () => { 122 | const { id, secretType, apiKey } = data.iam 123 | mockClient({ get: { api_key: apiKey } }) 124 | 125 | const secretPropertyValue = await ibmCloudSecretsManagerBackend._get({ 126 | key: id, 127 | specOptions: {}, 128 | keyOptions: { secretType } 129 | }) 130 | expect(secretPropertyValue).equals(`"${apiKey}"`) 131 | }) 132 | }) 133 | 134 | describe('with key by name enabled', () => { 135 | it('returns a secret that matches the given name and type', async () => { 136 | const { name, secretType, username, password } = data.creds 137 | const list = [ 138 | { name, secret_type: 'arbitrary' }, 139 | { name, secret_type: secretType }, 140 | { name: 'test-creds2', secret_type: secretType } 141 | ] 142 | mockClient({ list, get: { secret_data: { password, username } } }) 143 | 144 | const secretPropertyValue = await ibmCloudSecretsManagerBackend._get({ 145 | key: name, 146 | specOptions: { keyByName: true }, 147 | keyOptions: { secretType } 148 | }) 149 | expect(secretPropertyValue).equals('{"password":"p@ssw0rd","username":"johndoe"}') 150 | }) 151 | 152 | it('throws if there is no secret with the given name and type', async () => { 153 | mockClient({ list: [] }) 154 | 155 | try { 156 | await ibmCloudSecretsManagerBackend._get({ 157 | key: 'test-missing', 158 | specOptions: { keyByName: true }, 159 | keyOptions: { secretType: 'username_password' } 160 | }) 161 | } catch (error) { 162 | expect(error).to.have.property('message').that.includes('No username_password secret') 163 | return 164 | } 165 | expect.fail('expected to throw an error') 166 | }) 167 | 168 | // Defensive test: this condition does not appear to be possible currently with a real Secrets Manager instance. 169 | if (mock) { 170 | it('throws if there are multiple secrets with the given name and type', async () => { 171 | const { name, secretType, username, password } = data.creds 172 | const list = [ 173 | { name, secret_type: secretType }, 174 | { name, secret_type: secretType } 175 | ] 176 | mockClient({ list, get: { secret_data: { password, username } } }) 177 | 178 | try { 179 | await ibmCloudSecretsManagerBackend._get({ 180 | key: name, 181 | specOptions: { keyByName: true }, 182 | keyOptions: { secretType } 183 | }) 184 | } catch (error) { 185 | expect(error).to.have.property('message').that.includes('Multiple username_password secrets') 186 | return 187 | } 188 | expect.fail('expected to throw an error') 189 | }) 190 | } 191 | }) 192 | }) 193 | }) 194 | -------------------------------------------------------------------------------- /lib/backends/kv-backend.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const AbstractBackend = require('./abstract-backend') 4 | const { get, hasIn } = require('lodash') 5 | 6 | /** Key Value backend class. */ 7 | class KVBackend extends AbstractBackend { 8 | /** 9 | * Create a Key Value backend. 10 | * @param {Object} logger - Logger for logging stuff. 11 | */ 12 | constructor ({ logger }) { 13 | super() 14 | this._logger = logger 15 | } 16 | 17 | /** 18 | * Fetch Kubernetes secret property values. 19 | * @param {Object[]} data - Kubernetes secret properties. 20 | * @param {string} data[].key - Secret key in the backend. 21 | * @param {string} data[].name - Kubernetes Secret property name. 22 | * @param {string} data[].path - Kubernetes Secret path to fetch keys from. 23 | * @param {string} data[].property - If the backend secret is an 24 | * object, this is the property name of the value to use. 25 | * @param {string} data[].isBinary - If the backend secret shall be treated 26 | * as binary data represented by a base64-encoded string. Defaults to false. 27 | * @param {Object} specOptions - Options set on spec level. 28 | * @returns {Promise} Promise object representing secret property values. 29 | */ 30 | _fetchDataValues ({ data, specOptions }) { 31 | return Promise.all(data.map(async dataItem => { 32 | const { name, property = null, key, path, ...keyOptions } = dataItem 33 | 34 | let response = {} 35 | let plainOrObjValue 36 | 37 | // Supporting fetching by key or by path 38 | // If 'path' is not defined, we can assume 'key' will exist due to CRD validation 39 | let singleParameterKey = true 40 | if (path) { singleParameterKey = false } 41 | 42 | if (singleParameterKey) { 43 | // Single secret 44 | plainOrObjValue = await this._get({ key, keyOptions, specOptions }) 45 | } else { 46 | // All secrets inside the specified path 47 | plainOrObjValue = await this._getByPath({ path, keyOptions, specOptions }) 48 | } 49 | 50 | const shouldParseValue = 'property' in dataItem 51 | const isBinary = 'isBinary' in dataItem && dataItem.isBinary === true 52 | 53 | let value = plainOrObjValue 54 | if (shouldParseValue) { 55 | let parsedValue 56 | try { 57 | parsedValue = JSON.parse(value) 58 | } catch (err) { 59 | this._logger.warn(`Failed to JSON.parse value for '${key}',` + 60 | ' please verify that your secret value is correctly formatted as JSON.' + 61 | ` To use plain text secret remove the 'property: ${property}'`) 62 | return 63 | } 64 | 65 | if (!(hasIn(parsedValue, property))) { 66 | throw new Error(`Could not find property ${property} in ${key}`) 67 | } 68 | 69 | value = get(parsedValue, property) 70 | } 71 | 72 | if (isBinary) { 73 | // value in the backend is binary data which is already encoded in base64. 74 | if (typeof value === 'string') { 75 | // Skip this step if the value from the backend is not a string (e.g., AWS 76 | // SecretsManager will already return a `Buffer` with base64 encoding if the 77 | // secret contains `SecretBinary` instead of `SecretString`). 78 | value = Buffer.from(value, 'base64') 79 | } 80 | } 81 | 82 | if (singleParameterKey) { 83 | // Not path, return as is 84 | response = { [name]: value } 85 | } else { 86 | // Returning dict with path keys and values 87 | for (const records in value) { 88 | response[records] = value[records] 89 | } 90 | } 91 | 92 | return response 93 | })) 94 | } 95 | 96 | /** 97 | * Fetch Kubernetes secret property values. 98 | * @param {string[]} dataFrom - Array of secret keys in the backend 99 | * @param {string} specOptions - Options set on spec level that might be interesting for the backend 100 | * @returns {Promise} Promise object representing secret property values. 101 | */ 102 | _fetchDataFromValues ({ dataFrom, specOptions }) { 103 | return Promise.all(dataFrom.map(async key => { 104 | const value = await this._get({ key, specOptions, keyOptions: {} }) 105 | 106 | try { 107 | return JSON.parse(value) 108 | } catch (err) { 109 | this._logger.warn(`Failed to JSON.parse value for '${key}',` + 110 | ' please verify that your secret value is correctly formatted as JSON.') 111 | } 112 | })) 113 | } 114 | 115 | /** 116 | * Fetch Kubernetes secret property values with options. 117 | * @param {Object[]} dataFromWithOptions - Array of secret keys in the backend, including extra options 118 | * @param {string} specOptions - Options set on spec level that might be interesting for the backend 119 | * @returns {Promise} Promise object representing secret property values. 120 | */ 121 | _fetchDataFromValuesWithOptions ({ dataFromWithOptions, specOptions }) { 122 | return Promise.all(dataFromWithOptions.map(async dataItem => { 123 | const { key, ...keyOptions } = dataItem 124 | const value = await this._get({ key, specOptions, keyOptions }) 125 | 126 | try { 127 | return JSON.parse(value) 128 | } catch (err) { 129 | this._logger.warn(`Failed to JSON.parse value for '${dataItem}',` + 130 | ' please verify that your secret value is correctly formatted as JSON.') 131 | } 132 | })) 133 | } 134 | 135 | /** 136 | * Get a secret property value from Key Value backend. 137 | * @param {string} key - Secret key in the backend. 138 | * @param {string} keyOptions - Options for this specific key, eg version etc. 139 | * @param {string} specOptions - Options for this external secret, eg role 140 | * @returns {Promise} Promise object representing secret property values. 141 | */ 142 | _get ({ key, keyOptions, specOptions }) { 143 | throw new Error('_get not implemented') 144 | } 145 | 146 | /** 147 | * Get a secret property value from Key Value backend. 148 | * @param {string} path - Path from where to fetch secrets on the backend. 149 | * @param {string} keyOptions - Options for this specific key, eg version etc. 150 | * @param {string} specOptions - Options for this external secret, eg role 151 | * @returns {Promise} Promise object representing secret property values. 152 | */ 153 | _getByPath ({ path, keyOptions, specOptions }) { 154 | throw new Error('_getByPath not implemented') 155 | } 156 | 157 | /** 158 | * Convert secret value to buffer 159 | * @param {(string|Buffer|object)} plainValue - plain secret value 160 | * @returns {Buffer} Buffer containing value 161 | */ 162 | _toBuffer (plainValue) { 163 | if (plainValue instanceof Buffer) { 164 | return plainValue 165 | } 166 | 167 | if (typeof plainValue === 'object') { 168 | return Buffer.from(JSON.stringify(plainValue), 'utf-8') 169 | } 170 | 171 | return Buffer.from(`${plainValue}`, 'utf8') 172 | } 173 | 174 | /** 175 | * Fetch Kubernetes secret manifest data. 176 | * @param {ExternalSecretSpec} spec - Kubernetes ExternalSecret spec. 177 | * @returns {Promise} Promise object representing Kubernetes secret manifest data. 178 | */ 179 | async getSecretManifestData ({ 180 | spec: { 181 | // Use properties to be backwards compatible. 182 | properties = [], 183 | data = properties, 184 | dataFrom = [], 185 | dataFromWithOptions = [], 186 | ...specOptions 187 | } 188 | }) { 189 | const [dataFromValues, dataFromValuesWithOptions, dataValues] = await Promise.all([ 190 | this._fetchDataFromValues({ dataFrom, specOptions }), 191 | this._fetchDataFromValuesWithOptions({ dataFromWithOptions, specOptions }), 192 | this._fetchDataValues({ data, specOptions }) 193 | ]) 194 | 195 | const plainValues = dataFromValues.concat(dataFromValuesWithOptions).concat(dataValues) 196 | .reduce((acc, parsedValue) => ({ 197 | ...acc, 198 | ...parsedValue 199 | }), {}) 200 | 201 | const encodedEntries = Object.entries(plainValues) 202 | .map(([name, plainValue]) => [ 203 | name, 204 | this._toBuffer(plainValue).toString('base64') 205 | ]) 206 | 207 | return Object.fromEntries(encodedEntries) 208 | } 209 | } 210 | 211 | module.exports = KVBackend 212 | -------------------------------------------------------------------------------- /lib/backends/secrets-manager-backend.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const KVBackend = require('./kv-backend') 4 | 5 | /** Secrets Manager backend class. */ 6 | class SecretsManagerBackend extends KVBackend { 7 | /** 8 | * Create Secrets Manager backend. 9 | * @param {Object} client - Client for interacting with Secrets Manager. 10 | * @param {Object} logger - Logger for logging stuff. 11 | */ 12 | constructor ({ clientFactory, assumeRole, logger }) { 13 | super({ logger }) 14 | this._client = clientFactory() 15 | this._clientFactory = clientFactory 16 | this._assumeRole = assumeRole 17 | } 18 | 19 | /** 20 | * Get secret property value from Secrets Manager. 21 | * @param {string} key - Key used to store secret property value in Secrets Manager. 22 | * @param {object} keyOptions - Options for this specific key, eg version etc. 23 | * @param {string} keyOptions.versionStage - Version stage 24 | * @param {string} keyOptions.versionId - Version ID 25 | * @param {object} specOptions - Options for this external secret, eg role 26 | * @param {string} specOptions.roleArn - IAM role arn to assume 27 | * @returns {Promise} Promise object representing secret property value. 28 | */ 29 | async _get ({ key, specOptions: { roleArn, region }, keyOptions: { versionStage = 'AWSCURRENT', versionId = null } }) { 30 | this._logger.info(`fetching secret property ${key} with role: ${roleArn || 'pods role'} in region: ${region || 'pods region'}`) 31 | 32 | let client = this._client 33 | let factoryArgs = null 34 | if (roleArn) { 35 | const credentials = this._assumeRole({ 36 | RoleArn: roleArn, 37 | RoleSessionName: 'k8s-external-secrets' 38 | }) 39 | factoryArgs = { 40 | ...factoryArgs, 41 | credentials 42 | } 43 | } 44 | if (region) { 45 | factoryArgs = { 46 | ...factoryArgs, 47 | region 48 | } 49 | } 50 | if (factoryArgs) { 51 | client = this._clientFactory(factoryArgs) 52 | } 53 | let params 54 | if (versionId) { 55 | params = { SecretId: key, VersionId: versionId } 56 | } else { 57 | params = { SecretId: key, VersionStage: versionStage } 58 | } 59 | 60 | const data = await client 61 | .getSecretValue(params) 62 | .promise() 63 | 64 | if ('SecretBinary' in data) { 65 | return data.SecretBinary 66 | } else if ('SecretString' in data) { 67 | return data.SecretString 68 | } 69 | 70 | this._logger.error(`Unexpected data from Secrets Manager secret ${key}`) 71 | return null 72 | } 73 | } 74 | 75 | module.exports = SecretsManagerBackend 76 | -------------------------------------------------------------------------------- /lib/backends/secrets-manager-backend.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const { expect } = require('chai') 5 | const sinon = require('sinon') 6 | 7 | const SecretsManagerBackend = require('./secrets-manager-backend') 8 | 9 | describe('SecretsManagerBackend', () => { 10 | let clientMock 11 | let loggerMock 12 | let clientFactoryMock 13 | let assumeRoleMock 14 | let secretsManagerBackend 15 | const specOptions = {} 16 | const keyOptions = {} 17 | 18 | const assumeRoleCredentials = { 19 | fakeObject: 'Fake mock object' 20 | } 21 | 22 | beforeEach(() => { 23 | clientMock = sinon.mock() 24 | loggerMock = sinon.mock() 25 | loggerMock.info = sinon.stub() 26 | clientFactoryMock = sinon.fake.returns(clientMock) 27 | assumeRoleMock = sinon.fake.returns(assumeRoleCredentials) 28 | secretsManagerBackend = new SecretsManagerBackend({ 29 | clientFactory: clientFactoryMock, 30 | assumeRole: assumeRoleMock, 31 | logger: loggerMock 32 | }) 33 | }) 34 | 35 | describe('_get', () => { 36 | let getSecretValuePromise 37 | 38 | beforeEach(() => { 39 | getSecretValuePromise = sinon.mock() 40 | getSecretValuePromise.promise = sinon.stub() 41 | clientMock.getSecretValue = sinon.stub().returns(getSecretValuePromise) 42 | }) 43 | 44 | it('returns secret property value', async () => { 45 | getSecretValuePromise.promise.resolves({ 46 | SecretString: 'fakeSecretPropertyValue' 47 | }) 48 | 49 | const secretPropertyValue = await secretsManagerBackend._get({ 50 | key: 'fakeSecretKey', 51 | specOptions, 52 | keyOptions 53 | }) 54 | 55 | expect(clientMock.getSecretValue.calledWith({ 56 | SecretId: 'fakeSecretKey', 57 | VersionStage: 'AWSCURRENT' 58 | })).to.equal(true) 59 | expect(clientFactoryMock.getCall(0).args).deep.equals([]) 60 | expect(assumeRoleMock.callCount).equals(0) 61 | expect(secretPropertyValue).equals('fakeSecretPropertyValue') 62 | }) 63 | 64 | it('returns binary secret', async () => { 65 | getSecretValuePromise.promise.resolves({ 66 | SecretBinary: Buffer.from('fakeSecretPropertyValue', 'utf-8') 67 | }) 68 | 69 | const secretPropertyValue = await secretsManagerBackend._get({ 70 | key: 'fakeSecretKey', 71 | specOptions, 72 | keyOptions 73 | }) 74 | 75 | expect(clientMock.getSecretValue.calledWith({ 76 | SecretId: 'fakeSecretKey', 77 | VersionStage: 'AWSCURRENT' 78 | })).to.equal(true) 79 | expect(clientFactoryMock.getCall(0).args).deep.equals([]) 80 | expect(assumeRoleMock.callCount).equals(0) 81 | expect(secretPropertyValue.toString()).equals('fakeSecretPropertyValue') 82 | }) 83 | 84 | it('returns secret property value assuming a role with region', async () => { 85 | getSecretValuePromise.promise.resolves({ 86 | SecretString: 'fakeAssumeRoleSecretValue' 87 | }) 88 | 89 | const secretPropertyValue = await secretsManagerBackend._get({ 90 | key: 'fakeSecretKey', 91 | specOptions: { 92 | roleArn: 'my-role', 93 | region: 'foo-bar-baz' 94 | }, 95 | keyOptions 96 | }) 97 | 98 | expect(clientFactoryMock.lastArg).deep.equals({ 99 | credentials: assumeRoleCredentials, 100 | region: 'foo-bar-baz' 101 | }) 102 | expect(clientMock.getSecretValue.calledWith({ 103 | SecretId: 'fakeSecretKey', 104 | VersionStage: 'AWSCURRENT' 105 | })).to.equal(true) 106 | expect(clientFactoryMock.getCall(0).args).deep.equals([]) 107 | expect(clientFactoryMock.getCall(1).args).deep.equals([{ 108 | credentials: assumeRoleCredentials, 109 | region: 'foo-bar-baz' 110 | }]) 111 | expect(assumeRoleMock.callCount).equals(1) 112 | expect(secretPropertyValue).equals('fakeAssumeRoleSecretValue') 113 | }) 114 | 115 | it('returns secret property value from specific region', async () => { 116 | getSecretValuePromise.promise.resolves({ 117 | SecretString: 'fakeAssumeRoleSecretValue' 118 | }) 119 | 120 | const secretPropertyValue = await secretsManagerBackend._get({ 121 | key: 'fakeSecretKey', 122 | specOptions: { region: 'my-region' }, 123 | keyOptions 124 | }) 125 | 126 | expect(clientFactoryMock.lastArg).deep.equals({ 127 | region: 'my-region' 128 | }) 129 | expect(clientMock.getSecretValue.calledWith({ 130 | SecretId: 'fakeSecretKey', 131 | VersionStage: 'AWSCURRENT' 132 | })).to.equal(true) 133 | expect(clientFactoryMock.getCall(0).args).deep.equals([]) 134 | expect(clientFactoryMock.getCall(1).args).deep.equals([{ 135 | region: 'my-region' 136 | }]) 137 | expect(assumeRoleMock.callCount).equals(0) 138 | expect(secretPropertyValue).equals('fakeAssumeRoleSecretValue') 139 | }) 140 | 141 | it('returns secret property value with versionStage', async () => { 142 | getSecretValuePromise.promise.resolves({ 143 | SecretString: 'fakeSecretPropertyValuePreviousVersion' 144 | }) 145 | 146 | const secretPropertyValue = await secretsManagerBackend._get({ 147 | key: 'fakeSecretKey', 148 | specOptions, 149 | keyOptions: { 150 | versionStage: 'AWSPREVIOUS' 151 | } 152 | }) 153 | 154 | expect(clientMock.getSecretValue.calledWith({ 155 | SecretId: 'fakeSecretKey', 156 | VersionStage: 'AWSPREVIOUS' 157 | })).to.equal(true) 158 | expect(clientFactoryMock.getCall(0).args).deep.equals([]) 159 | expect(assumeRoleMock.callCount).equals(0) 160 | expect(secretPropertyValue).equals('fakeSecretPropertyValuePreviousVersion') 161 | }) 162 | 163 | it('returns secret property value with versionId', async () => { 164 | getSecretValuePromise.promise.resolves({ 165 | SecretString: 'fakeSecretPropertyValueVersionId' 166 | }) 167 | 168 | const secretPropertyValue = await secretsManagerBackend._get({ 169 | key: 'fakeSecretKey', 170 | specOptions, 171 | keyOptions: { 172 | versionId: 'ea9ef8d7-ea54-4a3b-b24b-99510e8d7a3d' 173 | } 174 | }) 175 | 176 | expect(clientMock.getSecretValue.calledWith({ 177 | SecretId: 'fakeSecretKey', 178 | VersionId: 'ea9ef8d7-ea54-4a3b-b24b-99510e8d7a3d' 179 | })).to.equal(true) 180 | expect(clientFactoryMock.getCall(0).args).deep.equals([]) 181 | expect(assumeRoleMock.callCount).equals(0) 182 | expect(secretPropertyValue).equals('fakeSecretPropertyValueVersionId') 183 | }) 184 | }) 185 | }) 186 | -------------------------------------------------------------------------------- /lib/backends/system-manager-backend.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const KVBackend = require('./kv-backend') 4 | 5 | /** System Manager backend class. */ 6 | class SystemManagerBackend extends KVBackend { 7 | /** 8 | * Create System Manager backend. 9 | * @param {Object} client - Client for interacting with System Manager. 10 | * @param {Object} logger - Logger for logging stuff. 11 | */ 12 | constructor ({ clientFactory, assumeRole, logger }) { 13 | super({ logger }) 14 | this._client = clientFactory() 15 | this._clientFactory = clientFactory 16 | this._assumeRole = assumeRole 17 | } 18 | 19 | /** 20 | * Get secret property value from System Manager. 21 | * @param {string} key - Key used to store secret property value in System Manager. 22 | * @param {object} specOptions - Options for this external secret, eg role 23 | * @param {string} specOptions.roleArn - IAM role arn to assume 24 | * @returns {Promise} Promise object representing secret property value. 25 | */ 26 | async _get ({ key, specOptions: { roleArn, region } }) { 27 | this._logger.info(`fetching secret property ${key} with role: ${roleArn || 'pods role'} in region: ${region || 'pods region'}`) 28 | 29 | let client = this._client 30 | let factoryArgs = null 31 | if (roleArn) { 32 | const credentials = this._assumeRole({ 33 | RoleArn: roleArn, 34 | RoleSessionName: 'k8s-external-secrets' 35 | }) 36 | factoryArgs = { 37 | ...factoryArgs, 38 | credentials 39 | } 40 | } 41 | 42 | if (region) { 43 | factoryArgs = { 44 | ...factoryArgs, 45 | region 46 | } 47 | } 48 | if (factoryArgs) { 49 | client = this._clientFactory(factoryArgs) 50 | } 51 | try { 52 | const data = await client 53 | .getParameter({ 54 | Name: key, 55 | WithDecryption: true 56 | }) 57 | .promise() 58 | return data.Parameter.Value 59 | } catch (err) { 60 | if (err.code === 'ParameterNotFound' && (!err.message || err.message === 'null')) { 61 | err.message = `ParameterNotFound: ${key} could not be found.` 62 | } 63 | 64 | throw err 65 | } 66 | } 67 | 68 | /** 69 | * Get secret property value from System Manager. 70 | * @param {string} path - Key used to store secret property value in System Manager. 71 | * @param {object} specOptions - Options for this external secret, eg role 72 | * @param {string} specOptions.roleArn - IAM role arn to assume 73 | * @returns {Promise} Promise object representing secret property value. 74 | */ 75 | async _getByPath ({ path, keyOptions, specOptions: { roleArn, region } }) { 76 | let client = this._client 77 | let factoryArgs = null 78 | const recursive = keyOptions.recursive || false 79 | 80 | this._logger.info(`fetching all secrets ${recursive ? '(recursively)' : ''} inside path ${path} with role ${roleArn !== ' from pod'} in region ${region}`) 81 | 82 | if (roleArn) { 83 | const credentials = this._assumeRole({ 84 | RoleArn: roleArn, 85 | RoleSessionName: 'k8s-external-secrets' 86 | }) 87 | factoryArgs = { 88 | ...factoryArgs, 89 | credentials 90 | } 91 | } 92 | if (region) { 93 | factoryArgs = { 94 | ...factoryArgs, 95 | region 96 | } 97 | } 98 | if (factoryArgs) { 99 | client = this._clientFactory(factoryArgs) 100 | } 101 | try { 102 | const getAllParameters = async () => { 103 | const EMPTY = Symbol('empty') 104 | this._logger.info(`fetching parameters for path ${path}`) 105 | const res = [] 106 | for await (const lf of (async function * () { 107 | let NextToken = EMPTY 108 | while (NextToken || NextToken === EMPTY) { 109 | const parameters = await client.getParametersByPath({ 110 | Path: path, 111 | WithDecryption: true, 112 | Recursive: recursive, 113 | NextToken: NextToken !== EMPTY ? NextToken : undefined 114 | }).promise() 115 | yield * parameters.Parameters 116 | NextToken = parameters.NextToken 117 | } 118 | })()) { 119 | res.push(lf) 120 | } 121 | return res 122 | } 123 | 124 | const parameters = {} 125 | const ssmData = await getAllParameters() 126 | for (const ssmRecord in ssmData) { 127 | const paramName = require('path').basename(ssmData[String(ssmRecord)].Name) 128 | const paramValue = ssmData[ssmRecord].Value 129 | parameters[paramName] = paramValue 130 | } 131 | 132 | return parameters 133 | } catch (err) { 134 | if (err.code === 'ParameterNotFound' && (!err.message || err.message === 'null')) { 135 | err.message = `ParameterNotFound: ${path} could not be found.` 136 | } 137 | 138 | throw err 139 | } 140 | } 141 | } 142 | 143 | module.exports = SystemManagerBackend 144 | -------------------------------------------------------------------------------- /lib/backends/system-manager-backend.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const { expect } = require('chai') 5 | const sinon = require('sinon') 6 | 7 | const SystemManagerBackend = require('./system-manager-backend') 8 | 9 | describe('SystemManagerBackend', () => { 10 | let clientMock 11 | let loggerMock 12 | let clientFactoryMock 13 | let assumeRoleMock 14 | let systemManagerBackend 15 | const specOptions = {} 16 | 17 | const assumeRoleCredentials = { 18 | fakeObject: 'Fake mock object' 19 | } 20 | 21 | beforeEach(() => { 22 | clientMock = sinon.mock() 23 | loggerMock = sinon.mock() 24 | loggerMock.info = sinon.stub() 25 | clientFactoryMock = sinon.fake.returns(clientMock) 26 | assumeRoleMock = sinon.fake.returns(assumeRoleCredentials) 27 | 28 | systemManagerBackend = new SystemManagerBackend({ 29 | client: clientMock, 30 | clientFactory: clientFactoryMock, 31 | assumeRole: assumeRoleMock, 32 | logger: loggerMock 33 | }) 34 | }) 35 | 36 | describe('_get', () => { 37 | let getParameterPromise 38 | 39 | beforeEach(() => { 40 | getParameterPromise = sinon.mock() 41 | getParameterPromise.promise = sinon.stub() 42 | clientMock.getParameter = sinon.stub().returns(getParameterPromise) 43 | }) 44 | 45 | it('returns secret property value', async () => { 46 | getParameterPromise.promise.resolves({ 47 | Parameter: { 48 | Value: 'fakeSecretPropertyValue' 49 | } 50 | }) 51 | 52 | const secretPropertyValue = await systemManagerBackend._get({ 53 | key: 'fakeSecretKey', 54 | specOptions 55 | }) 56 | 57 | expect(clientMock.getParameter.calledWith({ 58 | Name: 'fakeSecretKey', 59 | WithDecryption: true 60 | })).to.equal(true) 61 | expect(clientFactoryMock.getCall(0).args).deep.equals([]) 62 | expect(assumeRoleMock.callCount).equals(0) 63 | expect(secretPropertyValue).equals('fakeSecretPropertyValue') 64 | }) 65 | 66 | it('returns secret property value assuming a role with region', async () => { 67 | getParameterPromise.promise.resolves({ 68 | Parameter: { 69 | Value: 'fakeAssumeRoleSecretValue' 70 | } 71 | }) 72 | 73 | const secretPropertyValue = await systemManagerBackend._get({ 74 | key: 'fakeSecretKey', 75 | specOptions: { 76 | roleArn: 'my-role', 77 | region: 'my-region' 78 | } 79 | }) 80 | expect(clientFactoryMock.lastArg).deep.equals({ 81 | credentials: assumeRoleCredentials, 82 | region: 'my-region' 83 | }) 84 | expect(clientMock.getParameter.calledWith({ 85 | Name: 'fakeSecretKey', 86 | WithDecryption: true 87 | })).to.equal(true) 88 | expect(clientFactoryMock.getCall(0).args).deep.equals([]) 89 | expect(clientFactoryMock.getCall(1).args).deep.equals([{ 90 | credentials: assumeRoleCredentials, 91 | region: 'my-region' 92 | }]) 93 | expect(assumeRoleMock.callCount).equals(1) 94 | expect(secretPropertyValue).equals('fakeAssumeRoleSecretValue') 95 | }) 96 | 97 | it('returns secret property value from specific region', async () => { 98 | getParameterPromise.promise.resolves({ 99 | Parameter: { 100 | Value: 'fakeAssumeRoleSecretValue' 101 | } 102 | }) 103 | 104 | const secretPropertyValue = await systemManagerBackend._get({ 105 | key: 'fakeSecretKey', 106 | specOptions: { 107 | region: 'my-region' 108 | } 109 | }) 110 | expect(clientFactoryMock.lastArg).deep.equals({ 111 | region: 'my-region' 112 | }) 113 | expect(clientMock.getParameter.calledWith({ 114 | Name: 'fakeSecretKey', 115 | WithDecryption: true 116 | })).to.equal(true) 117 | expect(clientFactoryMock.getCall(0).args).deep.equals([]) 118 | expect(clientFactoryMock.getCall(1).args).deep.equals([{ 119 | region: 'my-region' 120 | }]) 121 | expect(secretPropertyValue).equals('fakeAssumeRoleSecretValue') 122 | expect(assumeRoleMock.callCount).equals(0) 123 | }) 124 | 125 | it('throws a meaningful message when the parameter does not exist', async () => { 126 | const error = new Error(null) 127 | error.code = 'ParameterNotFound' 128 | error.name = 'ParameterNotFound' 129 | 130 | getParameterPromise.promise.rejects(error) 131 | 132 | try { 133 | await systemManagerBackend._get({ 134 | key: 'fakeSecretKey', 135 | specOptions 136 | }) 137 | } catch (err) { 138 | expect(err.message).equals('ParameterNotFound: fakeSecretKey could not be found.') 139 | } 140 | }) 141 | }) 142 | }) 143 | -------------------------------------------------------------------------------- /lib/backends/vault-backend.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const KVBackend = require('./kv-backend') 4 | 5 | /** Vault backend class. */ 6 | class VaultBackend extends KVBackend { 7 | /** 8 | * Create Vault backend. 9 | * @param {Object} vaultFactory - arrow function to create a vault client. 10 | * @param {Number} tokenRenewThreshold - tokens are renewed when ttl reaches this threshold 11 | * @param {Object} logger - Logger for logging stuff. 12 | */ 13 | constructor ({ vaultFactory, tokenRenewThreshold, logger, defaultVaultMountPoint, defaultVaultRole }) { 14 | super({ logger }) 15 | this._vaultFactory = vaultFactory 16 | this._clients = new Map() 17 | this._tokenRenewThreshold = tokenRenewThreshold 18 | this._defaultVaultMountPoint = defaultVaultMountPoint 19 | this._defaultVaultRole = defaultVaultRole 20 | } 21 | 22 | /** 23 | * Fetch Kubernetes service account token. 24 | * @returns {string} String representing the token of the service account running this pod. 25 | */ 26 | _fetchServiceAccountToken () { 27 | if (!this._serviceAccountToken) { 28 | const fs = require('fs') 29 | this._serviceAccountToken = fs.readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/token', 'utf8') 30 | } 31 | return this._serviceAccountToken 32 | } 33 | 34 | /** 35 | * Get secret property value from Vault. 36 | * @param {string} key - Secret key in the backend. 37 | * @param {object} keyOptions - Options for this specific key, eg version etc. 38 | * @param {object} specOptions - Options for this external secret, eg role 39 | * @param {string} specOptions.vaultMountPoint - mount point 40 | * @param {string} specOptions.vaultRole - role 41 | * @param {number} specOptions.kvVersion - K/V Version 1 or 2 42 | * @returns {Promise} Promise object representing secret property values. 43 | */ 44 | async _get ({ key, specOptions: { vaultMountPoint = null, vaultRole = null, kvVersion = 2 } }) { 45 | const vaultMountPointGet = vaultMountPoint || this._defaultVaultMountPoint 46 | const vaultRoleGet = vaultRole || this._defaultVaultRole 47 | // Create cache key for auth specific client 48 | const clientCacheKey = `|m${vaultMountPointGet}|r${vaultRoleGet}|` 49 | // Lookup existing or create new vault client 50 | let client = this._clients.get(clientCacheKey) 51 | if (!client) { 52 | client = this._vaultFactory() 53 | this._clients.set(clientCacheKey, client) 54 | } 55 | 56 | // If we already have a cached token then inspect it... 57 | if (client.token) { 58 | try { 59 | this._logger.debug(`checking vault token expiry for role ${vaultRoleGet} on ${vaultMountPointGet}`) 60 | const tokenStatus = await client.tokenLookupSelf() 61 | this._logger.debug(`vault token (role ${vaultRoleGet} on ${vaultMountPointGet}) valid for ${tokenStatus.data.ttl} seconds, renews at ${this._tokenRenewThreshold}`) 62 | 63 | // If it needs renewing, renew it. 64 | if (Number(tokenStatus.data.ttl) <= this._tokenRenewThreshold) { 65 | this._logger.debug(`renewing role ${vaultRoleGet} on ${vaultMountPointGet} vault token`) 66 | if (!(await client.tokenRenewSelf())) { 67 | this._logger.debug(`cached token renewal failed. Clearing cached token for role ${vaultRoleGet} on ${vaultMountPointGet}`) 68 | client.token = null 69 | } 70 | } 71 | } catch { 72 | // If it can't be inspected/renewed, we clear the token. 73 | this._logger.debug(`cached token operation failed. Clearing cached token for role ${vaultRoleGet} on ${vaultMountPointGet}`) 74 | client.token = null 75 | } 76 | } 77 | 78 | // If we don't have a token here we either never had one or we just failed to renew it, so get a new one by logging-in 79 | if (!client.token) { 80 | const jwt = this._fetchServiceAccountToken() 81 | this._logger.debug(`fetching new token from vault for role ${vaultRoleGet} on ${vaultMountPointGet}`) 82 | await client.kubernetesLogin({ 83 | mount_point: vaultMountPointGet, 84 | role: vaultRoleGet, 85 | jwt: jwt 86 | }) 87 | } 88 | 89 | this._logger.debug(`reading secret key ${key} from vault`) 90 | const secretResponse = await client.read(key) 91 | 92 | if (kvVersion === 1) { 93 | return JSON.stringify(secretResponse.data) 94 | } 95 | 96 | if (kvVersion === 2) { 97 | return JSON.stringify(secretResponse.data.data) 98 | } 99 | 100 | throw new Error('Unknown "kvVersion" specified') 101 | } 102 | } 103 | 104 | module.exports = VaultBackend 105 | -------------------------------------------------------------------------------- /lib/daemon.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** Daemon class. */ 4 | class Daemon { 5 | /** 6 | * Create daemon. 7 | * @param {Object} backends - Backends for fetching secret properties. 8 | * @param {Object} kubeClient - Client for interacting with kubernetes cluster. 9 | * @param {Object} externalSecretEvents - Stream of external secret events. 10 | * @param {Object} logger - Logger for logging stuff. 11 | * @param {number} pollerIntervalMilliseconds - Interval time in milliseconds for polling secret properties. 12 | */ 13 | constructor ({ 14 | instanceId, 15 | externalSecretEvents, 16 | logger, 17 | pollerFactory 18 | }) { 19 | this._instanceId = instanceId 20 | this._externalSecretEvents = externalSecretEvents 21 | this._logger = logger 22 | this._pollerFactory = pollerFactory 23 | 24 | this._pollers = {} 25 | } 26 | 27 | /** 28 | * Create a poller descriptor from externalsecret resources. 29 | * @param {Object} object - externalsecret manifest. 30 | * @returns {Object} Poller descriptor. 31 | */ 32 | _createPollerDescriptor (externalSecret) { 33 | const { uid, name, namespace } = externalSecret.metadata 34 | 35 | return { id: uid, name, namespace, externalSecret } 36 | } 37 | 38 | /** 39 | * Remove a poller associated with a deleted or modified externalsecret. 40 | * @param {String} pollerId - ID of the poller to remove. 41 | */ 42 | _removePoller (pollerId) { 43 | if (this._pollers[pollerId]) { 44 | this._logger.debug(`stopping and removing poller ${pollerId}`) 45 | this._pollers[pollerId].stop() 46 | delete this._pollers[pollerId] 47 | } 48 | } 49 | 50 | _removePollers () { 51 | Object.keys(this._pollers).forEach(pollerId => this._removePoller(pollerId)) 52 | } 53 | 54 | _addPoller (descriptor) { 55 | this._logger.debug(`spinning up poller for ${descriptor.namespace}/${descriptor.name}`) 56 | 57 | const poller = this._pollerFactory.createPoller(descriptor) 58 | 59 | this._pollers[descriptor.id] = poller.start() 60 | } 61 | 62 | /** 63 | * Start daemon and create pollers. 64 | */ 65 | async start () { 66 | for await (const event of this._externalSecretEvents) { 67 | // Check if the externalSecret should be managed by this instance. 68 | if (event.object && event.object.spec) { 69 | const externalSecretMetadata = event.object.metadata 70 | const externalSecretController = event.object.spec.controllerId 71 | if ((this._instanceId || externalSecretController) && this._instanceId !== externalSecretController) { 72 | this._logger.debug('the secret %s/%s is not managed by this instance but by %s', 73 | externalSecretMetadata.namespace, externalSecretMetadata.name, externalSecretController) 74 | continue 75 | } 76 | } 77 | 78 | const descriptor = event.object ? this._createPollerDescriptor(event.object) : null 79 | 80 | switch (event.type) { 81 | case 'DELETED': { 82 | this._removePoller(descriptor.id) 83 | break 84 | } 85 | 86 | case 'ADDED': 87 | case 'MODIFIED': { 88 | this._removePoller(descriptor.id) 89 | this._addPoller(descriptor) 90 | break 91 | } 92 | 93 | case 'DELETED_ALL': { 94 | this._removePollers() 95 | break 96 | } 97 | 98 | default: { 99 | this._logger.warn(event, 'Unhandled event type %s', event.type) 100 | break 101 | } 102 | } 103 | } 104 | } 105 | 106 | /** 107 | * Destroy pollers and stop deamon. 108 | */ 109 | stop () { 110 | this._removePollers() 111 | this._externalSecretEvents.return(null) 112 | } 113 | } 114 | 115 | module.exports = Daemon 116 | -------------------------------------------------------------------------------- /lib/daemon.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const { expect } = require('chai') 5 | const sinon = require('sinon') 6 | 7 | const Daemon = require('./daemon') 8 | 9 | describe('Daemon', () => { 10 | let daemon 11 | let loggerMock 12 | let pollerMock 13 | let pollerFactory 14 | 15 | beforeEach(() => { 16 | loggerMock = sinon.mock() 17 | loggerMock.info = sinon.stub() 18 | loggerMock.warn = sinon.stub() 19 | loggerMock.debug = sinon.stub() 20 | 21 | pollerMock = sinon.mock() 22 | pollerMock.start = sinon.stub().returns(pollerMock) 23 | pollerMock.stop = sinon.stub().returns(pollerMock) 24 | 25 | pollerFactory = sinon.mock() 26 | pollerFactory.createPoller = sinon.stub().returns(pollerMock) 27 | 28 | daemon = new Daemon({ 29 | logger: loggerMock, 30 | pollerFactory 31 | }) 32 | }) 33 | 34 | afterEach(() => { 35 | sinon.restore() 36 | }) 37 | 38 | it('starts new pollers for external secrets', async () => { 39 | const fakeExternalSecretEvents = (async function * () { 40 | yield { 41 | type: 'ADDED', 42 | object: { 43 | metadata: { 44 | name: 'foo', 45 | namespace: 'foo', 46 | resourceVersion: '1' 47 | }, 48 | spec: {} 49 | } 50 | } 51 | }()) 52 | daemon._externalSecretEvents = fakeExternalSecretEvents 53 | 54 | await daemon.start() 55 | daemon.stop() 56 | 57 | expect(pollerMock.start.called).to.equal(true) 58 | expect(pollerMock.stop.called).to.equal(true) 59 | }) 60 | 61 | it('tries to remove existing poller on ADDED events', async () => { 62 | const fakeExternalSecretEvents = (async function * () { 63 | yield { 64 | type: 'ADDED', 65 | object: { 66 | metadata: { 67 | name: 'foo', 68 | namespace: 'foo', 69 | uid: 'test-id' 70 | } 71 | } 72 | } 73 | }()) 74 | 75 | daemon._externalSecretEvents = fakeExternalSecretEvents 76 | daemon._addPoller = sinon.mock() 77 | daemon._removePoller = sinon.mock() 78 | 79 | await daemon.start() 80 | daemon.stop() 81 | 82 | expect(daemon._addPoller.called).to.equal(true) 83 | expect(daemon._removePoller.calledWith('test-id')).to.equal(true) 84 | }) 85 | 86 | it('tries to remove existing poller on MODIFIED event', async () => { 87 | const fakeExternalSecretEvents = (async function * () { 88 | yield { 89 | type: 'MODIFIED', 90 | object: { 91 | metadata: { 92 | name: 'foo', 93 | namespace: 'foo', 94 | uid: 'test-id' 95 | } 96 | } 97 | } 98 | }()) 99 | 100 | daemon._externalSecretEvents = fakeExternalSecretEvents 101 | daemon._addPoller = sinon.mock() 102 | daemon._removePoller = sinon.mock() 103 | 104 | await daemon.start() 105 | daemon.stop() 106 | 107 | expect(daemon._addPoller.called).to.equal(true) 108 | expect(daemon._removePoller.calledWith('test-id')).to.equal(true) 109 | }) 110 | 111 | it('manage externalsecrets with unmatched controller id and instance id', async () => { 112 | const fakeExternalSecretEvents = (async function * () { 113 | yield { 114 | type: 'ADDED', 115 | object: { 116 | metadata: { 117 | name: 'foo', 118 | namespace: 'foo', 119 | uid: 'test-id' 120 | }, 121 | spec: { 122 | controllerId: 'instance01' 123 | } 124 | } 125 | } 126 | }()) 127 | 128 | daemon._instanceId = 'instance01' 129 | daemon._externalSecretEvents = fakeExternalSecretEvents 130 | daemon._addPoller = sinon.mock() 131 | daemon._removePoller = sinon.mock() 132 | 133 | await daemon.start() 134 | daemon.stop() 135 | 136 | expect(daemon._addPoller.called).to.equal(true) 137 | expect(daemon._removePoller.calledWith('test-id')).to.equal(true) 138 | }) 139 | 140 | it('do not manage externalsecrets with unmatched controller id and instance id', async () => { 141 | const fakeExternalSecretEvents = (async function * () { 142 | yield { 143 | type: 'ADDED', 144 | object: { 145 | metadata: { 146 | name: 'foo', 147 | namespace: 'foo', 148 | uid: 'test-id' 149 | }, 150 | spec: { 151 | controllerId: 'instance01' 152 | } 153 | } 154 | } 155 | }()) 156 | 157 | daemon._instanceId = 'instance02' 158 | daemon._externalSecretEvents = fakeExternalSecretEvents 159 | daemon._addPoller = sinon.mock() 160 | daemon._removePoller = sinon.mock() 161 | 162 | await daemon.start() 163 | daemon.stop() 164 | 165 | expect(daemon._addPoller.called).to.equal(false) 166 | }) 167 | }) 168 | -------------------------------------------------------------------------------- /lib/external-secret.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Creates an FIFO queue which you can put to and take from. 5 | * If theres nothing to take it will wait with resolving until 6 | * something is put to the queue. 7 | * @returns {Object} Queue instance with put and take methods 8 | */ 9 | function createEventQueue () { 10 | const queuedEvents = [] 11 | const waitingResolvers = [] 12 | 13 | return { 14 | take: () => queuedEvents.length > 0 15 | ? Promise.resolve(queuedEvents.shift()) 16 | : new Promise(resolve => waitingResolvers.push(resolve)), 17 | put: (msg) => waitingResolvers.length > 0 18 | ? waitingResolvers.shift()(msg) 19 | : queuedEvents.push(msg) 20 | } 21 | } 22 | 23 | async function startWatcher ({ 24 | kubeClient, 25 | namespace = null, 26 | customResourceManifest, 27 | logger, 28 | eventQueue, 29 | watchTimeout 30 | }) { 31 | const deathQueue = createEventQueue() 32 | const loggedNamespaceName = namespace || '*' 33 | 34 | try { 35 | while (true) { 36 | logger.debug('Starting watch stream for namespace %s', loggedNamespaceName) 37 | 38 | let api = kubeClient 39 | .apis[customResourceManifest.spec.group] 40 | .v1.watch 41 | 42 | if (namespace) { 43 | api = api.namespaces(namespace) 44 | } 45 | 46 | const stream = await api[customResourceManifest.spec.names.plural] 47 | .getObjectStream() 48 | 49 | let timeout 50 | const restartTimeout = () => { 51 | if (timeout) { 52 | clearTimeout(timeout) 53 | } 54 | 55 | const timeMs = watchTimeout 56 | timeout = setTimeout(() => { 57 | logger.info(`No watch event for ${timeMs} ms, restarting watcher for ${loggedNamespaceName}`) 58 | stream.end() 59 | }, timeMs) 60 | timeout.unref() 61 | } 62 | 63 | stream.on('data', (evt) => { 64 | eventQueue.put(evt) 65 | restartTimeout() 66 | }) 67 | 68 | stream.on('error', (err) => { 69 | logger.warn(err, 'Got error on stream for namespace %s', loggedNamespaceName) 70 | deathQueue.put('ERROR') 71 | clearTimeout(timeout) 72 | }) 73 | 74 | stream.on('end', () => { 75 | deathQueue.put('END') 76 | clearTimeout(timeout) 77 | }) 78 | 79 | restartTimeout() 80 | 81 | const deathEvent = await deathQueue.take() 82 | 83 | logger.info('Stopping watch stream for namespace %s due to event: %s', loggedNamespaceName, deathEvent) 84 | eventQueue.put({ type: 'DELETED_ALL' }) 85 | stream.end() 86 | } 87 | } catch (err) { 88 | logger.error(err, 'Watcher for namespace %s crashed', loggedNamespaceName) 89 | throw err 90 | } 91 | } 92 | 93 | /** 94 | * Get a stream of external secret events. This implementation uses 95 | * watch and yields as a stream of events. 96 | * @param {Object} kubeClient - Client for interacting with kubernetes cluster. 97 | * @param {Array} watchedNamespaces - List of scoped namespaces. 98 | * @param {Object} customResourceManifest - Custom resource manifest. 99 | * @returns {Object} An async generator that yields externalsecret events. 100 | */ 101 | function getExternalSecretEvents ({ 102 | kubeClient, 103 | watchedNamespaces, 104 | customResourceManifest, 105 | logger, 106 | watchTimeout 107 | }) { 108 | return (async function * () { 109 | const eventQueue = createEventQueue() 110 | 111 | // If the watchedNamespaces is set create a watcher for each namespace, otherwise create a watcher for all namespaces. 112 | if (watchedNamespaces.length) { 113 | // Create watcher for each namespace 114 | watchedNamespaces.forEach((namespace) => { 115 | startWatcher({ 116 | namespace, 117 | kubeClient, 118 | customResourceManifest, 119 | logger, 120 | eventQueue, 121 | watchTimeout 122 | }) 123 | }) 124 | } else { 125 | startWatcher({ 126 | kubeClient, 127 | customResourceManifest, 128 | logger, 129 | eventQueue, 130 | watchTimeout 131 | }) 132 | } 133 | 134 | while (true) { 135 | yield await eventQueue.take() 136 | } 137 | }()) 138 | } 139 | 140 | module.exports = { 141 | getExternalSecretEvents 142 | } 143 | -------------------------------------------------------------------------------- /lib/external-secret.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const { expect } = require('chai') 5 | const sinon = require('sinon') 6 | const { Readable } = require('stream') 7 | 8 | const { getExternalSecretEvents } = require('./external-secret') 9 | 10 | describe('getExternalSecretEvents', () => { 11 | let kubeClientMock 12 | let watchedNamespaces 13 | let fakeCustomResourceManifest 14 | let loggerMock 15 | let externalsecrets 16 | 17 | beforeEach(() => { 18 | fakeCustomResourceManifest = { 19 | spec: { 20 | group: 'kubernetes-client.io', 21 | names: { 22 | plural: 'externalsecrets' 23 | } 24 | } 25 | } 26 | 27 | externalsecrets = { 28 | getObjectStream: () => undefined 29 | } 30 | 31 | kubeClientMock = { 32 | apis: { 33 | 'kubernetes-client.io': { 34 | v1: { 35 | watch: { 36 | namespaces: () => { 37 | return { 38 | externalsecrets 39 | } 40 | }, 41 | externalsecrets 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | watchedNamespaces = [] 49 | 50 | loggerMock = sinon.mock() 51 | loggerMock.info = sinon.stub() 52 | loggerMock.warn = sinon.stub() 53 | loggerMock.error = sinon.stub() 54 | loggerMock.debug = sinon.stub() 55 | }) 56 | 57 | it('gets a stream of external secret events', async () => { 58 | const fakeExternalSecretObject = { 59 | apiVersion: 'kubernetes-client.io/v1', 60 | kind: 'ExternalSecret', 61 | metadata: { 62 | name: 'my-secret', 63 | namespace: 'default' 64 | }, 65 | spec: { backendType: 'secretsManager', data: [] } 66 | } 67 | 68 | const fakeStream = Readable.from([ 69 | { 70 | type: 'MODIFIED', 71 | object: fakeExternalSecretObject 72 | }, 73 | { 74 | type: 'ADDED', 75 | object: fakeExternalSecretObject 76 | }, 77 | { 78 | type: 'DELETED', 79 | object: fakeExternalSecretObject 80 | }, 81 | { 82 | type: 'DELETED_ALL' 83 | } 84 | ]) 85 | 86 | fakeStream.end = sinon.stub() 87 | externalsecrets.getObjectStream = () => fakeStream 88 | 89 | const events = getExternalSecretEvents({ 90 | kubeClient: kubeClientMock, 91 | watchedNamespaces: watchedNamespaces, 92 | customResourceManifest: fakeCustomResourceManifest, 93 | logger: loggerMock, 94 | watchTimeout: 5000 95 | }) 96 | 97 | const modifiedEvent = await events.next() 98 | expect(modifiedEvent.value.type).is.equal('MODIFIED') 99 | expect(modifiedEvent.value.object).is.deep.equal(fakeExternalSecretObject) 100 | 101 | const addedEvent = await events.next() 102 | expect(addedEvent.value.type).is.equal('ADDED') 103 | expect(addedEvent.value.object).is.deep.equal(fakeExternalSecretObject) 104 | 105 | const deletedEvent = await events.next() 106 | expect(deletedEvent.value.type).is.equal('DELETED') 107 | expect(deletedEvent.value.object).is.deep.equal(fakeExternalSecretObject) 108 | 109 | const deletedAllEvent = await events.next() 110 | expect(deletedAllEvent.value.type).is.equal('DELETED_ALL') 111 | expect(deletedAllEvent.value.object).is.deep.equal(undefined) 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /lib/metrics-server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const express = require('express') 4 | const Prometheus = require('prom-client') 5 | 6 | /** MetricsServer class. */ 7 | class MetricsServer { 8 | /** 9 | * Create Metrics Server 10 | * @param {number} port - the port to listen on 11 | * @param {Object} logger - Logger for logging stuff 12 | * @param {Object} register - Prometheus registry that holds metric data 13 | */ 14 | constructor ({ port, logger, registry }) { 15 | this._port = port 16 | this._logger = logger 17 | this._registry = registry 18 | 19 | this._app = express() 20 | this._app.get('/metrics', (req, res) => { 21 | res.set('Content-Type', Prometheus.register.contentType) 22 | res.end(this._registry.metrics()) 23 | }) 24 | } 25 | 26 | /** 27 | * Start the metrics server: Listen on a TCP port and serve metrics over HTTP 28 | */ 29 | start () { 30 | return new Promise((resolve, reject) => { 31 | this._server = this._app.listen(this._port, () => { 32 | this._logger.info(`MetricsServer listening on port ${this._port}`) 33 | resolve() 34 | }) 35 | this._app.on('error', err => reject(err)) 36 | }) 37 | } 38 | 39 | /** 40 | * Stop the metrics server 41 | */ 42 | stop () { 43 | return new Promise((resolve, reject) => { 44 | this._server.close(err => { 45 | if (err) { 46 | return reject(err) 47 | } 48 | resolve() 49 | }) 50 | }) 51 | } 52 | } 53 | 54 | module.exports = MetricsServer 55 | -------------------------------------------------------------------------------- /lib/metrics-server.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const { expect } = require('chai') 5 | const sinon = require('sinon') 6 | const Prometheus = require('prom-client') 7 | const request = require('supertest') 8 | 9 | const MetricsServer = require('./metrics-server') 10 | const Metrics = require('./metrics') 11 | 12 | describe('MetricsServer', () => { 13 | let server 14 | let loggerMock 15 | let registry 16 | let metrics 17 | 18 | beforeEach(async () => { 19 | loggerMock = sinon.mock() 20 | loggerMock.info = sinon.stub() 21 | registry = new Prometheus.Registry() 22 | metrics = new Metrics({ registry }) 23 | 24 | server = new MetricsServer({ 25 | logger: loggerMock, 26 | registry: registry, 27 | port: 3918 28 | }) 29 | 30 | await server.start() 31 | }) 32 | 33 | afterEach(async () => { 34 | sinon.restore() 35 | await server.stop() 36 | }) 37 | 38 | it('start server to serve metrics', async () => { 39 | metrics.observeSync({ 40 | name: 'foo', 41 | namespace: 'example', 42 | backend: 'foo', 43 | status: 'success' 44 | }) 45 | 46 | metrics.observeSync({ 47 | name: 'bar', 48 | namespace: 'example', 49 | backend: 'foo', 50 | status: 'failed' 51 | }) 52 | 53 | const res = await request('http://localhost:3918') 54 | .get('/metrics') 55 | .expect('Content-Type', Prometheus.register.contentType) 56 | .expect(200) 57 | 58 | expect(res.text).to.have.string('kubernetes_external_secrets_sync_calls_count{name="foo",namespace="example",backend="foo",status="success"} 1') 59 | expect(res.text).to.have.string('kubernetes_external_secrets_sync_calls_count{name="bar",namespace="example",backend="foo",status="failed"} 1') 60 | expect(res.text).to.have.string('kubernetes_external_secrets_last_sync_call_state{name="foo",namespace="example",backend="foo"} 1') 61 | expect(res.text).to.have.string('kubernetes_external_secrets_last_sync_call_state{name="bar",namespace="example",backend="foo"} -1') 62 | 63 | // Deprecated metrics. 64 | expect(res.text).to.have.string('sync_calls{name="foo",namespace="example",backend="foo",status="success"} 1') 65 | expect(res.text).to.have.string('sync_calls{name="bar",namespace="example",backend="foo",status="failed"} 1') 66 | expect(res.text).to.have.string('last_state{name="foo",namespace="example",backend="foo"} 1') 67 | expect(res.text).to.have.string('last_state{name="bar",namespace="example",backend="foo"} -1') 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /lib/metrics.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Prometheus = require('prom-client') 4 | 5 | /** Metrics class. */ 6 | class Metrics { 7 | /** 8 | * Create Metrics object 9 | */ 10 | constructor ({ registry }) { 11 | this._registry = registry 12 | this._syncCallsCount = new Prometheus.Counter({ 13 | name: 'kubernetes_external_secrets_sync_calls_count', 14 | help: 'Number of sync operations', 15 | labelNames: ['name', 'namespace', 'backend', 'status'], 16 | registers: [registry] 17 | }) 18 | this._syncCalls = new Prometheus.Counter({ 19 | name: 'sync_calls', 20 | help: '(Deprecated since 0.6.1, please use kubernetes_external_secrets_sync_calls_count) Number of sync operations', 21 | labelNames: ['name', 'namespace', 'backend', 'status'], 22 | registers: [registry] 23 | }) 24 | this._lastSyncCallState = new Prometheus.Gauge({ 25 | name: 'kubernetes_external_secrets_last_sync_call_state', 26 | help: 'State of last sync call of external secret. Value -1 if the last sync was a failure, 1 otherwise', 27 | labelNames: ['name', 'namespace', 'backend'], 28 | registers: [registry] 29 | }) 30 | this._lastState = new Prometheus.Gauge({ 31 | name: 'last_state', 32 | help: '(Deprecated since 0.6.1, please use kubernetes_external_secrets_last_sync_call_state) Value -1 if the last sync was a failure, 1 otherwise', 33 | labelNames: ['name', 'namespace', 'backend'], 34 | registers: [registry] 35 | }) 36 | } 37 | 38 | /** 39 | * Observe the result a sync process 40 | * @param {String} name - the name of the externalSecret 41 | * @param {String} namespace - the namespace of the externalSecret 42 | * @param {String} backend - the backend used to fetch the externalSecret 43 | * @param {String} status - the result of the sync process: error|success 44 | */ 45 | observeSync ({ name, namespace, backend, status }) { 46 | this._syncCallsCount.inc({ 47 | name, 48 | namespace, 49 | backend, 50 | status 51 | }) 52 | this._syncCalls.inc({ 53 | name, 54 | namespace, 55 | backend, 56 | status 57 | }) 58 | if (status === 'success') { 59 | this._lastSyncCallState.set({ 60 | name, 61 | namespace, 62 | backend 63 | }, 1) 64 | this._lastState.set({ 65 | name, 66 | namespace, 67 | backend 68 | }, 1) 69 | } else { 70 | this._lastSyncCallState.set({ 71 | name, 72 | namespace, 73 | backend 74 | }, -1) 75 | this._lastState.set({ 76 | name, 77 | namespace, 78 | backend 79 | }, -1) 80 | } 81 | } 82 | } 83 | 84 | module.exports = Metrics 85 | -------------------------------------------------------------------------------- /lib/metrics.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const { expect } = require('chai') 5 | const sinon = require('sinon') 6 | const Prometheus = require('prom-client') 7 | 8 | const Metrics = require('./metrics') 9 | 10 | describe('Metrics', () => { 11 | let registry 12 | let metrics 13 | 14 | beforeEach(async () => { 15 | registry = new Prometheus.Registry() 16 | metrics = new Metrics({ registry }) 17 | }) 18 | 19 | afterEach(async () => { 20 | sinon.restore() 21 | }) 22 | 23 | it('should store metrics', async () => { 24 | metrics.observeSync({ 25 | name: 'foo', 26 | namespace: 'example', 27 | backend: 'foo', 28 | status: 'success' 29 | }) 30 | expect(registry.metrics()).to.have.string('kubernetes_external_secrets_sync_calls_count{name="foo",namespace="example",backend="foo",status="success"} 1') 31 | // Deprecated metric. 32 | expect(registry.metrics()).to.have.string('sync_calls{name="foo",namespace="example",backend="foo",status="success"} 1') 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /lib/poller-factory.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Poller = require('./poller') 4 | 5 | class PollerFactory { 6 | /** 7 | * Create PollerFactory. 8 | * @param {Object} backends - Backends for fetching secret properties. 9 | * @param {Object} kubeClient - Client for interacting with kubernetes cluster. 10 | * @param {Object} metrics - Metrics client 11 | * @param {Object} customResourceManifest - CRD manifest 12 | * @param {Object} logger - Logger for logging stuff. 13 | * @param {number} pollerIntervalMilliseconds - Interval time in milliseconds for polling secret properties. 14 | * @param {String} rolePermittedAnnotation - namespace annotation that defines which roles can be assumed within this namespace 15 | */ 16 | constructor ({ 17 | backends, 18 | kubeClient, 19 | metrics, 20 | pollerIntervalMilliseconds, 21 | rolePermittedAnnotation, 22 | namingPermittedAnnotation, 23 | customResourceManifest, 24 | enforceNamespaceAnnotation, 25 | pollingDisabled, 26 | logger 27 | }) { 28 | this._logger = logger 29 | this._metrics = metrics 30 | this._backends = backends 31 | this._kubeClient = kubeClient 32 | this._pollerIntervalMilliseconds = pollerIntervalMilliseconds 33 | this._customResourceManifest = customResourceManifest 34 | this._rolePermittedAnnotation = rolePermittedAnnotation 35 | this._namingPermittedAnnotation = namingPermittedAnnotation 36 | this._enforceNamespaceAnnotation = enforceNamespaceAnnotation 37 | this._pollingDisabled = pollingDisabled 38 | } 39 | 40 | /** 41 | * Create poller 42 | * @param {Object} externalSecret - External Secret custom resource oject 43 | */ 44 | createPoller ({ externalSecret }) { 45 | const poller = new Poller({ 46 | backends: this._backends, 47 | intervalMilliseconds: this._pollerIntervalMilliseconds, 48 | kubeClient: this._kubeClient, 49 | logger: this._logger, 50 | metrics: this._metrics, 51 | customResourceManifest: this._customResourceManifest, 52 | rolePermittedAnnotation: this._rolePermittedAnnotation, 53 | namingPermittedAnnotation: this._namingPermittedAnnotation, 54 | enforceNamespaceAnnotation: this._enforceNamespaceAnnotation, 55 | pollingDisabled: this._pollingDisabled, 56 | externalSecret 57 | }) 58 | 59 | return poller 60 | } 61 | } 62 | 63 | module.exports = PollerFactory 64 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const yaml = require('js-yaml') 2 | const parseTemplate = require('lodash/template') 3 | const mapValues = require('lodash/mapValues') 4 | 5 | const compileTemplate = (template, data) => parseTemplate(template, { imports: { yaml }, variable: 'data' })(data) 6 | 7 | const compileObjectTemplateKeys = (object, data) => { 8 | return mapValues(object, (value) => { 9 | if (value) { 10 | const valueType = typeof value 11 | 12 | if (valueType === 'string') { 13 | return compileTemplate(value, data) 14 | } else if (valueType === 'object' && !Array.isArray(value)) { 15 | return compileObjectTemplateKeys(value, data) 16 | } 17 | } 18 | 19 | return value 20 | }) 21 | } 22 | 23 | module.exports = { 24 | compileTemplate, 25 | compileObjectTemplateKeys 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kubernetes-external-secrets", 3 | "version": "8.5.5", 4 | "description": "Kubernetes external secrets", 5 | "main": "bin/daemon.js", 6 | "scripts": { 7 | "coverage": "nyc ./node_modules/mocha/bin/_mocha --recursive lib", 8 | "lint": "eslint --fix --ignore-pattern /coverage/ ./", 9 | "local": "LOCALSTACK=1 AWS_ACCESS_KEY_ID=foobar AWS_SECRET_ACCESS_KEY=foobar nodemon", 10 | "localstack": "docker run -it -p 4566:4566 -p 9999:8080 -e SERVICES=ssm,secretsmanager,sts -e DEBUG=1 --rm localstack/localstack:latest", 11 | "release": "standard-version --tag-prefix='' -a", 12 | "start": "./bin/daemon.js", 13 | "nodemon": "nodemon ./bin/daemon.js", 14 | "test": "eslint --ignore-pattern /coverage/ ./ && mocha --recursive lib", 15 | "test-e2e": "./e2e/run-e2e-suite.sh" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/external-secrets/kubernetes-external-secrets" 20 | }, 21 | "keywords": [ 22 | "kubernetes", 23 | "secrets", 24 | "aws", 25 | "alicloud", 26 | "aliyun" 27 | ], 28 | "author": "GoDaddy Operating Company, LLC", 29 | "license": "MIT", 30 | "engines": { 31 | "node": "^14.18.0" 32 | }, 33 | "dependencies": { 34 | "@alicloud/kms20160120": "^1.1.0", 35 | "@azure/identity": "^2.0.1", 36 | "@azure/keyvault-secrets": "^4.3.0", 37 | "@google-cloud/secret-manager": "^3.2.3", 38 | "@ibm-cloud/secrets-manager": "^0.1.0", 39 | "akeyless": "^2.0.15", 40 | "akeyless-cloud-id": "^1.0.0", 41 | "aws-sdk": "^2.628.0", 42 | "express": "^4.17.1", 43 | "js-yaml": "^3.14.1", 44 | "kubernetes-client": "^9.0.0", 45 | "lodash": "^4.17.21", 46 | "make-promises-safe": "^5.1.0", 47 | "node-vault": "^0.9.18", 48 | "pino": "^7.0.5", 49 | "prom-client": "^12.0.0", 50 | "proxy-agent": "^5.0.0" 51 | }, 52 | "devDependencies": { 53 | "chai": "4.3.4", 54 | "dotenv": "10.0.0", 55 | "eslint": "7.32.0", 56 | "eslint-config-standard": "14.1.1", 57 | "eslint-plugin-import": "2.25.2", 58 | "eslint-plugin-node": "11.1.0", 59 | "eslint-plugin-promise": "5.1.0", 60 | "eslint-plugin-security": "1.4.0", 61 | "eslint-plugin-standard": "4.1.0", 62 | "mocha": "^9.2.0", 63 | "nodemon": "2.0.13", 64 | "nyc": "15.1.0", 65 | "sinon": "9.2.4", 66 | "standard-version": "^9.3.2", 67 | "supertest": "6.1.6" 68 | }, 69 | "resolutions": { 70 | "jose": "^1.28.1" 71 | }, 72 | "nyc": { 73 | "check-coverage": true, 74 | "reporter": [ 75 | "cobertura", 76 | "json-summary", 77 | "lcov", 78 | "text", 79 | "text-summary" 80 | ], 81 | "exclude": [ 82 | "config/", 83 | "coverage/", 84 | "bin/", 85 | "**/*.test.js" 86 | ], 87 | "lines": 4, 88 | "functions": 4, 89 | "all": true, 90 | "cache": false, 91 | "temp-directory": "./coverage/.nyc_output" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | --------------------------------------------------------------------------------