Integrating the External Secrets Operator (ESO) with Crossplane for secret injection is a powerful way to manage cloud provider secrets and inject them into your Kubernetes workloads, all while leveraging Crossplane’s infrastructure-as-code capabilities.
Here’s how it works in action. Imagine you have a PostgreSQL instance provisioned by Crossplane in AWS RDS. Crossplane creates a RDSInstance custom resource, and as part of its reconciliation, it can also create a Kubernetes Secret containing the database credentials. However, you want to manage these credentials more securely, perhaps by storing them in AWS Secrets Manager and then using ESO to fetch them into your application pods.
# Example Crossplane Composite Resource Definition (CRD) for RDS
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: rdsinstances.database.example.com
spec:
group: database.example.com
names:
kind: RDSInstance
plural: rdsinstances
versions:
- name: v1alpha1
served: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
# ... other RDS spec properties
writeConnectionSecretToRef:
type: object
properties:
name:
type: string
namespace:
type: string
required:
- writeConnectionSecretToRef
---
# Example Crossplane Composite Resource (XRC) instance
apiVersion: database.example.com/v1alpha1
kind: RDSInstance
metadata:
name: my-app-db
spec:
parameters:
# ... RDS instance parameters
writeConnectionSecretToRef:
name: my-app-db-conn
namespace: default
When Crossplane creates the RDSInstance my-app-db, it will, based on the writeConnectionSecretToRef, create a Kubernetes Secret named my-app-db-conn in the default namespace. This secret will contain connection details, including a username and password.
Now, let’s bring in ESO. We’ll configure ESO to fetch secrets from AWS Secrets Manager. First, you need an ExternalSecret resource that tells ESO where to find the secret in AWS and how to map it to a Kubernetes Secret.
# Example ExternalSecret resource
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: my-app-db-secret-synced
namespace: default
spec:
refreshInterval: "1h"
secretStoreRefs:
- name: aws-secretsmanager-store
kind: SecretStore
target:
name: my-app-db-application-secret
creationPolicy: Owner
data:
- secretKey: DB_USERNAME
remoteKey:
key: my-app-db/credentials # The name of the secret in AWS Secrets Manager
property: username # The specific property within the AWS secret
- secretKey: DB_PASSWORD
remoteKey:
key: my-app-db/credentials
property: password
And here’s the SecretStore that ESO will use to authenticate with AWS Secrets Manager:
# Example SecretStore for AWS Secrets Manager
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secretsmanager-store
namespace: default
spec:
provider:
aws:
auth:
jwt:
serviceAccountToken: "" # Use Kubernetes Service Account token for auth
region: us-east-1
# Optionally, specify role ARN if using IAM roles for service accounts (IRSA)
# roleARN: "arn:aws:iam::123456789012:role/MyCrossplaneESOSecretManagerRole"
Once my-app-db-secret-synced is applied, ESO will periodically check the specified AWS Secrets Manager secret (my-app-db/credentials) and sync its username and password properties into a Kubernetes Secret named my-app-db-application-secret.
The beauty of this is that your application pods can then consume my-app-db-application-secret.
# Example Kubernetes Deployment using the injected secret
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-application
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: my-application
template:
metadata:
labels:
app: my-application
spec:
containers:
- name: app-container
image: my-app-image:latest
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: my-app-db-application-secret
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: my-app-db-application-secret
key: DB_PASSWORD
ports:
- containerPort: 8080
Here, the my-application deployment’s container gets its DB_USERNAME and DB_PASSWORD directly from the my-app-db-application-secret, which ESO is keeping up-to-date from AWS Secrets Manager. Crossplane provisions the database, ESO manages the secure retrieval of its credentials, and your application consumes them securely.
The core problem this integration solves is decoupling sensitive credential management from infrastructure provisioning. Crossplane focuses on what infrastructure you need (e.g., an RDS instance), and ESO focuses on how to securely retrieve the resulting credentials from a dedicated secret store. This separation of concerns makes your infrastructure definitions cleaner and your secret management more robust.
The creationPolicy: Owner in the ExternalSecret’s target is crucial. It ensures that the Kubernetes Secret created by ESO is owned by the ExternalSecret resource itself. If the ExternalSecret is deleted, the Kubernetes Secret it created will also be garbage collected.
When you delete the RDSInstance provisioned by Crossplane, Crossplane will clean up its associated resources, including the initial Secret it created (e.g., my-app-db-conn). However, the ExternalSecret resource and the Secret it manages (my-app-db-application-secret) will persist independently unless you explicitly delete them. This is by design; the application secret is managed by ESO, not directly by Crossplane’s RDS resource.
One subtle but powerful aspect is how ESO handles secret rotation. If the credentials in AWS Secrets Manager are rotated, ESO, configured with a refreshInterval, will automatically detect the change and update the Kubernetes Secret. Your application pods, if configured to reload their environment variables or secrets dynamically (or if they simply restart), will then pick up the new credentials without manual intervention.
The next step you’ll likely encounter is managing multiple external secret stores or implementing more complex secret mapping strategies with ESO, such as using templating or conditional logic.