├── tests ├── __init__.py ├── k8s │ ├── some_secret.yml │ ├── nginx_deployment.yml │ └── nginx_deployment_bad.yml ├── test_patch.py ├── request.json └── test_tranform.py ├── .gitignore ├── pyproject.toml ├── requirements.txt ├── k8s ├── tesoro_namespace.yaml ├── tesoro_service.yaml ├── clusterrolebinding.yaml ├── clusterrole.yaml ├── tesoro_deployment.yaml ├── tesoro_secret.yaml └── tesoro_mutatingwebhook.yaml ├── docs └── images │ └── tesoro_logo.png ├── bin ├── tesoro └── generate_certs.sh ├── .pre-commit-config.yaml ├── tesoro ├── __init__.py ├── metrics.py ├── patch.py ├── transform.py ├── __main__.py ├── utils.py └── handlers.py ├── .github ├── workflows │ ├── python-black.yml │ └── docker-image.yml ├── dependabot.yml └── ISSUE_TEMPLATE │ └── issue_template.md ├── Dockerfile ├── chart ├── templates │ ├── clusterrolebinding.yaml │ ├── service.yaml │ ├── clusterrole.yaml │ ├── mutatingwebhook_bundle.yaml │ ├── _helpers.tpl │ └── deployment.yaml ├── .helmignore ├── Chart.yaml └── values.yaml ├── Makefile ├── .travis.yml ├── codeql-analysis.yml ├── README.md └── LICENSE /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .python-version 2 | __pycache__ 3 | *.swp 4 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 110 3 | target-version = ['py37'] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.10.9 2 | jsonpatch==1.33 3 | prometheus-client==0.21.0 4 | -------------------------------------------------------------------------------- /k8s/tesoro_namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: tesoro 5 | 6 | -------------------------------------------------------------------------------- /docs/images/tesoro_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kapicorp/tesoro/HEAD/docs/images/tesoro_logo.png -------------------------------------------------------------------------------- /bin/tesoro: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | BINPATH=`dirname $0` 4 | PYTHONPATH="$BINPATH/../:$PYTHONPATH" python3 -m tesoro $@ 5 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 22.10.0 4 | hooks: 5 | - id: black 6 | -------------------------------------------------------------------------------- /tesoro/__init__.py: -------------------------------------------------------------------------------- 1 | from kapitan.refs.base import RefController, Revealer 2 | 3 | REF_CONTROLLER = RefController("/tmp", embed_refs=True) 4 | REVEALER = Revealer(REF_CONTROLLER) 5 | -------------------------------------------------------------------------------- /.github/workflows/python-black.yml: -------------------------------------------------------------------------------- 1 | name: Python Lint 2 | on: [push, pull_request] 3 | jobs: 4 | lint: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - uses: psf/black@stable 9 | -------------------------------------------------------------------------------- /k8s/tesoro_service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: tesoro-admission-controller 6 | namespace: tesoro 7 | spec: 8 | selector: 9 | app: tesoro-admission-controller 10 | ports: 11 | - port: 443 12 | targetPort: tesoro-api 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM kapicorp/kapitan 2 | 3 | USER root 4 | WORKDIR /opt/venv/ 5 | 6 | COPY . /opt/venv/ 7 | RUN python -m venv /opt/venv && pip install --no-cache-dir -r requirements.txt 8 | 9 | #USER kapitan see https://github.com/kapicorp/tesoro/issues/1 10 | ENTRYPOINT [ "/opt/venv/bin/python", "-m", "tesoro" ] 11 | -------------------------------------------------------------------------------- /tests/k8s/some_secret.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: some-secret 5 | labels: 6 | tesoro.kapicorp.com: enabled 7 | type: Opaque 8 | stringData: 9 | cert.pem: "?{base64:eyJkYXRhIjogImNtVm1JREVnWkdGMFlRPT0iLCAiZW5jb2RpbmciOiAib3JpZ2luYWwiLCAidHlwZSI6ICJiYXNlNjQifQ==:embedded}" 10 | -------------------------------------------------------------------------------- /k8s/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | kind: ClusterRoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: tesoro-admission-controller-crb 5 | labels: 6 | app: tesoro-admission-controller 7 | roleRef: 8 | apiGroup: rbac.authorization.k8s.io 9 | kind: ClusterRole 10 | name: tesoro-admission-controller-cr 11 | -------------------------------------------------------------------------------- /chart/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | kind: ClusterRoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: {{ include "tesoro.fullname" . }} 5 | labels: 6 | {{- include "tesoro.labels" . | nindent 4 }} 7 | roleRef: 8 | apiGroup: rbac.authorization.k8s.io 9 | kind: ClusterRole 10 | name: {{ include "tesoro.fullname" . }} 11 | -------------------------------------------------------------------------------- /chart/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "tesoro.fullname" . }} 5 | labels: 6 | {{- include "tesoro.labels" . | nindent 4 }} 7 | spec: 8 | ports: 9 | - name: tesoro-api 10 | port: 443 11 | targetPort: tesoro-api 12 | - name: metrics 13 | port: 9095 14 | targetPort: metrics 15 | selector: 16 | {{- include "tesoro.selectorLabels" . | nindent 4 }} 17 | -------------------------------------------------------------------------------- /chart/.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 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /k8s/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: tesoro-admission-controller-cr 5 | labels: 6 | app: tesoro-admission-controller 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - pods 12 | - events 13 | - secrets 14 | - configmaps 15 | verbs: 16 | - "*" 17 | - apiGroups: 18 | - apps 19 | resources: 20 | - deployments 21 | - daemonsets 22 | - replicasets 23 | - statefulsets 24 | verbs: 25 | - "*" 26 | -------------------------------------------------------------------------------- /chart/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "tesoro.fullname" . }} 5 | labels: 6 | {{- include "tesoro.labels" . | nindent 4 }} 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - pods 12 | - events 13 | - secrets 14 | - configmaps 15 | verbs: 16 | - "*" 17 | - apiGroups: 18 | - apps 19 | resources: 20 | - deployments 21 | - daemonsets 22 | - replicasets 23 | - statefulsets 24 | verbs: 25 | - "*" 26 | -------------------------------------------------------------------------------- /tesoro/metrics.py: -------------------------------------------------------------------------------- 1 | from prometheus_client import Counter, start_http_server as prom_http_server 2 | 3 | TESORO_COUNTER = Counter("tesoro_requests", "Tesoro requests") 4 | TESORO_FAILED_COUNTER = Counter("tesoro_requests_failed", "Tesoro failed requests") 5 | REVEAL_COUNTER = Counter("kapitan_reveal_requests", "Kapitan reveal requests") 6 | REVEAL_FAILED_COUNTER = Counter("kapitan_reveal_requests_failed", "Kapitan reveal failed requests ") 7 | REVEAL_RETRY_COUNTER = Counter("kapitan_reveal_retry_requests", "Kapitan reveal retry requests ") 8 | -------------------------------------------------------------------------------- /bin/generate_certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | mkdir certs; cd certs 5 | 6 | openssl genrsa -out rootCA.key 4096 7 | openssl req -x509 -new -nodes -key rootCA.key -subj "/CN=CA-tesoro-admission-controller.tesoro.svc" -sha256 -days 1024 -out rootCA.crt 8 | openssl genrsa -out priv.key 2048 9 | openssl req -new -sha256 -key priv.key -subj "/CN=tesoro-admission-controller.tesoro.svc" -out csr.csr 10 | openssl x509 -req -in csr.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out cert.pem -days 500 -sha256 11 | openssl x509 -in cert.pem -text -noout 12 | -------------------------------------------------------------------------------- /tests/k8s/nginx_deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nginx-deployment 5 | labels: 6 | tesoro.kapicorp.com: enabled 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: nginx 11 | template: 12 | metadata: 13 | labels: 14 | app: nginx 15 | spec: 16 | containers: 17 | - name: nginx 18 | image: nginx:1.14.2 19 | ports: 20 | - containerPort: 80 21 | env: 22 | - name: KAP_REF 23 | value: "?{base64:eyJkYXRhIjogImNtVm1JREVnWkdGMFlRPT0iLCAiZW5jb2RpbmciOiAib3JpZ2luYWwiLCAidHlwZSI6ICJiYXNlNjQifQ==:embedded}" 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | labels: 13 | - "pip" 14 | - "dependencies" 15 | open-pull-requests-limit: 2 16 | reviewers: 17 | - "kapicorp/kapicorpdevs" 18 | -------------------------------------------------------------------------------- /tests/k8s/nginx_deployment_bad.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nginx-deployment 5 | labels: 6 | tesoro.kapicorp.com: enabled 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: nginx 11 | template: 12 | metadata: 13 | labels: 14 | app: nginx 15 | spec: 16 | containers: 17 | - name: nginx 18 | image: nginx:1.14.2 19 | ports: 20 | - containerPort: 80 21 | env: 22 | - name: KAP_REF 23 | value: "?{base64:eyJkYXRhIjogImNtVm1JREVnWkdGMFlRPT0iLCAiZW5jb2RpbmciOiAib3JpZ2luYWwiLCAidHlwZSI6ICJiYXNlNjQifQQ==:embedded}" 24 | - name: KAP_REF2 25 | value: "?{base64:i/do/not/exist}" 26 | 27 | -------------------------------------------------------------------------------- /tests/test_patch.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from hashlib import sha256 3 | from tesoro.patch import redact_patch 4 | 5 | 6 | class TestPatch(unittest.TestCase): 7 | def test_redact_patch(self): 8 | patch = [ 9 | {"op": "add", "path": "/a/b", "value": "secret value to redact"}, 10 | { 11 | "op": "add", 12 | "path": "/metadata/annotations/tesoro.kapicorp.com~1revealed", 13 | "value": "no redact", 14 | }, 15 | ] 16 | redacted = redact_patch(patch) 17 | 18 | self.assertEqual(patch[0]["value"], "secret value to redact") 19 | value_hash = sha256("secret value to redact".encode()).hexdigest() 20 | self.assertEqual(redacted[0]["value"], f"!REDACTED VALUE! sha256={value_hash}") 21 | self.assertEqual(redacted[1]["value"], "no redact") 22 | -------------------------------------------------------------------------------- /tests/request.json: -------------------------------------------------------------------------------- 1 | { 2 | "request":{ 3 | "uid": "uid-here", 4 | "namespace": "namespace-here", 5 | "kind": "kind-here", 6 | "resource": "resource-here", 7 | "object":{ 8 | "kind": "obj-here", 9 | "metadata":{ 10 | "name": "name-here", 11 | "labels":{ 12 | "tesoro.kapicorp.com":"enabled" 13 | } 14 | }, 15 | "spec":{ 16 | "containers":[ 17 | { 18 | "name": "test/image", 19 | "args":[ 20 | "--embedded-ref", 21 | "?{base64:eyJkYXRhIjogImNtVm1JREVnWkdGMFlRPT0iLCAiZW5jb2RpbmciOiAib3JpZ2luYWwiLCAidHlwZSI6ICJiYXNlNjQifQ==:embedded}" 22 | ] 23 | } 24 | ] 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: clean package 2 | 3 | .PHONY: test 4 | test: 5 | @echo ----- Running python tests ----- 6 | python3 -m unittest discover 7 | 8 | .PHONE: test_docker 9 | test_docker: 10 | @echo ----- Testing build of docker image ----- 11 | docker build . --no-cache -t tesoro 12 | @echo ----- Testing run of docker image ----- 13 | docker run -ti --rm tesoro --help 14 | 15 | .PHONY: test_coverage 16 | test_coverage: 17 | @echo ----- Testing code coverage ----- 18 | coverage run --source=tesoro -m unittest discover 19 | coverage report --fail-under=15 -m 20 | 21 | .PHONY: test_formatting 22 | test_formatting: 23 | @echo ----- Testing code formatting ----- 24 | black --check . 25 | @echo 26 | 27 | .PHONY: format_codestyle 28 | format_codestyle: 29 | which black || echo "Install black with pip3 install --user black" 30 | # ignores line length and reclass 31 | black . 32 | @echo 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: issue_template 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug/feature** 11 | A clear and concise description of what the bug/feature request is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem/idea. 25 | 26 | ** If it's a bug (please complete the following information):** 27 | - `python --version`: 28 | - `kubernetes version`: 29 | - `pip3 --version`: 30 | - `kapitan --version`: 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /k8s/tesoro_deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: tesoro-admission-controller 6 | labels: 7 | app: tesoro-admission-controller 8 | spec: 9 | selector: 10 | matchLabels: 11 | app: tesoro-admission-controller 12 | template: 13 | metadata: 14 | labels: 15 | app: tesoro-admission-controller 16 | spec: 17 | containers: 18 | - name: tesoro 19 | image: kapicorp/tesoro 20 | imagePullPolicy: Always 21 | ports: 22 | - containerPort: 443 23 | name: tesoro-api 24 | - containerPort: 9095 25 | name: metrics 26 | args: 27 | - --port=443 28 | - --cert-file=/certs/cert.pem 29 | - --key-file=/certs/priv.key 30 | volumeMounts: 31 | - name: tesoro-secrets 32 | mountPath: /certs 33 | readOnly: true 34 | volumes: 35 | - name: tesoro-secrets 36 | secret: 37 | secretName: tesoro-admission-controller-secret 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | cache: pip 3 | matrix: 4 | include: 5 | - python: 3.7 6 | dist: bionic 7 | sudo: true 8 | branches: 9 | only: 10 | - master 11 | notifications: 12 | recipients: 13 | - kapitan-admins@googlegroups.com 14 | email: 15 | on_success: change 16 | on_failure: always 17 | 18 | before_install: 19 | # Loop until update succeeds (timeouts can occur) 20 | - sudo sed -e '/postgresql/ s/^#*/#/' -i /etc/apt/sources.list.d/* 21 | - sudo add-apt-repository ppa:longsleep/golang-backports -y 22 | # workaround for https://travis-ci.community/t/then-sudo-apt-get-update-failed-public-key-is-not-available-no-pubkey-6b05f25d762e3157-in-ubuntu-xenial/1728 23 | - sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 762E3157 24 | # Loop until update succeeds (timeouts can occur) 25 | - travis_retry $(! sudo apt-get -qq update 2>&1 | grep Failed) 26 | - sudo apt-get install -y gnupg2 git 27 | 28 | install: 29 | - pip3 install -r requirements.txt 30 | - pip3 install coverage black kapitan==0.28.0-rc.1 31 | 32 | script: 33 | - make test && make test_coverage 34 | -------------------------------------------------------------------------------- /chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: tesoro 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | appVersion: latest 24 | -------------------------------------------------------------------------------- /codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master, ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 22 * * 1' 11 | 12 | jobs: 13 | analyse: 14 | name: Analyse 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | with: 21 | # We must fetch at least the immediate parents so that if this is 22 | # a pull request then we can checkout the head. 23 | fetch-depth: 2 24 | 25 | # If this run was triggered by a pull request event, then checkout 26 | # the head of the pull request instead of the merge commit. 27 | - run: git checkout HEAD^2 28 | if: ${{ github.event_name == 'pull_request' }} 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v1 33 | # Override language selection by uncommenting this and choosing your languages 34 | with: 35 | languages: python 36 | 37 | - name: Perform CodeQL Analysis 38 | uses: github/codeql-action/analyze@v1 39 | -------------------------------------------------------------------------------- /chart/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for tesoro. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: kapicorp/tesoro 9 | pullPolicy: Always 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: "" 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | fullnameOverride: "tesoro-admission-controller" 16 | 17 | podAnnotations: {} 18 | 19 | podSecurityContext: {} 20 | # fsGroup: 2000 21 | 22 | securityContext: {} 23 | # capabilities: 24 | # drop: 25 | # - ALL 26 | # readOnlyRootFilesystem: true 27 | # runAsNonRoot: true 28 | # runAsUser: 1000 29 | 30 | secrets: [] 31 | env: {} 32 | 33 | probes: {} 34 | 35 | resources: {} 36 | # We usually recommend not to specify default resources and to leave this as a conscious 37 | # choice for the user. This also increases chances charts run on environments with little 38 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 39 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 40 | # limits: 41 | # cpu: 100m 42 | # memory: 128Mi 43 | # requests: 44 | # cpu: 100m 45 | # memory: 128Mi 46 | 47 | nodeSelector: {} 48 | 49 | tolerations: [] 50 | 51 | affinity: {} 52 | 53 | -------------------------------------------------------------------------------- /chart/templates/mutatingwebhook_bundle.yaml: -------------------------------------------------------------------------------- 1 | {{ $ca := genCA "tesoro-admission-controller-ca" 3650 }} 2 | {{ $cn := printf "tesoro-admission-controller.%s.svc" .Release.Namespace }} 3 | {{ $server := genSignedCert $cn nil nil 365 $ca }} 4 | --- 5 | apiVersion: admissionregistration.k8s.io/v1beta1 6 | kind: MutatingWebhookConfiguration 7 | metadata: 8 | name: {{ include "tesoro.fullname" . }} 9 | labels: 10 | {{- include "tesoro.labels" . | nindent 4 }} 11 | webhooks: 12 | - name: {{ include "tesoro.fullname" . }}.tesoro.svc 13 | failurePolicy: Fail 14 | objectSelector: 15 | matchLabels: 16 | tesoro.kapicorp.com: enabled 17 | clientConfig: 18 | service: 19 | name: {{ include "tesoro.fullname" . }} 20 | namespace: {{ .Release.Namespace }} 21 | path: "/mutate" 22 | caBundle: {{ b64enc $ca.Cert }} 23 | rules: 24 | - operations: 25 | - CREATE 26 | - UPDATE 27 | apiGroups: 28 | - "" 29 | resources: 30 | - "*" 31 | apiVersions: 32 | - "*" 33 | - operations: 34 | - CREATE 35 | - UPDATE 36 | apiGroups: 37 | - "apps" 38 | resources: 39 | - "deployments" 40 | apiVersions: 41 | - "*" 42 | --- 43 | apiVersion: v1 44 | kind: Secret 45 | metadata: 46 | name: {{ include "tesoro.fullname" . }} 47 | labels: 48 | {{- include "tesoro.labels" . | nindent 4 }} 49 | type: Opaque 50 | data: 51 | cert.pem: {{ b64enc $server.Cert }} 52 | priv.key: {{ b64enc $server.Key }} 53 | -------------------------------------------------------------------------------- /chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "tesoro.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "tesoro.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 "tesoro.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "tesoro.labels" -}} 37 | helm.sh/chart: {{ include "tesoro.chart" . }} 38 | {{ include "tesoro.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "tesoro.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "tesoro.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "tesoro.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "tesoro.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /tesoro/patch.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from hashlib import sha256 3 | import json 4 | import jsonpatch 5 | import logging 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | def annotate_patch(patch): 11 | """ 12 | if patch not empty, annotates patch with list of revealed paths 13 | e.g. 'tesoro.kapicorp.com/revealed: ["/spec/templates/key1"]' 14 | """ 15 | revealed_paths = [] 16 | 17 | for p in patch: 18 | path = p.get("path") 19 | if path: 20 | revealed_paths.append(path) 21 | 22 | if revealed_paths: 23 | patch.append( 24 | { 25 | "op": "add", 26 | "path": "/metadata/annotations/tesoro.kapicorp.com~1revealed", 27 | "value": json.dumps(revealed_paths), 28 | } 29 | ) 30 | 31 | 32 | def make_patch(req_uid, src_json, dst_json): 33 | "returns jsonpatch for diff between src_json and dst_json" 34 | patch = jsonpatch.make_patch(src_json, dst_json) 35 | patch = patch.patch 36 | 37 | last_applied = "/metadata/annotations/" "kubectl.kubernetes.io~1last-applied-configuration" 38 | 39 | # remove last_applied from patch if found (meaning it was revealed) 40 | # as we don't want to interfer with previous state 41 | for idx, patch_item in enumerate(patch): 42 | if patch_item["path"] == last_applied: 43 | patch.pop(idx) 44 | logger.debug( 45 | 'message="Removed last-applied-configuration annotation from patch", req_uid=%s', req_uid 46 | ) 47 | 48 | return patch 49 | 50 | 51 | def redact_patch(patch): 52 | "returns a copy of patch with redacted values" 53 | redacted_patch = deepcopy(patch) 54 | 55 | for patch_item in redacted_patch: 56 | # don't redact this annotation 57 | if patch_item["path"] == "/metadata/annotations/tesoro.kapicorp.com~1revealed": 58 | continue 59 | value_hash = sha256(patch_item["value"].encode()).hexdigest() 60 | patch_item["value"] = f"!REDACTED VALUE! sha256={value_hash}" 61 | 62 | return redacted_patch 63 | -------------------------------------------------------------------------------- /tesoro/transform.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | from base64 import b64decode, b64encode 4 | 5 | from kapitan.refs.base import REF_TOKEN_TAG_PATTERN 6 | from tesoro import REF_CONTROLLER 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def prepare_obj(req_uid, req_obj): 13 | """ 14 | updates object and returns transformation operations 15 | on specific object kinds to perform post reveal 16 | """ 17 | transformations = {} 18 | obj_kind = req_obj["kind"] 19 | if obj_kind == "Secret": 20 | secret_name = req_obj["metadata"]["name"] 21 | transformations["Secret"] = {"data": {}} 22 | for item_name, item_value in req_obj["data"].items(): 23 | decoded_ref = b64decode(item_value).decode() 24 | 25 | is_valid_ref = re.match(REF_TOKEN_TAG_PATTERN, decoded_ref) 26 | if not is_valid_ref: 27 | continue # this is not a ref, do nothing 28 | else: 29 | logger.debug( 30 | 'message="Secret transformation", request_uid=%s, secret_name=%s, decoded_ref=%s', 31 | req_uid, 32 | secret_name, 33 | decoded_ref, 34 | ) 35 | # peek and register ref's encoding 36 | ref_obj = REF_CONTROLLER[decoded_ref] 37 | transformations["Secret"]["data"][item_name] = {"encoding": ref_obj.encoding} 38 | # override with ref so we can reveal 39 | req_obj["data"][item_name] = decoded_ref 40 | 41 | return transformations 42 | 43 | 44 | def transform_obj(req_obj, transformations): 45 | "updates req_obj with transformations" 46 | secret_tranformations = transformations.get("Secret", {}) 47 | secret_data_items = secret_tranformations.get("data", {}).items() 48 | for item_name, transform in secret_data_items: 49 | encoding = transform.get("encoding", None) 50 | if encoding == "original": 51 | item_value_encoded = b64encode(req_obj["data"][item_name].encode()).decode() 52 | req_obj["data"][item_name] = item_value_encoded 53 | -------------------------------------------------------------------------------- /tesoro/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from asyncio import Lock 4 | import argparse 5 | import logging 6 | import ssl 7 | from functools import partial 8 | 9 | from aiohttp import web 10 | from aiohttp.log import access_logger 11 | 12 | from tesoro.handlers import healthz_handler, mutate_handler 13 | from tesoro.metrics import prom_http_server 14 | from tesoro.utils import setup_logging 15 | 16 | setup_logging(kapitan_debug=False) 17 | logger = logging.getLogger() 18 | 19 | parser = argparse.ArgumentParser(description=("Tesoro" " - Kapitan Admission Controller")) 20 | parser.add_argument("--verbose", action="store_true", default=False) 21 | parser.add_argument("--verbose-no-redact", action="store_true", default=False) 22 | parser.add_argument("--verbose-kapitan", action="store_true", default=False) 23 | parser.add_argument("--access-log", action="store_true", default=False) 24 | parser.add_argument("--port", action="store", type=int, default=8080) 25 | parser.add_argument("--host", action="store", default="0.0.0.0") 26 | parser.add_argument("--cert-file", action="store", default=None) 27 | parser.add_argument("--key-file", action="store", default=None) 28 | parser.add_argument("--ca-file", action="store", default=None) 29 | parser.add_argument("--ca-path", action="store", default=None) 30 | parser.add_argument("--metrics-port", action="store", type=int, default=9095) 31 | parser.add_argument("--metrics-host", action="store", default="0.0.0.0") 32 | args = parser.parse_args() 33 | logger.info("Starting tesoro with args: %s", args) 34 | 35 | if args.verbose: 36 | setup_logging(level=logging.DEBUG, kapitan_debug=args.verbose_kapitan) 37 | logger.debug("Logging level set to DEBUG") 38 | 39 | reveal_lock = Lock() 40 | app = web.Application() 41 | app.add_routes( 42 | [ 43 | web.get("/healthz", healthz_handler), 44 | web.post( 45 | "/mutate", 46 | partial(mutate_handler, reveal_lock=reveal_lock, log_redact_patch=(not args.verbose_no_redact)), 47 | ), 48 | ] 49 | ) 50 | 51 | ssl_ctx = None 52 | if None not in (args.key_file, args.cert_file): 53 | ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH, cafile=args.ca_file, capath=args.ca_path) 54 | ssl_ctx.load_cert_chain(args.cert_file, args.key_file) 55 | 56 | access_log = None 57 | if args.access_log: 58 | access_log = access_logger 59 | 60 | prom_http_server(args.metrics_port, args.metrics_host) 61 | web.run_app(app, host=args.host, port=args.port, ssl_context=ssl_ctx, access_log=access_log) 62 | -------------------------------------------------------------------------------- /tesoro/utils.py: -------------------------------------------------------------------------------- 1 | from asyncio import get_running_loop 2 | import concurrent.futures 3 | from tesoro import REVEALER 4 | from tesoro.metrics import REVEAL_RETRY_COUNTER 5 | from sys import exc_info 6 | import logging 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def kapicorp_labels(req_uid, req_obj): 12 | "returns kapicorp labels dict for req_obj" 13 | labels = {} 14 | try: 15 | for label_key, label_value in req_obj["metadata"]["labels"].items(): 16 | if label_key.startswith("tesoro.kapicorp.com"): 17 | labels[label_key] = label_value 18 | except KeyError: 19 | logger.debug('message="Tesoro label not found", request_uid=%s', req_uid) 20 | return labels 21 | 22 | return labels 23 | 24 | 25 | async def run_blocking(func, lock=None): 26 | "run blocking function in async executor" 27 | loop = get_running_loop() 28 | retval = None 29 | if lock: 30 | async with lock: 31 | retval = await loop.run_in_executor(None, func) 32 | return retval 33 | else: 34 | return await loop.run_in_executor(None, func) 35 | 36 | 37 | def kapitan_reveal_json(req_uid, json_doc, retries=3): 38 | "return revealed object, total revealed tags (TODO)" 39 | for retry in range(retries): 40 | try: 41 | return REVEALER.reveal_obj(json_doc) 42 | except Exception as e: 43 | exc_type, exc_value, _ = exc_info() 44 | if retry + 1 <= retries: 45 | logger.error( 46 | 'message="Kapitan reveal failed, retrying", request_uid=%s, ' 47 | 'retry="%d of %d", exception_type=%s, error="%s"', 48 | req_uid, 49 | retry + 1, 50 | retries, 51 | exc_type, 52 | exc_value, 53 | ) 54 | REVEAL_RETRY_COUNTER.inc() 55 | continue 56 | raise 57 | 58 | 59 | def setup_logging(level=logging.INFO, kapitan_debug=False): 60 | "setup logging, set kapitan_debug to True for kapitan debug logging (dangerous)" 61 | for name, logger in logging.root.manager.loggerDict.items(): 62 | if name.startswith("kapitan."): 63 | logger.disabled = not kapitan_debug 64 | 65 | logging.basicConfig( 66 | format="%(asctime)s %(levelname)-8s %(message)s", level=level, datefmt="%Y-%m-%d %H:%M:%S" 67 | ) 68 | logging.getLogger("tesoro").setLevel(level) 69 | 70 | 71 | class KapitanRevealFail(Exception): 72 | pass 73 | -------------------------------------------------------------------------------- /chart/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ include "tesoro.fullname" . }} 6 | labels: 7 | {{- include "tesoro.labels" . | nindent 4 }} 8 | spec: 9 | replicas: {{ .Values.replicaCount }} 10 | selector: 11 | matchLabels: 12 | {{- include "tesoro.selectorLabels" . | nindent 6 }} 13 | template: 14 | metadata: 15 | {{- with .Values.podAnnotations }} 16 | annotations: 17 | {{- toYaml . | nindent 8 }} 18 | {{- end }} 19 | labels: 20 | {{- include "tesoro.selectorLabels" . | nindent 8 }} 21 | spec: 22 | {{- with .Values.imagePullSecrets }} 23 | imagePullSecrets: 24 | {{- toYaml . | nindent 8 }} 25 | {{- end }} 26 | securityContext: 27 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 28 | containers: 29 | - name: {{ .Chart.Name }} 30 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 31 | imagePullPolicy: {{ .Values.image.pullPolicy }} 32 | ports: 33 | - containerPort: 443 34 | name: tesoro-api 35 | - containerPort: 9095 36 | name: metrics 37 | args: 38 | - --port=443 39 | - --cert-file=/certs/cert.pem 40 | - --key-file=/certs/priv.key 41 | env: 42 | {{- range $index, $value := .Values.env }} 43 | - name: {{ $index }} 44 | value: {{ $value | quote }} 45 | {{- end }} 46 | envFrom: 47 | {{- range .Values.secrets }} 48 | - secretRef: 49 | name: {{.}} 50 | optional: false 51 | {{- end }} 52 | {{- with .Values.probes }} 53 | {{- toYaml . | nindent 10 }} 54 | {{- end }} 55 | {{- with .Values.resources }} 56 | resources: 57 | {{- toYaml . | nindent 12 }} 58 | {{- end }} 59 | volumeMounts: 60 | - name: tesoro-secrets 61 | mountPath: /certs 62 | readOnly: true 63 | volumes: 64 | - name: tesoro-secrets 65 | secret: 66 | secretName: {{ include "tesoro.fullname" . }} 67 | {{- with .Values.nodeSelector }} 68 | nodeSelector: 69 | {{- toYaml . | nindent 8 }} 70 | {{- end }} 71 | {{- with .Values.affinity }} 72 | affinity: 73 | {{- toYaml . | nindent 8 }} 74 | {{- end }} 75 | {{- with .Values.tolerations }} 76 | tolerations: 77 | {{- toYaml . | nindent 8 }} 78 | {{- end }} 79 | -------------------------------------------------------------------------------- /k8s/tesoro_secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: tesoro-admission-controller-secret 5 | type: Opaque 6 | stringData: 7 | # XXX Change me to your certs/cert.pem file 8 | cert.pem: | 9 | -----BEGIN CERTIFICATE----- 10 | MIID4TCCAckCCQCQYwrWE6n2hTANBgkqhkiG9w0BAQsFADA0MTIwMAYDVQQDDClD 11 | QS10ZXNvcm8tYWRtaXNzaW9uLWNvbnRyb2xsZXIudGVzb3JvLnN2YzAeFw0yMDA1 12 | MjExNDUyNTJaFw0yMTEwMDMxNDUyNTJaMDExLzAtBgNVBAMMJnRlc29yby1hZG1p 13 | c3Npb24tY29udHJvbGxlci50ZXNvcm8uc3ZjMIIBIjANBgkqhkiG9w0BAQEFAAOC 14 | AQ8AMIIBCgKCAQEA0q/DCOq2Sli7/O+vnwKMZ104V4TtFYsZx2g/tn9H/oZVmqX4 15 | FQg462v9A5cB3TxEgnTJV77Lad+lHFWGwCuzxEounf5o9RLC8Zy+7XzDYAhCok8k 16 | DjdnfU3nEI6axtSE3r1gEWlVAeunmyNu9vounHcJcRmbw9X6nlhXV/lRPnJQhLCC 17 | wy+mGQHQhezSVJ8fg1lRvNmGBI3WFrhBt/iuJd/Us7z2Qcu75E8P4OHdbQtRZ/Sc 18 | rNmto+OPdCFVfG1gw/jtEw4yf9fvxg//nFFJt1MJyh3YezZefx0oESC0CtZxpLMl 19 | ij37KFRmL4zvB2t2DNoL86wF6k9VL7Ysot1TJwIDAQABMA0GCSqGSIb3DQEBCwUA 20 | A4ICAQACYy7+sHoIxIBUsl65iL+uc7f7iIF3huI5U+Z43wA2MIxEZpz3nUCJVjER 21 | XiC7oymomu67WM+u2GODj22rQfMc0ZM0QWGm7wZcMGn//WasLk4obxn6JtHV+pip 22 | 77DT7LJ9+qEeXp7LrUcTKS+EqcO5GDzySWxhRCQOVXTOuh8/PeKEl2vz6zMZSEOk 23 | AxeQdpHVQqb6jxl38cvSl7n7hSqS2EvRwvASSURt0BMuD6qutYS4c43djnrZ/sBE 24 | ajya8X9LS98VGp8hZ1BkzmGvkCPchqGoyQjsWniypcb+DFnkVh5WuPHxhGYEX0U9 25 | FcZrEDzRsHF9IhHV1NUPGZWnTqy+EE4Hs9YWSQZAnTVIX+aU38XCFd6NeFsvtGF1 26 | 22TA6pMwblyCRG9EYDftBboHCrULUlVx4xmfapb15/kuqT2Pj2M1aT/S1WrN38zt 27 | NiYZDVQInk0UQKkEBJFSNN1XBNCR10CkT4/cxT3d/J+uUm7xOrkbpeZwAnmxaWTU 28 | iWZGYwS21baoKu0ByROFNX2i29ainxOqEaHhgDaERxdXn95WvATyhRPaSn6xV0Eb 29 | W48XW4MIOyFzchclTTCzqmoNllsqx6Y2fgxs2dYj6fQU+1PpSDpeIOk6YDwdNeT/ 30 | QKKiVqXRR4IkqkfT2MVU+q36TtssR7HY70R5FwqZ+cgf/VMB9A== 31 | -----END CERTIFICATE----- 32 | # XXX Change me to your certs/priv.pem file 33 | priv.key: | 34 | -----BEGIN RSA PRIVATE KEY----- 35 | MIIEowIBAAKCAQEA0q/DCOq2Sli7/O+vnwKMZ104V4TtFYsZx2g/tn9H/oZVmqX4 36 | FQg462v9A5cB3TxEgnTJV77Lad+lHFWGwCuzxEounf5o9RLC8Zy+7XzDYAhCok8k 37 | DjdnfU3nEI6axtSE3r1gEWlVAeunmyNu9vounHcJcRmbw9X6nlhXV/lRPnJQhLCC 38 | wy+mGQHQhezSVJ8fg1lRvNmGBI3WFrhBt/iuJd/Us7z2Qcu75E8P4OHdbQtRZ/Sc 39 | rNmto+OPdCFVfG1gw/jtEw4yf9fvxg//nFFJt1MJyh3YezZefx0oESC0CtZxpLMl 40 | ij37KFRmL4zvB2t2DNoL86wF6k9VL7Ysot1TJwIDAQABAoIBAQCLLVp8oHhNQrLk 41 | kv2D4C4Z9iXLAt+PNLWR2dcE2q4XgGw6Xds1IZpgQpUAcxfjKvXi6/05vqsnRHsQ 42 | 7Zq3hecaCxsiebah9oPbEf26aomx/aLvD7K0xXrS9sxmTp4GLMudTKKSve9O9npg 43 | 7JZYBssVxzjwgsc4JjmJsrf4QJWf1rYQRLAxDkzYdi7dmp5RdU5O5Xpr4TmWpy4b 44 | sAn4U4vuYPnVi5edl+fd0yAqpE0lpdZXRpfWdxb2MdqluAqi5cwuxuJqBR63TVc3 45 | yq7rUhxrzWWCOCoMlJn9GcIJIgysmnI1/PnLjyguWuCl6UIGxNl21W8PBv3W3xgy 46 | +T2/+37ZAoGBAP83D+5CnSf3NMpX8mFTrJX7h0caq46SO3bIJ1RCbojqhGA6Y4ID 47 | Skf9IvszQSrU0jLmLro9ocA+zsl/mbrc5iva9/T2yBWmLGc0jFfj5qD6pV3cCmww 48 | 7a/jHRzpfqJuIydVhTl8RLXBYAhqTeuRXVgmcZ66u7VlLPA8DWzRw669AoGBANNV 49 | pCASqqqbSY3abYlM8i8OtSjcEmP9a1PP+0E5KzOefhZZwLEN63Y4+zbUDIUlZpj+ 50 | APNkpWbWoJ8Mg/WZGRKrW7rNkOqLUeLnTbLuiLYmDv04kPtzPaOGHe8Y66jfrCj5 51 | Wqmg5K1OSk8EsHtyFo847t6liMZ3KW/BOpUIk4mzAoGAFHTQZROXzy1EYPedqOlx 52 | CvCHTk6384yLEaFHbbyJaZYKmD/12+TxnkWO+T2JeqpmUsgadLkd7u/Z2VLN+qlc 53 | FaXOfCcPsJSKOJeArEbLJJoSiSxq9XMSiieVHM0mZlSXor57GDl6tGrUjLggmMIl 54 | Djy+nK9w/kAm0vatSMyjd60CgYBcLvAvmVTepGv6N3wuNuUxR54YG57eBM3LOl6j 55 | vbcthgYD49ScprC92e8ipG5tqbnOJIXvpUhMynQ3zHLHL6fzL9IMctyyomDus+x9 56 | j3UM1x8Ur5b2R/dsG/V0tQ8ek6p6GQLGGUuGmS7qtuXwgvx6LuBrOBOoVpQ5jjTq 57 | i1on5wKBgE10c34Ur31p7AUCE4f/LBBDjT59Y82ZPgSmq8Z41Ij7Yi3tLDV23KQh 58 | RvsqNMknHDoQCj/+oZ1C1gCkSW/SjdHDYH/ooH1+Zyt6+mOuNOV7VIHwxV3+LdGh 59 | vSlUjJcfhtPt89+byT7AfUoSibdrWvRugMjqW0ShWmigVFuOMWc0 60 | -----END RSA PRIVATE KEY----- 61 | -------------------------------------------------------------------------------- /tests/test_tranform.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from base64 import b64encode 3 | from tesoro import REF_CONTROLLER 4 | from tesoro.transform import prepare_obj, transform_obj 5 | 6 | 7 | class TestPreprare(unittest.TestCase): 8 | def test_prepare_obj_k8s_secret(self): 9 | ref_tag = ( 10 | "?{base64:eyJkYXRhIjogImNtVm1JREVnWkdGMFlRP" 11 | "T0iLCAiZW5jb2RpbmciOiAib3JpZ2luYWwiLCAidHl" 12 | "wZSI6ICJiYXNlNjQifQ==:embedded}" 13 | ) 14 | k8s_obj = { 15 | "apiVersion": "v1", 16 | "kind": "Secret", 17 | "metadata": { 18 | "name": "some-secret", 19 | "labels": {"tesoro.kapicorp.com": "enabled"}, 20 | }, 21 | "type": "Opaque", 22 | "data": { 23 | "file1": b64encode(bytes(ref_tag.encode())), 24 | }, 25 | } 26 | transformations = prepare_obj("request_uid", k8s_obj) 27 | 28 | self.assertEqual(transformations, {"Secret": {"data": {"file1": {"encoding": "original"}}}}) 29 | self.assertEqual(k8s_obj["data"]["file1"], ref_tag) 30 | 31 | def test_prepare_obj_k8s_other_obj(self): 32 | k8s_obj = { 33 | "apiVersion": "v1", 34 | "kind": "NotAsecret", 35 | } 36 | transformations = prepare_obj("request_uid", k8s_obj) 37 | 38 | self.assertEqual(transformations, {}) 39 | 40 | 41 | class TestTransform(unittest.TestCase): 42 | def test_transform_obj_k8s_secret_original_encoding(self): 43 | # base64 tag with encoding: original 44 | ref_tag = ( 45 | "?{base64:eyJkYXRhIjogImNtVm1JREVnWkdGMFlRP" 46 | "T0iLCAiZW5jb2RpbmciOiAib3JpZ2luYWwiLCAidHl" 47 | "wZSI6ICJiYXNlNjQifQ==:embedded}" 48 | ) 49 | k8s_obj = { 50 | "apiVersion": "v1", 51 | "kind": "Secret", 52 | "metadata": { 53 | "name": "some-secret", 54 | "labels": {"tesoro.kapicorp.com": "enabled"}, 55 | }, 56 | "type": "Opaque", 57 | "data": { 58 | "file1": b64encode(bytes(ref_tag.encode())), 59 | }, 60 | } 61 | transformations = prepare_obj("request_uid", k8s_obj) 62 | # reveal base64_ref 63 | ref_obj = REF_CONTROLLER[ref_tag] 64 | ref_obj_revealed = ref_obj.reveal() 65 | k8s_obj["data"]["file1"] = ref_obj_revealed 66 | 67 | transform_obj(k8s_obj, transformations) 68 | 69 | self.assertEqual(k8s_obj["data"]["file1"], b64encode(ref_obj_revealed.encode()).decode()) 70 | 71 | def test_transform_obj_k8s_secret_base64_encoding(self): 72 | # base64 tag with encoding: base64 - needs kapitan 0.28+ 73 | ref_tag = ( 74 | "?{base64:eyJkYXRhIjogIllVZFdjMkpIT0QwPSIsICJlbmNvZGluZyI" 75 | "6ICJiYXNlNjQiLCAidHlwZSI6ICJiYXNlNjQifQ==:embedded}" 76 | ) 77 | k8s_obj = { 78 | "apiVersion": "v1", 79 | "kind": "Secret", 80 | "metadata": { 81 | "name": "some-secret", 82 | "labels": {"tesoro.kapicorp.com": "enabled"}, 83 | }, 84 | "type": "Opaque", 85 | "data": { 86 | "file1": b64encode(bytes(ref_tag.encode())), 87 | }, 88 | } 89 | transformations = prepare_obj("request_uid", k8s_obj) 90 | # reveal base64_ref 91 | ref_obj = REF_CONTROLLER[ref_tag] 92 | ref_obj_revealed = ref_obj.reveal() 93 | k8s_obj["data"]["file1"] = ref_obj_revealed 94 | 95 | transform_obj(k8s_obj, transformations) 96 | 97 | self.assertEqual(k8s_obj["data"]["file1"], ref_obj_revealed) 98 | -------------------------------------------------------------------------------- /k8s/tesoro_mutatingwebhook.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: admissionregistration.k8s.io/v1beta1 3 | kind: MutatingWebhookConfiguration 4 | metadata: 5 | name: tesoro-admission-controller 6 | webhooks: 7 | - name: tesoro-admission-controller.tesoro.svc 8 | failurePolicy: Fail 9 | objectSelector: 10 | matchLabels: 11 | tesoro.kapicorp.com: enabled 12 | clientConfig: 13 | service: 14 | name: tesoro-admission-controller 15 | namespace: tesoro 16 | path: "/mutate" 17 | # XXX Change to your base64'd certs/rootCA.crt file 18 | caBundle: | 19 | LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZQakNDQXlhZ0F3SUJBZ0lKQUxQdlV2a0Ns 20 | TG8xTUEwR0NTcUdTSWIzRFFFQkN3VUFNRFF4TWpBd0JnTlYKQkFNTUtVTkJMWFJsYzI5eWJ5MWha 21 | RzFwYzNOcGIyNHRZMjl1ZEhKdmJHeGxjaTUwWlhOdmNtOHVjM1pqTUI0WApEVEl3TURVeU1URTBO 22 | VEkxTWxvWERUSXpNRE14TVRFME5USTFNbG93TkRFeU1EQUdBMVVFQXd3cFEwRXRkR1Z6CmIzSnZM 23 | V0ZrYldsemMybHZiaTFqYjI1MGNtOXNiR1Z5TG5SbGMyOXlieTV6ZG1Nd2dnSWlNQTBHQ1NxR1NJ 24 | YjMKRFFFQkFRVUFBNElDRHdBd2dnSUtBb0lDQVFDNVJJS29JdCtZT2FVTVVISlBOUk5QOGNjWnNv 25 | MHp2T29VUE9OZwpLdFo1Q1BkS3cvMkNCUlVhQjFZRERXY2xQUHgyOUp3UEdJbEFtWWh3ZG91M3lJ 26 | NGNRZHcxT3hHWWR6bGpOdXlqCjlyUHhvaXdJSDVENmd3UTJDU2wra0JJbzdZblpwVlVyK1gxWUNZ 27 | Z0VmZVU3M3gyZWJGK3p3K043ejJYeTlXbjQKYU8yclhxZ0lRTGFkeVlhUE03Rkd1ZnFhUWt2dDY1 28 | VW1nbUx2RXl0K3R4YTVnbkxtbDROZVVxSjMyMnUwQ3NocwpDc2htRFVscUFQLzVpbENwL0hSVDgr 29 | ZU5WRnRKcm8vQm5qTXNXZnFJZGxkQXI3dS96THdVaVdoZnBBZEkwZk1uClJQYkZpRk1SU1FFR1l5 30 | c0MvdnV2YVlJR2g3aFBKY01YYTRiaG5UMlhXUTBGRU9FUWJaZlJxK3BSbjZ6ZHYrUm8KbHdDaUVM 31 | ZUlQVnAwc0VMaTNwTXArMWhydlJkcEhHRDdaemgzT25IRENNY1pXVEtIQW0wc0c0QVhsSUYwQTVN 32 | Ngpmelh1bm5TNHdGK2s3dmFRbmR1Z1pqVTVSaDh6NVBiM2FGZ3ErVzYweVRxYkx5TTJreHFBUm80 33 | TEtXT082azVoCjIxQWZhNmpjVEZYb0I3Z281Kzc4ei9zdXZvNUNuQXdyTjdXQWE5YVEycDRvd0E1 34 | cFVUek1oempqclVUYXhZMFYKNmNVUnZFS1dic09sSWRwOHljQlVVeG5ZSlhJSWIrNDJST1F4cXo1 35 | UzRubG1oMWlpU1ZvYlExcDh3RDNyZXpoMApocWJNS1ZDbVQ4b2ExOHhWWlhBcWJsZGFNQVc4ZW16 36 | Q2x3Unl2elF2Rm1uY0JGclNFY3d5a2x4QnBwZklRTnZDCnVncEIzd0lEQVFBQm8xTXdVVEFkQmdO 37 | VkhRNEVGZ1FVcUk4TzN5Rk8xYXVKSkVJbHd3NlM2dCtqb3JFd0h3WUQKVlIwakJCZ3dGb0FVcUk4 38 | TzN5Rk8xYXVKSkVJbHd3NlM2dCtqb3JFd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBTgpCZ2txaGtp 39 | Rzl3MEJBUXNGQUFPQ0FnRUFqeFdYZVF3d05IamhvQTZCRzhBTFpXN3BEZE5RTWFyd05MOE9CK3l0 40 | ClNVYXpxdEZBbjNvbm9vbzlySXE1bEN6REN1OVhuZU1JMlBVVHN1dktUeVJIZE5IWFUyU2RrZmlK 41 | QXhrTHhuNjAKNkZBVWh2b0tyWUoxNE53L2p6d1VSNUcrSjU2NEVvc3FaeGI4MEdYMGkxSHlVZTdD 42 | Y2UwRFpGUUlmaWswSHc2QwpadnVLbnN4UFdOTnZnbVdqSGZwbWJ1djJNbThsYmN1VldBNlN5ekQr 43 | TG1DUXBGNWJJOW8ycm5HQ2tZWTlwTlRsClEzYU5IZ1U5Z3FXdnV2Qkpia2UxaWttdHF2Z1RKTXFu 44 | NUIzMDBDUzVlRldqVVdjODhHWmRLbkovQm9YT3g3RWQKK0JoTFkvUTRLNUdVaVFhSjVhTVlCWnpZ 45 | SmhIR2R1U3VFdDJVMEFWOWxWSysvSTJtR25Rb0xKV0tDMWQ2Q0x2MwpLaWVZRXFURWV4Y09XODUy 46 | aVVRQXkrOWdLcndXR0FaWXYxbmk0TXlHK1RORG9rNzJSQTVJa0FHYWRuM3k0SHNlCktMa1NteUF5 47 | TUVWZzJWL2FrNU1HSVFZa1pWc1RVZTVUV0F6dnlQbndITk96MGNzaTJ1RE1xQi9NVlJCNFQxK1kK 48 | QURHZnptajBGK2ZMRkNEeC9Kb3YwYjVCVTBVbHVtZDhCZ3F2bFNsdEpJd1E4TjJBdzgzWE5Ubm00 49 | Rnc5eDUzWQpQekIvQWFydVVzbFZpVVVRRWozTzBEc2FpTHd5bHlqd0dBb0Z6MkxuMWorSmRSNkU2 50 | alovMFRNMWFtaGdYODhBCm1uTUhjd3lCcEJMU1NtMHYxbmxvNnhHQXN6alYrcWY4MXhlYXdudy83 51 | TE1tb2NmS0pHTlZOWm9OZEw2WEcvUGsKRXlnPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== 52 | rules: 53 | - operations: 54 | - CREATE 55 | - UPDATE 56 | apiGroups: 57 | - "" 58 | resources: 59 | - "*" 60 | apiVersions: 61 | - "*" 62 | - operations: 63 | - CREATE 64 | - UPDATE 65 | apiGroups: 66 | - "apps" 67 | resources: 68 | - "deployments" 69 | apiVersions: 70 | - "*" 71 | -------------------------------------------------------------------------------- /tesoro/handlers.py: -------------------------------------------------------------------------------- 1 | from base64 import b64encode 2 | from copy import deepcopy 3 | import json 4 | import logging 5 | from sys import exc_info 6 | from aiohttp import web 7 | from tesoro.metrics import TESORO_COUNTER, TESORO_FAILED_COUNTER, REVEAL_COUNTER, REVEAL_FAILED_COUNTER 8 | from tesoro.patch import make_patch, annotate_patch, redact_patch 9 | from tesoro.transform import prepare_obj, transform_obj 10 | from tesoro.utils import kapicorp_labels, run_blocking, kapitan_reveal_json, KapitanRevealFail 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | async def healthz_handler(request): 16 | return web.Response(status=200, text="ok") 17 | 18 | 19 | async def mutate_handler(request, reveal_lock=None, log_redact_patch=True): 20 | TESORO_COUNTER.inc() 21 | req_obj = {} 22 | req_uid = None 23 | req_namespace: None 24 | req_kind: None 25 | req_resource: None 26 | 27 | try: 28 | req_json = await request.json() 29 | req_uid = req_json["request"]["uid"] 30 | req_namespace = req_json["request"]["namespace"] 31 | req_kind = req_json["request"]["kind"] 32 | req_resource = req_json["request"]["resource"] 33 | req_obj = req_json["request"]["object"] 34 | req_obj_name = req_obj["metadata"]["name"] 35 | 36 | except json.decoder.JSONDecodeError: 37 | TESORO_FAILED_COUNTER.inc() 38 | logger.error('message="Invalid JSON on request"') 39 | return web.Response(status=500, reason="Request not JSON") 40 | except KeyError as e: 41 | TESORO_FAILED_COUNTER.inc() 42 | logger.error('message="Missing JSON objects on request", request_uid=%s, missing_key=%s', req_uid, e) 43 | return web.Response(status=500, reason="Invalid JSON request") 44 | 45 | labels = kapicorp_labels(req_uid, req_obj) 46 | logger.info( 47 | 'message="New request", request_uid=%s, object_name=%s, namespace=%s, kind=%s', 48 | req_uid, 49 | req_obj_name, 50 | req_namespace, 51 | req_kind, 52 | ) 53 | 54 | if labels.get("tesoro.kapicorp.com", None) == "enabled": 55 | try: 56 | logger.debug( 57 | 'message="Request detail", request_uid=%s, namespace=%s, kind="%s", object_name=%s, resource="%s"', 58 | req_uid, 59 | req_namespace, 60 | req_kind, 61 | req_obj_name, 62 | req_resource, 63 | ) 64 | req_copy = deepcopy(req_obj) 65 | 66 | transformations = prepare_obj(req_uid, req_copy) 67 | logger.debug( 68 | 'message="Transformations", request_uid=%s, transformations="%s"', req_uid, transformations 69 | ) 70 | 71 | reveal_req_func = lambda: kapitan_reveal_json(req_uid, req_copy) 72 | req_revealed = await run_blocking(reveal_req_func, lock=reveal_lock) 73 | if req_revealed is None: 74 | raise KapitanRevealFail("revealed object is None") 75 | 76 | transform_obj(req_revealed, transformations) 77 | patch = make_patch(req_uid, req_obj, req_revealed) 78 | annotate_patch(patch) 79 | REVEAL_COUNTER.inc() 80 | if log_redact_patch: 81 | logger.debug( 82 | 'message="Kapitan reveal successful", request_uid=%s, patch="%s"', 83 | req_uid, 84 | redact_patch(patch), 85 | ) 86 | else: 87 | logger.debug( 88 | 'message="Kapitan reveal successful", request_uid=%s, allowed with patch="%s"', 89 | req_uid, 90 | patch, 91 | ) 92 | logger.info('message="Kapitan reveal successful", request_uid=%s', req_uid) 93 | 94 | return make_response(req_uid, patch, allow=True) 95 | except Exception as e: 96 | exc_type, exc_value, _ = exc_info() 97 | logger.error( 98 | 'message="Kapitan reveal failed", request_uid=%s, exception_type=%s, error=%s', 99 | req_uid, 100 | exc_type, 101 | exc_value, 102 | ) 103 | REVEAL_FAILED_COUNTER.inc() 104 | return make_response(req_uid, [], allow=False, message="Kapitan reveal failed") 105 | else: 106 | # not labelled, default allow 107 | logger.info('message="Tesoro label not found", request_uid=%s', req_uid) 108 | return make_response(req_uid, [], allow=True) 109 | 110 | TESORO_FAILED_COUNTER.inc() 111 | logger.error('message="Unknown error", request_uid=%s', req_uid) 112 | return web.Response(status=500, reason="Unknown error") 113 | 114 | 115 | def make_response(uid, patch, allow=False, message=""): 116 | "returns new response with patch, allow and message" 117 | response = {"response": {"uid": uid, "allowed": allow}} 118 | 119 | if allow and (patch != []): 120 | patch_json = json.dumps(patch) 121 | b64_patch = b64encode(patch_json.encode()).decode() 122 | response["response"]["patchType"] = "JSONPatch" 123 | response["response"]["patch"] = b64_patch 124 | 125 | if message: 126 | response["response"]["status"] = {"message": message} 127 | 128 | # TODO this leaks unredacted patch when --verbose is on (remove response?) 129 | logger.debug('message="Response Successful", request_uid=%s, response="%s"', uid, response["response"]) 130 | return web.json_response(response) 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tesoro 2 | [Kapitan](https://kapitan.dev) Secrets Controller for Kubernetes 3 | 4 | 5 | 6 | [![Build Status](https://travis-ci.org/kapicorp/tesoro.svg?branch=master)](https://travis-ci.org/kapicorp/tesoro) 7 | 8 | Tesoro allows you to seamlessly apply Kubernetes manifests with Kapitan [secret refs](https://kapitan.dev/secrets/). As it runs in the cluster, it will reveal embedded Kapitan secret refs when they are applied. It supports all types of Kapitan secrets backends: AWS KMS, GCP KMS, Vault with more coming up. 9 | 10 | ## Example 11 | 12 | Say you have just setup Tesoro and have this compiled kapitan project: 13 | 14 | ``` 15 | compiled/my-target/manifests 16 | ├── my-deployment.yml 17 | └── my-secret.yml 18 | ... 19 | ``` 20 | 21 | And you have the Tesoro label and kapitan secret ref in `my-secret.yml`: 22 | 23 | ```yaml 24 | apiVersion: v1 25 | kind: Secret 26 | metadata: 27 | name: my-secret 28 | labels: 29 | tesoro.kapicorp.com: enabled 30 | type: Opaque 31 | stringData: 32 | secret_sauce: ?{gkms:my/secret1:deadbeef} 33 | ``` 34 | 35 | All you have to do is compile refs in [embedded format](https://kapitan.dev/secrets/#5-compile-refs-in-embedded-format): 36 | 37 | ```shell 38 | $ kapitan compile --embed-refs 39 | ``` 40 | 41 | ...and you will notice that your kapitan secret ref in `my-secret.yml` now looks like: 42 | ```yaml 43 | ... 44 | type: Opaque 45 | stringData: 46 | secret_sauce: ?{gkms:eyJkYXRhIjogImNtVm1JREVnWkdGMFlRPT0iLCAiZW5jb2RpbmciOiAib3JpZ2luYWwiLCAidHlwZSI6ICJiYXNlNjQifQ==:embedded}} 47 | ... 48 | ``` 49 | 50 | This means that your kubernetes manifests and secrets are ready to be applied: 51 | ```shell 52 | $ kubectl apply -f compiled/my-target/manifests/my-secret.yml 53 | secret/my-secret configured 54 | ``` 55 | 56 | Why is this a big deal? Because without Tesoro, you'd have to reveal secrets locally when applying: 57 | ```shell 58 | $ kapitan refs --reveal -f compiled/my-target/manifests/my-secret.yml | kubectl apply -f - 59 | ``` 60 | 61 | How do I know my secret refs revealed succesfully? You would see the following: 62 | ```shell 63 | $ kubectl apply -f compiled/my-target/manifests/my-secret.yml 64 | Error from server: error when creating "compiled/my-target/manifests/my-secret.yml": admission webhook "tesoro-admission-controller.tesoro.svc" denied the request: Kapitan reveal failed 65 | ``` 66 | You can also setup Prometheus monitoring for this. See [Monitoring](https://github.com/kapicorp/tesoro/#monitoring) 67 | 68 | ## Setup 69 | 70 | Tesoro is a Kubernetes Admission Controller [Mutating Webhook](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#mutatingadmissionwebhook), which means that you'll need at minimum a Kubernetes v1.9 cluster. 71 | 72 | 73 | ### Example Kubernetes Config 74 | 75 | You'll find the predefined example config in the [k8s/](./k8s) directory. Please make sure you read about setting up Mutating Webhooks [here](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#configure-admission-webhooks-on-the-fly)! 76 | 77 | #### 1 - ClusterRole and ClusterRoleBinding 78 | 79 | ```shell 80 | $ kubectl apply -f k8s/clusterrole.yaml 81 | $ kubectl apply -f k8s/clusterrolebinding.yaml 82 | ``` 83 | 84 | #### 2 - Tesoro Namespace 85 | 86 | We will be running the webhook in the `tesoro` namespace 87 | 88 | ```shell 89 | $ kubectl apply -f k8s/tesoro_namespace.yaml 90 | ``` 91 | 92 | #### 3 - Tesoro Webhook Config & Certs 93 | 94 | For convenience, you'll find valid certificates in `tesoro_mutatingwebhook.yaml` and `tesoro_secret.yaml` for testing purposes only. 95 | 96 | Security advice: FOR PROD, PLEASE SETUP YOUR OWN. 97 | 98 | ```shell 99 | $ kubectl -n tesoro apply -f k8s/tesoro_secret.yaml 100 | $ kubectl -n tesoro apply -f k8s/tesoro_service.yaml 101 | $ kubectl -n tesoro apply -f k8s/tesoro_deployment.yaml 102 | ``` 103 | 104 | Verify the tesoro pod is up and running: 105 | 106 | ```shell 107 | $ kubectl -n tesoro get pods 108 | NAME READY STATUS RESTARTS AGE 109 | tesoro-admission-controller-584b9d87c6-p69bx 1/1 Running 0 1m 110 | ``` 111 | 112 | And finally apply the MutatingWebhookConfiguration: 113 | 114 | ```shell 115 | $ kubectl apply -f k8s/tesoro_mutatingwebhook.yaml 116 | ``` 117 | 118 | #### 4 - Try a Kubernetes Manifest with Secret Refs 119 | 120 | This manifest with a valid ref, should work: 121 | 122 | ```shell 123 | $ kubectl apply -f tests/k8s/nginx_deployment.yml 124 | deployment.apps/nginx-deployment created 125 | ``` 126 | 127 | 128 | The following manifest with a bogus ref, should fail: 129 | 130 | ```shell 131 | kubectl apply -f tests/k8s/nginx_deployment_bad.yml 132 | Error from server: error when creating "nginx_deployment_bad.yml": admission webhook "tesoro-admission-controller.tesoro.svc" denied the request: Kapitan reveal failed 133 | ``` 134 | 135 | ### Helm chart 136 | 137 | This repository includes a helm chart which offers an alternative way to install Tesoro 138 | 139 | ``` 140 | kubectl create ns tesoro 141 | helm install tesoro chart -n tesoro 142 | ``` 143 | 144 | #### Vault support 145 | 146 | In order to support Vault references Tesoro will need a VAULT token, this can be created by logging into vault using your defined auth backend. 147 | This example uses github: 148 | 149 | ``` 150 | vault login -no-print -method=github token=XXXXXXXXXXX 151 | ``` 152 | 153 | The helm chart is installed specifying the addition of a VAULT_TOKEN 154 | 155 | ``` 156 | helm install tesoro chart -n tesoro --set env.VAULT_TOKEN=$(cat ~/.vault-token) 157 | ``` 158 | 159 | ##### Upgrading the token 160 | 161 | Should the token expire, it can be refreshed as follows: 162 | 163 | ``` 164 | vault login -no-print -method=github token=XXXXXXXXXXX 165 | helm upgrade tesoro chart -n tesoro --set env.VAULT_TOKEN=$(cat ~/.vault-token) 166 | ``` 167 | 168 | ##### Using a secret to store Vault token 169 | 170 | A more secure option is to save the token as a secret 171 | 172 | ``` 173 | kubectl create secret generic vault-creds --from-literal=VAULT_TOKEN=$(cat ~/.vault-token) -n tesoro 174 | helm install tesoro chart --set secrets[0]=vault-creds -n tesoro 175 | ``` 176 | 177 | ## Monitoring 178 | 179 | Tesoro exposes a Prometheus endpoint (by default on port 9095) and the following metrics: 180 | 181 | Metric | Description | Type 182 | ------------ | ------------- | ------------ 183 | tesoro_requests_total | Tesoro total requests | counter 184 | tesoro_requests_failed_total | Tesoro failed requests | counter 185 | kapitan_reveal_requests_total | Kapitan reveal total requests | counter 186 | kapitan_reveal_requests_failed_total | Kapitan reveal failed requests | counter 187 | kapitan_reveal_retry_requests | Kapitan reveal retry requests | counter 188 | 189 | ## Handling Failure 190 | 191 | Since revealing relies on external services (such as Google KMS, AWS KMS, etc...), 192 | Tesoro will retry up to 3 times should a reveal request fail. 193 | 194 | 195 | ### Local testing 196 | 197 | Run tesoro with `python -m tesoro --verbose` locally (uses 8080 port by default) and test it's endpoints by sending the same requests that k8s would send to it. 198 | E.g. 199 | 200 | ``` 201 | 202 | cd tests/ 203 | 204 | curl -X POST -H "Content-Type: application/json" --data @request.json http://localhost:8080/mutate 205 | 206 | ``` 207 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Test, Build and Publish docker image 2 | run-name: Docker Build for ${{ github.actor }} on branch ${{ github.ref_name }} 3 | 4 | concurrency: 5 | group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' 6 | cancel-in-progress: true 7 | 8 | on: 9 | push: 10 | branches: 11 | - master 12 | - main 13 | - test/* 14 | paths-ignore: 15 | - 'docs/**' 16 | - 'requirements.docs.txt' 17 | - 'mkdocs.yml' 18 | - 'CNAME' 19 | - 'Dockerfile.docs' 20 | 21 | release: 22 | types: [created] 23 | 24 | pull_request: 25 | paths-ignore: 26 | - 'docs/**' 27 | - 'requirements.docs.txt' 28 | - 'mkdocs.yml' 29 | - 'CNAME' 30 | - 'Dockerfile.docs' 31 | 32 | 33 | jobs: 34 | lint: 35 | name: linter 36 | runs-on: ubuntu-latest 37 | if: success() || failure() # Continue running if other jobs fail 38 | steps: 39 | - uses: actions/checkout@v4 40 | - uses: actions/setup-python@v4 41 | with: 42 | python-version: '3.9' 43 | - uses: psf/black@main 44 | 45 | test: 46 | name: python ${{ matrix.python-version }} tests 47 | runs-on: ubuntu-latest 48 | if: success() || failure() # Continue running if other jobs fail 49 | strategy: 50 | fail-fast: false 51 | matrix: 52 | python-version: [3.8, 3.9] 53 | 54 | steps: 55 | - name: Checkout recursively 56 | uses: actions/checkout@v4 57 | with: 58 | submodules: recursive 59 | 60 | - name: Set up Python ${{ matrix.python-version }} 61 | uses: actions/setup-python@v4 62 | with: 63 | cache: 'pip' 64 | python-version: ${{ matrix.python-version }} 65 | 66 | - name: Install testing dependencies 67 | run: | 68 | pip3 install kapitan 69 | pip3 install -r requirements.txt 70 | 71 | - name: Run tests 72 | run: |- 73 | make test 74 | build: 75 | name: build ${{ matrix.platform }} image 76 | if: success() || failure() # Continue running if other jobs fail 77 | runs-on: ubuntu-latest 78 | strategy: 79 | fail-fast: false 80 | matrix: 81 | platform: 82 | - linux/amd64 83 | - linux/arm64 84 | steps: 85 | - name: Checkout tesoro recursively 86 | uses: actions/checkout@v4 87 | with: 88 | submodules: recursive 89 | 90 | # Setup QEMU and Buildx to build multi-platform image 91 | # This was inspired by this example : https://docs.docker.com/build/ci/github-actions/examples/#multi-platform-images 92 | - name: Set up QEMU 93 | uses: docker/setup-qemu-action@v3 94 | - name: Set up Docker Buildx 95 | uses: docker/setup-buildx-action@v3 96 | 97 | # Builds docker image and allow scoped caching 98 | - name: build Tesoro Image 99 | uses: docker/build-push-action@v5 100 | with: 101 | push: False 102 | platforms: ${{ matrix.platform }} 103 | load: True 104 | file: Dockerfile 105 | tags: local-test-${{ matrix.platform }} 106 | cache-from: type=gha,scope=$GITHUB_REF_NAME-${{ matrix.platform }} 107 | cache-to: type=gha,mode=max,scope=$GITHUB_REF_NAME-${{ matrix.platform }} 108 | 109 | - name: Test Tesoro for ${{ matrix.platform }} 110 | run: | 111 | docker run -t --rm local-test-${{ matrix.platform }} -h 112 | 113 | 114 | publish: 115 | name: publish platform images 116 | # Only starts if everything else is successful 117 | needs: [lint, test, build] 118 | if: github.event_name != 'pull_request' 119 | runs-on: ubuntu-latest 120 | strategy: 121 | fail-fast: false 122 | matrix: 123 | platform: 124 | - linux/amd64 125 | - linux/arm64 126 | steps: 127 | - name: Checkout tesoro recursively 128 | uses: actions/checkout@v4 129 | with: 130 | submodules: recursive 131 | 132 | # Setup QEMU and Buildx to build multi-platform image 133 | # This was inspired by this example : https://docs.docker.com/build/ci/github-actions/examples/#multi-platform-images 134 | - name: Set up QEMU 135 | uses: docker/setup-qemu-action@v3 136 | - name: Set up Docker Buildx 137 | uses: docker/setup-buildx-action@v3 138 | 139 | - name: Login to DockerHub 140 | uses: docker/login-action@v3 141 | env: 142 | DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME}} 143 | if: env.DOCKERHUB_USERNAME != null 144 | with: 145 | username: ${{ secrets.DOCKERHUB_USERNAME }} 146 | password: ${{ secrets.DOCKERHUB_TOKEN }} 147 | 148 | - name: Docker meta 149 | id: meta 150 | uses: docker/metadata-action@v5 151 | env: 152 | DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME}} 153 | with: 154 | # list of Docker images to use as base name for tags 155 | images: | 156 | name=${{ vars.DOCKERHUB_REPOSITORY }}/tesoro 157 | # generate Docker tags based on the following events/attributes 158 | tags: | 159 | type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} 160 | type=ref,event=branch 161 | type=semver,pattern={{version}} 162 | type=semver,pattern={{major}}.{{minor}} 163 | type=semver,pattern={{major}} 164 | flavor: | 165 | suffix=-${{ matrix.platform }} 166 | 167 | - name: Build and push by digest 168 | id: push-digest 169 | env: 170 | DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME}} 171 | if: env.DOCKERHUB_USERNAME != null 172 | uses: docker/build-push-action@v5 173 | with: 174 | platforms: ${{ matrix.platform }} 175 | push: true 176 | tags: ${{ steps.meta.outputs.tags }} 177 | labels: ${{steps.meta.output.labels}} 178 | cache-from: type=gha,scope=$GITHUB_REF_NAME-${{ matrix.platform }} 179 | cache-to: type=gha,mode=max,scope=$GITHUB_REF_NAME-${{ matrix.platform }} 180 | 181 | build-multi-architecture: 182 | name: combine platform images 183 | needs: 184 | - publish 185 | runs-on: ubuntu-latest 186 | timeout-minutes: 10 187 | steps: 188 | # Setup QEMU and Buildx to build multi-platform image 189 | # This was inspired by this example : https://docs.docker.com/build/ci/github-actions/examples/#multi-platform-images 190 | - name: Set up QEMU 191 | uses: docker/setup-qemu-action@v3 192 | - name: Set up Docker Buildx 193 | uses: docker/setup-buildx-action@v3 194 | 195 | - name: Login to DockerHub 196 | uses: docker/login-action@v3 197 | env: 198 | DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME}} 199 | if: env.DOCKERHUB_USERNAME != null 200 | with: 201 | username: ${{ secrets.DOCKERHUB_USERNAME }} 202 | password: ${{ secrets.DOCKERHUB_TOKEN }} 203 | 204 | - name: Docker meta 205 | id: meta 206 | uses: docker/metadata-action@v5 207 | env: 208 | DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME}} 209 | with: 210 | # list of Docker images to use as base name for tags 211 | images: | 212 | name=${{ vars.DOCKERHUB_REPOSITORY }}/tesoro 213 | # generate Docker tags based on the following events/attributes 214 | tags: | 215 | type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} 216 | type=ref,event=branch 217 | type=semver,pattern={{version}} 218 | type=semver,pattern={{major}}.{{minor}} 219 | type=semver,pattern={{major}} 220 | 221 | - uses: int128/docker-manifest-create-action@v1 222 | with: 223 | tags: ${{ steps.meta.outputs.tags }} 224 | builder: buildx 225 | suffixes: | 226 | -linux-amd64 227 | -linux-arm64 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------