The next
VSecM Contributor Sync
will be on...
Thursday, 2024-04-25
at 8:00am Pacific time.
Mounting Secrets as Volumes
Situation Analysis
Certain apps may require their secrets to be mounted in “in-memory” volumes. Depending on the app, this could be the only way the app consumes secrets and configuration information.
When this is the only way to configure the app, especially when we don’t have access to the application’s configuration and source code, there won’t be any other feasible way to pass secrets to the app.
Strategy
Use VSecM Sidecar and VSecM Init Container to provide the secrets the workload needs when needed.
High-Level Diagram
Open the image in a new tab to see the full-size version:
Implementation
We will assume our workload has the name example
, deployed to the example-apps
namespace, and is associated with the example-sa
service account,
having a label example-app
.
Initial Application
Here is a sample deployment manifest of such a workload:
apiVersion: apps/v1
kind: Deployment
metadata:
name: example
namespace: example-apps
labels:
app: example
spec:
replicas: 1
selector:
matchLabels:
app: example-app
template:
metadata:
labels:
app: example-app
spec:
serviceAccountName: example-sa
containers:
- name: example-container
image: example-app:0.1.0
Create a ClusterSPIFFEID
For VSecM to communicate with this workload, a ClusterSPIFFEID
is needed.
Here is how such a ClusterSPIFFEID may look like:
apiVersion: spire.spiffe.io/v1alpha1
kind: ClusterSPIFFEID
metadata:
name: example
spec:
spiffeIDTemplate: "spiffe://vsecm.com\
/workload/example\
/ns/{{ .PodMeta.Namespace }}\
/sa/{{ .PodSpec.ServiceAccountName }}\
/n/{{ .PodMeta.Name }}"
podSelector:
matchLabels:
app: example-app
workloadSelectorTemplates:
- "k8s:ns:example-apps"
- "k8s:sa:example-sa"
When this ClusterSPIFFEID
is defined, our example pod will get a secure
x.509 certificate from the SPIFFE Workload API to talk to VSecM;
however, to fetch the certificate, we will need to modify the pod’s deployment
manifest slightly.
Let’s see that in the next section.
Let the Workload Consume SPIFFE Workload API
Here is the modified deployment manifest to consume SPIFFE Workload API:
apiVersion: apps/v1
kind: Deployment
metadata:
name: example
namespace: example-apps
labels:
app: example
spec:
replicas: 1
selector:
matchLabels:
app: example-app
template:
metadata:
labels:
app: example-app
spec:
serviceAccountName: example-sa
containers:
- name: example-container
image: example-app:0.1.0
## <--- BEGIN CHANGE
volumes:
- name: spire-agent-socket
csi:
driver: "csi.spiffe.io"
readOnly: true
## <-- END CHANGE
We added a special volume, and the [SPIFFE CSI Driver][spiffe-csi-drive] will handle the rest of the communication.
Here is the manifest without the change markers:
apiVersion: apps/v1
kind: Deployment
metadata:
name: example
namespace: example-apps
labels:
app: example
spec:
replicas: 1
selector:
matchLabels:
app: example-app
template:
metadata:
labels:
app: example-app
spec:
serviceAccountName: example-sa
containers:
- name: example-container
image: example-app:0.1.0
volumes:
- name: spire-agent-socket
csi:
driver: "csi.spiffe.io"
readOnly: true
Let’s say this workload needs an /opt/app/credentials
file as an initial
configuration file to execute its business logic. Based on this assumption,
let’s update the manifest accordingly to provide this file:
apiVersion: apps/v1
kind: Deployment
metadata:
name: example
namespace: example-apps
labels:
app: example
spec:
replicas: 1
selector:
matchLabels:
app: example-app
template:
metadata:
labels:
app: example-app
spec:
serviceAccountName: example-sa
containers:
- name: example-container
image: example-app:0.1.0
## <--- BEGIN CHANGE
volumeMounts:
- name: credentials-volume
mountPath: /opt/app/credentials
subPath: credentials
readOnly: true
## <--- END CHANGE
volumes:
- name: spire-agent-socket
csi:
driver: "csi.spiffe.io"
readOnly: true
## <--- BEGIN CHANGE
- name: credentials-volume
emptyDir:
medium: Memory
## <--- END CHANGE
However, the volume is empty; our app will likely require it to be populated before it can be used. We will address this real soon.
Here’s the manifest without the change markers:
apiVersion: apps/v1
kind: Deployment
metadata:
name: example
namespace: example-apps
labels:
app: example
spec:
replicas: 1
selector:
matchLabels:
app: example-app
template:
metadata:
labels:
app: example-app
spec:
serviceAccountName: example-sa
containers:
- name: example-container
image: example-app:0.1.0
volumeMounts:
- name: credentials-volume
mountPath: /opt/app/credentials
subPath: credentials
readOnly: true
volumes:
- name: spire-agent-socket
csi:
driver: "csi.spiffe.io"
readOnly: true
- name: credentials-volume
emptyDir:
medium: Memory
We will populate the volume using VSecM Sidecar.
Initializing the Volume Using VSecM Sidecar
Let’s add VSecM Sidecar to our manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: example
namespace: example-apps
labels:
app: example
spec:
replicas: 1
selector:
matchLabels:
app: example-app
template:
metadata:
labels:
app: example-app
spec:
serviceAccountName: example-sa
containers:
- name: example-container
image: example-app:0.1.0
volumeMounts:
- name: credentials-volume
mountPath: /opt/app/credentials
readOnly: true
## <--- BEGIN CHANGE
- name: sidecar
image: vsecm/vsecm-ist-sidecar:0.24.1
volumeMounts:
- mountPath: /opt/app/credentials
name: credentials-volume
- name: spire-agent-socket
mountPath: /spire-agent-socket
readOnly: true
env:
- name: VSECM_SIDECAR_SECRET_PATH
value: "/opt/app/credentials/secrets.json"
- name: SPIFFE_ENDPOINT_SOCKET
value: "unix:///spire-agent-socket/agent.sock"
## <--- END CHANGE
volumes:
- name: spire-agent-socket
csi:
driver: "csi.spiffe.io"
readOnly: true
- name: credentials-volume
emptyDir:
medium: Memory
In this setup, VSecM Sidecar will periodically poll VSecM Safe to fetch
the secret associated with the workload and update /opt/app/credentials
.
Help Needed
VSecM Sidecar currently creates a single file. If the application needs more than one file in the volume, you’ll need to create a specialized sidecar based on VSecM Sidecar.
There are upstream issues that will enable VSecM Sidecar to parse the incoming secret and create separate files in its associated volume.
If you need this functionality, you are welcome to contribute upstream.
Here’s the YAML manifest without change markers:
apiVersion: apps/v1
kind: Deployment
metadata:
name: example
namespace: example-apps
labels:
app: example
spec:
replicas: 1
selector:
matchLabels:
app: example-app
template:
metadata:
labels:
app: example-app
spec:
serviceAccountName: example-sa
containers:
- name: example-container
image: example-app:0.1.0
volumeMounts:
- name: credentials-volume
mountPath: /opt/app/credentials
readOnly: true
- name: sidecar
image: vsecm/vsecm-ist-sidecar:0.24.1
volumeMounts:
- mountPath: /opt/app/credentials
name: credentials-volume
- name: spire-agent-socket
mountPath: /spire-agent-socket
readOnly: true
env:
- name: VSECM_SIDECAR_SECRET_PATH
value: "/opt/app/credentials/secrets.json"
- name: SPIFFE_ENDPOINT_SOCKET
value: "unix:///spire-agent-socket/agent.sock"
volumes:
- name: spire-agent-socket
csi:
driver: "csi.spiffe.io"
readOnly: true
- name: credentials-volume
emptyDir:
medium: Memory
But now, we have another problem: The main application likely assumes the credentials are already there when it starts its lifecycle. The app will likely crash if the credentials are not there during its bootstrapping.
To fix this, we’ll need an init container that watches this volume and initializes the main app only after it is populated. We will implement this in the following section.
Adding an Init Container to Wait for The Volume to Populate
Let’s add an init container to complete our plan:
apiVersion: apps/v1
kind: Deployment
metadata:
name: example
namespace: example-apps
labels:
app: example
spec:
replicas: 1
selector:
matchLabels:
app: example-app
template:
metadata:
labels:
app: example-app
spec:
serviceAccountName: example-sa
## <--- BEGIN CHANGE
initContainers:
- name: vsecm-init-container
image: vsecm/vsecm-ist-init-container:latest
volumeMounts:
- name: credentials-volume
mountPath: /opt/vsecm/secrets
readOnly: true
## <--- END CHANGE
containers:
- name: example-container
image: example-app:0.1.0
volumeMounts:
- name: credentials-volume
mountPath: /opt/app/credentials
readOnly: true
- name: sidecar
image: vsecm/vsecm-ist-sidecar:0.24.1
volumeMounts:
- mountPath: /opt/app/credentials
name: credentials-volume
- name: spire-agent-socket
mountPath: /spire-agent-socket
readOnly: true
env:
- name: VSECM_SIDECAR_SECRET_PATH
value: "/opt/app/credentials/secrets.json"
- name: SPIFFE_ENDPOINT_SOCKET
value: "unix:///spire-agent-socket/agent.sock"
volumes:
- name: spire-agent-socket
csi:
driver: "csi.spiffe.io"
readOnly: true
- name: credentials-volume
emptyDir:
medium: Memory
VSecM Init Container will now watch the volume and only initialize the main app when everything is ready.
Help Needed
Enhancing the VSecM Init Container to monitor volume changes is an outstanding upstream issue. Your contributions are welcome.
Until this issue is resolved, you might need to add a second init container to introduce some delay (~10-15 seconds, on average) for VSecM Sidecar to consume the secret and create the file needed.
Providing Randomized Secrets to the Workload
VSecM Helm Charts can be configured to provide secure pattern-based random secrets
for workloads. For example, we can modify the initCommand stanza
of the sentinel Helm chart’s values.yaml
to register a random username and
password as secrets for our example workload.
# ./charts/sentinel/values.yaml
initCommand:
enabled: true
command: |
--
w:example
n:example-apps
s:gen:{"username":"admin-[a-z0-9]{6}","password":"[a-zA-Z0-9]{12}"}
t:{"ADMIN_USER":"{{.username}}","ADMIN_PASS":"{{.password}}"}
--
Conclusion
Integrating VSecM Sidecar and VSecM Init Container for secret management represents a forward-thinking solution to the challenge of securely providing secrets to Kubernetes workloads.
This approach embodies the principles of zero trust security by ensuring that secrets are dynamically managed and securely injected, thereby minimizing the risk of exposure. As Kubernetes continues to evolve, such patterns will be pivotal in addressing the complex security needs of cloud-native applications, making contributions and enhancements in this space invaluable.
Here are some key points covered in this use case:
- Strategy and Implementation: The approach involves using a sidecar and init container pattern to inject secrets securely into a Kubernetes pod.d. The example provided demonstrates how to deploy a workload with this pattern, highlighting the importance of security in modern cloud-native applications.
-
Secure Secret Management: By leveraging
ClusterSPIFFEID
s and the SPIFFE Workload API, the system ensures secure communication between the workload and VSecM. This method enhances security by providing each pod with a unique identity and secure certificates for authentication. - Dynamic Secret Injection: Introducing a sidecar container allows for dynamic secret fetching and updating, ensuring that workloads can access the most current secrets without restarting pods. This approach mainly benefits applications requiring high security and frequent secret rotations.
- Initialization and Secret Readiness: An init container ensures that the main application starts only after the secrets are properly fetched and mounted, addressing the challenge of secret readiness at application startup. This is critical for applications that expect specific configurations to be present before initialization.
- Customization and Contribution: The article also suggests customizing the sidecar for applications needing multiple secrets in separate files and invites contributions to enhance the VSecM Sidecar’s functionality.