The secrets distributor effectively copies secrets that are exposed to it to destination secrets on the same or other namespaces as defined in a custom resource. This way you can get the secrets from a third party location such as key vault and have the same secrets exposed to multiple other projects without having to hook up each container to azure keyvault etc. This greatly simplifies implementation of secrets when supporting multiple providers.
The secrets distributor implements the operator model via KOPF and each time a SecretsDistribution resource is created or amended it triggers the solution to read the secrets targeted from the /mnt/secrets folder. A new secret is then generated according to the specification in the SecretsDistribution Resource. The applications can then refer to the internal secrets from kubernetes Secret resources rather than having to have every pod refer to the azure key vault, AWS Secrets, or whatever other secret management that they may be using.
The problem
As an example, let’s say that we have an test application which requires a TLS certificate to function correctly.
We will assume for the purposes of this solution that the TLS certificate is a wildcard certificate that is going to be used in multiple applications.
The first application we’re going to call test-application-1 will use this certificate. This certificate is stored in Azure Keyvault in two secrets:
- WildcardCertificate - The certificate itself
- WildcardCertificateKey - The private key of the certificate
Traditionally, we would need to create a SecretProviderClass object on the namespace:
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: keyvault-sync
namespace: test-application-1
annotations:
xlscsde.nhs.uk/secretUsage: "Key Vault Sync"
spec:
provider: azure
parameters:
usePodIdentity: "false"
useVMManagedIdentity: "true"
userAssignedIdentityID: 9bc116bb-2090-4602-9ff6-c1adcb1f7e32
keyvaultName: some-vault
tenantId: 9bc116bb-2090-4602-9ff6-c1adcb1f7e32
objects: |
array:
- |
objectName: WildcardCertificate
objectType: secret
- |
objectName: WildcardCertificateKey
objectType: secret
We would then need to mount this into the definition of any container that was using it:
...
volumeMounts:
- name: secrets-store-inline
mountPath: /mnt/secrets
readOnly: true
...
volumes:
- name: secrets-store-inline
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "keyvault-sync"
This then means that we would have to implement the SecretProviderClass object in multiple different applications separately. This becomes problematic when we are working with a variety of different infrastructure and landscapes, where some might use Azure Keyvault, others may use AWS Secret store, others may just simply mount a local folder.
flowchart LR
caa[Container AAA] -->|SecretClassProvider 1| kv[KeyVault]
cab[Container AAB] -->|SecretClassProvider 1| kv
cba[Container BAA] -->|SecretClassProvider 2| kv
cbb[Container BBA] -->|SecretClassProvider 2| kv
subgraph Namespace A
subgraph Pod A
cab
caa
end
end
subgraph Namespace B
subgraph Pod A
cba
end
subgraph Pod B
cbb
end
end
The more of these that you have to maintain, the more difficult it becomes to manage and the solution. Then inside of our flux repositories we have to tell flux how to deal with all of these different eventualities and as flux does not handle control flow, it actually becomes very difficult to manage indeed.
The solution
The secrets distributor keeps things simple on the application side by making everything use secrets inside of kubernetes, these secrets are then managed by the distributor which can then in turn be hooked up to the relevant secret store.
flowchart LR
caa[Container AAA] -->|Reads| sa[Secret]
cab[Container AAB] -->|Reads| sa
cba[Container BAA] -->|Reads| sb[Secret]
cbb[Container BBA] -->|Reads| sb
secretdistributor[Operator Container] -->|SecretsProviderClass| kv((KeyVault))
secretdistributor -->|Writes| sa
secretdistributor -->|Writes| sb
subgraph NamespaceB
subgraph Pod A
cba
end
subgraph Pod B
cbb
end
sb
end
subgraph NamespaceA
subgraph Pod A
cab
caa
end
sa
end
subgraph SecretDistributor
secretdistributor
end
NamespaceA ~~~ SecretDistributor
NamespaceB ~~~ SecretDistributor
This makes for only one place that needs to be managed annd the secrets distribution then just configures how those secrets are distributed around the solution:
apiVersion: xlscsde.nhs.uk/v1
kind: SecretsDistribution
metadata:
name: wildcard-tls-distribution
namespace: test-application-1
annotations:
xlscsde.nhs.uk/secretUsage: "Wildcard Certificate"
spec:
name: wildcard-tls-secret
type: kubernetes.io/tls
secrets:
- from: WildcardCertificate
to: tls.crt
- from: WildcardCertificateKey
to: tls.key
In the above example a new secret will be created called wildcard-tls-secret this secret will have a type of kubernetes.io/tls and will define two entries in the secret tls.crt and tls.key these will be created from the values exposed in keyvault from the entries for WildcardCertificate and WildcardCertificateKey respectively.
...
volumeMounts:
- name: secrets-store-inline
mountPath: /mnt/secrets
readOnly: true
...
volumes:
- name: secrets-store-inline
secret:
secretName: wildcard-tls-secret
This also gives us the option of exposing them as environmental variables instead of as a volume mount allowing us to account for a variety of other scenarios.
Environmental Variables
The following environmental variables are exposed by the docker image:
Name | Purpose | Default |
---|---|---|
MANAGED_BY | Identifies the service that is currently managing the resources created & maintained by this service. This is to potentially allow multiple instances of the same service to run on the same server | secrets-distributor |
SECRETS_PATH | The location where the secrets are mounted | /mnt/secrets |