# SECURE: Service account lifecycle management
apiVersion: v1
kind: ConfigMap
metadata:
name: sa-lifecycle-scripts
namespace: platform-tools
data:
create-service-account.sh: |
#!/bin/bash
# Service account creation with validation
set -euo pipefail
APP_NAME="$1"
NAMESPACE="$2"
PERMISSIONS_FILE="$3"
# Validate inputs
if [[ ! $APP_NAME =~ ^[a-z0-9-]+$ ]]; then
echo "Error: APP_NAME must contain only lowercase letters, numbers, and hyphens"
exit 1
fi
if [[ ! -f "$PERMISSIONS_FILE" ]]; then
echo "Error: Permissions file not found: $PERMISSIONS_FILE"
exit 1
fi
SA_NAME="${APP_NAME}-sa"
echo "Creating service account: $SA_NAME in namespace: $NAMESPACE"
# Create service account with metadata
kubectl apply -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: $SA_NAME
namespace: $NAMESPACE
labels:
app: $APP_NAME
managed-by: platform-team
security-review-required: "true"
annotations:
platform.company.com/created-by: "$(whoami)"
platform.company.com/created-date: "$(date -Iseconds)"
platform.company.com/permissions-file: "$PERMISSIONS_FILE"
platform.company.com/review-date: "$(date -d '+90 days' -Iseconds)"
automountServiceAccountToken: false
EOF
# Apply permissions from file
echo "Applying permissions from: $PERMISSIONS_FILE"
envsubst < "$PERMISSIONS_FILE" | kubectl apply -f -
# Log creation for audit
echo "Service account created successfully: $SA_NAME" |
logger -t "k8s-sa-lifecycle" -p local0.info
echo "Created service account: $SA_NAME"
echo "Remember to:"
echo "1. Update your deployment to use serviceAccountName: $SA_NAME"
echo "2. Set automountServiceAccountToken: true if API access is needed"
echo "3. Review permissions in 90 days"
audit-service-accounts.sh: |
#!/bin/bash
# Comprehensive service account audit
set -euo pipefail
AUDIT_DATE=$(date -Iseconds)
AUDIT_FILE="/tmp/sa-audit-${AUDIT_DATE}.json"
echo "Starting service account audit: $AUDIT_DATE"
# Collect service account data
kubectl get serviceaccounts --all-namespaces -o json > "$AUDIT_FILE"
# Analyze service accounts
jq -r '.items[] |
select(.metadata.name != "default") |
{
namespace: .metadata.namespace,
name: .metadata.name,
automount: .automountServiceAccountToken,
age: .metadata.creationTimestamp,
labels: .metadata.labels,
annotations: .metadata.annotations
}' "$AUDIT_FILE" |
while IFS= read -r sa_data; do
namespace=$(echo "$sa_data" | jq -r '.namespace')
name=$(echo "$sa_data" | jq -r '.name')
echo "Analyzing: $namespace/$name"
# Check for role bindings
role_bindings=$(kubectl get rolebindings,clusterrolebindings --all-namespaces -o json |
jq -r --arg ns "$namespace" --arg name "$name"
'.items[] |
select(.subjects[]? | .kind=="ServiceAccount" and .name==$name and (.namespace // $ns)==$ns) |
{binding: .metadata.name, role: .roleRef.name, kind: .roleRef.kind}')
# Check for pods using this SA
pod_count=$(kubectl get pods --all-namespaces -o json |
jq -r --arg ns "$namespace" --arg name "$name"
'[.items[] | select(.spec.serviceAccountName == $name and .metadata.namespace == $ns)] | length')
# Generate security score
security_score=100
warnings=0
# Check automount setting
automount=$(echo "$sa_data" | jq -r '.automount // true')
if [[ "$automount" == "true" ]]; then
if [[ "$pod_count" -eq 0 ]]; then
echo " WARNING: automountServiceAccountToken=true but no pods use this SA"
security_score=$((security_score - 20))
warnings=$((warnings + 1))
fi
fi
# Check for cluster-admin access
if echo "$role_bindings" | jq -r '.role' | grep -q "cluster-admin"; then
echo " CRITICAL: Service account has cluster-admin access"
security_score=$((security_score - 50))
warnings=$((warnings + 1))
fi
# Check age and review date
created_date=$(echo "$sa_data" | jq -r '.age')
review_date=$(echo "$sa_data" | jq -r '.annotations["platform.company.com/review-date"] // empty')
if [[ -n "$review_date" && "$review_date" < "$AUDIT_DATE" ]]; then
echo " WARNING: Service account needs security review (due: $review_date)"
security_score=$((security_score - 10))
warnings=$((warnings + 1))
fi
# Output audit result
echo " Security Score: $security_score/100 (Warnings: $warnings)"
echo " Pods using SA: $pod_count"
echo " RBAC bindings: $(echo "$role_bindings" | jq -s 'length')"
echo
done
echo "Audit completed: $AUDIT_DATE"
cleanup-unused-sas.sh: |
#!/bin/bash
# Cleanup unused service accounts
set -euo pipefail
DRY_RUN=${DRY_RUN:-true}
MAX_AGE_DAYS=${MAX_AGE_DAYS:-30}
echo "Service Account Cleanup (DRY_RUN=$DRY_RUN, MAX_AGE_DAYS=$MAX_AGE_DAYS)"
# Find unused service accounts
kubectl get serviceaccounts --all-namespaces -o json |
jq -r '.items[] |
select(.metadata.name != "default") |
{namespace: .metadata.namespace, name: .metadata.name, created: .metadata.creationTimestamp}' |
while IFS= read -r sa_data; do
namespace=$(echo "$sa_data" | jq -r '.namespace')
name=$(echo "$sa_data" | jq -r '.name')
created=$(echo "$sa_data" | jq -r '.created')
# Check if any pods use this service account
pod_count=$(kubectl get pods -n "$namespace" -o json |
jq -r --arg name "$name"
'[.items[] | select(.spec.serviceAccountName == $name)] | length')
if [[ "$pod_count" -eq 0 ]]; then
# Check age
created_timestamp=$(date -d "$created" +%s)
current_timestamp=$(date +%s)
age_days=$(( (current_timestamp - created_timestamp) / 86400 ))
if [[ "$age_days" -gt "$MAX_AGE_DAYS" ]]; then
echo "Unused service account: $namespace/$name (age: $age_days days)"
if [[ "$DRY_RUN" == "false" ]]; then
# Remove role bindings first
kubectl get rolebindings,clusterrolebindings --all-namespaces -o json |
jq -r --arg ns "$namespace" --arg name "$name"
'.items[] |
select(.subjects[]? | .kind=="ServiceAccount" and .name==$name and (.namespace // $ns)==$ns) |
.metadata.namespace + " " + .metadata.name + " " + .kind' |
while read -r bind_ns bind_name bind_kind; do
echo " Removing $bind_kind: $bind_ns/$bind_name"
kubectl delete "$bind_kind" "$bind_name" -n "$bind_ns" || true
done
# Remove service account
echo " Deleting service account: $namespace/$name"
kubectl delete serviceaccount "$name" -n "$namespace"
fi
fi
fi
done
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: sa-lifecycle-manager
namespace: platform-tools
automountServiceAccountToken: true
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: sa-lifecycle-manager-role
rules:
- apiGroups: [""]
resources: ["serviceaccounts", "pods"]
verbs: ["get", "list", "create", "update", "patch", "delete"]
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["roles", "rolebindings", "clusterroles", "clusterrolebindings"]
verbs: ["get", "list", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: sa-lifecycle-manager-binding
subjects:
- kind: ServiceAccount
name: sa-lifecycle-manager
namespace: platform-tools
roleRef:
kind: ClusterRole
name: sa-lifecycle-manager-role
apiGroup: rbac.authorization.k8s.io
---
# Monitoring and alerting for service account security
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: service-account-security
namespace: monitoring
spec:
selector:
matchLabels:
app: sa-security-exporter
endpoints:
- port: metrics
interval: 30s
path: /metrics
---
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: service-account-alerts
namespace: monitoring
spec:
groups:
- name: service-account.rules
rules:
- alert: DefaultServiceAccountInUse
expr: kube_pod_spec_service_account{service_account="default"} > 0
for: 5m
labels:
severity: warning
annotations:
summary: "Pod using default service account"
description: "Pod {{ $labels.pod }} in namespace {{ $labels.namespace }} is using the default service account"
- alert: OverprivilegedServiceAccount
expr: |
(
sum by (service_account, namespace) (
kube_rolebinding_info{role_kind="ClusterRole", role_name="cluster-admin"}
) > 0
) and on (service_account, namespace) (
kube_pod_spec_service_account > 0
)
for: 10m
labels:
severity: critical
annotations:
summary: "Service account with cluster-admin privileges in use"
description: "Service account {{ $labels.service_account }} in namespace {{ $labels.namespace }} has cluster-admin privileges and is being used by pods"
- alert: UnusedServiceAccountWithPermissions
expr: |
(
sum by (service_account, namespace) (kube_rolebinding_info) > 0
) unless on (service_account, namespace) (
kube_pod_spec_service_account > 0
)
for: 1h
labels:
severity: info
annotations:
summary: "Unused service account with RBAC permissions"
description: "Service account {{ $labels.service_account }} in namespace {{ $labels.namespace }} has RBAC permissions but is not used by any pods"