OCI OKE Should Use Dedicated Service Accounts
More Info:
Each workload should run under a dedicated ServiceAccount with the minimum role bindings it needs, instead of sharing accounts across apps. This makes per-workload audit and revocation tractable.
Risk Level
Medium
Address
Compliance, Security
Compliance Standards
- CIS OKE
Triage and Remediation
- Remediation
Remediation
Using Console
In OKE this is a Kubernetes-level fix, but you can drive it from the OCI Console (using Cloud Shell or downloaded kubeconfig). Below are the steps using the OCI Console.
1. Open your OKE cluster from the OCI Console
- Sign in to OCI Console.
- In the left menu: Developer Services → Kubernetes Clusters (OKE).
- Select the Compartment that contains your cluster.
- Click your cluster name.
2. Get access to the cluster (from the Console)
You have two easy options:
Option A – Use OCI Cloud Shell (no local setup)
-
On the top-right of the console, click the Cloud Shell icon (a
>_terminal). -
In Cloud Shell, generate kubeconfig for this OKE cluster:
oci ce cluster create-kubeconfig \--cluster-id <your-cluster-ocid> \--file $HOME/.kube/config \--region <your-region> \--token-version 2.0.0 \--kube-endpoint PUBLIC_ENDPOINT -
Test access:
kubectl get nodes
Option B – Download kubeconfig for local use
-
In the OKE cluster details page, click Access Cluster.
-
Click Local Access and follow the instructions:
- Download the kubeconfig file.
- Set the
KUBECONFIGenvironment variable or merge into~/.kube/config.
-
Test with:
kubectl get nodes
3. Create a dedicated Service Account for each workload
Repeat per application / microservice.
-
Decide the namespace and SA name (for example: namespace
prod-apps, SAorders-sa). -
If the namespace doesn’t exist, create it:
kubectl create namespace prod-apps -
Create a ServiceAccount YAML file in Cloud Shell (or locally), e.g.
orders-sa.yaml:apiVersion: v1kind: ServiceAccountmetadata:name: orders-sanamespace: prod-apps -
Apply it:
kubectl apply -f orders-sa.yaml
4. Grant least-privilege RBAC to the Service Account
Create a Role/ClusterRole and RoleBinding/ClusterRoleBinding for each SA.
Example (namespace-scoped permissions):
-
Create
orders-role.yaml:apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata:namespace: prod-appsname: orders-rolerules:- apiGroups: [""]resources: ["pods", "services"]verbs: ["get", "list", "watch"] -
Create
orders-rolebinding.yaml:apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata:name: orders-rolebindingnamespace: prod-appssubjects:- kind: ServiceAccountname: orders-sanamespace: prod-appsroleRef:kind: Rolename: orders-roleapiGroup: rbac.authorization.k8s.io -
Apply them:
kubectl apply -f orders-role.yamlkubectl apply -f orders-rolebinding.yaml
5. Configure workloads to use the dedicated Service Account
For each Deployment/StatefulSet/Job, set serviceAccountName.
Example deployment manifest snippet:
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-deployment
namespace: prod-apps
spec:
replicas: 3
selector:
matchLabels:
app: orders
template:
metadata:
labels:
app: orders
spec:
serviceAccountName: orders-sa
containers:
- name: orders
image: <your-image>
...
Apply the updated deployment:
kubectl apply -f orders-deployment.yaml
Verify the pods use the correct SA:
kubectl get pods -n prod-apps -o custom-columns=NAME:.metadata.name,SA:.spec.serviceAccountName
6. Restrict or avoid the default Service Account
To enforce dedicated SAs:
-
Remove unnecessary permissions from the namespace’s default SA:
-
Inspect bindings:
kubectl get rolebinding,clusterrolebinding -A | grep default -
Remove any bindings that grant elevated rights to
defaultServiceAccounts.
-
-
Optionally, use an admission controller (e.g., OPA Gatekeeper or Kyverno) to deny pods that don’t specify
serviceAccountName.
This is configured inside the cluster, but can also be applied using manifests via Cloud Shell from the OCI Console.
If you share how your workloads are currently defined (Deployment YAML or Helm chart), I can give you the exact edits needed to switch them to dedicated Service Accounts.
Using CLI
Below are step‑by‑step remediation instructions to ensure OCI OKE workloads use dedicated Kubernetes service accounts (not the default one), starting from OCI CLI and then using kubectl.
1. Get kubeconfig for your OKE cluster via OCI CLI
-
Identify your OKE cluster OCID:
oci ce cluster list \--compartment-id <COMPARTMENT_OCID> \--query "data[].{name:name, id:id}" \--output table -
Generate/update kubeconfig for that cluster:
oci ce cluster create-kubeconfig \--cluster-id <CLUSTER_OCID> \--file $HOME/.kube/config \--region <OCI_REGION> \--token-version 2.0.0 \--kube-endpoint PUBLIC_ENDPOINT \--overwrite -
Test connectivity:
kubectl get nodes
2. Identify workloads using the default service account
Check each namespace (or target namespaces):
kubectl get pods --all-namespaces -o custom-columns=\
'NAMESPACE:.metadata.namespace,NAME:.metadata.name,SA:.spec.serviceAccountName'
Any pod with empty SA or default in the SA column is using the default service account.
3. Create a dedicated service account per application
Example for a namespace prod and app my-app:
kubectl create namespace prod # if not existing
kubectl create serviceaccount my-app-sa -n prod
Confirm:
kubectl get sa -n prod
4. (Optional but recommended) Bind minimal RBAC permissions
Create a minimal Role and RoleBinding for that service account.
-
Create a Role manifest (file:
my-app-role.yaml):apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata:name: my-app-rolenamespace: prodrules:- apiGroups: [""]resources: ["pods"]verbs: ["get", "list"] -
Apply the Role:
kubectl apply -f my-app-role.yaml -
Create a RoleBinding (file:
my-app-rolebinding.yaml):apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata:name: my-app-rolebindingnamespace: prodsubjects:- kind: ServiceAccountname: my-app-sanamespace: prodroleRef:kind: Rolename: my-app-roleapiGroup: rbac.authorization.k8s.io -
Apply the RoleBinding:
kubectl apply -f my-app-rolebinding.yaml
5. Update workloads to use the dedicated service account
Edit each Deployment/StatefulSet/Job/etc. so the pod spec uses your new service account.
Example patch for a Deployment:
kubectl patch deployment my-app-deployment \
-n prod \
--type merge \
-p '{"spec":{"template":{"spec":{"serviceAccountName":"my-app-sa"}}}}'
Or edit directly:
kubectl edit deployment my-app-deployment -n prod
Add under spec.template.spec:
serviceAccountName: my-app-sa
The Deployment rollout will recreate pods with the new service account.
6. Verify that pods are no longer using the default service account
kubectl get pods -n prod \
-o custom-columns=NAME:.metadata.name,SA:.spec.serviceAccountName
Ensure the relevant pods show my-app-sa (or another dedicated SA), not default.
7. (Optional) Enforce policy to prevent using default SA
If you use OPA Gatekeeper or Kyverno, deploy a policy that rejects workloads using default service accounts. Example (Gatekeeper ConstraintTemplate snippet-style):
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8sdisallowdefaultsa
spec:
crd:
spec:
names:
kind: K8sDisallowDefaultSA
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sdisallowdefaultsa
violation[{"msg": msg}] {
input.review.kind.kind == "Pod"
sa := input.review.object.spec.serviceAccountName
sa == "default" or sa == ""
msg := sprintf("Using default service account is not allowed: %v", [sa])
}
Apply the template and a corresponding Constraint to enforce it across namespaces.
Summary of OCI CLI involvement:
- Use OCI CLI to get kubeconfig for the OKE cluster.
- Perform the Kubernetes changes (
kubectl) to create dedicated service accounts, bind RBAC, and update workloads.
Using Python
To remediate “OCI OKE Should Use Dedicated Service Accounts” you need to:
- Stop using the
defaultservice account for Pods/Deployments. - Create dedicated service accounts per workload (or per trust boundary).
- Bind only the minimal RBAC permissions to each service account.
- Patch workloads to use those service accounts.
Below is a step‑by‑step guide and Python examples using the Kubernetes Python client.
1. Prerequisites
Install the Kubernetes Python client:
pip install kubernetes
Make sure your environment can authenticate to the OKE cluster, e.g. by:
kubectl config use-context <oke-cluster-context>
The Python client will reuse this kubeconfig.
2. Detect workloads using the default service account
from kubernetes import client, config
# Load kubeconfig (~/.kube/config)
config.load_kube_config()
apps_v1 = client.AppsV1Api()
core_v1 = client.CoreV1Api()
# List all namespaces
namespaces = [ns.metadata.name for ns in core_v1.list_namespace().items]
# Helper to check if pod template uses default SA
def uses_default_sa(pod_spec):
# If serviceAccountName is None, it will use 'default' in that namespace
return pod_spec.service_account_name in [None, "", "default"]
default_sa_workloads = []
for ns in namespaces:
# Deployments
deps = apps_v1.list_namespaced_deployment(namespace=ns).items
for d in deps:
if uses_default_sa(d.spec.template.spec):
default_sa_workloads.append(("Deployment", ns, d.metadata.name))
# StatefulSets
ssets = apps_v1.list_namespaced_stateful_set(namespace=ns).items
for s in ssets:
if uses_default_sa(s.spec.template.spec):
default_sa_workloads.append(("StatefulSet", ns, s.metadata.name))
# DaemonSets
dsets = apps_v1.list_namespaced_daemon_set(namespace=ns).items
for ds in dsets:
if uses_default_sa(ds.spec.template.spec):
default_sa_workloads.append(("DaemonSet", ns, ds.metadata.name))
print("Workloads using default ServiceAccount:")
for kind, ns, name in default_sa_workloads:
print(f"{kind} {ns}/{name}")
3. Create a dedicated service account (per workload or per app)
Example: one dedicated service account per deployment:
from kubernetes.client import V1ServiceAccount, V1ObjectMeta
def ensure_service_account(namespace, sa_name):
try:
core_v1.read_namespaced_service_account(name=sa_name, namespace=namespace)
print(f"ServiceAccount {namespace}/{sa_name} already exists")
except client.exceptions.ApiException as e:
if e.status == 404:
sa_body = V1ServiceAccount(
metadata=V1ObjectMeta(name=sa_name)
)
core_v1.create_namespaced_service_account(
namespace=namespace,
body=sa_body
)
print(f"Created ServiceAccount {namespace}/{sa_name}")
else:
raise
# Example: create one SA per workload found above
for kind, ns, name in default_sa_workloads:
sa_name = f"{name}-sa"
ensure_service_account(ns, sa_name)
4. Bind minimal RBAC permissions to the service account
Define Role and RoleBinding (adjust rules for what the workload actually needs):
from kubernetes.client import (
RbacAuthorizationV1Api,
V1Role, V1RoleRule,
V1RoleBinding, V1RoleRef, V1Subject
)
rbac_v1 = RbacAuthorizationV1Api()
def ensure_role(namespace, role_name, rules):
try:
rbac_v1.read_namespaced_role(role_name, namespace)
print(f"Role {namespace}/{role_name} already exists")
except client.exceptions.ApiException as e:
if e.status == 404:
role_body = V1Role(
metadata=V1ObjectMeta(name=role_name),
rules=rules
)
rbac_v1.create_namespaced_role(namespace, role_body)
print(f"Created Role {namespace}/{role_name}")
else:
raise
def ensure_role_binding(namespace, rb_name, role_name, sa_name):
try:
rbac_v1.read_namespaced_role_binding(rb_name, namespace)
print(f"RoleBinding {namespace}/{rb_name} already exists")
except client.exceptions.ApiException as e:
if e.status == 404:
rb_body = V1RoleBinding(
metadata=V1ObjectMeta(name=rb_name),
role_ref=V1RoleRef(
api_group="rbac.authorization.k8s.io",
kind="Role",
name=role_name
),
subjects=[
V1Subject(
kind="ServiceAccount",
name=sa_name,
namespace=namespace
)
]
)
rbac_v1.create_namespaced_role_binding(namespace, rb_body)
print(f"Created RoleBinding {namespace}/{rb_name}")
else:
raise
# Example minimal rule: allow reading ConfigMaps in same namespace (adjust as needed)
example_rules = [
V1RoleRule(
api_groups=[""],
resources=["configmaps"],
verbs=["get", "list"]
)
]
for kind, ns, name in default_sa_workloads:
sa_name = f"{name}-sa"
role_name = f"{name}-role"
rb_name = f"{name}-rb"
ensure_role(ns, role_name, example_rules)
ensure_role_binding(ns, rb_name, role_name, sa_name)
Customize example_rules per application’s real needs.
5. Patch workloads to use the dedicated service account
def patch_workload_service_account(kind, namespace, name, sa_name):
pod_spec_patch = {"spec": {"template": {"spec": {"serviceAccountName": sa_name}}}}
if kind == "Deployment":
apps_v1.patch_namespaced_deployment(
name=name,
namespace=namespace,
body=pod_spec_patch
)
elif kind == "StatefulSet":
apps_v1.patch_namespaced_stateful_set(
name=name,
namespace=namespace,
body=pod_spec_patch
)
elif kind == "DaemonSet":
apps_v1.patch_namespaced_daemon_set(
name=name,
namespace=namespace,
body=pod_spec_patch
)
else:
raise ValueError(f"Unsupported kind: {kind}")
print(f"Patched {kind} {namespace}/{name} to use ServiceAccount {sa_name}")
for kind, ns, name in default_sa_workloads:
sa_name = f"{name}-sa"
patch_workload_service_account(kind, ns, name, sa_name)
After patching, Kubernetes will roll Pods so they run with the new service account.
6. Validation
kubectl get pods -A -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,SA:.spec.serviceAccountName' | sort
Ensure that no application Pods are using the default service account (except where strictly intended, and ideally never in production namespaces).
This approach is generic for any OKE cluster, since OKE exposes a standard Kubernetes API; the Python code above can be run from any environment that has network access to the cluster and valid kubeconfig.
Using Terraform
This finding cannot be remediated on the oci_containerengine_cluster (OKE cluster) Terraform resource, because Kubernetes ServiceAccounts are workload-level objects inside the cluster, not an OCI cluster setting.
To fix it in Terraform you must manage the workloads themselves (via the Kubernetes provider or Helm), and for each workload define and reference its own ServiceAccount, for example:
# Provider pointing at your OKE cluster
provider "kubernetes" {
host = oci_containerengine_cluster.MY_CLUSTER.endpoints[0].kubernetes
cluster_ca_certificate = base64decode(oci_containerengine_cluster.MY_CLUSTER.metadata[0].cluster_ca)
token = data.oci_containerengine_cluster_kubeconfig.MY_CLUSTER_KUBECONFIG.token
}
resource "kubernetes_service_account" "APP_A_SA" {
metadata {
name = "app-a-sa"
namespace = "APP_A_NAMESPACE" # replace with the namespace for app A
}
}
resource "kubernetes_role" "APP_A_ROLE" {
metadata {
name = "app-a-role"
namespace = "APP_A_NAMESPACE"
}
rule {
api_groups = [""]
resources = ["configmaps"]
verbs = ["get", "list"] # minimum needed for this workload
}
}
resource "kubernetes_role_binding" "APP_A_RB" {
metadata {
name = "app-a-rb"
namespace = "APP_A_NAMESPACE"
}
role_ref {
api_group = "rbac.authorization.k8s.io"
kind = "Role"
name = kubernetes_role.APP_A_ROLE.metadata[0].name
}
subject {
kind = "ServiceAccount"
name = kubernetes_service_account.APP_A_SA.metadata[0].name
namespace = "APP_A_NAMESPACE"
}
}
resource "kubernetes_deployment" "APP_A_DEPLOYMENT" {
metadata {
name = "app-a"
namespace = "APP_A_NAMESPACE"
}
spec {
replicas = 2
selector {
match_labels = {
app = "app-a"
}
}
template {
metadata {
labels = {
app = "app-a"
}
}
spec {
service_account_name = kubernetes_service_account.APP_A_SA.metadata[0].name
container {
name = "app-a"
image = "APP_A_IMAGE" # replace with your image
}
}
}
}
}
Replace:
MY_CLUSTER/MY_CLUSTER_KUBECONFIGwith your actualoci_containerengine_clusterand kubeconfig data source.APP_A_NAMESPACEwith the namespace of each workload.APP_A_IMAGEwith the container image for the app.
No resource replacement of the OKE cluster is required; only workloads and their RBAC objects are created/updated.
Verification: terraform plan should show new kubernetes_service_account, kubernetes_role, kubernetes_role_binding, and updated workload specs (e.g., Deployment) using service_account_name per workload, with no changes to oci_containerengine_cluster.