Sometimes you need to dynamically get acces to secrets from different namespaces.
For example: You have dynamically created namespaces, so you need individual token for each namespace to access secret by individual key.
The easiest, but unscalable solution to create SecretStore in each namespace, but for that, you need to create one more token in Vault.
Inspired by this article - https://external-secrets.io/latest/guides/multi-tenancy/
We create Shared Secret Store, that has access to all secrets for needed namespaces
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: shared-secret-store
spec:
provider:
vault:
auth:
tokenSecretRef:
namespace: external-secrets
name: vault-shared-secret-store-token
key: vault-token
server: "http://vault.vault.svc.cluster.local:8200"
path: "k8s"
version: 'v2'
So we need the option, to set allowed base path for all secret keys in that namespace or ArgoProject
apiVersion: v1
kind: Namespace
metadata:
labels:
kubernetes.io/metadata.name: ns1
name: ns1
external-secrets/allowed-path: ns1/ # allowed base path for all secrets
name: ns1
Therefore this secret should be allowed
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: ns1-secret
namespace: ns1
spec:
secretStoreRef:
name: shared-secret-store
kind: ClusterSecretStore
refreshInterval: 5m
target:
deletionPolicy: Delete
dataFrom:
- extract:
key: ns1/story-1/task-1
But this not
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: ns2-secret
namespace: ns2
spec:
secretStoreRef:
name: shared-secret-store
kind: ClusterSecretStore
refreshInterval: 5m
target:
deletionPolicy: Delete
dataFrom:
- extract:
key: ns1/story-1/task-1 # take secret from one ns for another ns (steal secret)
Create ClusterPolicy to reject that behaviour
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: shared-secret-store-policy
spec:
validationFailureAction: Enforce
rules:
- name: allow-only-permitted-keys
context:
# Get allowed path for this namespace
- name: allowed_path
apiCall:
# Get namespace where resource is applied
urlPath: "/api/v1/namespaces/{{ request.namespace }}"
method: GET
# Get our label and cnonicalize it, e.g. remove redundant slashed // -> /
jmesPath: path_canonicalize(metadata.annotations."external-secrets/allowed-path")
- name: secret_store_name
variable:
# Name of ClusterSecretStore, so we allow create ExternalSecret from another SecretStore
value: "shared-secret-store"
match:
resources:
kinds:
- ExternalSecret
preconditions:
all:
- key: "{{ request.operation || 'BACKGROUND' }}"
operator: NotEquals
value: DELETE
validate:
message: "Path not allowed for this namespace"
pattern:
spec:
secretStoreRef:
<(name): "{{ secret_store_name }}"
# Or the target SecretStore namespaced
<(kind): ClusterSecretStore
dataFrom:
- extract:
key: "{{ allowed_path }}*" # wildcard match