Minimize Cluster Access To Read-Only
More Info:
Set up the kubeconfig file appropriately and restrict cluster access so users have only the minimum permissions needed, preferring read-only access where possible.
Risk Level
Medium
Address
Security
Compliance Standards
- CIS OKE
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
Identify how access is granted to the cluster
- On any machine with access to your tenancy, list all OKE clusters and note which IAM groups/users use them (via tags, naming, or documentation).
- In OCI Console: Developer Services → Kubernetes Clusters (OKE) → each cluster → Access Cluster, and note which IAM policies reference those compartments.
- In Terraform (if used), inspect all
oci_containerengine_cluster,oci_identity_policy, andoci_containerengine_cluster_kube_configresources to see who is granted access.
-
Review OCI IAM policies that permit OKE access
- In OCI Console: Identity & Security → Policies → search for statements with verbs related to OKE and Kubernetes access, for example
use cluster-family,manage cluster-family,use kubeconfig. - For each policy, record:
- The group(s)/dynamic group(s) it applies to.
- Whether it allows broad access (e.g.
manage all-resourceson the compartment/tenancy) versus specific OKE-related verbs.
- Decide which groups should be read-only versus admin/maintainer for OKE.
- In OCI Console: Identity & Security → Policies → search for statements with verbs related to OKE and Kubernetes access, for example
-
Inspect generated kubeconfig files and bound identities
- On any machine with kubectl access, list configured contexts and clusters:
KUBECONFIG=$HOME/.kube/config kubectl config get-contextsKUBECONFIG=$HOME/.kube/config kubectl config view --minify --raw
- For each user/context, determine whether the underlying OCI principal is a user or instance principal (from how the kubeconfig was created and from OCI CLI configuration in
~/.oci/config). - Map each user/context back to an OCI IAM group or dynamic group discovered in step 2.
- On any machine with kubectl access, list configured contexts and clusters:
-
Evaluate Kubernetes RBAC for those identities
- On any machine with kubectl access, gather RBAC assignments:
kubectl get clusterrolebindings,rolebindings -A -o yaml > rbac-bindings.yamlkubectl get clusterroles,roles -A -o yaml > rbac-roles.yaml
- In these files, look for
subjectsthat match the identities used in kubeconfig (service accounts, users, groups). - For each subject, verify whether the bound
Role/ClusterRoleis:- Read-only (e.g. only
get,list,watch) or - Broad/admin (e.g.
*verbs, built‑incluster-admin,edit,admin).
- Read-only (e.g. only
- Flag any identities that don’t need full privileges but are bound to admin or overly broad roles.
- On any machine with kubectl access, gather RBAC assignments:
-
Adjust access to minimize privileges (prefer read-only)
- In OCI IAM (console or IaC), for groups that should only have read-only cluster access, replace broad policy statements with least-privilege ones (for example,
inspect/readlevel verbs on OKE resources instead ofmanage all-resources). - In your IaC (Terraform or similar), update:
- Any
oci_identity_policyresources to narrow actions and scopes. - Any automation that generates kubeconfigs so that:
- Only appropriate groups/users can generate admin kubeconfigs.
- Others get kubeconfigs aligned with read-only RBAC where possible.
- Any
- In Kubernetes RBAC (via manifests in IaC), ensure:
- Operational/admin identities are bound to admin roles as required.
- Other identities are bound to custom or built-in read-only roles (
viewor custom roles with onlyget,list,watch). - Remove or downgrade unnecessary
cluster-admin/editbindings.
- In OCI IAM (console or IaC), for groups that should only have read-only cluster access, replace broad policy statements with least-privilege ones (for example,
-
Re-verify effective access
- From a sample read-only user’s environment, confirm access is limited:
kubectl auth can-i get pods --all-namespaceskubectl auth can-i delete pods --all-namespaceskubectl auth can-i '*''*' --all-namespaces
- Ensure:
- Read operations (
get,list,watch) required for that user are allowed. - Mutating operations (
create,update,patch,delete) and wildcard verbs/resources are denied for users that should be read-only.
- Read operations (
- Repeat for admin/maintainer roles to verify they still have the necessary permissions.
- From a sample read-only user’s environment, confirm access is limited:
Using kubectl
kubectl cannot be used to change how kubeconfig files are created or how Oracle Cloud Infrastructure access to the cluster is restricted; those settings are controlled in the OCI console / IAM / API key configuration and any associated IaC. To address this finding, follow the guidance in the Manual Steps section for hardening kubeconfig generation and IAM policies.
Automation
#!/usr/bin/env bash
#
# CIS OKE 5.1.3 – Minimize Cluster Access To Read‑Only
#
# This script ONLY reports on cluster‑side RBAC state so you can
# compare it against who should have read‑only vs broader access.
#
# Run from: any machine with kubectl access and appropriate privileges.
# Requirements: kubectl, jq
#
# Usage:
# ./cis_oke_5_1_3_rbac_report.sh > rbac-report.txt
set -euo pipefail
echo "===== CIS OKE 5.1.3 – RBAC READ-ONLY REVIEW REPORT ====="
echo "Cluster context: $(kubectl config current-context 2>/dev/null || echo 'N/A')"
echo
timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
echo "Generated at: $timestamp"
echo
# 1. List all users and groups that appear in RoleBindings / ClusterRoleBindings
echo "=== 1. Subjects in RoleBindings and ClusterRoleBindings ==="
echo
echo "--- ClusterRoleBindings (cluster‑wide subjects) ---"
kubectl get clusterrolebindings -o json | jq -r '
.items[]
| {
name: .metadata.name,
subjects: (.subjects // [])
}
| . as $crb
| $crb.subjects[]
| [ $crb.name, .kind, .name, (.namespace // "-"), (.apiGroup // "-") ]
| @tsv
' 2>/dev/null | sort -u | awk 'BEGIN { FS="\t"; OFS="\t";
print "CRB_NAME","SUBJECT_KIND","SUBJECT_NAME","SUBJECT_NAMESPACE","SUBJECT_APIGROUP"
} { print }'
echo
echo "--- RoleBindings (namespace‑scoped subjects) ---"
kubectl get rolebindings --all-namespaces -o json | jq -r '
.items[]
| {
name: .metadata.name,
namespace: .metadata.namespace,
subjects: (.subjects // [])
}
| . as $rb
| $rb.subjects[]
| [ $rb.namespace, $rb.name, .kind, .name, (.namespace // "-"), (.apiGroup // "-") ]
| @tsv
' 2>/dev/null | sort -u | awk 'BEGIN { FS="\t"; OFS="\t";
print "RB_NAMESPACE","RB_NAME","SUBJECT_KIND","SUBJECT_NAME","SUBJECT_NAMESPACE","SUBJECT_APIGROUP"
} { print }'
echo
echo ">>> Review: Identify human users / groups here that should be read‑only."
echo ">>> Any user or group listed below with powerful ClusterRoles/Roles (next sections)"
echo ">>> is a potential issue for this control."
echo
# 2. List ClusterRoles that are clearly more than read‑only
echo "=== 2. ClusterRoles with non‑read‑only permissions ==="
echo
kubectl get clusterroles -o json | jq -r '
.items[]
| {
name: .metadata.name,
rules: (.rules // [])
}
| select(
any(.rules[]?;
any(.verbs[]?;
. != "get" and . != "list" and . != "watch"
)
)
)
| . as $cr
| $cr.rules[]
| . as $r
| ($r.verbs[] | select(. != "get" and . != "list" and . != "watch")) as $v
| [
$cr.name,
($r.apiGroups | join(",")),
($r.resources | join(",")),
$v,
((.resourceNames // []) | join(","))
]
| @tsv
' 2>/dev/null | sort -u | awk 'BEGIN { FS="\t"; OFS="\t";
print "CLUSTERROLE","APIGROUPS","RESOURCES","NON_READONLY_VERB","RESOURCE_NAMES"
} { print }'
echo
echo ">>> Any ClusterRole shown here grants more than read‑only."
echo ">>> If bound to human users or groups from section 1, it is likely non‑compliant."
echo
# 3. List Roles that are more than read‑only (namespace‑scoped)
echo "=== 3. Roles with non‑read‑only permissions (namespace‑scoped) ==="
echo
kubectl get roles --all-namespaces -o json | jq -r '
.items[]
| {
name: .metadata.name,
namespace: .metadata.namespace,
rules: (.rules // [])
}
| select(
any(.rules[]?;
any(.verbs[]?;
. != "get" and . != "list" and . != "watch"
)
)
)
| . as $role
| $role.rules[]
| . as $r
| ($r.verbs[] | select(. != "get" and . != "list" and . != "watch")) as $v
| [
$role.namespace,
$role.name,
($r.apiGroups | join(",")),
($r.resources | join(",")),
$v,
((.resourceNames // []) | join(","))
]
| @tsv
' 2>/dev/null | sort -u | awk 'BEGIN { FS="\t"; OFS="\t";
print "NAMESPACE","ROLE","APIGROUPS","RESOURCES","NON_READONLY_VERB","RESOURCE_NAMES"
} { print }'
echo
echo ">>> Any Role shown here grants more than read‑only in its namespace."
echo
# 4. Map bindings to powerful ClusterRoles/Roles
echo "=== 4. Bindings that attach non‑read‑only roles to subjects ==="
echo
echo "--- ClusterRoleBindings with non‑read‑only ClusterRoles ---"
kubectl get clusterrolebindings -o json | jq -r '
# Build a lookup of ClusterRole -> has_non_readonly
.items as $crbs
| (input | .items) as $crs
' 2>/dev/null <<'EOF_CR_JSON' | sort -u | awk 'BEGIN { FS="\t"; OFS="\t";
print "CRB_NAME","CLUSTERROLE","SUBJECT_KIND","SUBJECT_NAME","SUBJECT_NAMESPACE","NON_READONLY"
} { print }'
{
"items": []
}
EOF_CR_JSON >/dev/null 2>&1 || true
# The above placeholder is a no-op to keep jq happy on some shells.
# Because joining two JSON streams with jq inline is cumbersome, do a simpler pass:
# 1) Get list of non‑read‑only ClusterRoles
# 2) Print ClusterRoleBindings that reference any of them
non_ro_crs="$(kubectl get clusterroles -o json | jq -r '
.items[]
| select(
any(.rules[]?;
any(.verbs[]?;
. != "get" and . != "list" and . != "watch"
)
)
)
| .metadata.name
')"
if [ -n "${non_ro_crs:-}" ]; then
echo "$non_ro_crs" | while read -r cr; do
[ -z "$cr" ] && continue
kubectl get clusterrolebindings -o json | jq -r --arg CR "$cr" '
.items[]
| select(.roleRef.kind=="ClusterRole" and .roleRef.name==$CR)
| . as $crb
| (.subjects // [])[]
| [ $crb.metadata.name, $CR, .kind, .name, (.namespace // "-"), "true" ]
| @tsv
' 2>/dev/null
done | sort -u | awk 'BEGIN { FS="\t"; OFS="\t";
print "CRB_NAME","CLUSTERROLE","SUBJECT_KIND","SUBJECT_NAME","SUBJECT_NAMESPACE","NON_READONLY"
} { print }'
else
echo "No ClusterRoles with non‑read‑only permissions found."
fi
echo
echo "--- RoleBindings with non‑read‑only Roles ---"
non_ro_roles="$(kubectl get roles --all-namespaces -o json | jq -r '
.items[]
| select(
any(.rules[]?;
any(.verbs[]?;
. != "get" and . != "list" and . != "watch"
)
)
)
| [.metadata.namespace, .metadata.name] | @tsv
')"
if [ -n "${non_ro_roles:-}" ]; then
echo "$non_ro_roles" | while IFS=$'\t' read -r ns role; do
[ -z "$ns" ] && continue
kubectl get rolebindings -n "$ns" -o json | jq -r --arg ROLE "$role" '
.items[]
| select(.roleRef.kind=="Role" and .roleRef.name==$ROLE)
| . as $rb
| (.subjects // [])[]
| [ $rb.metadata.namespace, $rb.metadata.name, $ROLE, .kind, .name, (.namespace // "-"), "true" ]
| @tsv
' 2>/dev/null
done | sort -u | awk 'BEGIN { FS="\t"; OFS="\t";
print "RB_NAMESPACE","RB_NAME","ROLE","SUBJECT_KIND","SUBJECT_NAME","SUBJECT_NAMESPACE","NON_READONLY"
} { print }'
else
echo "No Roles with non‑read‑only permissions found."
fi
echo
echo ">>> Any human user / group listed here has more than read‑only access."
echo ">>> Compare these subjects with your intended access model and kubeconfig recipients."
echo
echo "===== END OF REPORT ====="
How to interpret problems:
- In sections 2 and 3, any Role/ClusterRole listed has verbs beyond
get,list,watch. - In section 4, any human user or group (from your IdP) that appears is a candidate violation of “read‑only where possible”.
- Use this report together with your cloud/IaC configuration and kubeconfig distribution records to decide where access should be reduced or split into read‑only vs admin profiles.