├── .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 |
--------------------------------------------------------------------------------