Mount Secrets as Files
Mount secrets as files in Python containers — Kubernetes for Python Developers.
Focus: mount secrets as files in python containers
You've containerized your Python app, wired up ConfigMaps, and even created a Secret. But how does your Python code actually read that secret? Environment variables are the obvious answer, but they carry hidden risks: they show up in kubectl exec output, they can get logged accidentally, and they're stored in the pod's spec in plain sight. If your savvy attacker is staring at os.environ, your database password is already compromised. This lesson shows you a more robust pattern: mounting secrets as files — the standard way to deliver sensitive values to containers in production. You'll learn why it's safer, how to set it up, and how to read those files from Python. By the end, you'll be able to ship Python services that read credentials securely from the filesystem, just like the pros who run real workloads on Kubernetes.
The Problem This Lesson Solves
Imagine this: you have a Python FastAPI app that talks to PostgreSQL. The connection string lives in a Secret. The obvious move? Pass the Secret as an environment variable.
# deployment.yaml (risky approach)
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-deployment
spec:
template:
spec:
containers:
- name: api
image: my-api:latest
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: database-url
This works, but it bleeds secrets into the pod's environment. Now try kubectl exec into the pod and runenv— there's your password. If your app crashes and the logs dumpos.environ, you've just leaked the credential to your log aggregator. Worse, if someone gets read access to the pod spec (say, via a CI pipeline or a README), they see the key reference, and while the actual value isn't in the spec, the environment itself becomes a hunting ground for anyone who canexec`.
The pain points:
- Secrets in env are visible to anyone who can exec into the pod.
- Logs and error reporting tools can silently capture environment variables.
- Debugging sessions (print(os.environ)) leak secrets into terminals and log files.
- Many libraries (e.g., Django, SQLAlchemy) prefer reading a file path, not an env var.
Mounting secrets as files solves all of this by putting the secret value into a read-only file inside your container. Your Python process can access it without it ever touching the environment.
Core Concept / Mental Model
Think of Kubernetes Secrets like a safe deposit box. The Secret object stores a small piece of sensitive data — a password, a token, a private key — in etcd, encrypted if you've enabled encryption at rest. The pod is your app's home. To open the safe deposit box, you don't dump all the contents into your living room (that's env vars). Instead, you take out one item and place it on the coffee table for the app to read when needed. That coffee table is the mounted file.
A volume mount does exactly that: Kubernetes takes the Secret's data and materializes it as files on disk inside the container. Each key in the Secret becomes a file; the value is the file's content. For example, a Secret with correctly decoded bytes for api-key becomes a file at /etc/secrets/api-key containing the actual key. Your Python code can then read that file with normal open() and os.read(), nothing magical.
The beauty: the file is read-only (unless you explicitly make it writable), and it's isolated from the environment. Even if someone runs env in the pod, they see nothing. They'd have to know the exact file path and have filesystem access — which is far more restricted in any sane security model.
How It Works Step by Step
-
Define your Secret — Create a Secret object (via YAML manifest,
kubectl create secret, or the Python Kubernetes client). The Secret'sdatafield holds base64-encoded values. Optionally, you can usestringDatafor plain text and let Kubernetes encode it. -
Reference the Secret in a volume — In your Pod or Deployment spec, add a
volumesentry withsecretspecifying the secret name. This tells Kubernetes: "I want a volume backed by this Secret." -
Mount the volume into the container — In the container's
volumeMounts, choose a mount path like/etc/secrets. For each key in the Secret, a file appears at that path. The default permission is0444(read-only for all), but you can override withdefaultMode. -
Read the file in Python — In your app, open the file, read the bytes, and decode to a string. Wrap it in a try/except to fail gracefully if the file is missing.
-
Consume the secret — Use the string to build connection strings, sign requests, or initialize clients.
Let's see this in action.
Hands-On Walkthrough
Create a Secret and a Deployment that mounts it as a file, then run a Python script to read it.
Step 1: Create a Secret
Save this as db-secret.yaml:
apiVersion: v1
kind: Secret
metadata:
name: db-secret
type: Opaque
data:
# base64 of 'postgresql://user:pass@localhost:5432/appdb'
database-url: cG9zdGdyZXNxbDovL3VzZXI6cGFzc0Bsb2NhbGhvc3Q6NTQzMi9hcHBkYg==
Apply it:
kubectl apply -f db-secret.yaml
Step 2: Deploy a Python app that reads the secret file
Create a small Flask or plain Python script that reads the file at startup. Save this as app.py:
# app.py
import os
import sys
def load_secret_from_file(path):
"""Read and return the secret from a mounted file."""
try:
with open(path, 'r') as f:
return f.read().strip()
except FileNotFoundError:
print(f"FATAL: Secret file {path} not found", file=sys.stderr)
raise
def main():
db_url = load_secret_from_file('/etc/secrets/database-url')
print("Database URL loaded successfully (not shown for security)")
print(f"URL starts with: {db_url[:10]}...")
if __name__ == "__main__":
main()
Now create the Deployment YAML that mounts the secret:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: secret-reader-deployment
spec:
replicas: 1
selector:
matchLabels:
app: secret-reader
template:
metadata:
labels:
app: secret-reader
spec:
containers:
- name: secret-reader
image: python:3.10-slim
command: ["python", "-c", "exec(open('app.py').read())"]
volumeMounts:
- name: secrets
mountPath: /etc/secrets
readOnly: true
volumes:
- name: secrets
secret:
secretName: db-secret
Apply the deployment:
kubectl apply -f deployment.yaml
Check the pod's logs:
kubectl logs deployment/secret-reader-deployment
Expected output:
Database URL loaded successfully (not shown for security)
URL starts with: postgresql:
Your Python process now reads the secret from a file — no environment variables involved.
Step 3: Verify what's in the file system
You can exec into the pod to see the file:
kubectl exec -it deployment/secret-reader-deployment -- ls -l /etc/secrets
You'll see something like:
-r--r--r-- 1 root root 45 Jun 1 12:34 database-url
Then try cat /etc/secrets/database-url to see the actual value (for debugging only — never do this in production logs).
Alternatively, use the awk command to read the file with output redirection to prove it's not in the environment:
kubectl exec -it deployment/secret-reader-deployment -- sh -c 'env' | grep database
The grep finds nothing — the secret isn't in env.
Compare Options / When to Choose What
Environment variables vs. mounted files — here's a head-to-head:
| Feature | Environment Variables | Mounted Files |
|---|---|---|
Visibility in kubectl exec |
Yes, via env or /proc/1/environ |
No, unless you explicitly cat the file |
| Exposure in logs | High risk (if code or crash dumps log env) | Lower risk (file content not automatically logged) |
| Readability by Python libraries | Direct access via os.environ |
Requires open/read, but many libraries accept _FILE variables or paths |
| Update behavior | Changing the Secret requires pod restart | Kubernetes kubelet periodically syncs file changes (unless using subPath) |
| Permission control | None — every process in the container sees env | File permissions can be set via defaultMode |
| Complexity to set up | Low | Moderate (add volume + mount) |
| Use case | Simple, one-off env vars; non-sensitive config | Sensitive credentials: DB passwords, API tokens, TLS private keys |
When to choose files:
- Whenever the secret is sensitive (password, token, key).
- When your app or library expects a file path (e.g., GOOGLE_APPLICATION_CREDENTIALS pointing to a JSON file).
- When you need to update secrets without restarting the pod (though note: with subPath the file won't update, so prefer full volume mount for live updates).
When env vars are fine: - Non-secret configuration (e.g., feature flags that can be public). - very small strings that won't cause operational risk if accidentally logged.
Variation: subPath mounting — You can mount a single file instead of a whole directory using subPath. This is handy when you have a config directory with many files and you only want one secret file. But be warned: subPath changes the update behavior — the file will NOT be updated automatically when the Secret changes; you'd need to restart the pod.
Another variation: using the secret volume in an init container. You can clone the secrets to a shared emptyDir volume, then mount that into your app container. This lets you transform the secret (e.g., decode JSON) before the main app starts.
Troubleshooting & Edge Cases
1. Secret "db-secret" not found — The secret doesn't exist in the namespace, or it's in a different namespace. Check with kubectl get secrets -n your-namespace and ensure your pod is in the same namespace.
2. The file permission is too restrictive — By default, files get mode 0444, which is world-readable. For sensitive keys, you might want 0400 (owner-only read). Use defaultMode in the volume spec.
volumes:
- name: secrets
secret:
secretName: db-secret
defaultMode: 0400 # owner read only
3. The secret value has a trailing newline — When you create a secret with echo -n, you avoid the newline. But if you use stringData with a newline, the file content will have a trailing \n. Your Python read() will include it, so .strip() is your friend.
4. The file doesn't update when the Secret changes — With a full volume mount (no subPath), the kubelet periodically syncs changes (usually within a minute). But if you used subPath, the file is statically bound and won't update. For automatic updates, avoid subPath for secrets.
5. Permission denied when reading the file — If your container runs as non-root (best practice), ensure the file is readable. The default 0444 is readable by all users, but if you set defaultMode to 0400, your non-root user can't read it. Check your security context and adjust defaultMode accordingly.
6. kubectl exec complains about the secret file being empty — Check the data field encoding. Secrets are base64-encoded. If you accidentally put plain text in data, the file will contain garbage (because Kubernetes decodes the base64). Use stringData in YAML to avoid manual encoding.
apiVersion: v1
kind: Secret
metadata:
name: db-secret
stringData:
database-url: postgresql://user:pass@localhost:5432/appdb
7. Your Python app crashes with 'invalid file descriptor' — This is typical if you try to read a file after the pod restarts and the file is gone. Always wrap file reads in try/except with a clear error. Consider using an entrypoint script that waits for the file to appear if your init order is uncertain.
8. The pod fails with The volume "secrets" is invalid — Double-check that your volume name matches between volumes and volumeMounts. A typo in name will trigger this.
What You Learned & What's Next
You've mastered a core security best practice: mounting secrets as files in your Python containers. You learned why environment variables are too risky for sensitive data, how Kubernetes turns a Secret into a read-only file inside your pod, and how to read that file from Python with a simple open() call. You also compared this approach with env vars, learned about subPath and permission modes, and debugged common issues like missing secrets and stale files.
You now know how to protect database credentials, API keys, and tokens in your Python services. This is a fundamental skill for any production Kubernetes deployment.
Next up in the track: The next lesson dives into Persistent Volumes and Persistent Volume Claims. You'll learn how to give your Python apps durable storage that survives pod restarts — critical for stateful services like databases, file uploads, or any app that needs to retain data across failures. Check out the next lesson in the Kubernetes for Python Developers series.
Happy Kubernetes-ing, and keep those secrets safe!
Practice recap
Create a Secret with two keys (e.g., username and password) and mount it as a volume. Write a Python script that reads both files, prints the username, and checks that the password is never printed. Alternatively, try mounting a secret as a single file with subPath and observe the difference in update behavior when you edit the Secret.
Common mistakes
- Using
datainstead ofstringDatain a Secret and forgetting to base64-encode the value — the mounted file will contain binary garbage or an error. - Mounting the whole secret volume but expecting live updates when using
subPath— the file won't refresh until pod restart. - Not calling
.strip()on the file content, leaving a trailing newline fromechoorstringData, which breaks connection string parsing. - Using a full directory mount when your app reads a single config file, making unintended secret files visible to the app.
- Setting
defaultModeto0400while running the container as non-root (e.g., UID 1001) leading to permission denied errors.
Variations
- Mount a single secret key as a file using
subPath, which is handy when you have a config directory with multiple files but you only want one secret file. - Use an init container to copy and transform the secret (e.g., decode JSON) into an emptyDir volume before your main Python app starts.
- Use a projected volume to combine multiple secrets and config maps into one directory — cleaner than multiple volumes.
Real-world use cases
- A Django app reads its
DATABASE_PASSWORDfrom/etc/secrets/db-passwordto connect to a managed PostgreSQL instance in production. - A microservice authenticates to a third-party REST API by loading an
API_KEYfrom a mounted secret file at startup, avoiding env-var leakage. - A Python cron job reads a TLS client certificate from
/etc/secrets/tls.crtand key to make mTLS calls to an internal service.
Key takeaways
- Mounted secret files keep sensitive values out of the process environment, reducing leak risk via
kubectl execor logs. - Kubernetes turns each secret key into a file in the mounted directory; Python reads it with plain
open(). - The volume is read-only by default (
0444), and you can tighten permissions withdefaultMode. - Use
stringDatain YAML to avoid manual base64 encoding mistakes. - Full volume mounts auto-update when the Secret changes;
subPathmounts do not — pick based on your update needs. - Always handle missing files gracefully in Python to avoid crashes during pod startup or rotation.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.