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

Sponsored

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:

  1. ServiceAccount — the identity itself (like a user account for a machine).
  2. Role / ClusterRole — defines what actions are allowed (read pods, list secrets, etc.).
  3. RoleBinding / ClusterRoleBinding — connects a service account to a role, granting those permissions.
  4. 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-viewer to test permissions.
  • Token file not found: Ensure your pod is using the intended service account. Double-check spec.serviceAccountName and that the service account exists.
  • Token expired or invalid: If using projected tokens, check the audience matches what your client expects. Kubernetes client libraries often need the audience set correctly.
  • Multiple service accounts in one namespace: Be explicit — never rely on the default account implicitly. Always set serviceAccountName even 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: false accidentally 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

  1. Use projected service account tokens with a defined audience and expiration for enhanced security and automatic rotation.
  2. Use cloud-provider-specific Workload Identity Federation (e.g., GKE Workload Identity) to bind Kubernetes service accounts to IAM roles.
  3. Use kubectl create serviceaccount and kubectl create rolebinding for 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 list and delete permissions on pods.
  • A Python microservice that reads a specific ConfigMap to load feature flags, assigned a Role to get that ConfigMap only.
  • A Python operator that watches and updates custom resources, using a ClusterRole to get, list, watch, and update those 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 serviceAccountName in your deployment — never rely on the default service account.
  • Read the token from /var/run/secrets/kubernetes.io/serviceaccount/token in 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-i before guessing at code problems.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.