├── internal ├── decryptor │ ├── kustomize-controller │ │ ├── README.md │ │ ├── keyservice │ │ │ ├── keyservice.go │ │ │ ├── options.go │ │ │ └── utils_test.go │ │ ├── azkv │ │ │ ├── keysource_test.go │ │ │ ├── keysource_integration_test.go │ │ │ └── config.go │ │ ├── pgp │ │ │ └── testdata │ │ │ │ ├── public.gpg │ │ │ │ └── private.gpg │ │ └── gcpkms │ │ │ ├── keysource_integration_test.go │ │ │ └── keysource_test.go │ ├── testdata │ │ ├── secret.yaml │ │ ├── age.agekey │ │ ├── .sops.yaml │ │ ├── secret-age.yaml │ │ ├── secret-pgp.yaml │ │ └── .sops.pub.asc │ └── errors.go ├── meta │ ├── labels.go │ └── conditions.go ├── api │ ├── errors │ │ ├── secrets.go │ │ └── provider.go │ ├── origin.go │ └── sops.go └── metrics │ └── recorder.go ├── charts └── sops-operator │ ├── .schema.yaml │ ├── ci │ └── test-values.yaml │ ├── artifacthub-repo.yml │ ├── templates │ ├── serviceaccount.yaml │ ├── service.yaml │ ├── rbac-user.yaml │ ├── crd-lifecycle │ │ ├── _helpers.tpl │ │ ├── sa.yaml │ │ ├── rbac.yaml │ │ ├── crds.tpl │ │ └── job.yaml │ ├── rules.yaml │ ├── servicemonitor.yaml │ ├── rbac.yaml │ ├── deployment.yaml │ └── _helpers.tpl │ ├── .helmignore │ ├── Chart.yaml │ └── README.md.gotmpl ├── .ko.yaml ├── docs ├── assets │ └── sops-operator.drawio.png ├── installation.md ├── README.md ├── monitoring.md └── development.md ├── .dockerignore ├── e2e ├── manifests │ ├── distro │ │ ├── kustomization.yaml │ │ └── openbao.flux.yaml │ └── flux │ │ └── kustomization.yaml ├── kind.yaml ├── testdata │ ├── age │ │ ├── keys │ │ │ ├── key-3.yaml │ │ │ ├── key-1.agekey │ │ │ ├── key-2.agekey │ │ │ ├── key-1.yaml │ │ │ └── key-2.yaml │ │ ├── secret-key-1.yaml │ │ ├── secret-multi.yaml │ │ ├── global-key-1.yaml │ │ ├── .sops.yaml │ │ ├── secret-key-2.yaml │ │ ├── secret-key-1.enc.yaml │ │ ├── global-key-1.enc.yaml │ │ ├── secret-key-2.enc.yaml │ │ └── secret-multi.enc.yaml │ ├── openbao │ │ ├── token.yaml │ │ ├── secret-key-1.yaml │ │ ├── secret-multi.yaml │ │ ├── secret-quorum.yaml │ │ ├── secret-key-2.yaml │ │ └── .sops.yaml │ ├── gpg │ │ ├── keys │ │ │ ├── key-3 │ │ │ │ └── key.yaml │ │ │ ├── key-2 │ │ │ │ └── .sops.pub.asc │ │ │ └── key-1 │ │ │ │ └── .sops.pub.asc │ │ ├── secret-key-1.yaml │ │ ├── secret-multi.yaml │ │ ├── secret-quorum.yaml │ │ ├── secret-key-2.yaml │ │ ├── .sops.yaml │ │ ├── secret-key-1.enc.yaml │ │ ├── secret-key-2.enc.yaml │ │ ├── secret-multi.enc.yaml │ │ └── secret-quorum.enc.yaml │ └── providers.yaml ├── suite_test.go └── suite_client_test.go ├── Dockerfile.base ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-request.md │ └── bug.md ├── configs │ ├── ct.yaml │ └── lintconf.yaml ├── actions │ └── exists │ │ └── action.yaml └── workflows │ ├── check-commit.yml │ ├── check-actions.yaml │ ├── stale.yml │ ├── releaser.yml │ ├── check-pr.yml │ ├── gosec.yaml │ ├── e2e.yaml │ ├── lint.yaml │ ├── docker-build.yml │ ├── helm-test.yml │ ├── helm-publish.yml │ ├── coverage.yml │ └── docker-publish.yml ├── .nwa-config ├── hack ├── boilerplate.go.txt └── templates │ └── crds.tmpl ├── api └── v1alpha1 │ ├── secret_metadata.go │ ├── groupversion_info.go │ ├── sopsprovider_func.go │ ├── sopsprovider_types.go │ ├── globalsopssecret_types.go │ ├── sopsprovider_status.go │ ├── sopssecret_status.go │ └── sopssecret_types.go ├── .gitignore ├── PROJECT ├── .pre-commit-config.yaml ├── renovate.json ├── .golangci.yml ├── .goreleaser.yml ├── README.md ├── SECURITY.md └── cmd └── main.go /internal/decryptor/kustomize-controller/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /charts/sops-operator/.schema.yaml: -------------------------------------------------------------------------------- 1 | input: 2 | - values.yaml 3 | - ci/test-values.yaml 4 | -------------------------------------------------------------------------------- /charts/sops-operator/ci/test-values.yaml: -------------------------------------------------------------------------------- 1 | replicaCount: 2 2 | image: 3 | pullPolicy: Never 4 | tag: "latest" 5 | -------------------------------------------------------------------------------- /.ko.yaml: -------------------------------------------------------------------------------- 1 | defaultPlatforms: 2 | - linux/amd64 3 | - linux/arm64 4 | builds: 5 | - id: sops-operator 6 | main: cmd/ 7 | -------------------------------------------------------------------------------- /docs/assets/sops-operator.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peak-scale/sops-operator/HEAD/docs/assets/sops-operator.drawio.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | -------------------------------------------------------------------------------- /e2e/manifests/distro/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: flux-system 4 | resources: 5 | - openbao.flux.yaml 6 | -------------------------------------------------------------------------------- /Dockerfile.base: -------------------------------------------------------------------------------- 1 | FROM docker.io/library/alpine:3.23 2 | 3 | RUN apk --no-cache add ca-certificates gnupg \ 4 | && update-ca-certificates 5 | 6 | USER 65534:65534 7 | 8 | ENV GNUPGHOME=/tmp 9 | -------------------------------------------------------------------------------- /e2e/kind.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kind.x-k8s.io/v1alpha4 2 | kind: Cluster 3 | nodes: 4 | - role: control-plane 5 | extraPortMappings: 6 | - hostPort: 8200 7 | containerPort: 8200 8 | -------------------------------------------------------------------------------- /e2e/testdata/age/keys/key-3.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | age.agekey: bm90IGEga2V5Cg== 4 | metadata: 5 | name: e2e-age-key-3 6 | labels: 7 | sops.addons.projectcapsule.dev: "true" 8 | -------------------------------------------------------------------------------- /internal/decryptor/testdata/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: secret 5 | stringData: 6 | database_password: "VERY_SECRET" 7 | database_user: "MUCH_SECURE" 8 | -------------------------------------------------------------------------------- /e2e/testdata/openbao/token.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | stringData: 4 | sops.vault-token: "root" 5 | metadata: 6 | labels: 7 | sops.addons.projectcapsule.dev: "true" 8 | name: e2e-vault-token 9 | -------------------------------------------------------------------------------- /charts/sops-operator/artifacthub-repo.yml: -------------------------------------------------------------------------------- 1 | repositoryID: e408ffa3-0d74-42d3-8991-fa4ed1633a28 2 | owners: # (optional, used to claim repository ownership) 3 | - name: oliverbaehler 4 | email: oliverbaehler@hotmail.com 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Chat on Slack 4 | url: https://kubernetes.slack.com/archives/C03GETTJQRL 5 | about: Maybe chatting with the community can help 6 | -------------------------------------------------------------------------------- /e2e/testdata/age/keys/key-1.agekey: -------------------------------------------------------------------------------- 1 | # created: 2025-05-15T15:36:49+02:00 2 | # public key: age1s7t2vk2crlxaumgm7cacs568xwutkjs535pla69kt6w006t7wgzqhkfwvp 3 | AGE-SECRET-KEY-1APPWEKESFDG2XVAXX3839GA7QCVL8QDJWGQW0PS3P665DDGVAMHSWEKKKN 4 | -------------------------------------------------------------------------------- /e2e/testdata/age/keys/key-2.agekey: -------------------------------------------------------------------------------- 1 | # created: 2025-05-15T15:37:33+02:00 2 | # public key: age1dffcwct9zstd038u8f4a33jey3d04gwrpnznc0xwfc3n0ec8nyeq2jvhyr 3 | AGE-SECRET-KEY-13H03HQNK9YHM3WR90VCCZ5L888RY07FEPWP595YDPAG4WPSEE9TS6K9YS6 4 | -------------------------------------------------------------------------------- /e2e/testdata/gpg/keys/key-3/key.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | sops.asc: bm90IGEga2V5Cg== 4 | kind: Secret 5 | metadata: 6 | labels: 7 | sops.addons.projectcapsule.dev: "true" 8 | name: e2e-gpg-key-3 9 | type: Opaque 10 | -------------------------------------------------------------------------------- /internal/decryptor/testdata/age.agekey: -------------------------------------------------------------------------------- 1 | # created: 2023-08-09T15:30:46+02:00 2 | # public key: age1p0wmaw5vk8f00753t3frs4rev0du4vqdkz7sx53ml98lrcsrnuqqwwp4tl 3 | AGE-SECRET-KEY-1JZFAV45XK9RFDCHD7JG5R5T5R68SY7GTGVLQ9KSZRTLV8K6JFFJQMY6LCY 4 | -------------------------------------------------------------------------------- /e2e/testdata/providers.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: addons.projectcapsule.dev/v1alpha1 3 | kind: SopsProvider 4 | metadata: 5 | name: sample-provider 6 | spec: 7 | keys: 8 | - matchLabels: {} 9 | sops: 10 | - matchLabels: {} 11 | -------------------------------------------------------------------------------- /internal/meta/labels.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025 Peak Scale 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package meta 5 | 6 | const ( 7 | // This is mainly to keep reconciles performance. 8 | //nolint:gosec 9 | KeySecretLabel = "sops.addons.projectcapsule.dev" 10 | ) 11 | -------------------------------------------------------------------------------- /.nwa-config: -------------------------------------------------------------------------------- 1 | nwa: 2 | cmd: "update" 3 | holder: "Peak Scale" 4 | year: "2024-2025" 5 | spdxids: "Apache-2.0" 6 | skip: 7 | - "internal/decryptor/**/*.go" 8 | path: 9 | - "internal/**/*.go" 10 | - "cmd/**/*.go" 11 | - "api/**/*.go" 12 | mute: false 13 | verbose: true 14 | fuzzy: false 15 | -------------------------------------------------------------------------------- /.github/configs/ct.yaml: -------------------------------------------------------------------------------- 1 | ## Reference: https://github.com/helm/chart-testing/blob/master/doc/ct_lint-and-install.md 2 | ## 3 | remote: origin 4 | target-branch: main 5 | chart-dirs: 6 | - charts/ 7 | validate-chart-schema: false 8 | validate-maintainers: false 9 | validate-yaml: false 10 | exclude-deprecated: true 11 | check-version-increment: false 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature for the addon 4 | title: '' 5 | labels: blocked-needs-validation, feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Describe the feature 11 | 12 | A clear and concise description of the feature. 13 | 14 | # Expected behavior 15 | A clear and concise description of what you expect to happen. 16 | -------------------------------------------------------------------------------- /internal/decryptor/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Peak Scale 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package decryptor 5 | 6 | import "fmt" 7 | 8 | type MissingKubernetesSecretError struct { 9 | Secret string 10 | Namespace string 11 | } 12 | 13 | func (e *MissingKubernetesSecretError) Error() string { 14 | return fmt.Sprintf("Secret not found: %s/%s", e.Namespace, e.Secret) 15 | } 16 | -------------------------------------------------------------------------------- /charts/sops-operator/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "helm.serviceAccountName" . }} 6 | labels: 7 | {{- include "helm.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /internal/decryptor/testdata/.sops.yaml: -------------------------------------------------------------------------------- 1 | # creation rules are evaluated sequentially, the first match wins 2 | creation_rules: 3 | # files using age 4 | - path_regex: \-age.yaml$ 5 | encrypted_regex: ^(data|stringData)$ 6 | age: age1p0wmaw5vk8f00753t3frs4rev0du4vqdkz7sx53ml98lrcsrnuqqwwp4tl 7 | # fallback to PGP 8 | - encrypted_regex: ^(data|stringData)$ 9 | pgp: B01102D81246867C4BC24D863E7286BEE865E3C4 10 | -------------------------------------------------------------------------------- /charts/sops-operator/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "helm.fullname" . }}-metrics 5 | labels: 6 | {{- include "helm.labels" . | nindent 4 }} 7 | spec: 8 | type: "ClusterIP" 9 | ports: 10 | - port: 8080 11 | targetPort: metrics 12 | protocol: TCP 13 | name: metrics 14 | selector: 15 | {{- include "helm.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | The Installation of the addon is only supported via Helm-Chart. Any other method is not officially supported. 4 | 5 | [Artifact Hub](https://artifacthub.io/packages/helm/sops-operator/sops-operator) 6 | 7 | Currently we support installation via Helm-Chart click the badge or [here](https://artifacthub.io/packages/helm/sops-operator/sops-operator) to view instructions and possible values on the chart. 8 | -------------------------------------------------------------------------------- /e2e/testdata/age/keys/key-1.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | age.agekey: IyBjcmVhdGVkOiAyMDI1LTA1LTE1VDE1OjM2OjQ5KzAyOjAwCiMgcHVibGljIGtleTogYWdlMXM3dDJ2azJjcmx4YXVtZ203Y2FjczU2OHh3dXRranM1MzVwbGE2OWt0NncwMDZ0N3dnenFoa2Z3dnAKQUdFLVNFQ1JFVC1LRVktMUFQUFdFS0VTRkRHMlhWQVhYMzgzOUdBN1FDVkw4UURKV0dRVzBQUzNQNjY1RERHVkFNSFNXRUtLS04= 4 | kind: Secret 5 | metadata: 6 | name: e2e-age-key-1 7 | labels: 8 | sops.addons.projectcapsule.dev: "true" 9 | -------------------------------------------------------------------------------- /e2e/testdata/age/keys/key-2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | age.agekey: IyBjcmVhdGVkOiAyMDI1LTA1LTE1VDE1OjM3OjMzKzAyOjAwCiMgcHVibGljIGtleTogYWdlMWRmZmN3Y3Q5enN0ZDAzOHU4ZjRhMzNqZXkzZDA0Z3dycG56bmMweHdmYzNuMGVjOG55ZXEyanZoeXIKQUdFLVNFQ1JFVC1LRVktMTNIMDNIUU5LOVlITTNXUjkwVkNDWjVMODg4UlkwN0ZFUFdQNTk1WURQQUc0V1BTRUU5VFM2SzlZUzY= 4 | kind: Secret 5 | metadata: 6 | name: e2e-age-key-2 7 | labels: 8 | sops.addons.projectcapsule.dev: "true" 9 | -------------------------------------------------------------------------------- /internal/api/errors/secrets.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025 Peak Scale 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package errors 5 | 6 | type SecretReconciliationError struct { 7 | Message string 8 | } 9 | 10 | func (e *SecretReconciliationError) Error() string { 11 | return e.Message 12 | } 13 | 14 | func NewSecretReconciliationError(message string) error { 15 | return &SecretReconciliationError{ 16 | Message: message, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | See the following topics for more information on how to use this addon: 4 | 5 | - [Installation](installation.md) 6 | - [Usage](usage.md) 7 | - [Monitoring](monitoring.md) 8 | - [API Reference](reference.md) 9 | - [Development](development.md) 10 | 11 | If you notice any issues, please report them in the [GitHub issues](https://github.com/peak-scale/capsule-argo-addon/issues/new). We are happy for any contribution . 12 | -------------------------------------------------------------------------------- /charts/sops-operator/templates/rbac-user.yaml: -------------------------------------------------------------------------------- 1 | {{- if $.Values.rbac.secretsRole.create }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ include "helm.fullname" . }}-user 6 | labels: 7 | {{- toYaml $.Values.rbac.secretsRole.labels | nindent 4 }} 8 | rules: 9 | - apiGroups: ["addons.projectcapsule.dev"] 10 | resources: ["sopssecrets"] 11 | verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /charts/sops-operator/.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 | ci/ 25 | artifacthub-repo.yml 26 | .schema.yaml 27 | -------------------------------------------------------------------------------- /charts/sops-operator/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: sops-operator 3 | description: sops-operator 4 | type: application 5 | version: 0.0.0 6 | appVersion: "0.0.0" 7 | home: https://github.com/peak-scale/sops-operator 8 | icon: https://avatars.githubusercontent.com/u/129185620?s=48&v=4 9 | keywords: 10 | - kubernetes 11 | - operator 12 | - security 13 | - secrets 14 | - sealed 15 | - mozilla 16 | - sops 17 | - argocd 18 | - argo-cd 19 | - argo 20 | - gitops 21 | sources: 22 | - https://github.com/peak-scale/sops-operator/docs 23 | -------------------------------------------------------------------------------- /e2e/testdata/gpg/secret-key-1.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: SopsSecret 3 | metadata: 4 | name: gpg-secret-key-1 5 | spec: 6 | metadata: 7 | prefix: "gpg-" 8 | suffix: "-key-1" 9 | labels: 10 | gpg.top: "label" 11 | annotations: 12 | gpg.top: "label" 13 | secrets: 14 | - name: gpg-secret-key-1 15 | labels: 16 | gpg.bottom: "label" 17 | annotations: 18 | gpg.bottom: "annotation" 19 | data: 20 | data-name1: ZGF0YS12YWx1ZTE= 21 | -------------------------------------------------------------------------------- /e2e/testdata/gpg/secret-multi.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: SopsSecret 3 | metadata: 4 | name: gpg-multi-secret 5 | spec: 6 | metadata: 7 | prefix: "gpg-" 8 | suffix: "-key-multi" 9 | labels: 10 | gpg.top: "label" 11 | annotations: 12 | gpg.top: "label" 13 | secrets: 14 | - name: gpg-multi-secret-1 15 | labels: 16 | label1: value1 17 | stringData: 18 | data-name0: data-value0 19 | data: 20 | data-name1: ZGF0YS12YWx1ZTE= 21 | -------------------------------------------------------------------------------- /e2e/testdata/gpg/secret-quorum.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: SopsSecret 3 | metadata: 4 | name: gpp-quorum-secret 5 | spec: 6 | metadata: 7 | prefix: "gpg-" 8 | suffix: "-key-quorum" 9 | labels: 10 | gpg.top: "label" 11 | annotations: 12 | gpg.top: "label" 13 | secrets: 14 | - name: gpp-quorum-secret-1 15 | labels: 16 | label1: value1 17 | stringData: 18 | data-name0: data-value0 19 | data: 20 | data-name1: ZGF0YS12YWx1ZTE= 21 | -------------------------------------------------------------------------------- /e2e/testdata/openbao/secret-key-1.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: SopsSecret 3 | metadata: 4 | name: vault-secret-key-1 5 | spec: 6 | metadata: 7 | prefix: "bao-" 8 | suffix: "-key-1" 9 | labels: 10 | bao.top: "label" 11 | annotations: 12 | bao.top: "label" 13 | secrets: 14 | - name: vault-secret-key-1 15 | labels: 16 | bao.bottom: "label" 17 | annotations: 18 | bao.bottom: "annotation" 19 | data: 20 | data-name1: ZGF0YS12YWx1ZTE= 21 | -------------------------------------------------------------------------------- /e2e/testdata/age/secret-key-1.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: SopsSecret 3 | metadata: 4 | name: secret-key-1 5 | spec: 6 | metadata: 7 | prefix: "age-" 8 | suffix: "-key-1" 9 | labels: 10 | age.top: "label" 11 | annotations: 12 | age.top: "label" 13 | secrets: 14 | - name: my-secret-name-1 15 | labels: 16 | age.bottom: "label" 17 | annotations: 18 | age.bottom: "annotation" 19 | stringData: 20 | data-name0: data-value0 21 | data: 22 | data-name1: ZGF0YS12YWx1ZTE= 23 | -------------------------------------------------------------------------------- /internal/decryptor/kustomize-controller/keyservice/keyservice.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Peak Scale 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package keyservice 5 | 6 | import ( 7 | "github.com/getsops/sops/v3/age" 8 | "github.com/getsops/sops/v3/keys" 9 | "github.com/getsops/sops/v3/pgp" 10 | ) 11 | 12 | // IsOfflineMethod returns true for offline decrypt methods or false otherwise. 13 | func IsOfflineMethod(mk keys.MasterKey) bool { 14 | switch mk.(type) { 15 | case *pgp.MasterKey, *age.MasterKey: 16 | return true 17 | default: 18 | return false 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/actions/exists/action.yaml: -------------------------------------------------------------------------------- 1 | name: Checks if an input is defined 2 | 3 | description: Checks if an input is defined and outputs 'true' or 'false'. 4 | 5 | inputs: 6 | value: 7 | description: value to test 8 | required: true 9 | 10 | outputs: 11 | result: 12 | description: outputs 'true' or 'false' if input value is defined or not 13 | value: ${{ steps.check.outputs.result }} 14 | 15 | runs: 16 | using: composite 17 | steps: 18 | - shell: bash 19 | id: check 20 | run: | 21 | echo "result=${{ inputs.value != '' }}" >> $GITHUB_OUTPUT 22 | -------------------------------------------------------------------------------- /charts/sops-operator/templates/crd-lifecycle/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{- define "crds.name" -}} 2 | {{- printf "%s-crds" (include "helm.name" $) -}} 3 | {{- end }} 4 | 5 | {{- define "crds.annotations" -}} 6 | "helm.sh/hook": "pre-install,pre-upgrade" 7 | {{- with $.Values.global.jobs.annotations }} 8 | {{- . | toYaml | nindent 0 }} 9 | {{- end }} 10 | {{- end }} 11 | 12 | {{- define "crds.component" -}} 13 | crd-install-hook 14 | {{- end }} 15 | 16 | {{- define "crds.regexReplace" -}} 17 | {{- printf "%s" ($ | base | trimSuffix ".yaml" | regexReplaceAll "[_.]" "-") -}} 18 | {{- end }} 19 | -------------------------------------------------------------------------------- /charts/sops-operator/templates/crd-lifecycle/sa.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.crds.install (not $.Values.crds.inline) }} 2 | --- 3 | apiVersion: v1 4 | kind: ServiceAccount 5 | metadata: 6 | name: {{ include "crds.name" . }} 7 | namespace: {{ .Release.Namespace }} 8 | annotations: 9 | # create hook dependencies in the right order 10 | "helm.sh/hook-weight": "-4" 11 | {{- include "crds.annotations" . | nindent 4 }} 12 | labels: 13 | app.kubernetes.io/component: {{ include "crds.component" . | quote }} 14 | {{- include "helm.labels" . | nindent 4 }} 15 | {{- end }} 16 | -------------------------------------------------------------------------------- /e2e/testdata/age/secret-multi.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: SopsSecret 3 | metadata: 4 | name: secret-multi-key 5 | spec: 6 | metadata: 7 | prefix: "age-" 8 | suffix: "-multi" 9 | labels: 10 | age.top: "label" 11 | annotations: 12 | age.top: "label" 13 | secrets: 14 | - name: multi-secret-name-1 15 | labels: 16 | age.bottom: "label" 17 | annotations: 18 | age.bottom: "annotation" 19 | stringData: 20 | data-name0: data-value0 21 | data: 22 | data-name1: ZGF0YS12YWx1ZTE= 23 | -------------------------------------------------------------------------------- /e2e/testdata/openbao/secret-multi.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: SopsSecret 3 | metadata: 4 | name: vault-multi-secret 5 | spec: 6 | metadata: 7 | prefix: "bao-" 8 | suffix: "-multi" 9 | labels: 10 | bao.top: "label" 11 | annotations: 12 | bao.top: "label" 13 | secrets: 14 | - name: vault-multi-secret-1 15 | labels: 16 | bao.bottom: "label" 17 | annotations: 18 | bao.bottom: "annotation" 19 | stringData: 20 | data-name0: data-value0 21 | data: 22 | data-name1: ZGF0YS12YWx1ZTE= 23 | -------------------------------------------------------------------------------- /e2e/testdata/openbao/secret-quorum.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: SopsSecret 3 | metadata: 4 | name: vault-quorum-secret 5 | spec: 6 | metadata: 7 | prefix: "bao-" 8 | suffix: "-quorum" 9 | labels: 10 | bao.top: "label" 11 | annotations: 12 | bao.top: "label" 13 | secrets: 14 | - name: vault-quorum-secret-1 15 | labels: 16 | bao.bottom: "label" 17 | annotations: 18 | bao.bottom: "annotation" 19 | stringData: 20 | data-name0: data-value0 21 | data: 22 | data-name1: ZGF0YS12YWx1ZTE= 23 | -------------------------------------------------------------------------------- /.github/workflows/check-commit.yml: -------------------------------------------------------------------------------- 1 | name: Check Commit 2 | permissions: {} 3 | 4 | on: 5 | push: 6 | branches: 7 | - "*" 8 | pull_request: 9 | branches: 10 | - "*" 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | commit_lint: 18 | runs-on: ubuntu-24.04 19 | steps: 20 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 21 | with: 22 | fetch-depth: 0 23 | - uses: wagoid/commitlint-github-action@b948419dd99f3fd78a6548d48f94e3df7f6bf3ed # v6.2.1 24 | -------------------------------------------------------------------------------- /internal/api/errors/provider.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025 Peak Scale 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package errors 5 | 6 | import ( 7 | "fmt" 8 | 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | ) 11 | 12 | type NoDecryptionProviderError struct { 13 | Object client.Object 14 | } 15 | 16 | func (e *NoDecryptionProviderError) Error() string { 17 | return fmt.Sprintf("secret %s/%s has no decryption providers", e.Object.GetNamespace(), e.Object.GetName()) 18 | } 19 | 20 | func NewNoDecryptionProviderError(obj client.Object) error { 21 | return &NoDecryptionProviderError{Object: obj} 22 | } 23 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /e2e/testdata/age/global-key-1.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: GlobalSopsSecret 3 | metadata: 4 | name: global-secret-key-1 5 | spec: 6 | secrets: 7 | - name: secret-1 8 | namespace: "global-secret-1" 9 | labels: 10 | label1: value1 11 | stringData: 12 | data-name0: data-value0 13 | data: 14 | data-name1: ZGF0YS12YWx1ZTE= 15 | - name: secret-2 16 | namespace: "global-secret-2" 17 | labels: 18 | label1: value1 19 | stringData: 20 | data-name0: data-value0 21 | data: 22 | data-name1: ZGF0YS12YWx1ZTE= 23 | -------------------------------------------------------------------------------- /api/v1alpha1/secret_metadata.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-2025 Peak Scale 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package v1alpha1 7 | 8 | // SopsSecretSpec defines the desired state of SopsSecret. 9 | type SecretMetadata struct { 10 | // Prefix added to all generated Secrets names 11 | Prefix string `json:"prefix,omitempty"` 12 | // Suffix added to all generated Secrets names 13 | Suffix string `json:"suffix,omitempty"` 14 | // Labels added to all generated Secrets 15 | Labels map[string]string `json:"labels,omitempty"` 16 | // Annotations added to all generated Secrets 17 | Annotations map[string]string `json:"annotations,omitempty"` 18 | } 19 | -------------------------------------------------------------------------------- /internal/api/origin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025 Peak Scale 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package api 5 | 6 | import ( 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | k8stypes "k8s.io/apimachinery/pkg/types" 9 | ) 10 | 11 | type Origin struct { 12 | // Name of Object 13 | Name string `json:"name"` 14 | // namespace of Object 15 | Namespace string `json:"namespace,omitempty"` 16 | // namespace of Object 17 | UID k8stypes.UID `json:"uid,omitempty"` 18 | } 19 | 20 | func NewOrigin(obj metav1.Object) *Origin { 21 | return &Origin{ 22 | Name: obj.GetName(), 23 | Namespace: obj.GetNamespace(), 24 | UID: obj.GetUID(), 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | config 17 | .DS_Store 18 | 19 | # Dependency directories (remove the comment below to include it) 20 | # vendor/ 21 | 22 | # Go workspace file 23 | go.work 24 | go.work.sum 25 | 26 | # env file 27 | .env 28 | bin/ 29 | coverage.out 30 | e2e/testdata/openbao/*enc.yaml 31 | config/ 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve the addon 4 | title: '' 5 | labels: blocked-needs-validation, bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Bug description 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | # How to reproduce 15 | 16 | Steps to reproduce the behavior: 17 | 18 | 1. Relevant Translator manifests 19 | 2. Relevant ArgoAddon manifests 20 | 21 | # Expected behavior 22 | 23 | A clear and concise description of what you expected to happen. 24 | 25 | # Logs 26 | 27 | If applicable, please provide logs of `capsule-argo-addon`. 28 | 29 | # Additional context 30 | 31 | - Addon version: 32 | - Argo version: 33 | - Kubernetes version: 34 | -------------------------------------------------------------------------------- /charts/sops-operator/templates/rules.yaml: -------------------------------------------------------------------------------- 1 | 2 | {{- if and $.Values.monitoring.enabled $.Values.monitoring.rules.enabled }} 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: PrometheusRule 5 | metadata: 6 | name: {{ include "helm.fullname" . }} 7 | namespace: {{ .Values.monitoring.rules.namespace | default .Release.Namespace }} 8 | labels: 9 | {{- include "helm.labels" . | nindent 4 }} 10 | {{- with .Values.monitoring.rules.labels }} 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | {{- with .Values.monitoring.rules.annotations }} 14 | annotations: 15 | {{- toYaml . | nindent 4 }} 16 | {{- end }} 17 | spec: 18 | groups: 19 | {{- toYaml .Values.monitoring.rules.groups | nindent 4 }} 20 | {{- end }} 21 | -------------------------------------------------------------------------------- /.github/workflows/check-actions.yaml: -------------------------------------------------------------------------------- 1 | name: Check actions 2 | permissions: {} 3 | 4 | on: 5 | push: 6 | branches: 7 | - '*' 8 | pull_request: 9 | branches: 10 | - "main" 11 | 12 | jobs: 13 | check: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 18 | - name: Ensure SHA pinned actions 19 | uses: zgosalvez/github-actions-ensure-sha-pinned-actions@9e9574ef04ea69da568d6249bd69539ccc704e74 # v4.0.0 20 | with: 21 | # slsa-github-generator requires using a semver tag for reusable workflows. 22 | # See: https://github.com/slsa-framework/slsa-github-generator#referencing-slsa-builders-and-generators 23 | allowlist: | 24 | slsa-framework/slsa-github-generator 25 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025 Peak Scale 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package v1alpha1 contains API Schema definitions for the addons v1alpha1 API group. 5 | // +kubebuilder:object:generate=true 6 | // +groupName=addons.projectcapsule.dev 7 | package v1alpha1 8 | 9 | import ( 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "sigs.k8s.io/controller-runtime/pkg/scheme" 12 | ) 13 | 14 | var ( 15 | // GroupVersion is group version used to register these objects. 16 | GroupVersion = schema.GroupVersion{Group: "addons.projectcapsule.dev", Version: "v1alpha1"} 17 | 18 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme. 19 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 20 | 21 | // AddToScheme adds the types in this group-version to the given scheme. 22 | AddToScheme = SchemeBuilder.AddToScheme 23 | ) 24 | -------------------------------------------------------------------------------- /e2e/testdata/gpg/secret-key-2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: SopsSecret 3 | metadata: 4 | name: gpg-secret-key-2 5 | spec: 6 | metadata: 7 | prefix: "gpg-" 8 | suffix: "-key-2" 9 | labels: 10 | gpg.top: "label" 11 | annotations: 12 | gpg.top: "label" 13 | secrets: 14 | - name: gpg-jenkins-secret 15 | labels: 16 | "jenkins.io/credentials-type": "usernamePassword" 17 | annotations: 18 | "jenkins.io/credentials-description": "credentials from Kubernetes" 19 | stringData: 20 | username: myUsername 21 | password: 'Pa$$word' 22 | - name: gpg-docker-login 23 | type: 'kubernetes.io/dockerconfigjson' 24 | stringData: 25 | .dockerconfigjson: '{"auths":{"index.docker.io":{"username":"imyuser","password":"mypass","email":"myuser@abc.com","auth":"aW15dXNlcjpteXBhc3M="}}}' 26 | -------------------------------------------------------------------------------- /e2e/testdata/openbao/secret-key-2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: SopsSecret 3 | metadata: 4 | name: vault-secret-key-2 5 | spec: 6 | metadata: 7 | prefix: "bao-" 8 | suffix: "-key-2" 9 | labels: 10 | bao.top: "label" 11 | annotations: 12 | bao.top: "label" 13 | secrets: 14 | - name: vault-jenkins-secret 15 | labels: 16 | "jenkins.io/credentials-type": "usernamePassword" 17 | annotations: 18 | "jenkins.io/credentials-description": "credentials from Kubernetes" 19 | stringData: 20 | username: myUsername 21 | password: 'Pa$$word' 22 | - name: vault-docker-login 23 | type: 'kubernetes.io/dockerconfigjson' 24 | stringData: 25 | .dockerconfigjson: '{"auths":{"index.docker.io":{"username":"imyuser","password":"mypass","email":"myuser@abc.com","auth":"aW15dXNlcjpteXBhc3M="}}}' 26 | -------------------------------------------------------------------------------- /e2e/testdata/age/.sops.yaml: -------------------------------------------------------------------------------- 1 | creation_rules: 2 | - path_regex: "global-key-1.yaml" 3 | mac_only_encrypted: true 4 | encrypted_regex: ^(data|stringData)$ 5 | age: >- 6 | age1s7t2vk2crlxaumgm7cacs568xwutkjs535pla69kt6w006t7wgzqhkfwvp 7 | - path_regex: "secret-key-1.yaml" 8 | mac_only_encrypted: true 9 | encrypted_regex: ^(data|stringData)$ 10 | age: >- 11 | age1s7t2vk2crlxaumgm7cacs568xwutkjs535pla69kt6w006t7wgzqhkfwvp 12 | - path_regex: "secret-key-2.yaml" 13 | mac_only_encrypted: true 14 | encrypted_regex: ^(data|stringData)$ 15 | age: >- 16 | age1dffcwct9zstd038u8f4a33jey3d04gwrpnznc0xwfc3n0ec8nyeq2jvhyr 17 | - path_regex: "secret-multi.yaml" 18 | mac_only_encrypted: true 19 | encrypted_regex: ^(data|stringData)$ 20 | age: >- 21 | age1s7t2vk2crlxaumgm7cacs568xwutkjs535pla69kt6w006t7wgzqhkfwvp, 22 | age1dffcwct9zstd038u8f4a33jey3d04gwrpnznc0xwfc3n0ec8nyeq2jvhyr 23 | -------------------------------------------------------------------------------- /e2e/testdata/gpg/.sops.yaml: -------------------------------------------------------------------------------- 1 | creation_rules: 2 | - path_regex: secret-key-1.yaml 3 | encrypted_regex: ^(data|stringData)$ 4 | mac_only_encrypted: true 5 | pgp: CE411B68660C33B0F83A4EBD56FDA28155A45CB1 6 | - path_regex: secret-key-2.yaml 7 | encrypted_regex: ^(data|stringData)$ 8 | mac_only_encrypted: true 9 | pgp: 60684ED5F92EA3FD960E83E6CB8BC811D17A58DE 10 | - path_regex: secret-multi.yaml 11 | encrypted_regex: ^(data|stringData)$ 12 | mac_only_encrypted: true 13 | shamir_threshold: 1 14 | key_groups: 15 | - pgp: 16 | - CE411B68660C33B0F83A4EBD56FDA28155A45CB1 17 | - 60684ED5F92EA3FD960E83E6CB8BC811D17A58DE 18 | - path_regex: secret-quorum.yaml 19 | encrypted_regex: ^(data|stringData)$ 20 | mac_only_encrypted: true 21 | shamir_threshold: 2 22 | key_groups: 23 | - pgp: 24 | - CE411B68660C33B0F83A4EBD56FDA28155A45CB1 25 | - 60684ED5F92EA3FD960E83E6CB8BC811D17A58DE 26 | -------------------------------------------------------------------------------- /e2e/testdata/age/secret-key-2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: SopsSecret 3 | metadata: 4 | name: secret-key-2 5 | spec: 6 | metadata: 7 | prefix: "age-" 8 | suffix: "-key-2" 9 | labels: 10 | age.top: "label" 11 | annotations: 12 | age.top: "label" 13 | secrets: 14 | - name: jenkins-secret 15 | labels: 16 | "jenkins.io/credentials-type": "usernamePassword" 17 | annotations: 18 | "jenkins.io/credentials-description": "credentials from Kubernetes" 19 | stringData: 20 | username: myUsername 21 | password: 'Pa$$word' 22 | - name: docker-login 23 | labels: 24 | age.bottom: "label" 25 | annotations: 26 | age.bottom: "annotation" 27 | type: 'kubernetes.io/dockerconfigjson' 28 | stringData: 29 | .dockerconfigjson: '{"auths":{"index.docker.io":{"username":"imyuser","password":"mypass","email":"myuser@abc.com","auth":"aW15dXNlcjpteXBhc3M="}}}' 30 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: projectcapsule.dev 6 | layout: 7 | - go.kubebuilder.io/v4 8 | projectName: sops-operator 9 | repo: github.com/peak-scale/sops-operator 10 | resources: 11 | - api: 12 | crdVersion: v1 13 | domain: projectcapsule.dev 14 | group: addons 15 | kind: SopsProvider 16 | path: github.com/peak-scale/sops-operator/api/v1alpha1 17 | version: v1alpha1 18 | - api: 19 | crdVersion: v1 20 | namespaced: true 21 | domain: projectcapsule.dev 22 | group: addons 23 | kind: SopsSecret 24 | path: github.com/peak-scale/sops-operator/api/v1alpha1 25 | version: v1alpha1 26 | - api: 27 | crdVersion: v1 28 | domain: projectcapsule.dev 29 | group: addons 30 | kind: GlobalSopsSecret 31 | path: github.com/peak-scale/sops-operator/api/v1alpha1 32 | version: v1alpha1 33 | version: "3" 34 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Stale-Bot 2 | permissions: {} 3 | 4 | on: 5 | schedule: 6 | - cron: '0 0 * * *' # Run every day at midnight 7 | 8 | jobs: 9 | stale: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | actions: write 13 | contents: write # only for delete-branch option 14 | issues: write 15 | pull-requests: write 16 | steps: 17 | - name: Close stale pull requests 18 | uses: actions/stale@5611b9defa6b7799a950489b00163db69f7a3ece 19 | with: 20 | stale-issue-message: 'This pull request has been automatically closed because it has been inactive for more than 60 days. Please reopen if you still intend to submit this pull request.' 21 | days-before-stale: 60 22 | days-before-close: 30 23 | days-before-pr-stale: 30 24 | stale-pr-message: 'This pull request has been marked as stale because it has been inactive for more than 30 days. Please update this pull request or it will be automatically closed in 7 days.' 25 | stale-pr-label: stale 26 | -------------------------------------------------------------------------------- /.github/workflows/releaser.yml: -------------------------------------------------------------------------------- 1 | name: Go Release 2 | 3 | permissions: {} 4 | on: 5 | push: 6 | tags: 7 | - 'v*' 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | create-release: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: write 18 | id-token: write 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 22 | - uses: creekorful/goreportcard-action@1f35ced8cdac2cba28c9a2f2288a16aacfd507f9 # v1.0 23 | - uses: anchore/sbom-action/download-syft@43a17d6e7add2b5535efe4dcae9952337c479a93 24 | - name: Install Cosign 25 | uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 26 | - name: Run GoReleaser 27 | uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 28 | with: 29 | version: latest 30 | args: release --clean --timeout 90m 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/check-pr.yml: -------------------------------------------------------------------------------- 1 | name: "Check Pull Request" 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | permissions: 11 | pull-requests: write 12 | 13 | jobs: 14 | main: 15 | name: Validate PR title 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: amannn/action-semantic-pull-request@069817c298f23fab00a8f29a2e556a5eac0f6390 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | types: | 23 | chore 24 | ci 25 | docs 26 | feat 27 | fix 28 | test 29 | sec 30 | requireScope: false 31 | wip: false 32 | # If the PR only contains a single commit, the action will validate that 33 | # it matches the configured pattern. 34 | validateSingleCommit: true 35 | # Related to `validateSingleCommit` you can opt-in to validate that the PR 36 | # title matches a single commit to avoid confusion. 37 | validateSingleCommitMatchesPrTitle: true 38 | -------------------------------------------------------------------------------- /.github/workflows/gosec.yaml: -------------------------------------------------------------------------------- 1 | name: CI gosec 2 | permissions: 3 | # required for all workflows 4 | security-events: write 5 | # only required for workflows in private repositories 6 | actions: read 7 | contents: read 8 | on: 9 | pull_request: 10 | branches: 11 | - "*" 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | tests: 18 | runs-on: ubuntu-24.04 19 | env: 20 | GO111MODULE: on 21 | steps: 22 | - name: Checkout Source 23 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 24 | - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 25 | with: 26 | go-version-file: 'go.mod' 27 | - name: Run Gosec Security Scanner 28 | uses: securego/gosec@15d5c61e866bc2e2e8389376a31f1e5e09bde7d8 # v2.22.9 29 | with: 30 | args: '-no-fail -fmt sarif -out gosec.sarif ./...' 31 | - name: Upload SARIF file 32 | uses: github/codeql-action/upload-sarif@59ce4c1340a74f56c129f758767ef33668e572b0 33 | with: 34 | sarif_file: gosec.sarif 35 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yaml: -------------------------------------------------------------------------------- 1 | name: e2e 2 | permissions: {} 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - "*" 8 | paths: 9 | - '.github/workflows/e2e.yml' 10 | - 'api/**' 11 | - 'cmd/**' 12 | - 'internal/**' 13 | - 'e2e/*' 14 | - '.ko.yaml' 15 | - 'go.*' 16 | - 'main.go' 17 | - 'Makefile' 18 | 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.ref }} 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | kind: 25 | name: Kubernetes 26 | runs-on: ubuntu-24.04 27 | steps: 28 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 29 | with: 30 | fetch-depth: 0 31 | - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 32 | with: 33 | go-version-file: 'go.mod' 34 | - uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 35 | with: 36 | version: v3.14.2 37 | 38 | - name: Prepare E2E 39 | run: | 40 | echo "127.0.0.1 openbao.openbao.svc.cluster.local" | sudo tee -a /etc/hosts 41 | make openbao 42 | - name: e2e testing 43 | run: make e2e 44 | -------------------------------------------------------------------------------- /e2e/manifests/flux/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - https://github.com/fluxcd/flux2/releases/download/v2.4.0/install.yaml 5 | patches: 6 | - patch: | 7 | - op: add 8 | path: /spec/template/spec/containers/0/args/- 9 | value: --no-cross-namespace-refs=true 10 | target: 11 | kind: Deployment 12 | name: "(kustomize-controller|helm-controller|notification-controller|image-reflector-controller|image-automation-controller)" 13 | - patch: | 14 | - op: add 15 | path: /spec/template/spec/containers/0/args/- 16 | value: --no-remote-bases=true 17 | target: 18 | kind: Deployment 19 | name: "kustomize-controller" 20 | - patch: | 21 | - op: add 22 | path: /spec/template/spec/containers/0/args/- 23 | value: --default-service-account=default 24 | target: 25 | kind: Deployment 26 | name: "(kustomize-controller|helm-controller)" 27 | - patch: | 28 | - op: replace 29 | path: /spec/replicas 30 | value: 0 31 | target: 32 | kind: Deployment 33 | name: "(notification-controller|image-reflector-controller|image-automation-controller)" 34 | -------------------------------------------------------------------------------- /e2e/testdata/openbao/.sops.yaml: -------------------------------------------------------------------------------- 1 | creation_rules: 2 | - path_regex: secret-key-1.yaml 3 | encrypted_regex: ^(data|stringData)$ 4 | mac_only_encrypted: true 5 | hc_vault_transit_uri: "http://openbao.openbao.svc.cluster.local:8200/v1/sops/keys/key-1" 6 | 7 | - path_regex: secret-key-2.yaml 8 | encrypted_regex: ^(data|stringData)$ 9 | mac_only_encrypted: true 10 | hc_vault_transit_uri: "http://openbao.openbao.svc.cluster.local:8200/v1/sops/keys/key-2" 11 | 12 | - path_regex: secret-multi.yaml 13 | encrypted_regex: ^(data|stringData)$ 14 | mac_only_encrypted: true 15 | shamir_threshold: 1 16 | key_groups: 17 | - hc_vault: 18 | - "http://openbao.openbao.svc.cluster.local:8200/v1/sops/keys/key-1" 19 | - "http://openbao.openbao.svc.cluster.local:8200/v1/sops/keys/key-2" 20 | 21 | - path_regex: secret-quorum.yaml 22 | encrypted_regex: ^(data|stringData)$ 23 | mac_only_encrypted: true 24 | shamir_threshold: 2 25 | key_groups: 26 | - hc_vault: 27 | - "http://openbao.openbao.svc.cluster.local:8200/v1/sops/keys/key-1" 28 | - "http://openbao.openbao.svc.cluster.local:8200/v1/sops/keys/key-2" 29 | -------------------------------------------------------------------------------- /e2e/manifests/distro/openbao.flux.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: source.toolkit.fluxcd.io/v1 3 | kind: HelmRepository 4 | metadata: 5 | name: openbao 6 | namespace: flux-system 7 | spec: 8 | interval: 15s 9 | timeout: 1m0s 10 | url: https://openbao.github.io/openbao-helm 11 | --- 12 | apiVersion: helm.toolkit.fluxcd.io/v2 13 | kind: HelmRelease 14 | metadata: 15 | name: openbao 16 | namespace: flux-system 17 | spec: 18 | serviceAccountName: kustomize-controller 19 | interval: 1m 20 | targetNamespace: openbao 21 | releaseName: "openbao" 22 | chart: 23 | spec: 24 | chart: openbao 25 | version: "0.19.0" 26 | sourceRef: 27 | kind: HelmRepository 28 | name: openbao 29 | interval: 24h 30 | install: 31 | createNamespace: true 32 | remediation: 33 | retries: -1 34 | upgrade: 35 | remediation: 36 | remediateLastFailure: true 37 | driftDetection: 38 | mode: enabled 39 | values: 40 | server: 41 | hostNetwork: true 42 | service: 43 | type: NodePort 44 | dev: 45 | enabled: true 46 | devRootToken: "root" 47 | ui: 48 | enabled: true 49 | csi: 50 | enabled: false 51 | injector: 52 | enabled: false 53 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook 3 | rev: v9.23.0 4 | hooks: 5 | - id: commitlint 6 | stages: [commit-msg] 7 | additional_dependencies: ['@commitlint/config-conventional', 'commitlint-plugin-function-rules'] 8 | - repo: https://github.com/pre-commit/pre-commit-hooks 9 | rev: v6.0.0 10 | hooks: 11 | - id: check-executables-have-shebangs 12 | - id: double-quote-string-fixer 13 | - id: end-of-file-fixer 14 | - id: trailing-whitespace 15 | - repo: https://github.com/adrienverge/yamllint 16 | rev: v1.37.1 17 | hooks: 18 | - id: yamllint 19 | args: [-c=.github/configs/lintconf.yaml] 20 | - repo: local 21 | hooks: 22 | - id: run-helm-docs 23 | name: Execute helm-docs 24 | entry: make helm-docs 25 | language: system 26 | files: ^charts/ 27 | - id: run-helm-schema 28 | name: Execute helm-schema 29 | entry: make helm-schema 30 | language: system 31 | files: ^charts/ 32 | - id: run-helm-lint 33 | name: Execute helm-lint 34 | entry: make helm-lint 35 | language: system 36 | files: ^charts/ 37 | - id: golangci-lint 38 | name: Execute golangci-lint 39 | entry: make golint 40 | language: system 41 | files: \.go$ 42 | - id: go-test 43 | name: Execute go test 44 | entry: make test 45 | language: system 46 | files: \.go$ 47 | -------------------------------------------------------------------------------- /.github/configs/lintconf.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ignore: 3 | - config/ 4 | - charts/*/templates/ 5 | - charts/**/templates/ 6 | - docs/** 7 | - hack/** 8 | - e2e/testdata/** 9 | rules: 10 | truthy: 11 | level: warning 12 | allowed-values: 13 | - "true" 14 | - "false" 15 | - "on" 16 | - "off" 17 | check-keys: false 18 | braces: 19 | min-spaces-inside: 0 20 | max-spaces-inside: 0 21 | min-spaces-inside-empty: -1 22 | max-spaces-inside-empty: -1 23 | brackets: 24 | min-spaces-inside: 0 25 | max-spaces-inside: 0 26 | min-spaces-inside-empty: -1 27 | max-spaces-inside-empty: -1 28 | colons: 29 | max-spaces-before: 0 30 | max-spaces-after: 1 31 | commas: 32 | max-spaces-before: 0 33 | min-spaces-after: 1 34 | max-spaces-after: 1 35 | comments: 36 | require-starting-space: true 37 | min-spaces-from-content: 1 38 | document-end: disable 39 | document-start: disable # No --- to start a file 40 | empty-lines: 41 | max: 2 42 | max-start: 0 43 | max-end: 0 44 | hyphens: 45 | max-spaces-after: 1 46 | indentation: 47 | spaces: consistent 48 | indent-sequences: whatever # - list indentation will handle both indentation and without 49 | check-multi-line-strings: false 50 | key-duplicates: enable 51 | line-length: disable # Lines can be any length 52 | new-line-at-end-of-file: enable 53 | new-lines: 54 | type: unix 55 | trailing-spaces: enable 56 | -------------------------------------------------------------------------------- /internal/decryptor/testdata/secret-age.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: sops-age 5 | stringData: 6 | database_password: ENC[AES256_GCM,data:I9QUte/BtTaii6o=,iv:vaSxFo7yqfpOvWGUIfM8Aj+B25de7dYyCSF9dXeKOiU=,tag:deIwuIktYEKn2ufNKRSYyw==,type:str] 7 | database_user: ENC[AES256_GCM,data:SOpbzI0uFndBExA=,iv:icQb5nb8KnjK6dnOdxvLQDIQHycY67y3oQMmT9dSjOc=,tag:fj+4a2T5YtCDIy/yoFaZkw==,type:str] 8 | sops: 9 | kms: [] 10 | gcp_kms: [] 11 | azure_kv: [] 12 | hc_vault: [] 13 | age: 14 | - recipient: age1p0wmaw5vk8f00753t3frs4rev0du4vqdkz7sx53ml98lrcsrnuqqwwp4tl 15 | enc: | 16 | -----BEGIN AGE ENCRYPTED FILE----- 17 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvMW9LZmtvWUFXaFVQZkFq 18 | ZHFwV2oyYWhHeWIxMkM2ZkRWcSt4b2FpQmt3CmVYY3N6c002MTJWSHpBbDVuVW5B 19 | UlVQcGg2UG1uSFJGOEt5QWVPUjRLUVEKLS0tIEpwamNScUdlY2NqemFlOGJ3Sm53 20 | YnVzaEVFL1JqMlpBc3NxQmtBRVlGZmsKVJ7n7Lgk4dhWu3FK33gxbldVhcMegKpC 21 | i/2y+Ukg12cu1LpeU4GUqpIy96LzPYnfwjR15S5JhVRGs49b0hfJmw== 22 | -----END AGE ENCRYPTED FILE----- 23 | lastmodified: "2023-08-09T13:31:49Z" 24 | mac: ENC[AES256_GCM,data:XL5SwWzi0gYXSkFSWupvKYoXvyhrtyYELgy6hSqeDzrGO80XCgPY8+WOktlPu1d1FUb/F2Ez0RPoWROpyalVk3IkxADenCZ1JopTN8HZfMaBM6YxFqrAZ0mpqXVjK4LqxT+4kFojYChoU+RVEBexuhUFuQjFfQSFJ9bHo+WhlrU=,iv:bsxugaX/iczLKF6d5aDfztlO3iOmfWjBl00gRP33VsE=,tag:hL8lN0wXqG+95Yc+DaHRSg==,type:str] 25 | pgp: [] 26 | encrypted_regex: ^(data|stringData)$ 27 | version: 3.7.2 28 | -------------------------------------------------------------------------------- /e2e/suite_test.go: -------------------------------------------------------------------------------- 1 | //nolint:all 2 | package e2e_test 3 | 4 | import ( 5 | "testing" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | 10 | "k8s.io/client-go/rest" 11 | "k8s.io/kubectl/pkg/scheme" 12 | "k8s.io/utils/ptr" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | "sigs.k8s.io/controller-runtime/pkg/envtest" 15 | logf "sigs.k8s.io/controller-runtime/pkg/log" 16 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 17 | 18 | sopsv1alpha1 "github.com/peak-scale/sops-operator/api/v1alpha1" 19 | ) 20 | 21 | var ( 22 | cfg *rest.Config 23 | k8sClient client.Client 24 | testEnv *envtest.Environment 25 | ) 26 | 27 | func TestAPIs(t *testing.T) { 28 | RegisterFailHandler(Fail) 29 | 30 | RunSpecs(t, "Controller Suite") 31 | } 32 | 33 | var _ = BeforeSuite(func() { 34 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter))) 35 | 36 | By("bootstrapping test environment") 37 | testEnv = &envtest.Environment{ 38 | UseExistingCluster: ptr.To(true), 39 | } 40 | 41 | var err error 42 | cfg, err = testEnv.Start() 43 | Expect(err).ToNot(HaveOccurred()) 44 | Expect(cfg).ToNot(BeNil()) 45 | 46 | Expect(sopsv1alpha1.AddToScheme(scheme.Scheme)).NotTo(HaveOccurred()) 47 | 48 | ctrlClient, err := client.New(cfg, client.Options{Scheme: scheme.Scheme}) 49 | Expect(err).ToNot(HaveOccurred()) 50 | Expect(ctrlClient).ToNot(BeNil()) 51 | 52 | k8sClient = &e2eClient{Client: ctrlClient} 53 | 54 | }) 55 | 56 | var _ = AfterSuite(func() { 57 | By("tearing down the test environment") 58 | Expect(testEnv.Stop()).ToNot(HaveOccurred()) 59 | }) 60 | -------------------------------------------------------------------------------- /docs/monitoring.md: -------------------------------------------------------------------------------- 1 | # Monitoring 2 | 3 | Via the `/metrics` endpoint and the dedicated port you can scrape Prometheus Metrics. Amongst the standard [Kubebuilder Metrics](https://book-v1.book.kubebuilder.io/beyond_basics/controller_metrics) we provide metrics, to give you oversight of what's currently working and what's broken. This way you can always be informed, when something is not working as expected. Our custom metrics are prefixed with `sops_`: 4 | 5 | ```shell 6 | # HELP sops_provider_condition The current condition status of a Provider. 7 | # TYPE sops_provider_condition gauge 8 | sops_provider_condition{name="sample-provider",status="NotReady"} 0 9 | sops_provider_condition{name="sample-provider",status="Ready"} 1 10 | 11 | # HELP sops_secret_condition The current condition status of a Secret. 12 | # TYPE sops_secret_condition gauge 13 | sops_secret_condition{name="secret-key-1",namespace="default",status="NotReady"} 0 14 | sops_secret_condition{name="secret-key-1",namespace="default",status="Ready"} 1 15 | 16 | # HELP sops_global_secret_condition The current condition status of a Global Secret. 17 | # TYPE sops_global_secret_condition gauge 18 | sops_global_secret_condition{name="global-secret-key-1",status="NotReady"} 1 19 | sops_global_secret_condition{name="global-secret-key-1",status="Ready"} 0 20 | ``` 21 | 22 | The Helm-Chart comes with a [ServiceMonitor](https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api.md#servicemonitor) and [PrometheusRules](https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api.md#monitoring.coreos.com/v1.PrometheusRule) 23 | -------------------------------------------------------------------------------- /charts/sops-operator/templates/crd-lifecycle/rbac.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.crds.install (not $.Values.crds.inline) }} 2 | --- 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | name: {{ include "crds.name" . }} 7 | namespace: {{ .Release.Namespace | quote }} 8 | annotations: 9 | # create hook dependencies in the right order 10 | "helm.sh/hook-weight": "-3" 11 | {{- include "crds.annotations" . | nindent 4 }} 12 | labels: 13 | app.kubernetes.io/component: {{ include "crds.component" . | quote }} 14 | {{- include "helm.labels" . | nindent 4 }} 15 | rules: 16 | - apiGroups: 17 | - "" 18 | resources: 19 | - jobs 20 | verbs: 21 | - create 22 | - delete 23 | - apiGroups: 24 | - apiextensions.k8s.io 25 | resources: 26 | - customresourcedefinitions 27 | verbs: 28 | - create 29 | - delete 30 | - get 31 | - patch 32 | --- 33 | apiVersion: rbac.authorization.k8s.io/v1 34 | kind: ClusterRoleBinding 35 | metadata: 36 | name: {{ include "crds.name" . }} 37 | namespace: {{ .Release.Namespace | quote }} 38 | annotations: 39 | # create hook dependencies in the right order 40 | "helm.sh/hook-weight": "-2" 41 | {{- include "crds.annotations" . | nindent 4 }} 42 | labels: 43 | app.kubernetes.io/component: {{ include "crds.component" . | quote }} 44 | {{- include "helm.labels" . | nindent 4 }} 45 | roleRef: 46 | apiGroup: rbac.authorization.k8s.io 47 | kind: ClusterRole 48 | name: {{ include "crds.name" . }} 49 | subjects: 50 | - kind: ServiceAccount 51 | name: {{ include "crds.name" . }} 52 | namespace: {{ .Release.Namespace | quote }} 53 | {{- end }} 54 | -------------------------------------------------------------------------------- /charts/sops-operator/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if and $.Values.monitoring.enabled $.Values.monitoring.serviceMonitor.enabled }} 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | name: {{ include "helm.fullname" . }}-monitor 6 | namespace: {{ .Values.monitoring.serviceMonitor.namespace | default .Release.Namespace }} 7 | labels: 8 | {{- include "helm.labels" . | nindent 4 }} 9 | {{- with .Values.monitoring.serviceMonitor.labels }} 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- with .Values.monitoring.serviceMonitor.annotations }} 13 | annotations: 14 | {{- toYaml . | nindent 4 }} 15 | {{- end }} 16 | spec: 17 | endpoints: 18 | {{- with .Values.monitoring.serviceMonitor.endpoint }} 19 | - interval: {{ .interval }} 20 | port: metrics 21 | path: /metrics 22 | {{- with .scrapeTimeout }} 23 | scrapeTimeout: {{ . }} 24 | {{- end }} 25 | {{- with .metricRelabelings }} 26 | metricRelabelings: {{- toYaml . | nindent 6 }} 27 | {{- end }} 28 | {{- with .relabelings }} 29 | relabelings: {{- toYaml . | nindent 6 }} 30 | {{- end }} 31 | {{- end }} 32 | jobLabel: {{ .Values.monitoring.serviceMonitor.jobLabel }} 33 | {{- with .Values.monitoring.serviceMonitor.targetLabels }} 34 | targetLabels: {{- toYaml . | nindent 4 }} 35 | {{- end }} 36 | selector: 37 | matchLabels: 38 | {{- if .Values.monitoring.serviceMonitor.matchLabels }} 39 | {{- toYaml .Values.monitoring.serviceMonitor.matchLabels | nindent 6 }} 40 | {{- else }} 41 | {{- include "helm.selectorLabels" . | nindent 6 }} 42 | {{- end }} 43 | namespaceSelector: 44 | matchNames: 45 | - {{ .Release.Namespace }} 46 | {{- end }} 47 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | permissions: {} 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | pull_request: 8 | branches: 9 | - "*" 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | jobs: 14 | manifests: 15 | name: diff 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 19 | with: 20 | fetch-depth: 0 21 | - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 22 | with: 23 | go-version-file: 'go.mod' 24 | - name: Generate manifests 25 | run: | 26 | make manifests 27 | if [[ $(git diff --stat) != '' ]]; then 28 | echo -e '\033[0;31mManifests outdated! (Run make manifests locally and commit)\033[0m ❌' 29 | git diff --color 30 | exit 1 31 | else 32 | echo -e '\033[0;32mDocumentation up to date\033[0m ✔' 33 | fi 34 | yamllint: 35 | name: yamllint 36 | runs-on: ubuntu-24.04 37 | steps: 38 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 39 | - name: Install yamllint 40 | run: pip install yamllint 41 | - name: Lint YAML files 42 | run: yamllint -c=.github/configs/lintconf.yaml . 43 | golangci: 44 | name: lint 45 | runs-on: ubuntu-24.04 46 | steps: 47 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 48 | - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 49 | with: 50 | go-version-file: 'go.mod' 51 | - name: Run golangci-lint 52 | run: make golint 53 | -------------------------------------------------------------------------------- /e2e/suite_client_test.go: -------------------------------------------------------------------------------- 1 | //nolint:all 2 | 3 | package e2e_test 4 | 5 | import ( 6 | "context" 7 | "time" 8 | 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | ) 11 | 12 | type e2eClient struct { 13 | client.Client 14 | } 15 | 16 | func (e *e2eClient) sleep() { 17 | time.Sleep(1000 * time.Millisecond) 18 | } 19 | 20 | func (e *e2eClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 21 | defer e.sleep() 22 | 23 | return e.Client.Get(ctx, key, obj, opts...) 24 | } 25 | 26 | func (e *e2eClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { 27 | defer e.sleep() 28 | 29 | return e.Client.List(ctx, list, opts...) 30 | } 31 | 32 | func (e *e2eClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { 33 | defer e.sleep() 34 | obj.SetResourceVersion("") 35 | 36 | return e.Client.Create(ctx, obj, opts...) 37 | } 38 | 39 | func (e *e2eClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { 40 | defer e.sleep() 41 | 42 | return e.Client.Delete(ctx, obj, opts...) 43 | } 44 | 45 | func (e *e2eClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { 46 | defer e.sleep() 47 | 48 | return e.Client.Update(ctx, obj, opts...) 49 | } 50 | 51 | func (e *e2eClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { 52 | defer e.sleep() 53 | 54 | return e.Client.Patch(ctx, obj, patch, opts...) 55 | } 56 | 57 | func (e *e2eClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { 58 | defer e.sleep() 59 | 60 | return e.Client.DeleteAllOf(ctx, obj, opts...) 61 | } 62 | -------------------------------------------------------------------------------- /api/v1alpha1/sopsprovider_func.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025 Peak Scale 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package v1alpha1 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | corev1 "k8s.io/api/core/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | ) 14 | 15 | // GatherProviderSecrets selects unique secrets based on ProviderSelectors. 16 | func (s *SopsProvider) GatherProviderSecrets(ctx context.Context, client client.Client) ([]corev1.Secret, error) { 17 | secretList := &corev1.SecretList{} 18 | if err := client.List(ctx, secretList); err != nil { 19 | return nil, fmt.Errorf("failed to list secrets: %w", err) 20 | } 21 | 22 | uniqueSecrets := make(map[string]*corev1.Secret) 23 | 24 | for _, selector := range s.Spec.ProviderSecrets { 25 | if selector == nil { 26 | continue 27 | } 28 | 29 | matchingSecrets, err := selector.MatchObjects(ctx, client, toObjectList(secretList.Items)) 30 | if err != nil { 31 | return nil, fmt.Errorf("error matching secrets: %w", err) 32 | } 33 | 34 | for _, sec := range matchingSecrets { 35 | secret, ok := sec.(*corev1.Secret) 36 | if ok { 37 | uniqueSecrets[secret.Namespace+"/"+secret.Name] = secret 38 | } 39 | } 40 | } 41 | 42 | finalSecrets := make([]corev1.Secret, 0, len(uniqueSecrets)) 43 | for _, sec := range uniqueSecrets { 44 | finalSecrets = append(finalSecrets, *sec) 45 | } 46 | 47 | return finalSecrets, nil 48 | } 49 | 50 | // Helper function to convert []corev1.Secret to []metav1.Object. 51 | func toObjectList(secrets []corev1.Secret) []metav1.Object { 52 | objectList := make([]metav1.Object, len(secrets)) 53 | for i := range secrets { 54 | objectList[i] = &secrets[i] 55 | } 56 | 57 | return objectList 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | name: Build images 2 | permissions: {} 3 | on: 4 | pull_request: 5 | branches: 6 | - "*" 7 | paths: 8 | - '.github/workflows/docker-*.yml' 9 | - 'api/**' 10 | - 'internal/**' 11 | - 'e2e/*' 12 | - '.ko.yaml' 13 | - 'go.*' 14 | - 'main.go' 15 | - 'Makefile' 16 | 17 | jobs: 18 | build-images: 19 | runs-on: ubuntu-latest 20 | permissions: 21 | security-events: write 22 | actions: read 23 | contents: read 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 27 | - name: Setup QEMU 28 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 29 | - name: Setup Docker Buildx 30 | id: buildx 31 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 32 | - name: ko build 33 | run: VERSION=${{ github.sha }} make ko-build-all 34 | - name: Trivy Scan Image 35 | uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1 36 | with: 37 | scan-type: 'fs' 38 | ignore-unfixed: true 39 | format: 'sarif' 40 | output: 'trivy-results.sarif' 41 | severity: 'CRITICAL,HIGH' 42 | env: 43 | # Trivy is returning TOOMANYREQUESTS 44 | # See: https://github.com/aquasecurity/trivy-action/issues/389#issuecomment-2385416577 45 | TRIVY_DB_REPOSITORY: 'public.ecr.aws/aquasecurity/trivy-db:2' 46 | - name: Upload Trivy scan results to GitHub Security tab 47 | uses: github/codeql-action/upload-sarif@59ce4c1340a74f56c129f758767ef33668e572b0 48 | with: 49 | sarif_file: 'trivy-results.sarif' 50 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended", ":dependencyDashboard"], 4 | "baseBranches": ["main"], 5 | "prHourlyLimit": 0, 6 | "prConcurrentLimit": 0, 7 | "branchConcurrentLimit": 0, 8 | "mode": "full", 9 | "commitMessageLowerCase": "auto", 10 | "semanticCommits": "enabled", 11 | "flux": { 12 | "fileMatch": ["^.*flux\\.yaml$"] 13 | }, 14 | "packageRules": [ 15 | { 16 | "matchManagers": ["github-actions", "flux"], 17 | "groupName": "all-ci-dependencies", 18 | "updateTypes": ["major", "minor", "patch"] 19 | } 20 | ], 21 | "customManagers": [ 22 | { 23 | "customType": "regex", 24 | "fileMatch": ["^Makefile$"], 25 | "matchStrings": [ 26 | "(?[A-Z0-9_]+)_VERSION\\s*[:=?]+\\s*\"?(?[^\"\\r\\n]+)\"?[\\s\\S]*?(?[A-Z0-9_]+)_LOOKUP\\s*[:=?]+\\s*\"?(?[^\"\\r\\n]+)\"?(?:[\\s\\S]*?(?[A-Z0-9_]+)_SOURCE\\s*[:=?]+\\s*\"?(?[^\"\\r\\n]+)\"?)?" 27 | ], 28 | "depNameTemplate": "{{lookupValue}}", 29 | "datasourceTemplate": "{{#sourceValue}}{{sourceValue}}{{/sourceValue}}{{^sourceValue}}github-tags{{/sourceValue}}", 30 | "lookupNameTemplate": "{{lookupValue}}", 31 | "versioningTemplate": "semver" 32 | }, 33 | { 34 | "customType": "regex", 35 | "fileMatch": [".*\\.pre-commit-config\\.ya?ml$"], 36 | "matchStrings": [ 37 | "repo:\\s*https://github\\.com/(?[^/]+/[^\\s]+)[\\s\\S]*?rev:\\s*(?v?\\d+\\.\\d+\\.\\d+)" 38 | ], 39 | "depNameTemplate": "{{lookupValue}}", 40 | "datasourceTemplate": "github-tags", 41 | "lookupNameTemplate": "{{lookupValue}}", 42 | "versioningTemplate": "semver" 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /hack/templates/crds.tmpl: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | Packages: 4 | {{range .Groups}} 5 | - [{{.Group}}/{{.Version}}](#{{ anchorize (printf "%s/%s" .Group .Version) }}) 6 | {{- end -}}{{/* range .Groups */}} 7 | 8 | {{- range .Groups }} 9 | {{- $group := . }} 10 | 11 | # {{.Group}}/{{.Version}} 12 | 13 | Resource Types: 14 | {{range .Kinds}} 15 | - [{{.Name}}](#{{ anchorize .Name }}) 16 | {{end}}{{/* range .Kinds */}} 17 | 18 | {{range .Kinds}} 19 | {{$kind := .}} 20 | ## {{.Name}} 21 | 22 | {{range .Types}} 23 | 24 | {{if not .IsTopLevel}} 25 | ### {{.Name}} 26 | {{end}} 27 | 28 | 29 | {{.Description}} 30 | 31 | | **Name** | **Type** | **Description** | **Required** | 32 | | :---- | :---- | :----------- | :-------- | 33 | {{- if .IsTopLevel }} 34 | | **apiVersion** | string | {{$group.Group}}/{{$group.Version}} | true | 35 | | **kind** | string | {{$kind.Name}} | true | 36 | | **[metadata](https://kubernetes.io/docs/reference/generated/kubernetes-api/latest/#objectmeta-v1-meta)** | object | Refer to the Kubernetes API documentation for the fields of the `metadata` field. | true | 37 | {{- end -}} 38 | {{- range .Fields }} 39 | | **{{if .TypeKey}}[{{.Name}}](#{{.TypeKey}}){{else}}{{.Name}}{{end}}** | {{.Type}} | {{.Description}} {{- if or .Schema.Format .Schema.Enum .Schema.Default .Schema.Minimum .Schema.Maximum }}
{{- end -}} {{- if .Schema.Format }}Format: {{.Schema.Format}}
{{- end}} {{- if .Schema.Enum }}Enum: {{.Schema.Enum | toStrings | join ", "}}
{{- end}} {{- if .Schema.Default }}Default: {{.Schema.Default}}
{{- end}} {{- if .Schema.Minimum }}Minimum: {{.Schema.Minimum}}
{{- end}} {{- if .Schema.Maximum }}Maximum: {{.Schema.Maximum}}
{{- end}} | {{.Required}} | 40 | {{- end }} 41 | 42 | {{- end}}{{/* range .Types */}} 43 | {{- end}}{{/* range .Kinds */}} 44 | {{- end}}{{/* range .Groups */}} 45 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Getting started locally is pretty easy. You can execute: 4 | 5 | ```shell 6 | make e2e-build 7 | ``` 8 | 9 | This installs all required operators an installs the operator within a [KinD Cluster](https://kind.sigs.k8s.io/). The required binaries are also downloaded. 10 | 11 | If you wish to test against a specific Kubernetes version, you can pass that via variable: 12 | 13 | ```shell 14 | KIND_K8S_VERSION="v1.31.0" make e2e-build 15 | ``` 16 | 17 | When you want to quickly develop, you can scale down the operator within the cluster: 18 | 19 | ```shell 20 | kubectl scale deploy sops-operator --replicas=0 -n sops-operator 21 | ``` 22 | 23 | And then execute the binary: 24 | 25 | ```shell 26 | go run cmd/main.go -zap-log-level=10 27 | ``` 28 | 29 | You might need to first export the Kubeconfig for the cluster (If you are using multiple clusters at the same time): 30 | 31 | ```shell 32 | bin/kind get kubeconfig --name sops-operator > /tmp/sops-operator 33 | export KUBECONFIG="/tmp/sops-operator" 34 | ``` 35 | 36 | ## Testing 37 | 38 | When you are done with the development run the following commands. 39 | 40 | For Liniting 41 | 42 | ```shell 43 | make golint 44 | ``` 45 | 46 | For Unit-Testing 47 | 48 | ```shell 49 | make test 50 | ``` 51 | 52 | For Unit-Testing (Use clean KinD Cluster): 53 | 54 | ```shell 55 | make e2e-exec 56 | ``` 57 | 58 | ## Helm Chart 59 | 60 | When making changes to the Helm-Chart, Update the documentation by running: 61 | 62 | ```shell 63 | make helm-docs 64 | ``` 65 | 66 | Linting and Testing the chart: 67 | 68 | ```shell 69 | make helm-lint 70 | make helm-test 71 | ``` 72 | 73 | ## Performance 74 | 75 | Use [PProf](https://book.kubebuilder.io/reference/pprof-tutorial) for profiling: 76 | 77 | ```shell 78 | curl -s "http://127.0.0.1:8082/debug/pprof/profile" > ./cpu-profile.out 79 | 80 | go tool pprof -http=:8080 ./cpu-profile.out 81 | ``` 82 | -------------------------------------------------------------------------------- /e2e/testdata/age/secret-key-1.enc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: SopsSecret 3 | metadata: 4 | name: secret-key-1 5 | spec: 6 | metadata: 7 | prefix: age- 8 | suffix: -key-1 9 | labels: 10 | age.top: label 11 | annotations: 12 | age.top: label 13 | secrets: 14 | - name: my-secret-name-1 15 | labels: 16 | age.bottom: label 17 | annotations: 18 | age.bottom: annotation 19 | stringData: 20 | data-name0: ENC[AES256_GCM,data:hH0VL7I7VZVX4QI=,iv:UN03MyJvcVoisHqb66+OgNTtJuZrNSpCb4T7UX3VJW4=,tag:LakXGgU3WA48c1o18sUC3A==,type:str] 21 | data: 22 | data-name1: ENC[AES256_GCM,data:t5/qFnXUFSC9w0mVER5XcA==,iv:+cS+jizXCs7zZbVGDcOb4FvJUH8h6QWrE3q5vnRATss=,tag:JCTSTi3Fb52Cf+InEkTM9g==,type:str] 23 | sops: 24 | kms: [] 25 | gcp_kms: [] 26 | azure_kv: [] 27 | hc_vault: [] 28 | age: 29 | - recipient: age1s7t2vk2crlxaumgm7cacs568xwutkjs535pla69kt6w006t7wgzqhkfwvp 30 | enc: | 31 | -----BEGIN AGE ENCRYPTED FILE----- 32 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCVUFCNksxdW4vQzk2TFV4 33 | V2xxWXluR0dhQlNrcDV5cTR4U3g1WTFMRmpVCllySlBKQzQwQ0w5ZHJHN3dieDBo 34 | QVpHcVc5WmowcVhRU1A4dTN2eUdUL2MKLS0tIEQ1c2lVSWNMNnlnYjQxbjlKdk1k 35 | eWNCWjRZb1JjL3NSMWc2MEFzMS8zSkUKYtPKCtwAPYxio9eCb4qrc5dVZqJ6pbOJ 36 | oQKz+tsnWKqvdtBRN0CovhJdUc2miJpwzQNx15QqoSRo5SIEC1aPRw== 37 | -----END AGE ENCRYPTED FILE----- 38 | lastmodified: "2025-09-02T11:21:15Z" 39 | mac: ENC[AES256_GCM,data:Q8yqm1FutNNnf7JBJ8lPfxu22XenClOmpkKFY3CrEmMJo+DToNnRTkUHlf7BU6ftCG9nQDq9fteL7XLXd2osa2CskKJHicwXLNDkubsaSkb7gfSyAPmobQMjmnEYgQf5HlD1czEP+uKthrCX33oCDm/kZEoK7avIUA08oNGsHyo=,iv:ft04jjSsl44QvlCkCTC1ta39gheAV08CUEP7AD5F9xs=,tag:Gw3N2Yz+6VZ3ZUEnSGs+FA==,type:str] 40 | pgp: [] 41 | encrypted_regex: ^(data|stringData)$ 42 | version: 3.8.1 43 | -------------------------------------------------------------------------------- /api/v1alpha1/sopsprovider_types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025 Peak Scale 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package v1alpha1 5 | 6 | import ( 7 | "github.com/peak-scale/sops-operator/internal/api" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | // SopsProviderSpec defines the desired state of SopsProvider. 12 | type SopsProviderSpec struct { 13 | // Selector Referencing which Secrets can be encrypted by this provider 14 | // This selects effective SOPS Secrets 15 | SOPSSelectors []*api.NamespacedSelector `json:"sops"` 16 | // Select namespaces or secrets where decryption information for this 17 | // provider can be sourced from 18 | ProviderSecrets []*api.NamespacedSelector `json:"keys"` 19 | } 20 | 21 | // +kubebuilder:object:root=true 22 | // +kubebuilder:subresource:status 23 | // +kubebuilder:resource:scope=Cluster 24 | // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.condition.type",description="Status" 25 | // +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.condition.message",description="Message" 26 | // +kubebuilder:printcolumn:name="Providers",type="integer",JSONPath=".status.size",description="Amount of providers" 27 | // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="" 28 | 29 | // SopsProvider is the Schema for the sopsproviders API. 30 | type SopsProvider struct { 31 | metav1.TypeMeta `json:",inline"` 32 | metav1.ObjectMeta `json:"metadata,omitempty"` 33 | 34 | Spec SopsProviderSpec `json:"spec,omitempty"` 35 | Status SopsProviderStatus `json:"status,omitempty"` 36 | } 37 | 38 | // +kubebuilder:object:root=true 39 | 40 | // SopsProviderList contains a list of SopsProvider. 41 | type SopsProviderList struct { 42 | metav1.TypeMeta `json:",inline"` 43 | metav1.ListMeta `json:"metadata,omitempty"` 44 | Items []SopsProvider `json:"items"` 45 | } 46 | 47 | func init() { 48 | SchemeBuilder.Register(&SopsProvider{}, &SopsProviderList{}) 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/helm-test.yml: -------------------------------------------------------------------------------- 1 | name: Test charts 2 | permissions: {} 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - "*" 8 | paths: 9 | - '.github/workflows/helm-*.yml' 10 | - 'api/**' 11 | - 'cmd/**' 12 | - 'internal/**' 13 | - 'charts/**' 14 | - 'e2e/*' 15 | - '.ko.yaml' 16 | - 'go.*' 17 | - 'main.go' 18 | - 'Makefile' 19 | jobs: 20 | linter-artifacthub: 21 | runs-on: ubuntu-latest 22 | container: 23 | image: artifacthub/ah 24 | options: --user root 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 28 | - name: Run ah lint 29 | working-directory: ./charts/ 30 | run: ah lint 31 | lint: 32 | runs-on: ubuntu-24.04 33 | steps: 34 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 35 | with: 36 | fetch-depth: 0 37 | - uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 38 | - name: Run chart-testing (lint) 39 | run: make helm-lint 40 | - name: Run docs-testing (helm-docs) 41 | id: helm-docs 42 | run: | 43 | make helm-docs 44 | if [[ $(git diff --stat) != '' ]]; then 45 | echo -e '\033[0;31mDocumentation outdated! (Run make helm-docs locally and commit)\033[0m ❌' 46 | git diff --color 47 | exit 1 48 | else 49 | echo -e '\033[0;32mDocumentation up to date\033[0m ✔' 50 | fi 51 | - name: Run schema-testing (helm-schema) 52 | id: helm-schema 53 | run: | 54 | make helm-schema 55 | if [[ $(git diff --stat) != '' ]]; then 56 | echo -e '\033[0;31mSchema outdated! (Run make helm-schema locally and commit)\033[0m ❌' 57 | git diff --color 58 | exit 1 59 | else 60 | echo -e '\033[0;32mSchema up to date\033[0m ✔' 61 | fi 62 | - name: Run chart-testing (install) 63 | run: make helm-test 64 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | tests: false 4 | allow-parallel-runners: true 5 | linters: 6 | default: all 7 | disable: 8 | - depguard 9 | - err113 10 | - exhaustruct 11 | - funlen 12 | - gochecknoglobals 13 | - gochecknoinits 14 | - gomoddirectives 15 | - ireturn 16 | - lll 17 | - mnd 18 | - nilnil 19 | - nonamedreturns 20 | - paralleltest 21 | - perfsprint 22 | - recvcheck 23 | - testpackage 24 | - unparam 25 | - varnamelen 26 | - wrapcheck 27 | - goconst 28 | settings: 29 | cyclop: 30 | max-complexity: 27 31 | dupl: 32 | threshold: 100 33 | gocognit: 34 | min-complexity: 50 35 | goconst: 36 | min-len: 2 37 | min-occurrences: 2 38 | goheader: 39 | template: |- 40 | Copyright 2024-2025 Peak Scale 41 | SPDX-License-Identifier: Apache-2.0 42 | inamedparam: 43 | skip-single-param: true 44 | nakedret: 45 | max-func-lines: 50 46 | exclusions: 47 | generated: lax 48 | presets: 49 | - comments 50 | - common-false-positives 51 | - legacy 52 | - std-error-handling 53 | rules: 54 | - linters: 55 | - lll 56 | - tagliatelle 57 | - prealloc 58 | path: api/* 59 | - linters: 60 | - dupl 61 | - lll 62 | path: internal/* 63 | paths: 64 | - zz_.*\.go$ 65 | - .+\.generated.go 66 | - .+_test.go 67 | - .+_test_.+.go 68 | - third_party$ 69 | - builtin$ 70 | - examples$ 71 | - ^internal/decryptor/ 72 | formatters: 73 | enable: 74 | - gci 75 | - gofmt 76 | - gofumpt 77 | - goimports 78 | settings: 79 | gci: 80 | sections: 81 | - standard 82 | - default 83 | gofumpt: 84 | module-path: github.com/peak-scale/sops-operator 85 | extra-rules: false 86 | exclusions: 87 | generated: lax 88 | paths: 89 | - zz_.*\.go$ 90 | - .+\.generated.go 91 | - .+_test.go 92 | - .+_test_.+.go 93 | - third_party$ 94 | - builtin$ 95 | - examples$ 96 | -------------------------------------------------------------------------------- /.github/workflows/helm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish charts 2 | permissions: read-all 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | jobs: 8 | publish-helm: 9 | runs-on: ubuntu-24.04 10 | permissions: 11 | contents: write 12 | id-token: write 13 | packages: write 14 | outputs: 15 | chart-digest: ${{ steps.helm_publish.outputs.digest }} 16 | steps: 17 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 18 | - uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 19 | - name: "Extract Version" 20 | id: extract_version 21 | run: | 22 | GIT_TAG=${GITHUB_REF##*/} 23 | VERSION=${GIT_TAG##v} 24 | echo "version=$(echo $VERSION)" >> $GITHUB_OUTPUT 25 | - name: Helm | Publish 26 | id: helm_publish 27 | uses: peak-scale/github-actions/helm-oci-chart@a441cca016861c546ab7e065277e40ce41a3eb84 # v0.2.0 28 | with: 29 | registry: ghcr.io 30 | repository: ${{ github.repository_owner }}/charts 31 | name: "sops-operator" 32 | path: "./charts/sops-operator/" 33 | app-version: ${{ steps.extract_version.outputs.version }} 34 | version: ${{ steps.extract_version.outputs.version }} 35 | registry-username: ${{ github.actor }} 36 | registry-password: ${{ secrets.GITHUB_TOKEN }} 37 | update-dependencies: 'true' # Defaults to false 38 | sign-image: 'true' 39 | signature-repository: ghcr.io/${{ github.repository_owner }}/charts/sops-operator 40 | helm-provenance: 41 | needs: publish-helm 42 | permissions: 43 | id-token: write # To sign the provenance. 44 | packages: write # To upload assets to release. 45 | actions: read # To read the workflow path. 46 | uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 47 | with: 48 | image: ghcr.io/${{ github.repository_owner }}/charts/sops-operator 49 | digest: "${{ needs.publish-helm.outputs.chart-digest }}" 50 | registry-username: ${{ github.actor }} 51 | secrets: 52 | registry-password: ${{ secrets.GITHUB_TOKEN }} 53 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | types: 9 | - opened 10 | - reopened 11 | - synchronize 12 | branches: 13 | - "main" 14 | 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | sast: 21 | name: "SAST" 22 | runs-on: ubuntu-24.04 23 | env: 24 | GO111MODULE: on 25 | permissions: 26 | security-events: write 27 | actions: read 28 | contents: read 29 | steps: 30 | - name: Checkout Source 31 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 32 | - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 33 | with: 34 | go-version-file: 'go.mod' 35 | - name: Run Gosec Security Scanner 36 | uses: securego/gosec@15d5c61e866bc2e2e8389376a31f1e5e09bde7d8 # v2.22.9 37 | with: 38 | args: '-no-fail -fmt sarif -out gosec.sarif ./...' 39 | - name: Upload SARIF file 40 | uses: github/codeql-action/upload-sarif@59ce4c1340a74f56c129f758767ef33668e572b0 41 | with: 42 | sarif_file: gosec.sarif 43 | unit_tests: 44 | name: "Unit tests" 45 | runs-on: ubuntu-24.04 46 | steps: 47 | - name: Checkout 48 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 49 | - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 50 | with: 51 | go-version-file: 'go.mod' 52 | - name: Unit Test 53 | run: make test 54 | - name: Check secret 55 | id: checksecret 56 | uses: ./.github/actions/exists 57 | with: 58 | value: ${{ secrets.CODECOV_TOKEN }} 59 | - name: Upload Report to Codecov 60 | if: ${{ steps.checksecret.outputs.result == 'true' }} 61 | uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 62 | with: 63 | token: ${{ secrets.CODECOV_TOKEN }} 64 | slug: peak-scale/sops-operator 65 | files: ./coverage.out 66 | fail_ci_if_error: true 67 | verbose: true 68 | -------------------------------------------------------------------------------- /internal/decryptor/testdata/secret-pgp.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: sops-pgp 5 | stringData: 6 | database_password: ENC[AES256_GCM,data:xap6r9CPuHN3T/c=,iv:oZ8gcWTlKWRBjW85h4ugCdTmzG4Ak1WsPAH69O6DYQs=,tag:kJYNlYCGvQX9aZYMq+0IRg==,type:str] 7 | database_user: ENC[AES256_GCM,data:Wx5woeKL4Zs1EIM=,iv:fE7GCyPRUvMHbUtLDPSQqMn297Cls6LKVgnRnC2+2dA=,tag:NfsPXTXBAi5WClj4wW3Jyg==,type:str] 8 | sops: 9 | kms: [] 10 | gcp_kms: [] 11 | azure_kv: [] 12 | hc_vault: [] 13 | age: [] 14 | lastmodified: "2023-08-09T13:29:15Z" 15 | mac: ENC[AES256_GCM,data:CXNyGQZ74EFPYgTPzPfplyDjTXksqbShtli3y+gC/0FYaCi2nngjIj6nV5ibcnuHpCwjnJvkCp9vzfhamZn8LfO5qznLCZifZ2Wf9rjzmVPCVLNlxqyz50NKeRWveWoNqr/G1Z1rMadjARfCWtf1atFJyTx+XSDmR7dMh/qwPqY=,iv:kDelUNjBkTbyvSDwYKNhKAbJCNhTNxzQiPDUGfbKzKI=,tag:s4mXaSWpH3xqAgYcjJPULQ==,type:str] 16 | pgp: 17 | - created_at: "2023-08-09T13:29:14Z" 18 | enc: | 19 | -----BEGIN PGP MESSAGE----- 20 | 21 | hQIMA0RuT4C0qyryAQ//flqFS2clVtcKTxbzn5Ravj7v2/R/RY62HmBmxBoRVELS 22 | OuFNqhxGsa9efrDgdQQKSsOxsEXTeTcZuJRGZvHHBaWZsWfvPN1Tfcm7RGTI6eIy 23 | Qlb9r4X5D47oQdkN8LnK7JgV93ZIN4wLc2CnRFjSRz2bam3sImhhckBd6KZgQk9g 24 | Zhf/+cGxp+sSM1MnHobVHabtLwEnks3stdJc/A5Gmo9W5NkGihGlfhNtaKOFXT8r 25 | cmi/uOvEU6tDH61eIbmwECYF4m2VrI52ukyavPloo26VmpM7lCYB5I+RCWT+MsIk 26 | 5U+iws4Ud/xbQ1sGMw2riPofde5rOKAzMAeJQl7ZHbUCgwjjVnlAMVw17Zr2ohEc 27 | 6ZR2BbbJYZEZTsAWXstnyTZvTINRBXAg48Eh+Pp1bj0hAIV9ek7ffFeGSijw833C 28 | EjAxgtiAZ7bs8o4PyUUa+QqsCvzDo8I6Z1xdTkmGBN12+jHUYNlsFy1iYlSWgZmC 29 | CLkHKUKD8gWbNGRriuvSve6ZYbVt2u53llxJJaTYHdNAloQ8cX5BvI2Lbplyej+W 30 | fCGdGq/ZA6xe8WhwE6X2kGD4GMGk5yrCvFeZRoemwiUAC6vrixVrhIPfexzFp35X 31 | RqIR8hWRX6UIEMfig7egnBRzQ5GG5GymdYnfibAgZGqdIE6jDjQTtY4TogcLRHLU 32 | aAEJAhAwoOvLNjNtH7jhvAmH683O0Qy21oaUMZSWz0NQZL6aeWn+F84iYtQcQ/eL 33 | KR1ndRrGgI9BU5PEPudWgIAWY6EZN+ujazRdM4hp/ZPJhi71gIDW3HDNxK+/3oJ1 34 | vTjeGkz5RIOy 35 | =BHWN 36 | -----END PGP MESSAGE----- 37 | fp: B01102D81246867C4BC24D863E7286BEE865E3C4 38 | encrypted_regex: ^(data|stringData)$ 39 | version: 3.7.2 40 | -------------------------------------------------------------------------------- /e2e/testdata/age/global-key-1.enc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: GlobalSopsSecret 3 | metadata: 4 | name: global-secret-key-1 5 | spec: 6 | secrets: 7 | - name: secret-1 8 | namespace: global-secret-1 9 | labels: 10 | label1: value1 11 | stringData: 12 | data-name0: ENC[AES256_GCM,data:dHtQ/GjXvFlUd9o=,iv:2+JREF0rMeC1iJ8KJ8CQJr8B1b/dUyTatcxPjzhqJHc=,tag:8jB+/sZJce4d/JQQMnlDKw==,type:str] 13 | data: 14 | data-name1: ENC[AES256_GCM,data:dFvDrNpswNrumJw6z3rIPg==,iv:RLf418zhGQ8cO0xs4DJQ7qgC6J0cTx9P1hBpTzCMUB4=,tag:xnMf/+ETZzqUHriWFLKXTg==,type:str] 15 | - name: secret-2 16 | namespace: global-secret-2 17 | labels: 18 | label1: value1 19 | stringData: 20 | data-name0: ENC[AES256_GCM,data:v9kSyWLg/5P9zq4=,iv:KCM3l6x35fGrLUdCMzb+b1shORgUG/+MkUdfWtdbEjc=,tag:TxrzQd7K3If7ZN9NYkLD7w==,type:str] 21 | data: 22 | data-name1: ENC[AES256_GCM,data:O3tLUNhUCJVUAPNSFhK7hw==,iv:E+zQkzl/lM0xHFareeghpmUeuMXJZ+HwhgqFF8ifPtY=,tag:f5iaL2+2P8U0pqbOiTwHug==,type:str] 23 | sops: 24 | kms: [] 25 | gcp_kms: [] 26 | azure_kv: [] 27 | hc_vault: [] 28 | age: 29 | - recipient: age1s7t2vk2crlxaumgm7cacs568xwutkjs535pla69kt6w006t7wgzqhkfwvp 30 | enc: | 31 | -----BEGIN AGE ENCRYPTED FILE----- 32 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6eGFGdzEzTEdEWHQrRWRl 33 | a29UdFpJT2ZFL2Z6ckIyS29DV0s5SXhQNHpNCmZtajRhdGFZWDFudC92eG9nZFYz 34 | eEtTOG9zNVhoa0hNb09lS1VjYjVnaDQKLS0tIGZHdzZJdGtMRkpvclJ6RzJxUnRr 35 | L0RTSzEzcTRTL1VqNE1HNHg3M2NEeGsKjDq/F2Nn4t1favFHjp5r90Bd7QcvmAxD 36 | cheAzHWAIJY11fAcuCbZ7uqmdbWR91tNr0TJeYalICy6iZcqh7pehQ== 37 | -----END AGE ENCRYPTED FILE----- 38 | lastmodified: "2025-07-22T16:53:13Z" 39 | mac: ENC[AES256_GCM,data:DepSuQRpNPPdXAcxJsct60vEkcyWgj+Ro3nDf322HKeHGBBIiaw1cZ+tVhYwn4LpqFTD2d8S3RYCqGAWaVSwb783ONn/Fo/22xXzAZzpRbF3YD5Hpi6lDBqn8UdFh9XKqzJeN1BjtIZG0+JpGxBYMACKiG5NO/ZxyUaCaI8kXQw=,iv:EAdbWUERuuoCDtTfnrPcuo9jpH+aQHNyNB1ckGT9JO4=,tag:TjMpOU/2zW5hfvfm5W3u8A==,type:str] 40 | pgp: [] 41 | encrypted_regex: ^(data|stringData)$ 42 | version: 3.8.1 43 | -------------------------------------------------------------------------------- /internal/decryptor/kustomize-controller/azkv/keysource_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 The Flux authors 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, v. 2.0. If a copy of the MPL was not distributed with this 5 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | package azkv 8 | 9 | import ( 10 | "testing" 11 | "time" 12 | 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | func TestToken_ApplyToMasterKey(t *testing.T) { 17 | g := NewWithT(t) 18 | 19 | token, err := TokenFromAADConfig( 20 | AADConfig{TenantID: "tenant", ClientID: "client", ClientSecret: "secret"}, 21 | ) 22 | g.Expect(err).ToNot(HaveOccurred()) 23 | 24 | key := &MasterKey{} 25 | token.ApplyToMasterKey(key) 26 | g.Expect(key.token).To(Equal(token.token)) 27 | } 28 | 29 | func TestMasterKey_EncryptedDataKey(t *testing.T) { 30 | g := NewWithT(t) 31 | 32 | key := &MasterKey{EncryptedKey: "some key"} 33 | g.Expect(key.EncryptedDataKey()).To(BeEquivalentTo(key.EncryptedKey)) 34 | } 35 | 36 | func TestMasterKey_SetEncryptedDataKey(t *testing.T) { 37 | g := NewWithT(t) 38 | 39 | encryptedKey := []byte("encrypted") 40 | key := &MasterKey{} 41 | key.SetEncryptedDataKey(encryptedKey) 42 | g.Expect(key.EncryptedKey).To(BeEquivalentTo(encryptedKey)) 43 | } 44 | 45 | func TestMasterKey_NeedsRotation(t *testing.T) { 46 | g := NewWithT(t) 47 | 48 | key := MasterKeyFromURL("", "", "") 49 | g.Expect(key.NeedsRotation()).To(BeFalse()) 50 | 51 | key.CreationDate = key.CreationDate.Add(-(azkvTTL + time.Second)) 52 | g.Expect(key.NeedsRotation()).To(BeTrue()) 53 | } 54 | 55 | func TestMasterKey_ToString(t *testing.T) { 56 | g := NewWithT(t) 57 | 58 | key := MasterKeyFromURL("https://myvault.vault.azure.net", "key-name", "key-version") 59 | g.Expect(key.ToString()).To(Equal("https://myvault.vault.azure.net/keys/key-name/key-version")) 60 | } 61 | 62 | func TestMasterKey_ToMap(t *testing.T) { 63 | g := NewWithT(t) 64 | 65 | key := MasterKeyFromURL("https://myvault.vault.azure.net", "key-name", "key-version") 66 | key.EncryptedKey = "data" 67 | g.Expect(key.ToMap()).To(Equal(map[string]interface{}{ 68 | "vaultUrl": key.VaultURL, 69 | "key": key.Name, 70 | "version": key.Version, 71 | "created_at": key.CreationDate.UTC().Format(time.RFC3339), 72 | "enc": key.EncryptedKey, 73 | })) 74 | } 75 | -------------------------------------------------------------------------------- /api/v1alpha1/globalsopssecret_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024-2025 Peak Scale 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package v1alpha1 7 | 8 | import ( 9 | "github.com/peak-scale/sops-operator/internal/api" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | ) 12 | 13 | // SopsSecretSpec defines the desired state of SopsSecret. 14 | type GlobalSopsSecretSpec struct { 15 | // Define Secrets to replicate, when secret is decrypted 16 | Secrets []*GlobalSopsSecretItem `json:"secrets"` 17 | // Define additional Metadata for the generated secrets 18 | Metadata SecretMetadata `json:"metadata,omitempty"` 19 | } 20 | 21 | // GlobalSopsSecretItem defines the desired state of GlobalSopsSecret. 22 | type GlobalSopsSecretItem struct { 23 | // Namespace must be declared since this is a cluster scoped resource 24 | Namespace string `json:"namespace" protobuf:"bytes,1,opt,name=namespace"` 25 | 26 | SopsSecretItem `json:",inline"` 27 | } 28 | 29 | func (s *GlobalSopsSecret) GetSopsMetadata() *api.Metadata { 30 | return s.Sops 31 | } 32 | 33 | // +kubebuilder:object:root=true 34 | // +kubebuilder:subresource:status 35 | // +kubebuilder:resource:scope=Cluster 36 | // +kubebuilder:printcolumn:name="Secrets",type="integer",JSONPath=".status.size",description="The amount of secrets being managed" 37 | // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.condition.type",description="The actual state of the GlobalSopsSecret" 38 | // +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.condition.message",description="Condition Message" 39 | // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age" 40 | 41 | // GlobalSopsSecret is the Schema for the globalsopssecrets API. 42 | type GlobalSopsSecret struct { 43 | metav1.TypeMeta `json:",inline"` 44 | metav1.ObjectMeta `json:"metadata,omitempty"` 45 | 46 | Spec GlobalSopsSecretSpec `json:"spec,omitempty"` 47 | Status SopsSecretStatus `json:"status,omitempty"` 48 | Sops *api.Metadata `json:"sops"` 49 | } 50 | 51 | // +kubebuilder:object:root=true 52 | 53 | // GlobalSopsSecretList contains a list of GlobalSopsSecret. 54 | type GlobalSopsSecretList struct { 55 | metav1.TypeMeta `json:",inline"` 56 | metav1.ListMeta `json:"metadata,omitempty"` 57 | Items []GlobalSopsSecret `json:"items"` 58 | } 59 | 60 | func init() { 61 | SchemeBuilder.Register(&GlobalSopsSecret{}, &GlobalSopsSecretList{}) 62 | } 63 | -------------------------------------------------------------------------------- /e2e/testdata/gpg/secret-key-1.enc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: SopsSecret 3 | metadata: 4 | name: gpg-secret-key-1 5 | spec: 6 | metadata: 7 | prefix: gpg- 8 | suffix: -key-1 9 | labels: 10 | gpg.top: label 11 | annotations: 12 | gpg.top: label 13 | secrets: 14 | - name: gpg-secret-key-1 15 | labels: 16 | gpg.bottom: label 17 | annotations: 18 | gpg.bottom: annotation 19 | data: 20 | data-name1: ENC[AES256_GCM,data:3TJRya3S5eWZpLDAFkY3Mw==,iv:7tOfC77tYr4N3OsQy2haT6nJ/mRz+NAPasBrAJIVAGU=,tag:kiOK31KsNGLV24nwY2KkHw==,type:str] 21 | sops: 22 | kms: [] 23 | gcp_kms: [] 24 | azure_kv: [] 25 | hc_vault: [] 26 | age: [] 27 | lastmodified: "2025-09-03T19:49:56Z" 28 | mac: ENC[AES256_GCM,data:lRM3l8paoDAEr/ZtPPClmB/5ysJrrbtDJi9+pV9jx/vrhLqWvpIaQ3ElmHwF6PIUc43o7DeW5frIvBUbW/UwpLZsKsDhGHCPshC/wNAIqB1GOaV2zrUJanZfjht4N+9v9mGHGLrRO6YaJc4k/kwPghUbbC0u3BjciRa1loKDlJs=,iv:40QLibADwCMc2R208a8zAYWAOPxH8cRI0Ld66/S6t4k=,tag:NRmP5gwQxOYqAJnm1C/cZg==,type:str] 29 | pgp: 30 | - created_at: "2025-09-03T19:49:56Z" 31 | enc: |- 32 | -----BEGIN PGP MESSAGE----- 33 | 34 | hQIMA4Zyy+rN8BAMARAAopHh9A1RdIXh3IWiojae702AKuW5+HExkWCUEpyxoq5w 35 | +yEpd85ZpiPkaFVMRXz/7+3w/evAo7F6ek1qY9CNT4C1UGjjbvkRFakVLnxX/pZ6 36 | v6Ctl5/woFsZkmQk8x1bD/lGaJcjPGgbmuczhNbbo4z5tEwzyOGjoIMehEs28wjN 37 | EBV31Jh20chbWFGzVCh97eNlYtHCQkFGQngJFWLb+IxWiagghfgW/ZhqQOgnCp1a 38 | JjTlZ7s8+8MMJ9l44wLPJQ/vOBzkf+fTknyjxwajPSTrSpSB5i5EieroZsjbutub 39 | 795FnN87Kxmz3L4Iom73SOaPpUoA5lEF5n3CeyZj0ezqa3BGp4S7ZKlQ1BsOsK+X 40 | 8Y+WQH7jH/AZaEdQkI31eLiBi8lzjqpOXcQQVquhmX3yR6NI6ajXsp2BJq4efS2r 41 | ASt3kie0Strs8/PSGoI8vdAlpmyP9mCX4ZkHNzfiWylzdwZHHX1bOikifjv4asuv 42 | nCxOnByrRECREOCmyRLwyrW93lHYNsdKia+nc/SvtFQRoT6N2LRkBK7YqMEp2Rw+ 43 | isyDrmtilU01kmSQH9oFhVVbj1B1Ko4ogAMxpqqtxJ1vGkiNhmrG08LgZtnRrntY 44 | lhp/9nVnErp5+pUspiAhxrul6qW2CyOuck0L1mZ25RCisfibB3ZpmY+ftqO4IATU 45 | aAEJAhD5DOq+cUU0IdNoWbHK1dg6VBbjyda5glhvQ/Qjjtv7A5XtGWOQ1vkZhbCz 46 | jD6OvobI6w1Ttdz9FC+UY7fPxotIMWNqAe0KrxN/cs9AMvWLfghhl0YlFqtygfSo 47 | c5iwB5MQDsb3 48 | =bnaZ 49 | -----END PGP MESSAGE----- 50 | fp: CE411B68660C33B0F83A4EBD56FDA28155A45CB1 51 | encrypted_regex: ^(data|stringData)$ 52 | version: 3.8.1 53 | -------------------------------------------------------------------------------- /charts/sops-operator/templates/crd-lifecycle/crds.tpl: -------------------------------------------------------------------------------- 1 | {{/* CustomResources Lifecycle */}} 2 | {{- if $.Values.crds.install }} 3 | {{ range $path, $_ := .Files.Glob "crds/**.yaml" }} 4 | {{- with $ }} 5 | {{- $content := (tpl (.Files.Get $path) $) -}} 6 | {{- $p := (fromYaml $content) -}} 7 | {{- if $p.Error }} 8 | {{- fail (printf "found YAML error in file %s - %s - raw:\n\n%s" $path $p.Error $content) -}} 9 | {{- end -}} 10 | 11 | 12 | {{/* Add Common Lables */}} 13 | {{- $_ := set $p.metadata "labels" (mergeOverwrite (default dict (get $p.metadata "labels")) (default dict $.Values.crds.labels) (fromYaml (include "helm.labels" $))) -}} 14 | 15 | 16 | {{/* Add Common Lables */}} 17 | {{- $_ := set $p.metadata "annotations" (mergeOverwrite (default dict (get $p.metadata "annotations")) (default dict $.Values.crds.annotations)) -}} 18 | 19 | {{/* Add Keep annotation to CRDs */}} 20 | {{- if $.Values.crds.keep }} 21 | {{- $_ := set $p.metadata.annotations "helm.sh/resource-policy" "keep" -}} 22 | {{- end }} 23 | 24 | {{/* Add Spec Patches for the CRD */}} 25 | {{- $patchFile := $path | replace ".yaml" ".patch" }} 26 | {{- $patchRawContent := (tpl (.Files.Get $patchFile) $) -}} 27 | {{- if $patchRawContent -}} 28 | {{- $patchContent := (fromYaml $patchRawContent) -}} 29 | {{- if $patchContent.Error }} 30 | {{- fail (printf "found YAML error in patch file %s - %s - raw:\n\n%s" $patchFile $patchContent.Error $patchRawContent) -}} 31 | {{- end -}} 32 | {{- $tmp := deepCopy $p | mergeOverwrite $patchContent -}} 33 | {{- $p = $tmp -}} 34 | {{- end -}} 35 | {{- if $p }} 36 | {{- if $.Values.crds.inline }} 37 | {{- printf "---\n%s" (toYaml $p) | nindent 0 }} 38 | {{- else }} 39 | --- 40 | apiVersion: v1 41 | kind: ConfigMap 42 | metadata: 43 | name: {{ include "crds.name" . }}-{{ $path | base | trimSuffix ".yaml" | regexFind "[^_]+$" }} 44 | namespace: {{ .Release.Namespace | quote }} 45 | annotations: 46 | # create hook dependencies in the right order 47 | "helm.sh/hook-weight": "-5" 48 | {{- include "crds.annotations" . | nindent 4 }} 49 | labels: 50 | app.kubernetes.io/component: {{ include "crds.component" . | quote }} 51 | {{- include "helm.labels" . | nindent 4 }} 52 | data: 53 | content: | 54 | {{- printf "---\n%s" (toYaml $p) | nindent 4 }} 55 | 56 | {{- end }} 57 | {{ end }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | -------------------------------------------------------------------------------- /api/v1alpha1/sopsprovider_status.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025 Peak Scale 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package v1alpha1 5 | 6 | import ( 7 | "github.com/peak-scale/sops-operator/internal/api" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | // SopsProviderStatus defines the observed state of SopsProvider. 12 | type SopsProviderStatus struct { 13 | // Amount of providers 14 | //+kubebuilder:default=0 15 | ProvidersAmount uint `json:"size,omitempty"` 16 | // List Validated Providers 17 | Providers []*SopsProviderItemStatus `json:"providers,omitempty"` 18 | // Conditions represent the latest available observations of an instances state 19 | Condition metav1.Condition `json:"condition,omitempty"` 20 | } 21 | 22 | // Get an instance current status. 23 | func (ms *SopsProviderStatus) GetInstance(stat *SopsProviderItemStatus) *SopsProviderItemStatus { 24 | for _, source := range ms.Providers { 25 | if ms.instancequal(source, stat) { 26 | return source 27 | } 28 | } 29 | 30 | return nil 31 | } 32 | 33 | // Add/Update the status for a single instance. 34 | func (ms *SopsProviderStatus) UpdateInstance(stat *SopsProviderItemStatus) { 35 | for i, source := range ms.Providers { 36 | if ms.instancequal(source, stat) { 37 | if source.Type == stat.Type && 38 | source.Status == stat.Status && 39 | source.Reason == stat.Reason && source.Message == stat.Message { 40 | return 41 | } 42 | 43 | ms.Providers[i] = stat 44 | 45 | return 46 | } 47 | } 48 | 49 | ms.Providers = append(ms.Providers, stat) 50 | ms.updateStats() 51 | } 52 | 53 | // Removes an instance. 54 | func (ms *SopsProviderStatus) RemoveInstance(stat *SopsProviderItemStatus) { 55 | filter := []*SopsProviderItemStatus{} 56 | 57 | for _, source := range ms.Providers { 58 | if !ms.instancequal(source, stat) { 59 | filter = append(filter, source) 60 | } 61 | } 62 | 63 | ms.Providers = filter 64 | ms.updateStats() 65 | } 66 | 67 | // Get an instance current status. 68 | func (ms *SopsProviderStatus) updateStats() *SopsProviderItemStatus { 69 | ms.ProvidersAmount = uint(len(ms.Providers)) 70 | 71 | return nil 72 | } 73 | 74 | func (ms *SopsProviderStatus) instancequal(a, b *SopsProviderItemStatus) bool { 75 | return a.Origin == b.Origin 76 | } 77 | 78 | type SopsProviderItemStatus struct { 79 | // Conditions represent the latest available observations of an instances state 80 | metav1.Condition `json:"condition,omitempty"` 81 | // The Origin this Provider originated from 82 | api.Origin `json:",inline"` 83 | } 84 | -------------------------------------------------------------------------------- /e2e/testdata/age/secret-key-2.enc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: SopsSecret 3 | metadata: 4 | name: secret-key-2 5 | spec: 6 | metadata: 7 | prefix: age- 8 | suffix: -key-2 9 | labels: 10 | age.top: label 11 | annotations: 12 | age.top: label 13 | secrets: 14 | - name: jenkins-secret 15 | labels: 16 | jenkins.io/credentials-type: usernamePassword 17 | annotations: 18 | jenkins.io/credentials-description: credentials from Kubernetes 19 | stringData: 20 | username: ENC[AES256_GCM,data:lxTFXEu9oMiDtg==,iv:x9kY7cHOdghfXug1dULt2WsR5dUmmBglnIPVbXMX+Uk=,tag:wQ+5XPbtcs4uYQmZEFUirA==,type:str] 21 | password: ENC[AES256_GCM,data:oUfounVFZ6o=,iv:MAewkmuKZRCvpm9l7V5G9hpyRxfhOAx9GDkWE4FwsUk=,tag:w7h1qXwFd+B7Qgc6drvvWw==,type:str] 22 | - name: docker-login 23 | labels: 24 | age.bottom: label 25 | annotations: 26 | age.bottom: annotation 27 | type: kubernetes.io/dockerconfigjson 28 | stringData: 29 | .dockerconfigjson: ENC[AES256_GCM,data:5kJaYw3rXES2jR//4N8UPq6Ll/YQ0zGPzRY+iJl7wLdcMeWKwJEUkwDbZmPYFYUzC8nlQHg+iZoeWI4x8KvPhdcfwtqdryIb8zym/xq0e2Y/Jm4ZZEJZ9MG2z7Y8V21H5ShYjOx4A3Nc4A9r/kQEkWXtXYtGYg+PNF+ftBnTeQ==,iv:B7wnRN/CQYV444XRHQuxjjWP3xOV6NPFrnfcm0RvUOY=,tag:+szPpfLxXGIruyWnuQXV2g==,type:str] 30 | sops: 31 | kms: [] 32 | gcp_kms: [] 33 | azure_kv: [] 34 | hc_vault: [] 35 | age: 36 | - recipient: age1dffcwct9zstd038u8f4a33jey3d04gwrpnznc0xwfc3n0ec8nyeq2jvhyr 37 | enc: | 38 | -----BEGIN AGE ENCRYPTED FILE----- 39 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsMXJLMGNzQkF4eDlsWXZh 40 | ekRaNTl5aW9IVmsxbVdkc2dQbWRlc3hYeVRrClQxRm9qMTVhcVErZm1WNXUwUEti 41 | ckJrVXU0cW54TmN2RVdVb1N3R0htU28KLS0tIDl2TTJjdStQU0hCMWJGWWhJallz 42 | enhBeFBNTDhwckU3Snlhb1dXZjNKbDQKNXheWuGiIvFcl7y0xp8YXmwZ4OtGt74c 43 | HqKpGHtVAyHq4zmAZzfhGMbtTfKhXMRvMtnNE/6M5WzzYHjTLonU0g== 44 | -----END AGE ENCRYPTED FILE----- 45 | lastmodified: "2025-09-02T11:21:22Z" 46 | mac: ENC[AES256_GCM,data:BCNmJBaQtwPuJ5wP74jEe0Rb1+Yi1fZj0F02kowQKRncHnjEW6GTic+hAP2CTMd0oEBV0fvpX71Z4fUVUV1tREZBpAlmKV8ukqOoLgBrYZTkKVN2cBS5GM85e7ZLb2Ed8hocsISoHrV1ELyMA61oXcw/2eKrxSZE29z7N6nvct0=,iv:yjmUq/q1+nIlqZDf8BUeE+er27snqOFnlIg7PwSf1uI=,tag:AO4vjO4htnhabnn0U1QcTA==,type:str] 47 | pgp: [] 48 | encrypted_regex: ^(data|stringData)$ 49 | version: 3.8.1 50 | -------------------------------------------------------------------------------- /e2e/testdata/age/secret-multi.enc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: SopsSecret 3 | metadata: 4 | name: secret-multi-key 5 | spec: 6 | metadata: 7 | prefix: age- 8 | suffix: -multi 9 | labels: 10 | age.top: label 11 | annotations: 12 | age.top: label 13 | secrets: 14 | - name: multi-secret-name-1 15 | labels: 16 | age.bottom: label 17 | annotations: 18 | age.bottom: annotation 19 | stringData: 20 | data-name0: ENC[AES256_GCM,data:Yj8dzOtAqiq1jYA=,iv:otFxPokZJTHRHdeLb0SF0KCNmu7EH8zzfTF1n2ACFqc=,tag:9EPHrnn/qtWbHS/j5jeZlA==,type:str] 21 | data: 22 | data-name1: ENC[AES256_GCM,data:V9dHvndbDaoj7/nuIYLfzA==,iv:Q7YN9WfyFP3jdPm/vI97YmkwEx3S94+jjYzEKJL3gtM=,tag:cXiZAFFAMv94NJISnIxYBQ==,type:str] 23 | sops: 24 | kms: [] 25 | gcp_kms: [] 26 | azure_kv: [] 27 | hc_vault: [] 28 | age: 29 | - recipient: age1s7t2vk2crlxaumgm7cacs568xwutkjs535pla69kt6w006t7wgzqhkfwvp 30 | enc: | 31 | -----BEGIN AGE ENCRYPTED FILE----- 32 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCSHlaNmJ2T216emI2WWhC 33 | ckpBMDF5YzRhdU1vQm1pUlB2T2lxRndaZ0hNCmpad1NSY3ZlR0VCU0E1aTdoaGVQ 34 | WlRXaGRBemRqUG1iSEZFeWhmMVFMaDAKLS0tIHVMcytxTHRXVzJFS3JzaHVlU2JK 35 | YnZ4dTBDRjBpc1Bmamo5ZWdOdk1EUjQKIDfLiRcLsoap4MCQ3nxQ+4at+Y3rduJi 36 | TjRw/i4Lsy14IAm0RcjTIujOc8tGUSSf5uwTmrfrsDjo2E+I5qKsSw== 37 | -----END AGE ENCRYPTED FILE----- 38 | - recipient: age1dffcwct9zstd038u8f4a33jey3d04gwrpnznc0xwfc3n0ec8nyeq2jvhyr 39 | enc: | 40 | -----BEGIN AGE ENCRYPTED FILE----- 41 | YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWTDVUbkVObmQxSStxSjY1 42 | TUZuak16SWhySHRBMkVtK0tQYXZMZFV2SlNJClRUaW9qd0h1S2h3RUkwSGEyNUdP 43 | MVgwRVhvM21oeHN4VmErYk8vOG4zRTgKLS0tIHQ5Q2FESmpQcHlWcStNWktoQnNt 44 | WXhxOXc3ZTdldEZibkJYUVkzRkU2NXcKXAt1O1X1HZOjb9izwn4eQT4EWDZAtpkQ 45 | 24/b0Px73xyG9kdVGQH/qRHrEIM+deTGTPy55pFPm3B8TfrHZ7LrpQ== 46 | -----END AGE ENCRYPTED FILE----- 47 | lastmodified: "2025-09-02T11:21:38Z" 48 | mac: ENC[AES256_GCM,data:7TnW+B7ZjoGCZZ/eRCP7P9WIfxLdBR1nUo+zGUNpoB8LcEGJFY9GXt2JPFdk4zwNuAwglbEoUHNthKR+yq2B9xlRORpxBM8ylAXjjCsMAi3ZGJ6zTDnweglaWufBeHwHOK3IpovaxRHUZRTZKDsrZG3+sMg6dLsoU04GRXj1Pro=,iv:YR+CMriv+DW9gPkPc7Gw+3mJi3tA2LFDSqiirGJqlfI=,tag:pmTUGNJDcj76JxCa9+dXsA==,type:str] 49 | pgp: [] 50 | encrypted_regex: ^(data|stringData)$ 51 | version: 3.8.1 52 | -------------------------------------------------------------------------------- /internal/decryptor/kustomize-controller/pgp/testdata/public.gpg: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 39 | dg== 40 | =05GI 41 | -----END PGP PUBLIC KEY BLOCK----- 42 | -------------------------------------------------------------------------------- /charts/sops-operator/templates/rbac.yaml: -------------------------------------------------------------------------------- 1 | {{- if $.Values.rbac.enabled }} 2 | --- 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | name: {{ include "helm.fullname" . }}-controller 7 | labels: 8 | {{- include "helm.labels" . | nindent 4 }} 9 | rules: 10 | - apiGroups: 11 | - "" 12 | resources: 13 | - secrets 14 | verbs: 15 | - "*" 16 | - apiGroups: 17 | - "" 18 | resources: 19 | - namespaces 20 | verbs: 21 | - "list" 22 | - "watch" 23 | - "get" 24 | - apiGroups: 25 | - "" 26 | resources: 27 | - events 28 | verbs: 29 | - list 30 | - update 31 | - create 32 | - patch 33 | - apiGroups: 34 | - "addons.projectcapsule.dev" 35 | resources: 36 | - "sopsproviders" 37 | - "sopsproviders/status" 38 | - "sopssecrets" 39 | - "sopssecrets/status" 40 | - "globalsopssecrets" 41 | - "globalsopssecrets/status" 42 | verbs: 43 | - "*" 44 | --- 45 | apiVersion: rbac.authorization.k8s.io/v1 46 | kind: ClusterRoleBinding 47 | metadata: 48 | name: {{ include "helm.fullname" . }}-controller 49 | labels: 50 | {{- include "helm.labels" . | nindent 4 }} 51 | roleRef: 52 | apiGroup: rbac.authorization.k8s.io 53 | kind: ClusterRole 54 | name: {{ include "helm.fullname" . }}-controller 55 | subjects: 56 | - name: {{ include "helm.serviceAccountName" . }} 57 | kind: ServiceAccount 58 | namespace: {{ .Release.Namespace | quote }} 59 | --- 60 | apiVersion: rbac.authorization.k8s.io/v1 61 | kind: Role 62 | metadata: 63 | name: {{ include "helm.fullname" . }} 64 | labels: 65 | {{- include "helm.labels" . | nindent 4 }} 66 | rules: 67 | - apiGroups: 68 | - "" 69 | resources: 70 | - configmaps 71 | verbs: 72 | - get 73 | - list 74 | - watch 75 | - create 76 | - update 77 | - patch 78 | - delete 79 | - apiGroups: 80 | - coordination.k8s.io 81 | resources: 82 | - leases 83 | verbs: 84 | - get 85 | - list 86 | - watch 87 | - create 88 | - update 89 | - patch 90 | - delete 91 | --- 92 | apiVersion: rbac.authorization.k8s.io/v1 93 | kind: RoleBinding 94 | metadata: 95 | name: {{ include "helm.fullname" . }} 96 | labels: 97 | {{- include "helm.labels" . | nindent 4 }} 98 | namespace: {{ .Release.Namespace | quote }} 99 | roleRef: 100 | apiGroup: rbac.authorization.k8s.io 101 | kind: Role 102 | name: {{ include "helm.fullname" . }} 103 | subjects: 104 | - name: {{ include "helm.serviceAccountName" . }} 105 | kind: ServiceAccount 106 | namespace: {{ .Release.Namespace | quote }} 107 | {{- end }} 108 | -------------------------------------------------------------------------------- /charts/sops-operator/README.md.gotmpl: -------------------------------------------------------------------------------- 1 | # SOPS Operator 2 | 3 | We have always loved how [Flux handles Secrets with SOPS](https://fluxcd.io/flux/guides/mozilla-sops/), it's such a seamless experience. However we have noticed, that it's kind of hard to actually distribute keys to users in a kubernetes native way. That's why we built this operator. 4 | 5 | ## Installation 6 | 7 | 1. Install Helm Chart: 8 | 9 | $ helm install sops-operator oci://ghcr.io/peak-scale/charts/sops-operator -n secrets-system 10 | 11 | 3. Show the status: 12 | 13 | $ helm status sops-operator -n secrets-system 14 | 15 | 4. Upgrade the Chart 16 | 17 | $ helm upgrade sops-operator oci://ghcr.io/peak-scale/charts/sops-operator --version 0.1.0 18 | 19 | 5. Uninstall the Chart 20 | 21 | $ helm uninstall sops-operator -n secrets-system 22 | 23 | ## Values 24 | 25 | The following Values are available for this chart. 26 | 27 | ### Global Values 28 | 29 | | Key | Type | Default | Description | 30 | |-----|------|---------|-------------| 31 | {{- range .Values }} 32 | {{- if (hasPrefix "global" .Key) }} 33 | | {{ .Key }} | {{ .Type }} | {{ if .Default }}{{ .Default }}{{ else }}{{ .AutoDefault }}{{ end }} | {{ if .Description }}{{ .Description }}{{ else }}{{ .AutoDescription }}{{ end }} | 34 | {{- end }} 35 | {{- end }} 36 | 37 | 38 | ### CustomResourceDefinition Lifecycle 39 | 40 | | Key | Type | Default | Description | 41 | |-----|------|---------|-------------| 42 | {{- range .Values }} 43 | {{- if (hasPrefix "crds" .Key) }} 44 | | {{ .Key }} | {{ .Type }} | {{ if .Default }}{{ .Default }}{{ else }}{{ .AutoDefault }}{{ end }} | {{ if .Description }}{{ .Description }}{{ else }}{{ .AutoDescription }}{{ end }} | 45 | {{- end }} 46 | {{- end }} 47 | 48 | ### General Parameters 49 | 50 | | Key | Type | Default | Description | 51 | |-----|------|---------|-------------| 52 | {{- range .Values }} 53 | {{- if not (or (hasPrefix "monitoring" .Key) (hasPrefix "proxy" .Key) (hasPrefix "global" .Key) (hasPrefix "crds" .Key) (hasPrefix "serviceMonitor" .Key)) }} 54 | | {{ .Key }} | {{ .Type }} | {{ if .Default }}{{ .Default }}{{ else }}{{ .AutoDefault }}{{ end }} | {{ if .Description }}{{ .Description }}{{ else }}{{ .AutoDescription }}{{ end }} | 55 | {{- end }} 56 | {{- end }} 57 | 58 | ### Monitoring Parameters 59 | 60 | | Key | Type | Default | Description | 61 | |-----|------|---------|-------------| 62 | {{- range .Values }} 63 | {{- if hasPrefix "monitoring" .Key }} 64 | | {{ .Key }} | {{ .Type }} | {{ if .Default }}{{ .Default }}{{ else }}{{ .AutoDefault }}{{ end }} | {{ if .Description }}{{ .Description }}{{ else }}{{ .AutoDescription }}{{ end }} | 65 | {{- end }} 66 | {{- end }} 67 | -------------------------------------------------------------------------------- /api/v1alpha1/sopssecret_status.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025 Peak Scale 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package v1alpha1 5 | 6 | import ( 7 | "github.com/peak-scale/sops-operator/internal/api" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | k8stypes "k8s.io/apimachinery/pkg/types" 10 | ) 11 | 12 | // SopsSecretStatus defines the observed state of SopsSecret. 13 | type SopsSecretStatus struct { 14 | // Amount of Secrets 15 | //+kubebuilder:default=0 16 | Size uint `json:"size,omitempty"` 17 | // Secrets being replicated by this SopsSecret 18 | Secrets []*SopsSecretItemStatus `json:"secrets,omitempty"` 19 | // Conditions represent the latest available observations of an instances state 20 | Condition metav1.Condition `json:"condition,omitempty"` 21 | // Providers used on this secret 22 | Providers []*api.Origin `json:"providers,omitempty"` 23 | } 24 | 25 | // Get an instance current status. 26 | func (ms *SopsSecretStatus) GetInstance(stat *SopsSecretItemStatus) *SopsSecretItemStatus { 27 | for _, source := range ms.Secrets { 28 | if ms.instancequal(source, stat) { 29 | return source 30 | } 31 | } 32 | 33 | ms.updateStats() 34 | 35 | return nil 36 | } 37 | 38 | // Add/Update the status for a single instance. 39 | func (ms *SopsSecretStatus) UpdateInstance(stat *SopsSecretItemStatus) { 40 | // Check if the tenant is already present in the status 41 | for i, source := range ms.Secrets { 42 | if ms.instancequal(source, stat) { 43 | ms.Secrets[i] = stat 44 | 45 | return 46 | } 47 | } 48 | 49 | // If tenant not found, append it to the list 50 | ms.Secrets = append(ms.Secrets, stat) 51 | ms.updateStats() 52 | } 53 | 54 | // Removes an instance. 55 | func (ms *SopsSecretStatus) RemoveInstance(stat *SopsSecretItemStatus) { 56 | // Filter out the datasource with given UID 57 | filter := []*SopsSecretItemStatus{} 58 | 59 | for _, source := range ms.Secrets { 60 | if !ms.instancequal(source, stat) { 61 | filter = append(filter, source) 62 | } 63 | } 64 | 65 | // Update the tenants and adjust the size 66 | ms.Secrets = filter 67 | ms.updateStats() 68 | } 69 | 70 | // Get an instance current status. 71 | func (ms *SopsSecretStatus) updateStats() { 72 | ms.Size = uint(len(ms.Secrets)) 73 | } 74 | 75 | func (ms *SopsSecretStatus) instancequal(a, b *SopsSecretItemStatus) bool { 76 | if a.Name == b.Name && a.Namespace == b.Namespace { 77 | return true 78 | } 79 | 80 | return false 81 | } 82 | 83 | type SopsSecretItemStatus struct { 84 | Condition metav1.Condition `json:"condition"` 85 | Name string `json:"name"` 86 | Namespace string `json:"namespace"` 87 | UID k8stypes.UID `json:"uid,omitempty"` 88 | } 89 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish images 2 | permissions: {} 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref }} 9 | cancel-in-progress: true 10 | jobs: 11 | publish-images: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | packages: write 15 | id-token: write 16 | outputs: 17 | container-digest: ${{ steps.publish.outputs.digest }} 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 21 | - name: "Extract Version" 22 | id: extract_version 23 | run: | 24 | GIT_TAG=${GITHUB_REF##*/} 25 | VERSION=${GIT_TAG##v} 26 | echo "Extracted version: $VERSION" 27 | echo "version=$VERSION" >> $GITHUB_OUTPUT 28 | - name: Install Cosign 29 | uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 30 | 31 | - name: Setup QEMU 32 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 33 | - name: Setup Docker Buildx 34 | id: buildx 35 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 36 | 37 | - name: Publish with KO 38 | id: publish 39 | uses: peak-scale/github-actions/make-ko-publish@a441cca016861c546ab7e065277e40ce41a3eb84 # v0.2.0 40 | with: 41 | makefile-target: ko-publish-all 42 | registry: ghcr.io 43 | registry-username: ${{ github.actor }} 44 | registry-password: ${{ secrets.GITHUB_TOKEN }} 45 | repository: ${{ github.repository_owner }} 46 | version: ${{ steps.extract_version.outputs.version }} 47 | sign-image: true 48 | sbom-name: sops-operator 49 | sbom-repository: ghcr.io/${{ github.repository_owner }}/sops-operator 50 | signature-repository: ghcr.io/${{ github.repository_owner }}/sops-operator 51 | main-path: ./cmd/ 52 | env: 53 | REPOSITORY: ${{ github.repository }} 54 | generate-provenance: 55 | needs: publish-images 56 | permissions: 57 | id-token: write # To sign the provenance. 58 | packages: write # To upload assets to release. 59 | actions: read # To read the workflow path. 60 | uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 61 | with: 62 | image: ghcr.io/${{ github.repository_owner }}/sops-operator 63 | digest: "${{ needs.publish-images.outputs.container-digest }}" 64 | registry-username: ${{ github.actor }} 65 | secrets: 66 | registry-password: ${{ secrets.GITHUB_TOKEN }} 67 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: sops-operator 2 | env: 3 | - COSIGN_EXPERIMENTAL=true 4 | - GO111MODULE=on 5 | before: 6 | hooks: 7 | - go mod download 8 | gomod: 9 | proxy: false 10 | 11 | builds: 12 | - main: ./cmd 13 | binary: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}" 14 | env: 15 | - CGO_ENABLED=0 16 | goarch: 17 | - amd64 18 | - arm64 19 | goos: 20 | - linux 21 | flags: 22 | - -trimpath 23 | mod_timestamp: '{{ .CommitTimestamp }}' 24 | ldflags: 25 | - >- 26 | -X github.com/peak-scale/{{ .ProjectName }}/cmd.Version={{ .Tag }} 27 | -X github.com/peak-scale/{{ .ProjectName }}/cmd.GitCommit={{ .Commit }} 28 | -X github.com/peak-scale/{{ .ProjectName }}/cmd.BuildDate={{ .Date }} 29 | release: 30 | footer: | 31 | **Full Changelog**: https://github.com/peak-scale/{{ .ProjectName }}/compare/{{ .PreviousTag }}...{{ .Tag }} 32 | 33 | **Docker Images** 34 | - `ghcr.io/peak-scale/{{ .ProjectName }}:{{ .Tag }}` 35 | - `ghcr.io/peak-scale/{{ .ProjectName }}:latest` 36 | 37 | **Helm Chart** 38 | View this release on [Artifact Hub](https://artifacthub.io/packages/helm/sops-operator/sops-operator) 39 | 40 | [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/sops-operator)](https://artifacthub.io/packages/search?repo=sops-operator) 41 | 42 | > [!IMPORTANT] 43 | > Supported Kubernetes versions 44 | > 45 | > | Kubernetes version | Minimum required | 46 | > |--------------------|------------------| 47 | > | `v1.34` | `>= 1.34.0` | 48 | 49 | checksum: 50 | name_template: 'checksums.txt' 51 | changelog: 52 | sort: asc 53 | use: github 54 | filters: 55 | exclude: 56 | - '^test:' 57 | - '^chore' 58 | - 'merge conflict' 59 | - Merge pull request 60 | - Merge remote-tracking branch 61 | - Merge branch 62 | groups: 63 | # https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional 64 | - title: '🛠 Dependency updates' 65 | regexp: '^fix\(deps\):|^feat\(deps\):' 66 | order: 300 67 | - title: '✨ New Features' 68 | regexp: '^feat(\([^)]*\))?:' 69 | order: 100 70 | - title: '🐛 Bug fixes' 71 | regexp: '^fix(\([^)]*\))?:' 72 | order: 200 73 | - title: '📖 Documentation updates' 74 | regexp: '^docs(\([^)]*\))?:' 75 | order: 400 76 | - title: '🛡️ Security updates' 77 | regexp: '^sec(\([^)]*\))?:' 78 | order: 500 79 | - title: '🚀 Build process updates' 80 | regexp: '^(build|ci)(\([^)]*\))?:' 81 | order: 600 82 | - title: '📦 Other work' 83 | regexp: '^chore(\([^)]*\))?:|^chore:' 84 | sboms: 85 | - artifacts: archive 86 | signs: 87 | - cmd: cosign 88 | args: 89 | - "sign-blob" 90 | - "--output-signature=${signature}" 91 | - "${artifact}" 92 | - "--yes" 93 | artifacts: all 94 | -------------------------------------------------------------------------------- /e2e/testdata/gpg/secret-key-2.enc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: SopsSecret 3 | metadata: 4 | name: gpg-secret-key-2 5 | spec: 6 | metadata: 7 | prefix: gpg- 8 | suffix: -key-2 9 | labels: 10 | gpg.top: label 11 | annotations: 12 | gpg.top: label 13 | secrets: 14 | - name: gpg-jenkins-secret 15 | labels: 16 | jenkins.io/credentials-type: usernamePassword 17 | annotations: 18 | jenkins.io/credentials-description: credentials from Kubernetes 19 | stringData: 20 | username: ENC[AES256_GCM,data:wnNIigb9ZAfBvQ==,iv:9a49/D0m2ZfRNZDAcsX9BcYgmj+nQtopD4Z7Mrdioa0=,tag:n6vzmKU1zHCH2yqWhoZ5WQ==,type:str] 21 | password: ENC[AES256_GCM,data:AgLaIKrHgCM=,iv:EDUE3fUEOGiIBx+AmLkmy5ZuXOM/TtDojHM2S4wnJyU=,tag:2ZGwd6wxbYYrBwDYxMEvYQ==,type:str] 22 | - name: gpg-docker-login 23 | type: kubernetes.io/dockerconfigjson 24 | stringData: 25 | .dockerconfigjson: ENC[AES256_GCM,data:edGt7GO6C54Qke8VShXhO48Dfis/UY4W/UCsfsc+Q2WwTysy5pr+r2QZdQnlrnAr68WKHGJRDmrrZxZlV31Lr97zSYVYrYPJK2KY5S4Kd4nnpe4ZP9RFovtsKqliZ2NgnemMvo5TYjJzL8+e8JhlQfWH//BZ5FRglTa3duW+Tw==,iv:Syk3+eQB2I8JNWT4R/EY1xtSeCvgztCeb7+rBlguetA=,tag:RvLCSwEbc9GgiljfLSv6Cg==,type:str] 26 | sops: 27 | kms: [] 28 | gcp_kms: [] 29 | azure_kv: [] 30 | hc_vault: [] 31 | age: [] 32 | lastmodified: "2025-09-03T19:50:06Z" 33 | mac: ENC[AES256_GCM,data:KCqVE6WsC/LdFYoqFLxxYSigtXYi+ED0EApYZmbs3LhYuNJgAlq/mpxOdkSi7dSLo77Uq1idJw2yOXXAHOqlkkYdWevsAEFf5owF3hUH2EeSzodcv5X+P9Nw5Ci+ijasIplyWFtuMnnTZsAfuQFBoMLLzy++QzkhEjMWMQYrFNw=,iv:CVuMpAp5WntEUENn6+zTiaHfAWOVNcWkE1L8XtQBacI=,tag:wg1p6bEvid0oio7tKMnnaA==,type:str] 34 | pgp: 35 | - created_at: "2025-09-03T19:50:06Z" 36 | enc: |- 37 | -----BEGIN PGP MESSAGE----- 38 | 39 | hQIMA7J+D+Iybf4CAQ/+PymticBOkJobq14gy8ZBY4BEKq9bMGJZ8sMq1JO4/DbU 40 | 3YdUbYDYRG+6q/LjHE9+g6I+JTNCYWZ9ax/jXIenPU1K563IQjQVgcDjWN5759a3 41 | U53q/9M19GZH+D+RNzaXhpevey8mRjW1kFy5pvqMPcajWn94CGD4CYD5L9pKQU+b 42 | CMYWgnpmqSiKmJZOSioLL32mJoAAmQWNkL+VJVTbQjUVo41tQahCTnWJhYoCuH7L 43 | G3FOW9sC8wgoi6Df/Xt3XDLointHkuP0fNNZAJYxquxnQhlNMKPh4tbtXUpoVeJ9 44 | rgbKYRaE/u1Al/tRw9xXc7Cblg+8z8Ekv8vGfD8lpkQ80sDnHzleCM8lArfKpqvy 45 | aK7+bo0ChOE7I/BnFC0FQT6m/fuiNT39c10WY8RtgqcmMCGVZtFHSM/q5mMlBcJk 46 | /3rxkKURW7gxBkaoEHgPSx/VLUKBsIoetzj9tqjLQ7w+Ud6RyPUg49nUCp2MfsEW 47 | Kr9l0lGTOSxpTxNKW4O7CqH7lYZe0z+aRNuLqIZo9XUppfpzNp5hUG5NBqog0usX 48 | KKDqBLpPAStg4zqUZj9GqfvLhDOk0jNNNfx6Cei+UJ7hiCf7aKD7oVh7Ngzte2+U 49 | rKU6UWE7/RRWAlEGP6XkiH/A9OjtJmGVlPctgwTZUkmekgpl5GrS/tKcZl7/Ga/U 50 | aAEJAhB/xom9LiUzP4mhdcftOPX1t4l7HaEi64qDeRoJQQuDs2u0WPCiA/DntPo5 51 | sL65ia8j2JylzJ/i1Y6nurm63Ml2tCMtxs8AMasqGYMkAii4jF7Mmc+7nL5f9hp6 52 | nrBeyLE5pYaS 53 | =5TvQ 54 | -----END PGP MESSAGE----- 55 | fp: 60684ED5F92EA3FD960E83E6CB8BC811D17A58DE 56 | encrypted_regex: ^(data|stringData)$ 57 | version: 3.8.1 58 | -------------------------------------------------------------------------------- /internal/decryptor/kustomize-controller/keyservice/options.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Peak Scale 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package keyservice 5 | 6 | import ( 7 | extage "filippo.io/age" 8 | "github.com/getsops/sops/v3/keyservice" 9 | "github.com/peak-scale/sops-operator/internal/decryptor/kustomize-controller/age" 10 | "github.com/peak-scale/sops-operator/internal/decryptor/kustomize-controller/awskms" 11 | "github.com/peak-scale/sops-operator/internal/decryptor/kustomize-controller/azkv" 12 | "github.com/peak-scale/sops-operator/internal/decryptor/kustomize-controller/gcpkms" 13 | "github.com/peak-scale/sops-operator/internal/decryptor/kustomize-controller/hcvault" 14 | "github.com/peak-scale/sops-operator/internal/decryptor/kustomize-controller/pgp" 15 | ) 16 | 17 | // ServerOption is some configuration that modifies the Server. 18 | type ServerOption interface { 19 | // ApplyToServer applies this configuration to the given Server. 20 | ApplyToServer(s *Server) 21 | } 22 | 23 | // WithGnuPGHome configures the GnuPG home directory on the Server. 24 | type WithGnuPGHome string 25 | 26 | // ApplyToServer applies this configuration to the given Server. 27 | func (o WithGnuPGHome) ApplyToServer(s *Server) { 28 | s.gnuPGHome = pgp.GnuPGHome(o) 29 | } 30 | 31 | // WithVaultToken configures the Hashicorp Vault token on the Server. 32 | type WithVaultToken string 33 | 34 | // ApplyToServer applies this configuration to the given Server. 35 | func (o WithVaultToken) ApplyToServer(s *Server) { 36 | s.vaultToken = hcvault.VaultToken(o) 37 | } 38 | 39 | // WithAgeIdentities configures the parsed age identities on the Server. 40 | type WithAgeIdentities []extage.Identity 41 | 42 | // ApplyToServer applies this configuration to the given Server. 43 | func (o WithAgeIdentities) ApplyToServer(s *Server) { 44 | s.ageIdentities = age.ParsedIdentities(o) 45 | } 46 | 47 | // WithAWSKeys configures the AWS credentials on the Server. 48 | type WithAWSKeys struct { 49 | CredsProvider *awskms.CredsProvider 50 | } 51 | 52 | // ApplyToServer applies this configuration to the given Server. 53 | func (o WithAWSKeys) ApplyToServer(s *Server) { 54 | s.awsCredsProvider = o.CredsProvider 55 | } 56 | 57 | // WithGCPCredsJSON configures the GCP service account credentials JSON on the 58 | // Server. 59 | type WithGCPCredsJSON []byte 60 | 61 | // ApplyToServer applies this configuration to the given Server. 62 | func (o WithGCPCredsJSON) ApplyToServer(s *Server) { 63 | s.gcpCredsJSON = gcpkms.CredentialJSON(o) 64 | } 65 | 66 | // WithAzureToken configures the Azure credential token on the Server. 67 | type WithAzureToken struct { 68 | Token *azkv.Token 69 | } 70 | 71 | // ApplyToServer applies this configuration to the given Server. 72 | func (o WithAzureToken) ApplyToServer(s *Server) { 73 | s.azureToken = o.Token 74 | } 75 | 76 | // WithDefaultServer configures the fallback default server on the Server. 77 | type WithDefaultServer struct { 78 | Server keyservice.KeyServiceServer 79 | } 80 | 81 | // ApplyToServer applies this configuration to the given Server. 82 | func (o WithDefaultServer) ApplyToServer(s *Server) { 83 | s.defaultServer = o.Server 84 | } 85 | -------------------------------------------------------------------------------- /internal/meta/conditions.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025 Peak Scale 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package meta 5 | 6 | import ( 7 | sopsv1alpha1 "github.com/peak-scale/sops-operator/api/v1alpha1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | ) 11 | 12 | const ( 13 | // ReadyCondition indicates the resource is ready and fully reconciled. 14 | // If the Condition is False, the resource SHOULD be considered to be in the process of reconciling and not a 15 | // representation of actual state. 16 | ReadyCondition string = "Ready" 17 | NotReadyCondition string = "NotReady" 18 | 19 | // SucceededReason indicates a condition or event observed a success. 20 | SucceededReason string = "Loaded" 21 | 22 | // FailedReason indicates a condition or event observed a failure. 23 | FailedReason string = "Failed" 24 | 25 | // FailedReason indicates a condition or event observed a failure. 26 | NotSopsEncryptedReason string = "NotSopsEncrypted" 27 | 28 | // FailedReason indicates a condition or event observed a failure. 29 | DecryptionFailedReason string = "DecryptionFailure" 30 | 31 | // FailedReason indicates a condition or event observed a failure. 32 | SecretsReplicationFailedReason string = "ReplicationFailure" 33 | ) 34 | 35 | // Should be used on translator level. 36 | func NewReadyCondition(obj client.Object) metav1.Condition { 37 | return metav1.Condition{ 38 | Type: ReadyCondition, 39 | Status: metav1.ConditionTrue, 40 | ObservedGeneration: obj.GetGeneration(), 41 | Reason: SucceededReason, 42 | Message: "Reconciliation Succeeded", 43 | LastTransitionTime: metav1.Now(), 44 | } 45 | } 46 | 47 | func NewNotReadyCondition(obj client.Object, msg string) metav1.Condition { 48 | return metav1.Condition{ 49 | Type: NotReadyCondition, 50 | Status: metav1.ConditionFalse, 51 | ObservedGeneration: obj.GetGeneration(), 52 | Reason: FailedReason, 53 | Message: msg, 54 | LastTransitionTime: metav1.Now(), 55 | } 56 | } 57 | 58 | func NewReadySecretStatusCondition(obj client.Object) *sopsv1alpha1.SopsSecretItemStatus { 59 | return &sopsv1alpha1.SopsSecretItemStatus{ 60 | UID: obj.GetUID(), 61 | Name: obj.GetName(), 62 | Namespace: obj.GetNamespace(), 63 | Condition: metav1.Condition{ 64 | Type: ReadyCondition, 65 | Status: metav1.ConditionTrue, 66 | ObservedGeneration: obj.GetGeneration(), 67 | Reason: SucceededReason, 68 | Message: "Reconciliation Succeeded", 69 | LastTransitionTime: metav1.Now(), 70 | }, 71 | } 72 | } 73 | 74 | func NewNotReadySecretStatusCondition(obj client.Object, msg string) *sopsv1alpha1.SopsSecretItemStatus { 75 | return &sopsv1alpha1.SopsSecretItemStatus{ 76 | UID: obj.GetUID(), 77 | Name: obj.GetName(), 78 | Namespace: obj.GetNamespace(), 79 | Condition: metav1.Condition{ 80 | Type: NotReadyCondition, 81 | Status: metav1.ConditionFalse, 82 | ObservedGeneration: obj.GetGeneration(), 83 | Reason: FailedReason, 84 | Message: msg, 85 | LastTransitionTime: metav1.Now(), 86 | }, 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/decryptor/kustomize-controller/keyservice/utils_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 The Flux authors 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, v. 2.0. If a copy of the MPL was not distributed with this 5 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | package keyservice 8 | 9 | import ( 10 | "context" 11 | "fmt" 12 | 13 | "github.com/getsops/sops/v3/keys" 14 | "github.com/getsops/sops/v3/keyservice" 15 | 16 | "github.com/peak-scale/sops-operator/internal/decryptor/kustomize-controller/age" 17 | "github.com/peak-scale/sops-operator/internal/decryptor/kustomize-controller/awskms" 18 | "github.com/peak-scale/sops-operator/internal/decryptor/kustomize-controller/azkv" 19 | "github.com/peak-scale/sops-operator/internal/decryptor/kustomize-controller/gcpkms" 20 | "github.com/peak-scale/sops-operator/internal/decryptor/kustomize-controller/hcvault" 21 | "github.com/peak-scale/sops-operator/internal/decryptor/kustomize-controller/pgp" 22 | ) 23 | 24 | // KeyFromMasterKey converts a SOPS internal MasterKey to an RPC Key that can 25 | // be serialized with Protocol Buffers. 26 | func KeyFromMasterKey(k keys.MasterKey) keyservice.Key { 27 | switch mk := k.(type) { 28 | case *pgp.MasterKey: 29 | return keyservice.Key{ 30 | KeyType: &keyservice.Key_PgpKey{ 31 | PgpKey: &keyservice.PgpKey{ 32 | Fingerprint: mk.Fingerprint, 33 | }, 34 | }, 35 | } 36 | case *hcvault.MasterKey: 37 | return keyservice.Key{ 38 | KeyType: &keyservice.Key_VaultKey{ 39 | VaultKey: &keyservice.VaultKey{ 40 | VaultAddress: mk.VaultAddress, 41 | EnginePath: mk.EnginePath, 42 | KeyName: mk.KeyName, 43 | }, 44 | }, 45 | } 46 | case *awskms.MasterKey: 47 | return keyservice.Key{ 48 | KeyType: &keyservice.Key_KmsKey{ 49 | KmsKey: &keyservice.KmsKey{ 50 | Arn: mk.Arn, 51 | }, 52 | }, 53 | } 54 | case *azkv.MasterKey: 55 | return keyservice.Key{ 56 | KeyType: &keyservice.Key_AzureKeyvaultKey{ 57 | AzureKeyvaultKey: &keyservice.AzureKeyVaultKey{ 58 | VaultUrl: mk.VaultURL, 59 | Name: mk.Name, 60 | Version: mk.Version, 61 | }, 62 | }, 63 | } 64 | case *age.MasterKey: 65 | return keyservice.Key{ 66 | KeyType: &keyservice.Key_AgeKey{ 67 | AgeKey: &keyservice.AgeKey{ 68 | Recipient: mk.Recipient, 69 | }, 70 | }, 71 | } 72 | case *gcpkms.MasterKey: 73 | return keyservice.Key{ 74 | KeyType: &keyservice.Key_GcpKmsKey{ 75 | GcpKmsKey: &keyservice.GcpKmsKey{ 76 | ResourceId: mk.ResourceID, 77 | }, 78 | }, 79 | } 80 | default: 81 | panic(fmt.Sprintf("tried to convert unknown MasterKey type %T to keyservice.Key", mk)) 82 | } 83 | } 84 | 85 | type MockKeyServer struct { 86 | encryptReqs []*keyservice.EncryptRequest 87 | decryptReqs []*keyservice.DecryptRequest 88 | } 89 | 90 | func NewMockKeyServer() *MockKeyServer { 91 | return &MockKeyServer{ 92 | encryptReqs: make([]*keyservice.EncryptRequest, 0), 93 | decryptReqs: make([]*keyservice.DecryptRequest, 0), 94 | } 95 | } 96 | 97 | func (ks *MockKeyServer) Encrypt(_ context.Context, req *keyservice.EncryptRequest) (*keyservice.EncryptResponse, error) { 98 | ks.encryptReqs = append(ks.encryptReqs, req) 99 | return nil, fmt.Errorf("not actually implemented") 100 | } 101 | 102 | func (ks *MockKeyServer) Decrypt(_ context.Context, req *keyservice.DecryptRequest) (*keyservice.DecryptResponse, error) { 103 | ks.decryptReqs = append(ks.decryptReqs, req) 104 | return nil, fmt.Errorf("not actually implemented") 105 | } 106 | -------------------------------------------------------------------------------- /charts/sops-operator/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "helm.fullname" . }} 5 | labels: 6 | {{- include "helm.labels" . | nindent 4 }} 7 | spec: 8 | replicas: {{ .Values.replicaCount }} 9 | selector: 10 | matchLabels: 11 | {{- include "helm.selectorLabels" . | nindent 6 }} 12 | template: 13 | metadata: 14 | annotations: 15 | {{- with .Values.podAnnotations }} 16 | {{- toYaml . | nindent 8 }} 17 | {{- end }} 18 | {{- if .Values.crds.install }} 19 | projectcapsule.dev/crds-size-hash: {{ include "helm.crdsSizeHash" . | quote }} 20 | {{- end }} 21 | labels: 22 | {{- include "helm.selectorLabels" . | nindent 8 }} 23 | {{- with .Values.podLabels }} 24 | {{- toYaml . | nindent 8 }} 25 | {{- end }} 26 | spec: 27 | {{- with .Values.imagePullSecrets }} 28 | imagePullSecrets: 29 | {{- toYaml . | nindent 8 }} 30 | {{- end }} 31 | serviceAccountName: {{ include "helm.serviceAccountName" . }} 32 | {{- if $.Values.podSecurityContext.enabled }} 33 | securityContext: {{- omit $.Values.podSecurityContext "enabled" | toYaml | nindent 8 }} 34 | {{- end }} 35 | {{- with .Values.volumes }} 36 | volumes: 37 | {{- toYaml . | nindent 8 }} 38 | {{- end }} 39 | containers: 40 | - name: {{ .Chart.Name }} 41 | {{- if $.Values.securityContext.enabled }} 42 | securityContext: {{- omit $.Values.securityContext "enabled" | toYaml | nindent 12 }} 43 | {{- end }} 44 | image: "{{ .Values.image.registry | trimSuffix "/" }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 45 | imagePullPolicy: {{ .Values.image.pullPolicy }} 46 | args: 47 | - --zap-log-level={{ default 4 .Values.args.logLevel }} 48 | - --enable-pprof={{ .Values.args.pprof }} 49 | {{- with .Values.args.extraArgs }} 50 | {{- toYaml . | nindent 12 }} 51 | {{- end }} 52 | env: 53 | - name: NAMESPACE 54 | valueFrom: 55 | fieldRef: 56 | fieldPath: metadata.namespace 57 | {{- with .Values.env }} 58 | {{- toYaml . | nindent 10 }} 59 | {{- end }} 60 | ports: 61 | {{- if .Values.args.pprof }} 62 | - name: pprof 63 | containerPort: 8082 64 | protocol: TCP 65 | {{- end }} 66 | {{- if $.Values.monitoring.enabled }} 67 | - name: metrics 68 | containerPort: 8080 69 | protocol: TCP 70 | {{- end }} 71 | livenessProbe: 72 | {{- toYaml .Values.livenessProbe | nindent 12}} 73 | readinessProbe: 74 | {{- toYaml .Values.readinessProbe | nindent 12}} 75 | resources: 76 | {{- toYaml .Values.resources | nindent 12 }} 77 | {{- with .Values.volumeMounts }} 78 | volumeMounts: 79 | {{- toYaml . | nindent 10 }} 80 | {{- end }} 81 | priorityClassName: {{ .Values.priorityClassName }} 82 | {{- with .Values.nodeSelector }} 83 | nodeSelector: 84 | {{- toYaml . | nindent 8 }} 85 | {{- end }} 86 | {{- with .Values.tolerations }} 87 | tolerations: 88 | {{- toYaml . | nindent 8 }} 89 | {{- end }} 90 | {{- with .Values.affinity }} 91 | affinity: 92 | {{- toYaml . | nindent 8 }} 93 | {{- end }} 94 | {{- with .Values.topologySpreadConstraints }} 95 | topologySpreadConstraints: 96 | {{- toYaml . | nindent 8 }} 97 | {{- end }} 98 | -------------------------------------------------------------------------------- /charts/sops-operator/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "helm.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 "helm.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 "helm.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "helm.labels" -}} 37 | helm.sh/chart: {{ include "helm.chart" . }} 38 | {{ include "helm.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 "helm.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "helm.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 "helm.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "helm.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | 64 | {{/* Plugin Config-Name */}} 65 | {{- define "config.name" -}} 66 | {{ default "default" $.Values.config.name }} 67 | {{- end }} 68 | 69 | 70 | {{/* 71 | Determine the Kubernetes version to use for jobsFullyQualifiedDockerImage tag 72 | */}} 73 | {{- define "helm.jobsTagKubeVersion" -}} 74 | {{- if contains "-eks-" .Capabilities.KubeVersion.GitVersion }} 75 | {{- print "v" .Capabilities.KubeVersion.Major "." (.Capabilities.KubeVersion.Minor | replace "+" "") -}} 76 | {{- else }} 77 | {{- print "v" .Capabilities.KubeVersion.Major "." .Capabilities.KubeVersion.Minor -}} 78 | {{- end }} 79 | {{- end }} 80 | 81 | {{/* 82 | Create the jobs fully-qualified Docker image to use 83 | */}} 84 | {{- define "helm.jobsFullyQualifiedDockerImage" -}} 85 | {{- if .Values.global.jobs.kubectl.image.tag }} 86 | {{- printf "%s/%s:%s" .Values.global.jobs.kubectl.image.registry .Values.global.jobs.kubectl.image.repository .Values.global.jobs.kubectl.image.tag -}} 87 | {{- else }} 88 | {{- printf "%s/%s:%s" .Values.global.jobs.kubectl.image.registry .Values.global.jobs.kubectl.image.repository (include "helm.jobsTagKubeVersion" .) -}} 89 | {{- end }} 90 | {{- end }} 91 | 92 | {{- define "helm.crdsSizeHash" -}} 93 | {{- $paths := list -}} 94 | {{- range $p, $_ := .Files.Glob "crds/**.yaml" }} 95 | {{- $paths = append $paths $p -}} 96 | {{- end -}} 97 | {{- $paths = sortAlpha $paths -}} 98 | 99 | {{- $sizes := list -}} 100 | {{- range $paths }} 101 | {{- $sizes = append $sizes (len ($.Files.Get .)) -}} 102 | {{- end -}} 103 | 104 | {{- $joined := join "," $sizes -}} 105 | {{- sha256sum $joined -}} 106 | {{- end -}} 107 | -------------------------------------------------------------------------------- /e2e/testdata/gpg/secret-multi.enc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: SopsSecret 3 | metadata: 4 | name: gpg-multi-secret 5 | spec: 6 | metadata: 7 | prefix: gpg- 8 | suffix: -key-multi 9 | labels: 10 | gpg.top: label 11 | annotations: 12 | gpg.top: label 13 | secrets: 14 | - name: gpg-multi-secret-1 15 | labels: 16 | label1: value1 17 | stringData: 18 | data-name0: ENC[AES256_GCM,data:xyi+m4nTEDYXzK4=,iv:v7caU/2RJNzGkwXbFla//02C/0rYcTrH4s5+00Txqhc=,tag:dvZeIZjLtCh/CgnXvI/ICQ==,type:str] 19 | data: 20 | data-name1: ENC[AES256_GCM,data:BZKNEbYloPLjSPnzJSrV6w==,iv:fX/u/ooMz+r62iqduoseS9uIFQUS6omqaztv60+3j+M=,tag:iIb+uwjqBBXziMwrs8EC4g==,type:str] 21 | sops: 22 | shamir_threshold: 1 23 | kms: [] 24 | gcp_kms: [] 25 | azure_kv: [] 26 | hc_vault: [] 27 | age: [] 28 | lastmodified: "2025-09-03T19:50:34Z" 29 | mac: ENC[AES256_GCM,data:MGaL/qMB+cEZDN0L3Npe3cNR9JFTuWOHdW4VZFDQSbMGMa3MX4WtMsziawiqiRa59I6JynG3ZGG1il4FR3YVInV72tLkZonWyx6/19qr+HM0K/aKKtcluGxdiK036vBPFAyUFdiyzUO9yUzkwVa1CcIKkBaIa4hpRzg0O36pmwo=,iv:wIgnvY9PLNfCeIiLHJoEmpQ/dN+Se4DFTv5tNMCniXE=,tag:yeeoE3CvRHiKaoi7V4LZHg==,type:str] 30 | pgp: 31 | - created_at: "2025-09-03T19:50:34Z" 32 | enc: |- 33 | -----BEGIN PGP MESSAGE----- 34 | 35 | hQIMA4Zyy+rN8BAMAQ//U0QkuM8krZO3vzO9ASzXs5il3VJy4rmIGt0BvPmpRuVj 36 | iwsPQpRldrH/vMdK6GwicVYJNb4yURS564enaZkxeYQLFjPuVz1dLzQGzK0NmV6P 37 | T+JM59lRK/O0xNFzQ3DDzmZsRqZv6oy+5xh8Or+pIvqxJYnRFOrUVdGRPynX48g6 38 | YEbKVBk+sUsHDINk3MyVMsH/1579SRgE8dMxp8P0d4EH/OHsBATCkOsJq7l89L0N 39 | ag3T1skR0HM3fG8zqvVMS4rPPaZEIREotejNu4q4TM4PXjiTKKwtHeT26mLoMNn5 40 | OfK+2c+kh0RLbFgusRJ7/cBLj//s9mCfZiniEXAnWCNjfQxqra0AymyWWUMOlZ10 41 | FDNheuuNwu7bIBsVOvHbQJx1nuxK8i/j8rhEcsKJ0FLjKOA3W/V/6/Bppe1UbXwk 42 | JUjRugnJnsRuKI0gSYSNihlQ7i3nPTLuMIRZ7f8ENWOUM2ORyBHuLElUDVyOQiWf 43 | QCV0LNV8XE3BDs0vs2YKqbVgdB4LpbR02VAvQnp6qgcGRbYr6SD1JpfkrHHbO2b1 44 | slXm8DRMNdv3BVevxi2801rg/ZxWlP1sSjmaOYwg5ny1jc1iKjxoE/VVKIsumqB7 45 | S6m5Gf62l+3uzavKivzo7lr9M+RDXQQjH+YpFphAxKaSzxr12Z1920ifkLhqggXU 46 | aAEJAhD+lYWILL/oZKf/tVUa6rOIvfrf5H9OtACBxc5PUvLLqbJ5imuHVvoiD8cr 47 | vt5MpKx7ASRoWjxJe9ElPsObmGTqFC2+a2iD+TTL7/VVqk2XS1WLLkB01sMFsGCD 48 | 9ALZO6NgRUI9 49 | =noqm 50 | -----END PGP MESSAGE----- 51 | fp: CE411B68660C33B0F83A4EBD56FDA28155A45CB1 52 | - created_at: "2025-09-03T19:50:34Z" 53 | enc: |- 54 | -----BEGIN PGP MESSAGE----- 55 | 56 | hQIMA7J+D+Iybf4CARAAwb/VJi16yVj7W4jt2nWcWDC7C+RCa71i1PJFOvA2A1BX 57 | sPoUHhC0tMsVmh0mfF3FqMHzSaBsHb+QTaBQjaKznNVfc3x+lFaIxuXILsCINOER 58 | xXNfHrwOb3bKnwHFU1sDFkh8IR7BkMKu3rNhC0VskhF9rn5b8jfTs+/5GeWcMX/j 59 | z5q7EqbIzotBx8hIy8Fe06Vkhr/hcT2Y6tqGgziBiK0aaxe6W4JTDrrfKw02gSsT 60 | FV6cVRbV+cYs7ShDPqPoih/CPE153jKaqOLgCKme+zyzhrg3c+KgZmQwmNSdPN8v 61 | OBIrgm0rcJjJgHZZEHG9uLBI0M4bC84JQmHkYDDV5BBCRC2NnZScIe+qsF9SyTdt 62 | L50NzSbjjfsJkvjXZxEEjWpJyyTEAu5FKgq8UmFP45J6VWc62Bj/ovKbBRSb5b9Y 63 | eU/eRKDUCnT4wJz2n4AfAe6PFTuD8GzqLyPybpyE0USlnbhLXBTmPk/oqQkcYv/H 64 | 5b2vw+XGxfzE+EzdlshMdMQHWnXCX8kvyt8KzFLJYlx0SVF+Nr/CyS6dPuqFHjkR 65 | R5yRInk32cH3iXx/5jCZpXmeUkySzRREKwBvB4mSKeTIyUICDu6BNGoBJR9Igv6Y 66 | YJ66I+N+sRqa3PnwpuPOu9Khf0J9GkLdEZvXqDFWsqWXVi6QAcNKNRFbk9Y2U+3U 67 | aAEJAhDYwOkAfvaTi/u7DKxakuE2tKWVeZE4EmjxOljY8lXM7sWU/PrKCCQfCiaH 68 | LK1zMWGB4HvJNkxev0aeGErV/8L3E9qZogrMX5NBMcrQQziBa+8b9w2fPfV61d1k 69 | G9DyYFm4AmFt 70 | =dUzq 71 | -----END PGP MESSAGE----- 72 | fp: 60684ED5F92EA3FD960E83E6CB8BC811D17A58DE 73 | encrypted_regex: ^(data|stringData)$ 74 | version: 3.8.1 75 | -------------------------------------------------------------------------------- /e2e/testdata/gpg/secret-quorum.enc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: addons.projectcapsule.dev/v1alpha1 2 | kind: SopsSecret 3 | metadata: 4 | name: gpp-quorum-secret 5 | spec: 6 | metadata: 7 | prefix: gpg- 8 | suffix: -key-quorum 9 | labels: 10 | gpg.top: label 11 | annotations: 12 | gpg.top: label 13 | secrets: 14 | - name: gpp-quorum-secret-1 15 | labels: 16 | label1: value1 17 | stringData: 18 | data-name0: ENC[AES256_GCM,data:ETGxtG8OXdwDivM=,iv:3Mi7MChQI5fNzODzmob58PEovRS45udnrCvjCrH1RU4=,tag:zol1frp6/Y8ufNEhxbH9Ug==,type:str] 19 | data: 20 | data-name1: ENC[AES256_GCM,data:fGCuDAJ3gIXZI2BsPlt+fg==,iv:MncjvUTU2yWtamnjNhnDsiaWEqUY6L/uL10uTclRgH0=,tag:o1/06qstRNb0ADqgNDoHwQ==,type:str] 21 | sops: 22 | shamir_threshold: 2 23 | kms: [] 24 | gcp_kms: [] 25 | azure_kv: [] 26 | hc_vault: [] 27 | age: [] 28 | lastmodified: "2025-09-03T19:51:00Z" 29 | mac: ENC[AES256_GCM,data:kzPbYKFF7caQ1uj8r0JQxaWSXaVytNhQ8dmmFCFijtdztyPqj1PyGOmpunORGseYdG/dsq6EmOxcxmNX330eM8vBk1WE4iUGn/xlHaSSUBp2oWNQPl6rWzQi82rGHAeQceCxkRVfj1K5TjxOytDrMkDE4myQ/03iqnDJP3hHTZk=,iv:2Xkc/nybOabuvWg99iFPG9miOc1PzMWMONlRaTyuKro=,tag:J16faVAkqofL1iKsBL3TAA==,type:str] 30 | pgp: 31 | - created_at: "2025-09-03T19:51:00Z" 32 | enc: |- 33 | -----BEGIN PGP MESSAGE----- 34 | 35 | hQIMA4Zyy+rN8BAMAQ//auUq/0hE68NuX505LdS+ASbUn8X4rFQOfSlJZHZzCgM/ 36 | ZDvzoh44C42aCmvHFD5mI7N+4fbHUCO8Un9zl/FFOLsEgHu/BfNmHqKyr7VtMCU2 37 | XJ0NyUDb1A6eSlmLbe3C+smil+MJA0sekfLc/CfDwYZIqQC3aJYVSiaHE2p3QIHs 38 | lyLMaGdwcHUspSdYTxbIpfwgC5HgK+yq/S8OwkBTw7nCMyyzsCN4HuIfEqTv/FBD 39 | Cr3thHuNpOvfHvPUA5PXeQEfg9Uo54C5eBxoKzDa+OJSEtdNokGeaoz0E9vHqbJm 40 | LtWVKwd3koJq7atP2JVVumakakXv5sHQfXXBrhQOtqgonkihekZmTDlx+Av9cLEd 41 | R0DNVP76q50Xr13d9bF/pQgFKvgQzu8YUxX5yA48h6+kMij3tcAxEToStRUZ+47m 42 | zhe7y2yQ7FmDOcu7Q6grqit1rFil7tbz4klOAzF8UIp4dSbaZAzqNz18b6MCfdE6 43 | Xqgd1ohICLhDzXuVbTroZI0erYufUEhQuD3O3gDXk6etULfBO7ltDxPFpulFTZgl 44 | x9ZJrR3Iy+i/Tb6sbcRMrCPD2iGpE0NKMfcbgP+Sbh2RxqCBUEQXf2zIbHyMldEY 45 | KlBJCaR1jJ5nOiftF8c+uaMBH4iPya+DQLCLDbBUnRvokpZ0FgiKa+HbzgFYx0vU 46 | aAEJAhD6ThB94r3U4YjthdilBTHysvSt+gAW6wR0gKTsn3eOj6rGeXTO5Vw/lMTp 47 | ieAua77xT+5Be7v2aqYzgt4Of6Hfs215oQm69TPiZRNNAogdLq82mFnder3ri3kH 48 | CN/oPj3UhSpP 49 | =ApR1 50 | -----END PGP MESSAGE----- 51 | fp: CE411B68660C33B0F83A4EBD56FDA28155A45CB1 52 | - created_at: "2025-09-03T19:51:00Z" 53 | enc: |- 54 | -----BEGIN PGP MESSAGE----- 55 | 56 | hQIMA7J+D+Iybf4CAQ//VSSM6n8ToNGnePuMpD9vyZB77OKqJxxp84bdsmdk/Ay3 57 | rnU+J7Pdp66V3T9Z5n8xc3oTOxTQjOPwc5BFa73y30SLC2/8JxPB1dHuRAIH2sID 58 | j1O4yyWm5H944bQO8298u+ORoltDuomavV92JkMa6lMQcV9ZJ71uBajjKJIb4+9w 59 | s/NuLJjevnvzgILqocuVH5Vs7K/Dv6M9+1pXh5lt36orJ57gk++cxh6btG4WD8ea 60 | Fv5mg7kTmDQdtuRw6ynQHzzNDCGGCEWQI75v2m0+oAnpBDzfIykQTgeM2xGpRj4c 61 | B4Lo5Ym06CYYtNSxOGIMCtaYRfyd1APJG/+9ALrtnpLYVseGb0buC3mWjivoT+Ye 62 | Kgl8MIMPIw9yoa8H9sA3W5NyqNXP0BQJAiEVQM+N0RRDyGutk0E+DQx+6yJl2TfM 63 | 46dbQcYIhczSyU5ks5Yf4O6QyKBcA7dvYWya3ytincS5akeblDa0dY3NpWrJ5A6/ 64 | fAtPu8tlc1GERKLfsuQ9c9a9ZzIWb1azYHZXTaisBSds2xpYBeXhILDjsaBNvFuN 65 | 5NGR5Zupw3+ZxTqLSoT5VWqm5IIQfjZoowfyavAc1Axw+oSxc+5EBo6PeBo3pz/7 66 | wRDAeMSGnEn9gTZ6NV/RiM45AKrUFMkSghJ8Vi/MZ/vyycRx6gsCJOkZ6KlLCV7U 67 | aAEJAhBe+rs12OtniAEXTs37j25tKd5k9wIs1+Y97x8/2s56DBjZO1hQ+QP9YgYQ 68 | PZrvuWI+iDcG4wH4uIomPmFv0mRCHlQyKECg36duR8Fb2JgSMpUrkZd1nP+NCpdR 69 | JQ38dfVgN8no 70 | =J7Kk 71 | -----END PGP MESSAGE----- 72 | fp: 60684ED5F92EA3FD960E83E6CB8BC811D17A58DE 73 | encrypted_regex: ^(data|stringData)$ 74 | version: 3.8.1 75 | -------------------------------------------------------------------------------- /internal/decryptor/testdata/.sops.pub.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 61 | c73ZHZESEa+qNjfYk7pCGr3vew== 62 | =N/nc 63 | -----END PGP PUBLIC KEY BLOCK----- 64 | -------------------------------------------------------------------------------- /e2e/testdata/gpg/keys/key-2/.sops.pub.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 62 | =CQgh 63 | -----END PGP PUBLIC KEY BLOCK----- 64 | -------------------------------------------------------------------------------- /e2e/testdata/gpg/keys/key-1/.sops.pub.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 62 | sOvFGryZsg== 63 | =/u1G 64 | -----END PGP PUBLIC KEY BLOCK----- 65 | -------------------------------------------------------------------------------- /internal/api/sops.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025 Peak Scale 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package api 5 | 6 | import ( 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | type SopsImplementation interface { 11 | client.Object 12 | GetSopsMetadata() *Metadata 13 | } 14 | 15 | // Metadata is stored in SOPS encrypted files, and it contains the information necessary to decrypt the file. 16 | // This struct is just used for serialization, and SOPS uses another struct internally, sops.Metadata. It exists 17 | // in order to allow the binary format to stay backwards compatible over time, but at the same time allow the internal 18 | // representation SOPS uses to change over time. 19 | // +kubebuilder:object:generate=true 20 | type Metadata struct { 21 | ShamirThreshold int `json:"shamir_threshold,omitempty"` 22 | KeyGroups []Keygroup `json:"key_groups,omitempty"` 23 | Kmskeys []Kmskey `json:"kms,omitempty"` 24 | GcpKmskeys []GcpKmskey `json:"gcp_kms,omitempty"` 25 | AzureKeyVaultkeys []Azkvkey `json:"azure_kv,omitempty"` 26 | Vaultkeys []Vaultkey `json:"hc_vault,omitempty"` 27 | Agekeys []Agekey `json:"age,omitempty"` 28 | LastModified string `json:"lastmodified"` 29 | MessageAuthenticationCode string `json:"mac"` 30 | Pgpkeys []Pgpkey `json:"pgp,omitempty"` 31 | UnencryptedSuffix string `json:"unencrypted_suffix,omitempty"` 32 | EncryptedSuffix string `json:"encrypted_suffix,omitempty"` 33 | UnencryptedRegex string `json:"unencrypted_regex,omitempty"` 34 | EncryptedRegex string `json:"encrypted_regex,omitempty"` 35 | UnencryptedCommentRegex string `json:"unencrypted_comment_regex,omitempty"` 36 | EncryptedCommentRegex string `json:"encrypted_comment_regex,omitempty"` 37 | MACOnlyEncrypted bool `json:"mac_only_encrypted,omitempty"` 38 | Version string `json:"version,omitempty"` 39 | } 40 | 41 | // +kubebuilder:object:generate=true 42 | type Keygroup struct { 43 | Pgpkeys []Pgpkey `json:"pgp,omitempty"` 44 | Kmskeys []Kmskey `json:"kms,omitempty"` 45 | GcpKmskeys []GcpKmskey `json:"gcp_kms,omitempty"` 46 | AzureKeyVaultkeys []Azkvkey `json:"azure_kv,omitempty"` 47 | Vaultkeys []Vaultkey `json:"hc_vault,omitempty"` 48 | Agekeys []Agekey `json:"age,omitempty"` 49 | } 50 | 51 | // +kubebuilder:object:generate=true 52 | type Pgpkey struct { 53 | CreatedAt string `json:"created_at,omitempty"` 54 | EncryptedDataKey string `json:"enc,omitempty"` 55 | Fingerprint string `json:"fp,omitempty"` 56 | } 57 | 58 | // +kubebuilder:object:generate=true 59 | type Kmskey struct { 60 | Arn string `json:"arn"` 61 | Role string `json:"role,omitempty"` 62 | Context map[string]*string `json:"context,omitempty"` 63 | CreatedAt string `json:"created_at"` 64 | EncryptedDataKey string `json:"enc"` 65 | AwsProfile string `json:"aws_profile"` 66 | } 67 | 68 | // +kubebuilder:object:generate=true 69 | type GcpKmskey struct { 70 | ResourceID string `json:"resource_id"` 71 | CreatedAt string `json:"created_at"` 72 | EncryptedDataKey string `json:"enc"` 73 | } 74 | 75 | // +kubebuilder:object:generate=true 76 | type Vaultkey struct { 77 | VaultAddress string `json:"vault_address"` 78 | EnginePath string `json:"engine_path"` 79 | KeyName string `json:"key_name"` 80 | CreatedAt string `json:"created_at"` 81 | EncryptedDataKey string `json:"enc"` 82 | } 83 | 84 | // +kubebuilder:object:generate=true 85 | type Azkvkey struct { 86 | VaultURL string `json:"vault_url"` 87 | Name string `json:"name"` 88 | Version string `json:"version"` 89 | CreatedAt string `json:"created_at"` 90 | EncryptedDataKey string `json:"enc"` 91 | } 92 | 93 | // +kubebuilder:object:generate=true 94 | type Agekey struct { 95 | Recipient string `json:"recipient"` 96 | EncryptedDataKey string `json:"enc"` 97 | } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | GitHub release (latest SemVer) 4 | 5 | 6 | Artifact Hub 7 | 8 | 9 | 10 | codecov 11 | 12 |

13 | 14 | > [!IMPORTANT] 15 | > Regarding the code, most of the SOPS implementation was taken from the [Flux kustomize-constroller](https://github.com/fluxcd/kustomize-controller/blob/main/internal/decryptor/decryptor.go) project. We have left the License-Header as-is, if further attribution is wished, please open an issue. We go the idea from the existing [sops-operator](https://github.com/isindir/sops-secrets-operator). However the implementation was not optimal for our use-cases, that's why we decided to release our own solution. 16 | 17 | # SOPS-Operator ❤️ 18 | 19 | ![SOPS](https://avatars.githubusercontent.com/u/129185620?s=48&v=4) 20 | 21 | We have always loved how [Flux handles Secrets with SOPS](https://fluxcd.io/flux/guides/mozilla-sops/), it's such a seamless experience. However we have noticed, that it's kind of hard to actually distribute keys to users in a kubernetes native way. That's why we built this operator. It introduces [Providers](docs/usage.md#providers), which essentially match Kubernetes resources which represent Keys or access to KMS stores. On the Provides you also declare, which [Secrets](docs/usage.md#secrets) you want to encrypt with that provider. **Currently only works with PGP and AGE for n-secrets** That leaves open that, N-providers can load private keys for one Secret, in complex scenarios. Also we want to provide a general solution to decrypting secrets, not a solution which is dependent on a gitops engine. 22 | 23 | 24 | ## Concept 25 | 26 | This Operators introduces the concept of [SopsProviders](./docs/usage.md#providers). `SopsProviders` are created by Cluster-Administrators and are essentially a connecting-piece for collecting private-keys and [`SopsSecrets`](./docs/usage.md#sopssecrets), which can use these keys for decryption. 27 | 28 | With this option an Kubernetes users may manage their own keys and [`SopsSecrets`](./docs/usage.md#sopssecrets). The implementation of `SopsSecrets` allows them to be applied to the Kubernetes API with sops encryption-meta. The entire decryption happens within the cluster. So a `SopsSecret` is applied the way it's stored eg. in git. 29 | 30 | ![Sops Operator](./docs/assets/sops-operator.drawio.png) 31 | 32 | ## Documentation 33 | 34 | See the [Documentation](docs/README.md) for more information on how to use this addon. 35 | 36 | ## Demo 37 | 38 | Spin up a live demonstration of the addon on Killercoda: 39 | 40 | - [https://killercoda.com/peakscale/course/solutions/multi-tenant-sops](https://killercoda.com/peakscale/course/solutions/multi-tenant-sops) 41 | 42 | ## Support 43 | 44 | This addon is developed by the community. For enterprise support (production ready setup,tailor-made features) reach out to [Peak Scale](https://peakscale.ch/en/) 45 | 46 | ## License 47 | 48 | Copyright 2024. 49 | 50 | Licensed under the Apache License, Version 2.0 (the "License"); 51 | you may not use this file except in compliance with the License. 52 | You may obtain a copy of the License at 53 | 54 | http://www.apache.org/licenses/LICENSE-2.0 55 | 56 | Unless required by applicable law or agreed to in writing, software 57 | distributed under the License is distributed on an "AS IS" BASIS, 58 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 59 | See the License for the specific language governing permissions and 60 | limitations under the License. 61 | 62 | 63 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fpeak-scale%2Fsops-operator.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fpeak-scale%2Fsops-operator?ref=badge_large) 64 | -------------------------------------------------------------------------------- /charts/sops-operator/templates/crd-lifecycle/job.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.crds.install (not $.Values.crds.inline) }} 2 | apiVersion: batch/v1 3 | kind: Job 4 | metadata: 5 | name: {{ include "crds.name" . }} 6 | namespace: {{ .Release.Namespace | quote }} 7 | annotations: 8 | # create hook dependencies in the right order 9 | "helm.sh/hook-weight": "-1" 10 | {{- include "crds.annotations" . | nindent 4 }} 11 | {{- with .Values.global.jobs.kubectl.annotations }} 12 | {{- toYaml . | nindent 4 }} 13 | {{- end }} 14 | labels: 15 | app.kubernetes.io/component: {{ include "crds.component" . | quote }} 16 | {{- include "helm.labels" . | nindent 4 }} 17 | {{- with .Values.global.jobs.kubectl.labels }} 18 | {{- toYaml . | nindent 4 }} 19 | {{- end }} 20 | spec: 21 | ttlSecondsAfterFinished: {{ .Values.global.jobs.kubectl.ttlSecondsAfterFinished }} 22 | template: 23 | metadata: 24 | name: "{{ include "crds.name" . }}" 25 | annotations: 26 | {{- with .Values.global.jobs.kubectl.podAnnotations }} 27 | {{- toYaml . | nindent 8 }} 28 | {{- end }} 29 | {{- with .Values.podAnnotations }} 30 | {{- toYaml . | nindent 8 }} 31 | {{- end }} 32 | labels: 33 | app.kubernetes.io/component: {{ include "crds.component" . | quote }} 34 | {{- include "helm.selectorLabels" . | nindent 8 }} 35 | {{- with .Values.global.jobs.kubectl.podLabels }} 36 | {{- toYaml . | nindent 8 }} 37 | {{- end }} 38 | {{- with .Values.podLabels }} 39 | {{- toYaml . | nindent 8 }} 40 | {{- end }} 41 | spec: 42 | restartPolicy: {{ $.Values.global.jobs.kubectl.restartPolicy }} 43 | {{- if $.Values.global.jobs.kubectl.podSecurityContext.enabled }} 44 | securityContext: {{- omit $.Values.global.jobs.kubectl.podSecurityContext "enabled" | toYaml | nindent 8 }} 45 | {{- end }} 46 | {{- with .Values.global.jobs.kubectl.nodeSelector }} 47 | nodeSelector: 48 | {{- toYaml . | nindent 8 }} 49 | {{- end }} 50 | {{- with .Values.global.jobs.kubectl.affinity }} 51 | affinity: 52 | {{- toYaml . | nindent 8 }} 53 | {{- end }} 54 | {{- with .Values.global.jobs.kubectl.tolerations }} 55 | tolerations: 56 | {{- toYaml . | nindent 8 }} 57 | {{- end }} 58 | {{- with .Values.global.jobs.kubectl.topologySpreadConstraints }} 59 | topologySpreadConstraints: 60 | {{- toYaml . | nindent 8 }} 61 | {{- end }} 62 | {{- with .Values.global.jobs.kubectl.priorityClassName }} 63 | priorityClassName: {{ . }} 64 | {{- end }} 65 | {{- with .Values.imagePullSecrets }} 66 | imagePullSecrets: 67 | {{- toYaml . | nindent 8 }} 68 | {{- end }} 69 | serviceAccountName: {{ include "crds.name" . }} 70 | containers: 71 | - name: crds-hook 72 | image: {{ include "helm.jobsFullyQualifiedDockerImage" $ }} 73 | imagePullPolicy: {{ .Values.global.jobs.kubectl.image.pullPolicy }} 74 | {{- if $.Values.global.jobs.kubectl.securityContext.enabled }} 75 | securityContext: {{- omit $.Values.global.jobs.kubectl.securityContext "enabled" | toYaml | nindent 10 }} 76 | {{- end }} 77 | command: 78 | - sh 79 | - -c 80 | - | 81 | set -o errexit ; set -o xtrace ; set -o nounset 82 | 83 | # piping stderr to stdout means kubectl's errors are surfaced 84 | # in the pod's logs. 85 | 86 | kubectl apply --server-side=true --overwrite=true --force-conflicts=true -f /data/ 2>&1 87 | volumeMounts: 88 | {{- range $path, $_ := .Files.Glob "crds/**.yaml" }} 89 | - name: {{ $path | base | trimSuffix ".yaml" | regexFind "[^_]+$" }} 90 | mountPath: /data/{{ $path | base }} 91 | subPath: {{ $path | base }} 92 | {{- end }} 93 | {{- with .Values.global.jobs.kubectl.resources }} 94 | resources: 95 | {{- toYaml . | nindent 10 }} 96 | {{- end }} 97 | volumes: 98 | {{ $currentScope := . }} 99 | {{- range $path, $_ := .Files.Glob "crds/**.yaml" }} 100 | {{- with $currentScope }} 101 | - name: {{ $path | base | trimSuffix ".yaml" | regexFind "[^_]+$" }} 102 | configMap: 103 | name: {{ include "crds.name" $ }}-{{ $path | base | trimSuffix ".yaml" | regexFind "[^_]+$" }} 104 | items: 105 | - key: content 106 | path: {{ $path | base }} 107 | {{- end }} 108 | {{- end }} 109 | backoffLimit: 4 110 | {{- end }} 111 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Release Artifacts 2 | 3 | [See all the available artifacts](https://github.com/orgs/peak-scale/packages?repo_name=sops-operator) 4 | 5 | ## Verifing 6 | 7 | To verify artifacts you need to have [cosign installed](https://github.com/sigstore/cosign#installation). This guide assumes you are using v2.x of cosign. All of the signatures are created using [keyless signing](https://docs.sigstore.dev/verifying/verify/#keyless-verification-using-openid-connect). 8 | 9 | # For Docker-Image 10 | export COSIGN_REPOSITORY=ghcr.io/peak-scale/sops-operator 11 | 12 | # For Helm-Chart 13 | export COSIGN_REPOSITORY=ghcr.io/peak-scale/charts/sops-operator 14 | 15 | To verify the signature of the docker image, run the following command. Replace `` with an [available release tag](https://github.com/peak-scale/sops-operator/pkgs/container/sops-operator). The value `release_tag` is a release but without the prefix `v` (eg. `0.1.0-alpha.3`). 16 | 17 | VERSION= COSIGN_REPOSITORY=ghcr.io/peak-scale/sops-operator cosign verify ghcr.io/peak-scale/sops-operator:${VERSION} \ 18 | --certificate-identity-regexp="https://github.com/peak-scale/sops-operator/.github/workflows/docker-publish.yml@refs/tags/*" \ 19 | --certificate-oidc-issuer="https://token.actions.githubusercontent.com" | jq 20 | 21 | To verify the signature of the helm image, run the following command. Replace `` with an [available release tag](https://github.com/peak-scale/sops-operator/pkgs/container/charts%2Fsops-operator). The value `release_tag` is a release but without the prefix `v` (eg. `0.1.0-alpha.3`) 22 | 23 | VERSION= COSIGN_REPOSITORY=ghcr.io/peak-scale/charts/sops-operator cosign verify ghcr.io/peak-scale/charts/sops-operator:${VERSION} \ 24 | --certificate-identity-regexp="https://github.com/peak-scale/sops-operator/.github/workflows/helm-publish.yml@refs/tags/*" \ 25 | --certificate-oidc-issuer="https://token.actions.githubusercontent.com" | jq 26 | 27 | ## Verifying Provenance 28 | 29 | We create and attest the provenance of our builds using the [SLSA standard](https://slsa.dev/spec/v0.2/provenance) and meets the [SLSA Level 3](https://slsa.dev/spec/v0.1/levels) specification. The attested provenance may be verified using the cosign tool. 30 | 31 | Verify the provenance of the docker image. Replace `` with an [available release tag](https://github.com/peak-scale/sops-operator/pkgs/container/sops-operator). The value `release_tag` is a release but without the prefix `v` (eg. `0.1.0-alpha.3`) 32 | 33 | ```bash 34 | cosign verify-attestation --type slsaprovenance \ 35 | --certificate-identity-regexp="https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/*" \ 36 | --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ 37 | ghcr.io/peak-scale/sops-operator: | jq .payload -r | base64 --decode | jq 38 | ``` 39 | 40 | Verify the provenance of the helm image. Replace `` with an [available release tag](https://github.com/peak-scale/sops-operator/pkgs/container/charts%sops-operator). The value `release_tag` is a release but without the prefix `v` (eg. `0.1.0-alpha.3`) 41 | 42 | ```bash 43 | VERSION= cosign verify-attestation --type slsaprovenance \ 44 | --certificate-identity-regexp="https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/*" \ 45 | --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ 46 | "ghcr.io/peak-scale/charts/sops-operator:${VERSION}" | jq .payload -r | base64 --decode | jq 47 | ``` 48 | 49 | ## Software Bill of Materials (SBOM) 50 | 51 | An SBOM (Software Bill of Materials) in CycloneDX JSON format is published for each release, including pre-releases. You can set the environment variable `COSIGN_REPOSITORY` to point to this repository. For example: 52 | 53 | # For Docker-Image 54 | export COSIGN_REPOSITORY=ghcr.io/peak-scale/sops-operator 55 | 56 | # For Helm-Chart 57 | export COSIGN_REPOSITORY=ghcr.io/peak-scale/charts/sops-operator 58 | 59 | To inspect the SBOM of the docker image, run the following command. Replace `` with an [available release tag](https://github.com/peak-scale/sops-operator/pkgs/container/sops-operator): 60 | 61 | 62 | COSIGN_REPOSITORY=ghcr.io/peak-scale/sops-operator cosign download sbom ghcr.io/peak-scale/sops-operator: 63 | 64 | To inspect the SBOM of the helm image, run the following command. Replace `` with an [available release tag](https://github.com/peak-scale/sops-operator/pkgs/container/charts%2Fsops-operator): 65 | 66 | COSIGN_REPOSITORY=ghcr.io/peak-scale/sops-operator cosign download sbom ghcr.io/peak-scale/charts/sops-operator: 67 | -------------------------------------------------------------------------------- /api/v1alpha1/sopssecret_types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025 Peak Scale 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package v1alpha1 5 | 6 | import ( 7 | "github.com/peak-scale/sops-operator/internal/api" 8 | corev1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | ) 11 | 12 | // SopsSecretSpec defines the desired state of SopsSecret. 13 | type SopsSecretSpec struct { 14 | // Define Secrets to replicate, when secret is decrypted 15 | Secrets []*SopsSecretItem `json:"secrets"` 16 | // Define additional Metadata for the generated secrets 17 | Metadata SecretMetadata `json:"metadata,omitempty"` 18 | } 19 | 20 | // SopsSecretTemplate defines the map of secrets to create 21 | // +kubebuilder:object:root=false 22 | type SopsSecretItem struct { 23 | // Name must be unique within a namespace. Is required when creating resources, although 24 | // some resources may allow a client to request the generation of an appropriate name 25 | // automatically. Name is primarily intended for creation idempotence and configuration 26 | // definition. 27 | // Cannot be updated. 28 | // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names 29 | Name string `json:"name" protobuf:"bytes,1,opt,name=name"` 30 | // Map of string keys and values that can be used to organize and categorize 31 | // (scope and select) objects. May match selectors of replication controllers 32 | // and services. 33 | // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels 34 | // +optional 35 | Labels map[string]string `json:"labels,omitempty" protobuf:"bytes,11,rep,name=labels"` 36 | // Map of string keys and values that can be used to organize and categorize 37 | // (scope and select) objects. May match selectors of replication controllers 38 | // and services. 39 | // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels 40 | // +optional 41 | Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,11,rep,name=labels"` 42 | // Kubernetes secret type. 43 | // Defaults to Opaque. 44 | // Allowed values: 45 | // - Opaque 46 | // - kubernetes.io/service-account-token 47 | // - kubernetes.io/dockercfg 48 | // - kubernetes.io/dockerconfigjson 49 | // - kubernetes.io/basic-auth 50 | // - kubernetes.io/ssh-auth 51 | // - kubernetes.io/tls 52 | // - bootstrap.kubernetes.io/token 53 | // +kubebuilder:validation:Enum=Opaque;kubernetes.io/service-account-token;kubernetes.io/dockercfg;kubernetes.io/dockerconfigjson;kubernetes.io/basic-auth;kubernetes.io/ssh-auth;kubernetes.io/tls;bootstrap.kubernetes.io/token 54 | Type corev1.SecretType `json:"type,omitempty"` 55 | // Data map to use in Kubernetes secret (equivalent to Kubernetes Secret object data, please see for more 56 | // information: https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets) 57 | //+optional 58 | Data map[string]string `json:"data,omitempty"` 59 | // stringData map to use in Kubernetes secret (equivalent to Kubernetes Secret object stringData, please see for more 60 | // information: https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets) 61 | //+optional 62 | StringData map[string]string `json:"stringData,omitempty"` 63 | // Immutable, if set to true, ensures that data stored in the Secret cannot 64 | // be updated (only object metadata can be modified). 65 | // If not set to true, the field can be modified at any time. 66 | // Defaulted to nil. 67 | // +optional 68 | Immutable *bool `json:"immutable,omitempty" protobuf:"varint,5,opt,name=immutable"` 69 | } 70 | 71 | func (s *SopsSecret) GetSopsMetadata() *api.Metadata { 72 | return s.Sops 73 | } 74 | 75 | // +kubebuilder:object:root=true 76 | // +kubebuilder:subresource:status 77 | // +kubebuilder:printcolumn:name="Secrets",type="integer",JSONPath=".status.size",description="The amount of secrets being managed" 78 | // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.condition.type",description="The actual state of the SopsSecret" 79 | // +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.condition.message",description="Condition Message" 80 | // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age" 81 | 82 | // SopsSecret is the Schema for the sopssecrets API. 83 | type SopsSecret struct { 84 | metav1.TypeMeta `json:",inline"` 85 | metav1.ObjectMeta `json:"metadata,omitempty"` 86 | 87 | Spec SopsSecretSpec `json:"spec,omitempty"` 88 | Status SopsSecretStatus `json:"status,omitempty"` 89 | Sops *api.Metadata `json:"sops"` 90 | } 91 | 92 | // +kubebuilder:object:root=true 93 | 94 | // SopsSecretList contains a list of SopsSecret. 95 | type SopsSecretList struct { 96 | metav1.TypeMeta `json:",inline"` 97 | metav1.ListMeta `json:"metadata,omitempty"` 98 | Items []SopsSecret `json:"items"` 99 | } 100 | 101 | func init() { 102 | SchemeBuilder.Register(&SopsSecret{}, &SopsSecretList{}) 103 | } 104 | -------------------------------------------------------------------------------- /internal/decryptor/kustomize-controller/gcpkms/keysource_integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration && disabled 2 | // +build integration,disabled 3 | 4 | // Copyright (C) 2022 The Flux authors 5 | // 6 | // This Source Code Form is subject to the terms of the Mozilla Public 7 | // License, v. 2.0. If a copy of the MPL was not distributed with this 8 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 9 | 10 | package gcpkms 11 | 12 | import ( 13 | "context" 14 | "fmt" 15 | "io/ioutil" 16 | "os" 17 | "testing" 18 | 19 | "github.com/getsops/sops/v3/gcpkms" 20 | . "github.com/onsi/gomega" 21 | 22 | "google.golang.org/api/option" 23 | kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" 24 | "google.golang.org/grpc/codes" 25 | "google.golang.org/grpc/status" 26 | 27 | kms "cloud.google.com/go/kms/apiv1" 28 | ) 29 | 30 | var ( 31 | project = os.Getenv("TEST_PROJECT") 32 | testKeyring = os.Getenv("TEST_KEYRING") 33 | testKey = os.Getenv("TEST_CRYPTO_KEY") 34 | testCredsJSON = os.Getenv("TEST_CRED_JSON") 35 | resourceID = fmt.Sprintf("projects/%s/locations/global/keyRings/%s/cryptoKeys/%s", 36 | project, testKeyring, testKey) 37 | ) 38 | 39 | func TestMasterKey_Decrypt_SOPS_Compat(t *testing.T) { 40 | g := NewWithT(t) 41 | os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", testCredsJSON) 42 | 43 | g.Expect(createKMSKeyIfNotExists(resourceID)).To(Succeed()) 44 | 45 | dataKey := []byte("blue golden light") 46 | encryptedKey := gcpkms.NewMasterKeyFromResourceID(resourceID) 47 | g.Expect(encryptedKey.Encrypt(dataKey)).To(Succeed()) 48 | 49 | decryptionKey := MasterKeyFromResourceID(resourceID) 50 | creds, err := ioutil.ReadFile(testCredsJSON) 51 | g.Expect(err).ToNot(HaveOccurred()) 52 | decryptionKey.EncryptedKey = encryptedKey.EncryptedKey 53 | decryptionKey.credentialJSON = creds 54 | dec, err := decryptionKey.Decrypt() 55 | g.Expect(err).ToNot(HaveOccurred()) 56 | g.Expect(dec).To(Equal(dataKey)) 57 | } 58 | 59 | func TestMasterKey_Encrypt_SOPS_Compat(t *testing.T) { 60 | os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", testCredsJSON) 61 | g := NewWithT(t) 62 | 63 | g.Expect(createKMSKeyIfNotExists(resourceID)).To(Succeed()) 64 | 65 | dataKey := []byte("silver golden lights") 66 | 67 | encryptionKey := MasterKeyFromResourceID(resourceID) 68 | creds, err := ioutil.ReadFile(testCredsJSON) 69 | g.Expect(err).ToNot(HaveOccurred()) 70 | encryptionKey.credentialJSON = creds 71 | err = encryptionKey.Encrypt(dataKey) 72 | g.Expect(err).ToNot(HaveOccurred()) 73 | 74 | decryptionKey := gcpkms.NewMasterKeyFromResourceID(resourceID) 75 | decryptionKey.EncryptedKey = encryptionKey.EncryptedKey 76 | dec, err := decryptionKey.Decrypt() 77 | g.Expect(err).ToNot(HaveOccurred()) 78 | g.Expect(dec).To(Equal(dataKey)) 79 | } 80 | 81 | func TestMasterKey_EncryptDecrypt_RoundTrip(t *testing.T) { 82 | g := NewWithT(t) 83 | 84 | g.Expect(createKMSKeyIfNotExists(resourceID)).To(Succeed()) 85 | 86 | key := MasterKeyFromResourceID(resourceID) 87 | creds, err := ioutil.ReadFile(testCredsJSON) 88 | g.Expect(err).ToNot(HaveOccurred()) 89 | key.credentialJSON = creds 90 | 91 | datakey := []byte("a thousand splendid sons") 92 | g.Expect(key.Encrypt(datakey)).To(Succeed()) 93 | g.Expect(key.EncryptedKey).ToNot(BeEmpty()) 94 | 95 | dec, err := key.Decrypt() 96 | g.Expect(err).ToNot(HaveOccurred()) 97 | g.Expect(dec).To(Equal(datakey)) 98 | } 99 | 100 | func createKMSKeyIfNotExists(resourceID string) error { 101 | ctx := context.Background() 102 | // check if crypto key exists if not create it 103 | c, err := kms.NewKeyManagementClient(ctx, option.WithCredentialsFile(testCredsJSON)) 104 | if err != nil { 105 | return fmt.Errorf("err creating client: %q", err) 106 | } 107 | 108 | getCryptoKeyReq := &kmspb.GetCryptoKeyRequest{ 109 | Name: resourceID, 110 | } 111 | _, err = c.GetCryptoKey(ctx, getCryptoKeyReq) 112 | if err == nil { 113 | return nil 114 | } 115 | 116 | e, ok := status.FromError(err) 117 | if !ok || (ok && e.Code() != codes.NotFound) { 118 | return fmt.Errorf("err getting crypto key: %q", err) 119 | } 120 | 121 | projectID := fmt.Sprintf("projects/%s/locations/global", project) 122 | createKeyRingReq := &kmspb.CreateKeyRingRequest{ 123 | Parent: projectID, 124 | KeyRingId: testKeyring, 125 | } 126 | 127 | _, err = c.CreateKeyRing(ctx, createKeyRingReq) 128 | e, ok = status.FromError(err) 129 | if err != nil && !(ok && e.Code() == codes.AlreadyExists) { 130 | return fmt.Errorf("err creating key ring: %q", err) 131 | } 132 | 133 | keyRingName := fmt.Sprintf("%s/keyRings/%s", projectID, testKeyring) 134 | keyReq := &kmspb.CreateCryptoKeyRequest{ 135 | Parent: keyRingName, 136 | CryptoKeyId: testKey, 137 | CryptoKey: &kmspb.CryptoKey{ 138 | Purpose: kmspb.CryptoKey_ENCRYPT_DECRYPT, 139 | }, 140 | } 141 | _, err = c.CreateCryptoKey(ctx, keyReq) 142 | e, ok = status.FromError(err) 143 | if err != nil && !(ok && e.Code() == codes.AlreadyExists) { 144 | return fmt.Errorf("err creating crypto key: %q", err) 145 | } 146 | 147 | return nil 148 | } 149 | -------------------------------------------------------------------------------- /internal/decryptor/kustomize-controller/gcpkms/keysource_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 The Flux authors 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, v. 2.0. If a copy of the MPL was not distributed with this 5 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 6 | 7 | package gcpkms 8 | 9 | import ( 10 | "encoding/base64" 11 | "fmt" 12 | "log" 13 | "net" 14 | "testing" 15 | "time" 16 | 17 | kmspb "cloud.google.com/go/kms/apiv1/kmspb" 18 | . "github.com/onsi/gomega" 19 | "google.golang.org/grpc" 20 | ) 21 | 22 | var ( 23 | testResourceID = "projects/test-flux/locations/global/keyRings/test-flux/cryptoKeys/sops" 24 | decryptedData = "decrypted data" 25 | encryptedData = "encrypted data" 26 | ) 27 | 28 | func TestMasterKey_EncryptedDataKey(t *testing.T) { 29 | g := NewWithT(t) 30 | key := MasterKey{EncryptedKey: encryptedData} 31 | g.Expect(key.EncryptedDataKey()).To(BeEquivalentTo(encryptedData)) 32 | } 33 | 34 | func TestMasterKey_SetEncryptedDataKey(t *testing.T) { 35 | g := NewWithT(t) 36 | enc := "encrypted key" 37 | key := &MasterKey{} 38 | key.SetEncryptedDataKey([]byte(enc)) 39 | g.Expect(key.EncryptedDataKey()).To(BeEquivalentTo(enc)) 40 | } 41 | 42 | func TestMasterKey_EncryptIfNeeded(t *testing.T) { 43 | g := NewWithT(t) 44 | key := MasterKey{EncryptedKey: "encrypted key"} 45 | g.Expect(key.EncryptedDataKey()).To(BeEquivalentTo(key.EncryptedKey)) 46 | 47 | err := key.EncryptIfNeeded([]byte("sops data key")) 48 | g.Expect(err).ToNot(HaveOccurred()) 49 | g.Expect(key.EncryptedDataKey()).To(BeEquivalentTo(key.EncryptedKey)) 50 | } 51 | 52 | func TestMasterKey_ToString(t *testing.T) { 53 | rsrcId := testResourceID 54 | g := NewWithT(t) 55 | key := MasterKeyFromResourceID(rsrcId) 56 | g.Expect(key.ToString()).To(Equal(rsrcId)) 57 | } 58 | 59 | func TestMasterKey_ToMap(t *testing.T) { 60 | g := NewWithT(t) 61 | key := MasterKey{ 62 | credentialJSON: []byte("sensitive creds"), 63 | CreationDate: time.Date(2016, time.October, 31, 10, 0, 0, 0, time.UTC), 64 | ResourceID: testResourceID, 65 | EncryptedKey: "this is encrypted", 66 | } 67 | g.Expect(key.ToMap()).To(Equal(map[string]interface{}{ 68 | "resource_id": testResourceID, 69 | "enc": "this is encrypted", 70 | "created_at": "2016-10-31T10:00:00Z", 71 | })) 72 | } 73 | 74 | func TestMasterKey_createCloudKMSService(t *testing.T) { 75 | g := NewWithT(t) 76 | 77 | tests := []struct { 78 | key MasterKey 79 | errString string 80 | }{ 81 | { 82 | key: MasterKey{ 83 | ResourceID: "/projects", 84 | credentialJSON: []byte("some secret"), 85 | }, 86 | errString: "no valid resourceId", 87 | }, 88 | { 89 | key: MasterKey{ 90 | ResourceID: testResourceID, 91 | credentialJSON: []byte(`{ "client_id": ".apps.googleusercontent.com", 92 | "client_secret": "", 93 | "type": "authorized_user"}`), 94 | }, 95 | }, 96 | } 97 | 98 | for _, tt := range tests { 99 | _, err := tt.key.newKMSClient() 100 | if tt.errString != "" { 101 | g.Expect(err).To(HaveOccurred()) 102 | g.Expect(err.Error()).To(ContainSubstring(tt.errString)) 103 | } else { 104 | g.Expect(err.Error()).To(ContainSubstring("auth: refresh token must be provided")) 105 | } 106 | } 107 | } 108 | 109 | func TestMasterKey_Decrypt(t *testing.T) { 110 | g := NewWithT(t) 111 | 112 | mockKeyManagement.err = nil 113 | mockKeyManagement.reqs = nil 114 | mockKeyManagement.resps = append(mockKeyManagement.resps[:0], &kmspb.DecryptResponse{ 115 | Plaintext: []byte(decryptedData), 116 | }) 117 | key := MasterKey{ 118 | grpcConn: newGRPCServer("0"), 119 | ResourceID: testResourceID, 120 | EncryptedKey: "encryptedKey", 121 | } 122 | data, err := key.Decrypt() 123 | g.Expect(err).ToNot(HaveOccurred()) 124 | g.Expect(data).To(BeEquivalentTo(decryptedData)) 125 | } 126 | 127 | func TestMasterKey_Encrypt(t *testing.T) { 128 | g := NewWithT(t) 129 | 130 | mockKeyManagement.err = nil 131 | mockKeyManagement.reqs = nil 132 | mockKeyManagement.resps = append(mockKeyManagement.resps[:0], &kmspb.EncryptResponse{ 133 | Ciphertext: []byte(encryptedData), 134 | }) 135 | 136 | key := MasterKey{ 137 | grpcConn: newGRPCServer("0"), 138 | ResourceID: testResourceID, 139 | } 140 | err := key.Encrypt([]byte("encrypt")) 141 | g.Expect(err).ToNot(HaveOccurred()) 142 | g.Expect(key.EncryptedDataKey()).To(BeEquivalentTo(base64.StdEncoding.EncodeToString([]byte(encryptedData)))) 143 | } 144 | 145 | var ( 146 | mockKeyManagement mockKeyManagementServer 147 | ) 148 | 149 | func newGRPCServer(port string) *grpc.ClientConn { 150 | serv := grpc.NewServer() 151 | kmspb.RegisterKeyManagementServiceServer(serv, &mockKeyManagement) 152 | 153 | lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%s", port)) 154 | if err != nil { 155 | log.Fatal(err) 156 | } 157 | go serv.Serve(lis) 158 | 159 | conn, err := grpc.Dial(lis.Addr().String(), grpc.WithInsecure()) 160 | if err != nil { 161 | log.Fatal(err) 162 | } 163 | 164 | return conn 165 | } 166 | -------------------------------------------------------------------------------- /internal/metrics/recorder.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025 Peak Scale 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package metrics 5 | 6 | import ( 7 | sopsv1alpha1 "github.com/peak-scale/sops-operator/api/v1alpha1" 8 | "github.com/peak-scale/sops-operator/internal/meta" 9 | "github.com/prometheus/client_golang/prometheus" 10 | crtlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" 11 | ) 12 | 13 | type Recorder struct { 14 | providerConditionGauge *prometheus.GaugeVec 15 | secretConditionGauge *prometheus.GaugeVec 16 | globalSecretConditionGauge *prometheus.GaugeVec 17 | } 18 | 19 | func MustMakeRecorder() *Recorder { 20 | metricsRecorder := NewRecorder() 21 | crtlmetrics.Registry.MustRegister(metricsRecorder.Collectors()...) 22 | 23 | return metricsRecorder 24 | } 25 | 26 | func NewRecorder() *Recorder { 27 | namespace := "sops" 28 | 29 | return &Recorder{ 30 | providerConditionGauge: prometheus.NewGaugeVec( 31 | prometheus.GaugeOpts{ 32 | Namespace: namespace, 33 | Name: "provider_condition", 34 | Help: "The current condition status of a Provider.", 35 | }, 36 | []string{"name", "status"}, 37 | ), 38 | secretConditionGauge: prometheus.NewGaugeVec( 39 | prometheus.GaugeOpts{ 40 | Namespace: namespace, 41 | Name: "secret_condition", 42 | Help: "The current condition status of a Secret.", 43 | }, 44 | []string{"name", "namespace", "status"}, 45 | ), 46 | globalSecretConditionGauge: prometheus.NewGaugeVec( 47 | prometheus.GaugeOpts{ 48 | Namespace: namespace, 49 | Name: "global_secret_condition", 50 | Help: "The current condition status of a Global Secret.", 51 | }, 52 | []string{"name", "status"}, 53 | ), 54 | } 55 | } 56 | 57 | func (r *Recorder) Collectors() []prometheus.Collector { 58 | return []prometheus.Collector{ 59 | r.providerConditionGauge, 60 | r.secretConditionGauge, 61 | r.globalSecretConditionGauge, 62 | } 63 | } 64 | 65 | // RecordCondition records the condition as given for the ref. 66 | func (r *Recorder) RecordProviderCondition(provider *sopsv1alpha1.SopsProvider) { 67 | for _, status := range []string{meta.ReadyCondition, meta.NotReadyCondition} { 68 | var value float64 69 | if provider.Status.Condition.Type == status { 70 | value = 1 71 | } 72 | 73 | r.providerConditionGauge.WithLabelValues(provider.Name, status).Set(value) 74 | } 75 | } 76 | 77 | // RecordCondition records the condition as given for the ref. 78 | func (r *Recorder) RecordSecretCondition(secret *sopsv1alpha1.SopsSecret) { 79 | for _, status := range []string{meta.ReadyCondition, meta.NotReadyCondition} { 80 | var value float64 81 | if secret.Status.Condition.Type == status { 82 | value = 1 83 | } 84 | 85 | r.secretConditionGauge.WithLabelValues(secret.Name, secret.Namespace, status).Set(value) 86 | } 87 | } 88 | 89 | // RecordCondition records the condition as given for the ref. 90 | func (r *Recorder) RecordGlobalSecretCondition(secret *sopsv1alpha1.GlobalSopsSecret) { 91 | for _, status := range []string{meta.ReadyCondition, meta.NotReadyCondition} { 92 | var value float64 93 | if secret.Status.Condition.Type == status { 94 | value = 1 95 | } 96 | 97 | r.globalSecretConditionGauge.WithLabelValues(secret.Name, status).Set(value) 98 | } 99 | } 100 | 101 | // DeleteCondition deletes the condition metrics for the ref. 102 | func (r *Recorder) DeleteProvider(provider *sopsv1alpha1.SopsProvider) { 103 | r.providerConditionGauge.DeletePartialMatch(map[string]string{ 104 | "name": provider.Name, 105 | }) 106 | } 107 | 108 | // DeleteCondition deletes the condition metrics for the ref. 109 | func (r *Recorder) DeleteProviderCondition(provider *sopsv1alpha1.SopsProvider) { 110 | for _, status := range []string{meta.ReadyCondition, meta.NotReadyCondition} { 111 | r.providerConditionGauge.DeleteLabelValues(provider.Name, status) 112 | } 113 | } 114 | 115 | // DeleteCondition deletes the condition metrics for the ref. 116 | func (r *Recorder) DeleteSecret(secret *sopsv1alpha1.SopsSecret) { 117 | r.secretConditionGauge.DeletePartialMatch(map[string]string{ 118 | "name": secret.Name, 119 | "namespace": secret.Namespace, 120 | }) 121 | } 122 | 123 | // DeleteCondition deletes the condition metrics for the ref. 124 | func (r *Recorder) DeleteSecretCondition(secret *sopsv1alpha1.SopsSecret) { 125 | for _, status := range []string{meta.ReadyCondition, meta.NotReadyCondition} { 126 | r.secretConditionGauge.DeleteLabelValues(secret.Name, secret.Namespace, status) 127 | } 128 | } 129 | 130 | // DeleteCondition deletes the condition metrics for the ref. 131 | func (r *Recorder) DeleteGlobalSecret(secret *sopsv1alpha1.GlobalSopsSecret) { 132 | r.globalSecretConditionGauge.DeletePartialMatch(map[string]string{ 133 | "name": secret.Name, 134 | }) 135 | } 136 | 137 | // DeleteCondition deletes the condition metrics for the ref. 138 | func (r *Recorder) DeleteGlobalSecretCondition(secret *sopsv1alpha1.GlobalSopsSecret) { 139 | for _, status := range []string{meta.ReadyCondition, meta.NotReadyCondition} { 140 | r.globalSecretConditionGauge.DeleteLabelValues(secret.Name, status) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /internal/decryptor/kustomize-controller/azkv/keysource_integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | // Copyright (C) 2022 The Flux authors 5 | // 6 | // This Source Code Form is subject to the terms of the Mozilla Public 7 | // License, v. 2.0. If a copy of the MPL was not distributed with this 8 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 9 | 10 | package azkv 11 | 12 | import ( 13 | "context" 14 | "encoding/base64" 15 | "os" 16 | "testing" 17 | "time" 18 | 19 | "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" 20 | "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys" 21 | "github.com/getsops/sops/v3/azkv" 22 | . "github.com/onsi/gomega" 23 | ) 24 | 25 | // The following values should be created based on the instructions in: 26 | // https://github.com/mozilla/sops#encrypting-using-azure-key-vault 27 | var ( 28 | testVaultURL = os.Getenv("TEST_AZURE_VAULT_URL") 29 | testVaultKeyName = os.Getenv("TEST_AZURE_VAULT_KEY_NAME") 30 | testVaultKeyVersion = os.Getenv("TEST_AZURE_VAULT_KEY_VERSION") 31 | testAADConfig = AADConfig{ 32 | TenantID: os.Getenv("TEST_AZURE_TENANT_ID"), 33 | ClientID: os.Getenv("TEST_AZURE_CLIENT_ID"), 34 | ClientSecret: os.Getenv("TEST_AZURE_CLIENT_SECRET"), 35 | } 36 | ) 37 | 38 | func TestMasterKey_Encrypt(t *testing.T) { 39 | g := NewWithT(t) 40 | 41 | key := MasterKeyFromURL(testVaultURL, testVaultKeyName, testVaultKeyVersion) 42 | token, err := TokenFromAADConfig(testAADConfig) 43 | g.Expect(err).ToNot(HaveOccurred()) 44 | token.ApplyToMasterKey(key) 45 | 46 | g.Expect(key.Encrypt([]byte("foo"))).To(Succeed()) 47 | g.Expect(key.EncryptedDataKey()).ToNot(BeEmpty()) 48 | } 49 | 50 | func TestMasterKey_Decrypt(t *testing.T) { 51 | g := NewWithT(t) 52 | 53 | key := MasterKeyFromURL(testVaultURL, testVaultKeyName, testVaultKeyVersion) 54 | token, err := TokenFromAADConfig(testAADConfig) 55 | g.Expect(err).ToNot(HaveOccurred()) 56 | token.ApplyToMasterKey(key) 57 | 58 | dataKey := []byte("this is super secret data") 59 | c, err := azkeys.NewClient(key.VaultURL, key.token, nil) 60 | g.Expect(err).ToNot(HaveOccurred()) 61 | resp, err := c.Encrypt(context.Background(), key.Name, key.Version, azkeys.KeyOperationParameters{ 62 | Algorithm: to.Ptr(azkeys.EncryptionAlgorithmRSAOAEP256), 63 | Value: dataKey, 64 | }, nil) 65 | g.Expect(err).ToNot(HaveOccurred()) 66 | key.EncryptedKey = base64.RawURLEncoding.EncodeToString(resp.Result) 67 | g.Expect(key.EncryptedKey).ToNot(BeEmpty()) 68 | g.Expect(key.EncryptedKey).ToNot(Equal(dataKey)) 69 | 70 | got, err := key.Decrypt() 71 | g.Expect(err).ToNot(HaveOccurred()) 72 | g.Expect(got).To(Equal(dataKey)) 73 | } 74 | 75 | func TestMasterKey_EncryptDecrypt_RoundTrip(t *testing.T) { 76 | g := NewWithT(t) 77 | 78 | key := MasterKeyFromURL(testVaultURL, testVaultKeyName, testVaultKeyVersion) 79 | token, err := TokenFromAADConfig(testAADConfig) 80 | g.Expect(err).ToNot(HaveOccurred()) 81 | token.ApplyToMasterKey(key) 82 | 83 | dataKey := []byte("some-data-that-should-be-secret") 84 | 85 | g.Expect(key.Encrypt(dataKey)).To(Succeed()) 86 | g.Expect(key.EncryptedDataKey()).ToNot(BeEmpty()) 87 | 88 | dec, err := key.Decrypt() 89 | g.Expect(err).ToNot(HaveOccurred()) 90 | g.Expect(dec).To(Equal(dataKey)) 91 | } 92 | 93 | func TestMasterKey_Encrypt_SOPS_Compat(t *testing.T) { 94 | g := NewWithT(t) 95 | 96 | encryptKey := MasterKeyFromURL(testVaultURL, testVaultKeyName, testVaultKeyVersion) 97 | token, err := TokenFromAADConfig(testAADConfig) 98 | g.Expect(err).ToNot(HaveOccurred()) 99 | token.ApplyToMasterKey(encryptKey) 100 | 101 | dataKey := []byte("foo") 102 | g.Expect(encryptKey.Encrypt(dataKey)).To(Succeed()) 103 | 104 | t.Setenv("AZURE_CLIENT_ID", testAADConfig.ClientID) 105 | t.Setenv("AZURE_TENANT_ID", testAADConfig.TenantID) 106 | t.Setenv("AZURE_CLIENT_SECRET", testAADConfig.ClientSecret) 107 | 108 | decryptKey := &azkv.MasterKey{ 109 | VaultURL: testVaultURL, 110 | Name: testVaultKeyName, 111 | Version: testVaultKeyVersion, 112 | EncryptedKey: encryptKey.EncryptedKey, 113 | CreationDate: time.Now(), 114 | } 115 | 116 | dec, err := decryptKey.Decrypt() 117 | g.Expect(err).ToNot(HaveOccurred()) 118 | g.Expect(dec).To(Equal(dataKey)) 119 | } 120 | 121 | func TestMasterKey_Decrypt_SOPS_Compat(t *testing.T) { 122 | g := NewWithT(t) 123 | 124 | t.Setenv("AZURE_CLIENT_ID", testAADConfig.ClientID) 125 | t.Setenv("AZURE_TENANT_ID", testAADConfig.TenantID) 126 | t.Setenv("AZURE_CLIENT_SECRET", testAADConfig.ClientSecret) 127 | 128 | dataKey := []byte("foo") 129 | 130 | encryptKey := &azkv.MasterKey{ 131 | VaultURL: testVaultURL, 132 | Name: testVaultKeyName, 133 | Version: testVaultKeyVersion, 134 | CreationDate: time.Now(), 135 | } 136 | g.Expect(encryptKey.Encrypt(dataKey)).To(Succeed()) 137 | 138 | decryptKey := MasterKeyFromURL(testVaultURL, testVaultKeyName, testVaultKeyVersion) 139 | token, err := TokenFromAADConfig(testAADConfig) 140 | g.Expect(err).ToNot(HaveOccurred()) 141 | token.ApplyToMasterKey(decryptKey) 142 | 143 | decryptKey.EncryptedKey = encryptKey.EncryptedKey 144 | dec, err := decryptKey.Decrypt() 145 | g.Expect(err).ToNot(HaveOccurred()) 146 | g.Expect(dec).To(Equal(dataKey)) 147 | } 148 | -------------------------------------------------------------------------------- /internal/decryptor/kustomize-controller/pgp/testdata/private.gpg: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PRIVATE KEY BLOCK----- 2 | 3 | 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 79 | sgS+XwT2h+fdCBJHJYmboxXpxC02xHY= 80 | =G8JF 81 | -----END PGP PRIVATE KEY BLOCK----- 82 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024-2025 Peak Scale 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "os" 10 | "time" 11 | 12 | sopsv1alpha1 "github.com/peak-scale/sops-operator/api/v1alpha1" 13 | "github.com/peak-scale/sops-operator/internal/controllers" 14 | "github.com/peak-scale/sops-operator/internal/metrics" 15 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | "k8s.io/apimachinery/pkg/runtime" 17 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 18 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 19 | _ "k8s.io/client-go/plugin/pkg/client/auth" 20 | ctrl "sigs.k8s.io/controller-runtime" 21 | "sigs.k8s.io/controller-runtime/pkg/healthz" 22 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 23 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 24 | ) 25 | 26 | var ( 27 | scheme = runtime.NewScheme() 28 | setupLog = ctrl.Log.WithName("setup") 29 | ) 30 | 31 | func init() { 32 | //+kubebuilder:scaffold:scheme 33 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 34 | utilruntime.Must(sopsv1alpha1.AddToScheme(scheme)) 35 | } 36 | 37 | func main() { 38 | var metricsAddr, secretErrorIntervalStr string 39 | 40 | var enableLeaderElection, enablePprof, enableStatus bool 41 | 42 | var probeAddr string 43 | 44 | flag.StringVar(&secretErrorIntervalStr, "secret-error-interval", "60s", "The requeued interval for failed kubernetes secret reconciliations") 45 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 46 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":10080", "The address the probe endpoint binds to.") 47 | flag.BoolVar(&enablePprof, "enable-pprof", false, "Enables Pprof endpoint for profiling (not recommend in production)") 48 | flag.BoolVar(&enableStatus, "enable-provider-status", true, "Add all available providers to the status of the SopsSecret resource") 49 | flag.BoolVar(&enableLeaderElection, "leader-elect", true, 50 | "Enable leader election for controller manager. "+ 51 | "Enabling this will ensure there is only one active controller manager.") 52 | 53 | opts := zap.Options{ 54 | Development: true, 55 | } 56 | opts.BindFlags(flag.CommandLine) 57 | flag.Parse() 58 | 59 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 60 | 61 | secretErrorInterval, err := time.ParseDuration(secretErrorIntervalStr) 62 | if err != nil { 63 | fmt.Fprintf(os.Stderr, "invalid duration for --secret-error-interval: %v\n", err) 64 | os.Exit(1) 65 | } 66 | 67 | ctrlConfig := ctrl.Options{ 68 | Scheme: scheme, 69 | Metrics: metricsserver.Options{BindAddress: metricsAddr}, 70 | HealthProbeBindAddress: probeAddr, 71 | LeaderElection: enableLeaderElection, 72 | LeaderElectionNamespace: os.Getenv("NAMESPACE"), 73 | LeaderElectionID: "2e0ffcfb.peakscale.ch", 74 | } 75 | 76 | if enablePprof { 77 | ctrlConfig.PprofBindAddress = ":8082" 78 | } 79 | 80 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrlConfig) 81 | if err != nil { 82 | setupLog.Error(err, "unable to start manager") 83 | os.Exit(1) 84 | } 85 | 86 | metricsRecorder := metrics.MustMakeRecorder() 87 | 88 | if err = (&controllers.SopsSecretReconciler{ 89 | Client: mgr.GetClient(), 90 | Log: ctrl.Log.WithName("Controllers").WithName("SopsSecrets"), 91 | Metrics: metricsRecorder, 92 | Scheme: mgr.GetScheme(), 93 | }).SetupWithManager(mgr, controllers.SopsSecretReconcilerConfig{ 94 | EnableStatus: enableStatus, 95 | FailedSecretsInterval: metav1.Duration{Duration: secretErrorInterval}, 96 | ControllerName: "sopssecret", 97 | }); err != nil { 98 | setupLog.Error(err, "unable to create controller", "controller", "SopsSecret") 99 | os.Exit(1) 100 | } 101 | 102 | if err = (&controllers.GlobalSopsSecretReconciler{ 103 | Client: mgr.GetClient(), 104 | Log: ctrl.Log.WithName("Controllers").WithName("GlobalSopsSecrets"), 105 | Metrics: metricsRecorder, 106 | Scheme: mgr.GetScheme(), 107 | }).SetupWithManager(mgr, controllers.SopsSecretReconcilerConfig{ 108 | EnableStatus: enableStatus, 109 | FailedSecretsInterval: metav1.Duration{Duration: secretErrorInterval}, 110 | ControllerName: "globalsopssecret", 111 | }); err != nil { 112 | setupLog.Error(err, "unable to create controller", "controller", "GlobalSopsSecret") 113 | os.Exit(1) 114 | } 115 | 116 | if err = (&controllers.SopsProviderReconciler{ 117 | Client: mgr.GetClient(), 118 | Log: ctrl.Log.WithName("Controllers").WithName("Providers"), 119 | Metrics: metricsRecorder, 120 | Scheme: mgr.GetScheme(), 121 | }).SetupWithManager(mgr); err != nil { 122 | setupLog.Error(err, "unable to create controller", "controller", "SopsProvider") 123 | os.Exit(1) 124 | } 125 | //+kubebuilder:scaffold:builder 126 | 127 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 128 | setupLog.Error(err, "unable to set up health check") 129 | os.Exit(1) 130 | } 131 | 132 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 133 | setupLog.Error(err, "unable to set up ready check") 134 | os.Exit(1) 135 | } 136 | 137 | setupLog.Info("starting manager") 138 | 139 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 140 | setupLog.Error(err, "problem running manager") 141 | os.Exit(1) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /internal/decryptor/kustomize-controller/azkv/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Peak Scale 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package azkv 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/Azure/azure-sdk-for-go/sdk/azcore" 10 | "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" 11 | "github.com/Azure/azure-sdk-for-go/sdk/azidentity" 12 | "sigs.k8s.io/yaml" 13 | ) 14 | 15 | // LoadAADConfigFromBytes attempts to load the given bytes into the given AADConfig. 16 | // By first decoding it if UTF-16, and then unmarshalling it into the given struct. 17 | // It returns an error for any failure. 18 | func LoadAADConfigFromBytes(b []byte, s *AADConfig) error { 19 | b, err := decode(b) 20 | if err != nil { 21 | return fmt.Errorf("failed to decode Azure authentication file bytes: %w", err) 22 | } 23 | 24 | if err = yaml.Unmarshal(b, s); err != nil { 25 | err = fmt.Errorf("failed to unmarshal Azure authentication file: %w", err) 26 | } 27 | 28 | return err 29 | } 30 | 31 | // AADConfig contains the selection of fields from an Azure authentication file 32 | // required for Active Directory authentication. 33 | type AADConfig struct { 34 | AZConfig 35 | TenantID string `json:"tenantId,omitempty"` 36 | ClientID string `json:"clientId,omitempty"` 37 | ClientSecret string `json:"clientSecret,omitempty"` 38 | ClientCertificate string `json:"clientCertificate,omitempty"` 39 | ClientCertificatePassword string `json:"clientCertificatePassword,omitempty"` 40 | ClientCertificateSendChain bool `json:"clientCertificateSendChain,omitempty"` 41 | AuthorityHost string `json:"authorityHost,omitempty"` 42 | } 43 | 44 | // AZConfig contains the Service Principal fields as generated by `az`. 45 | // Ref: https://docs.microsoft.com/en-us/azure/aks/kubernetes-service-principal?tabs=azure-cli#manually-create-a-service-principal 46 | type AZConfig struct { 47 | AppID string `json:"appId,omitempty"` 48 | Tenant string `json:"tenant,omitempty"` 49 | Password string `json:"password,omitempty"` 50 | } 51 | 52 | // TokenFromAADConfig attempts to construct a Token using the AADConfig values. 53 | // It detects credentials in the following order: 54 | // 55 | // - azidentity.ClientSecretCredential when `tenantId`, `clientId` and 56 | // `clientSecret` fields are found. 57 | // - azidentity.ClientCertificateCredential when `tenantId`, 58 | // `clientCertificate` (and optionally `clientCertificatePassword`) fields 59 | // are found. 60 | // - azidentity.ClientSecretCredential when AZConfig fields are found. 61 | // - azidentity.ManagedIdentityCredential for a User ID, when a `clientId` 62 | // field but no `tenantId` is found. 63 | // 64 | // If no set of credentials is found or the azcore.TokenCredential can not be 65 | // created, an error is returned. 66 | func TokenFromAADConfig(c AADConfig) (t *Token, err error) { 67 | var token azcore.TokenCredential 68 | 69 | if c.TenantID != "" && c.ClientID != "" { 70 | if c.ClientSecret != "" { 71 | if token, err = azidentity.NewClientSecretCredential(c.TenantID, c.ClientID, c.ClientSecret, &azidentity.ClientSecretCredentialOptions{ 72 | ClientOptions: azcore.ClientOptions{ 73 | Cloud: c.GetCloudConfig(), 74 | }, 75 | }); err != nil { 76 | return t, err 77 | } 78 | 79 | return NewToken(token), nil 80 | } 81 | 82 | if c.ClientCertificate != "" { 83 | certs, pk, err := azidentity.ParseCertificates([]byte(c.ClientCertificate), []byte(c.ClientCertificatePassword)) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | if token, err = azidentity.NewClientCertificateCredential(c.TenantID, c.ClientID, certs, pk, &azidentity.ClientCertificateCredentialOptions{ 89 | SendCertificateChain: c.ClientCertificateSendChain, 90 | ClientOptions: azcore.ClientOptions{ 91 | Cloud: c.GetCloudConfig(), 92 | }, 93 | }); err != nil { 94 | return nil, err 95 | } 96 | 97 | return NewToken(token), nil 98 | } 99 | } 100 | 101 | switch { 102 | case c.Tenant != "" && c.AppID != "" && c.Password != "": 103 | if token, err = azidentity.NewClientSecretCredential(c.Tenant, c.AppID, c.Password, &azidentity.ClientSecretCredentialOptions{ 104 | ClientOptions: azcore.ClientOptions{ 105 | Cloud: c.GetCloudConfig(), 106 | }, 107 | }); err != nil { 108 | return t, err 109 | } 110 | 111 | return NewToken(token), nil 112 | case c.ClientID != "": 113 | if token, err = azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ 114 | ID: azidentity.ClientID(c.ClientID), 115 | }); err != nil { 116 | return t, err 117 | } 118 | 119 | return NewToken(token), nil 120 | default: 121 | return nil, fmt.Errorf("invalid data: requires a '%s' field, a combination of '%s', '%s' and '%s', or '%s', '%s' and '%s'", 122 | "clientId", "tenantId", "clientId", "clientSecret", "tenantId", "clientId", "clientCertificate") 123 | } 124 | } 125 | 126 | // GetCloudConfig returns a cloud.Configuration with the AuthorityHost, or the 127 | // Azure Public Cloud default. 128 | func (s AADConfig) GetCloudConfig() cloud.Configuration { 129 | if s.AuthorityHost != "" { 130 | return cloud.Configuration{ 131 | ActiveDirectoryAuthorityHost: s.AuthorityHost, 132 | Services: map[cloud.ServiceName]cloud.ServiceConfiguration{}, 133 | } 134 | } 135 | 136 | return cloud.AzurePublic 137 | } 138 | --------------------------------------------------------------------------------