Service Accounts for Python Apps
Run Python apps with service accounts securely — Kubernetes for Python Developers. This lesson covers creating and binding service accounts, using them in pods, and avoiding common security pitfalls.
Focus: run python apps with service accounts securely
Your Python service in Kubernetes is about to call the Kubernetes API, fetch a secret from Vault, or read files from a cloud storage bucket — and it needs credentials. The default token mounted into every pod is powerful, shared, and often over-privileged, creating a security hazard that keeps DevOps engineers up at night. This lesson shows you how to create and use dedicated service accounts for your Python apps, so you can run them with exactly the right permissions and nothing more.
The problem this lesson solves
When you deploy a Python app to Kubernetes, the kubelet automatically mounts a service account token into every pod at /var/run/secrets/kubernetes.io/serviceaccount/token. This token is the pod's identity inside the cluster. By default, pods run under the default service account, which may have broad or even cluster-admin permissions depending on your cluster's RBAC configuration.
Consider a typical microservice that only needs to read a specific ConfigMap. If it runs with the default service account, any compromise — a code injection, a leaked environment variable, a malicious dependency — can give an attacker access to far more than that one ConfigMap. They might list secrets, create pods, or worse. The principle of least privilege demands that each app gets its own identity with only the permissions it needs.
Beyond security, using dedicated service accounts simplifies auditing. When a pod acts under a unique identity, you can trace every action back to a specific application in your logs. With the shared default account, attribution becomes fuzzy and compliance reporting turns into a nightmare.
Core concept / mental model
Think of a service account as a badge for your pod. When your Python app wants to talk to the Kubernetes API or any external service that trusts Kubernetes tokens, it "shows" this badge. The badge includes two essential pieces:
- The token — a signed JWT that proves the pod's identity.
- The RBAC bindings — the permissions attached to that identity.
Here's a mental model for how it all fits together:
- ServiceAccount — the identity itself (like a user account for a machine).
- Role / ClusterRole — defines what actions are allowed (read pods, list secrets, etc.).
- RoleBinding / ClusterRoleBinding — connects a service account to a role, granting those permissions.
- Pod spec — tells Kubernetes which service account to use.
When your pod starts, Kubernetes mounts the token into the filesystem. Your Python app reads that token and uses it to authenticate API calls. If you've configured the right RBAC, the app can do its job and nothing else.
Token vs. kubeconfig
Most Python developers are familiar with kubeconfig files for local kubectl usage. Service account tokens are different: they're designed for in-cluster authentication, are automatically rotated (when using projected tokens), and are scoped to the pod's service account. You should never share a kubeconfig into a pod — that would expose cluster admin credentials.
How it works step by step
Let's walk through the entire lifecycle of running a Python app with a dedicated service account:
1. Create a ServiceAccount
First, define a service account in a YAML manifest or via kubectl create.
2. Define RBAC permissions
Create a Role (or ClusterRole) that specifies the exact API operations your app needs. Then bind that role to the service account with a RoleBinding (or ClusterRoleBinding).
3. Assign the service account to your pod
In your Deployment or Pod spec, set spec.serviceAccountName to the service account you created.
4. Use the token from your Python app
Inside the container, your Python code reads the mounted token file and uses it with the Kubernetes client library or requests to authenticate.
5. Verify permissions
Test by calling the API and observing what succeeds and what returns a 403 Forbidden.
Hands-on walkthrough
Let's put that into practice. We'll create a minimal Python app that lists pods in its own namespace — a common task for automation tools.
Step 1: Create the ServiceAccount
Save the following as sa.yaml:
apiVersion: v1
kind: ServiceAccount
metadata:
name: pod-viewer
namespace: default
Apply it:
kubectl apply -f sa.yaml
Step 2: Create Role and RoleBinding
Save as rbac.yaml:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
namespace: default
name: read-pods
subjects:
- kind: ServiceAccount
name: pod-viewer
namespace: default
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
Apply it:
kubectl apply -f rbac.yaml
Step 3: Deploy a Python app that uses the service account
Create a file pod_listener.py:
from kubernetes import client, config
config.load_incluster_config()
v1 = client.CoreV1Api()
pods = v1.list_namespaced_pod(namespace="default")
print("Pods in default namespace:")
for pod in pods.items:
print(f" - {pod.metadata.name}")
Now create a Dockerfile and deployment. Here's the deployment manifest deploy.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: pod-listener
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: pod-listener
template:
metadata:
labels:
app: pod-listener
spec:
serviceAccountName: pod-viewer
containers:
- name: pod-listener
image: python:3.11-slim
command: ["/bin/sh", "-c"]
args:
- pip install kubernetes && python pod_listener.py
volumeMounts:
- name: code
mountPath: /app
volumes:
- name: code
configMap:
name: pod-listener-code
You'd also create a ConfigMap with the Python script. For brevity, assume the script is mounted at /app. Apply the deployment:
kubectl apply -f deploy.yaml
Step 4: Verify it works
Get the pod name and check logs:
kubectl get pods
kubectl logs <pod-name>
Expected output:
Pods in default namespace:
- pod-listener-xxxxx
Now try to list secrets with the same app — it should fail with a 403, proving least privilege works.
Using the token with requests
If you're not using the official client, you can read the token and call the API directly:
import os
import requests
with open("/var/run/secrets/kubernetes.io/serviceaccount/token") as f:
token = f.read()
namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read()
resp = requests.get(
f"https://kubernetes.default.svc/api/v1/namespaces/{namespace}/pods",
headers={"Authorization": f"Bearer {token}"},
verify="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt",
timeout=5,
)
print(resp.status_code)
This approach works with any HTTP client, not just the Kubernetes library.
Compare options / when to choose what
When running Python apps with service accounts, you have a few authentication choices:
| Method | Use case | Pros | Cons |
|---|---|---|---|
| Static service account token | Simple apps, no rotation needed | Easy to set up, works everywhere | Long-lived, harder to rotate manually |
| Projected token (audience, expiration) | Production apps needing short-lived identities | Auto-rotation, scoped audience | Slightly more config |
| Workload Identity Federation (GKE/EKS/AKS) | Hybrid cloud scenarios | Feels like IAM, no K8s secret exposure | Vendor-specific |
| Service account as secret (legacy) | Rarely recommended | Backward compatibility | Security risk, static secret |
Choose a dedicated service account over the default account in almost every scenario. Choose projected tokens for production Python apps that make frequent API calls and need automatic rotation. Choose workload identity when your cloud provider offers it and you want to avoid storing tokens in the cluster entirely.
Troubleshooting & edge cases
Even with the right setup, things can go wrong. Here are the most common failures and fixes:
- 403 Forbidden when calling the API: Almost always an RBAC issue. Verify your RoleBinding exists and matches the namespace. Use
kubectl auth can-i list pods --as=system:serviceaccount:default:pod-viewerto test permissions. - Token file not found: Ensure your pod is using the intended service account. Double-check
spec.serviceAccountNameand that the service account exists. - Token expired or invalid: If using projected tokens, check the
audiencematches what your client expects. Kubernetes client libraries often need theaudienceset correctly. - Multiple service accounts in one namespace: Be explicit — never rely on the
defaultaccount implicitly. Always setserviceAccountNameeven if you think the namespace has only one. - Error: service account not found: Timing issue — you applied the deployment before the service account was created. Apply the service account first, then the deployment.
What you learned & what's next
You now understand how to run Python apps with service accounts securely. You learned to create a dedicated service account, bind it with RBAC roles, assign it to pods, and authenticate from Python code. Most importantly, you applied the principle of least privilege, drastically reducing your app's attack surface.
This is a critical security foundation. Next, you'll explore Pod Security Standards (or another security topic in your track) to further harden your Python deployments. With service accounts under your belt, you're ready to tackle more advanced security constraints.
Practice recap
As a hands-on exercise, extend the pod-listener example to also delete a pod. Create a new service account with delete permission, update the deployment, and verify that listing works but deleting without permission returns a 403. Then add the delete verb and watch it succeed. This solidifies your understanding of RBAC and service account binding.
Common mistakes
- Using the default service account for all pods out of convenience, which grants broad permissions and makes auditing impossible.
- Creating a service account but forgetting to add the RBAC RoleBinding, resulting in confusing 403 errors when the app starts.
- Setting
automountServiceAccountToken: falseaccidentally in a pod that needs API access, causing token file not found errors. - Mounting the service account token into a sidecar container that doesn't need it, increasing the attack surface unnecessarily.
Variations
- Use projected service account tokens with a defined audience and expiration for enhanced security and automatic rotation.
- Use cloud-provider-specific Workload Identity Federation (e.g., GKE Workload Identity) to bind Kubernetes service accounts to IAM roles.
- Use
kubectl create serviceaccountandkubectl create rolebindingfor quick CLI-only setups without YAML manifests.
Real-world use cases
- A Python cron job that cleans up completed pods in a namespace, needing only
listanddeletepermissions on pods. - A Python microservice that reads a specific ConfigMap to load feature flags, assigned a Role to
getthat ConfigMap only. - A Python operator that watches and updates custom resources, using a ClusterRole to
get,list,watch, andupdatethose resources cluster-wide.
Key takeaways
- Service accounts give each Python pod a unique identity, enabling least-privilege RBAC and clean audit trails.
- Create a Role/ClusterRole with only the required verbs and resources, then bind it to your service account with a RoleBinding/ClusterRoleBinding.
- Explicitly set
serviceAccountNamein your deployment — never rely on thedefaultservice account. - Read the token from
/var/run/secrets/kubernetes.io/serviceaccount/tokenin your Python code to authenticate API calls. - Prefer projected tokens with short expiry for production apps to reduce the risk of token leakage.
- Troubleshoot permission issues with
kubectl auth can-ibefore guessing at code problems.
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.