Skip to main content

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

Manual Steps
  1. 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, and oci_containerengine_cluster_kube_config resources to see who is granted access.
  2. 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-resources on the compartment/tenancy) versus specific OKE-related verbs.
    • Decide which groups should be read-only versus admin/maintainer for OKE.
  3. 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-contexts
      KUBECONFIG=$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.
  4. Evaluate Kubernetes RBAC for those identities

    • On any machine with kubectl access, gather RBAC assignments:
      kubectl get clusterrolebindings,rolebindings -A -o yaml > rbac-bindings.yaml
      kubectl get clusterroles,roles -A -o yaml > rbac-roles.yaml
    • In these files, look for subjects that match the identities used in kubeconfig (service accounts, users, groups).
    • For each subject, verify whether the bound Role/ClusterRole is:
      • Read-only (e.g. only get, list, watch) or
      • Broad/admin (e.g. * verbs, built‑in cluster-admin, edit, admin).
    • Flag any identities that don’t need full privileges but are bound to admin or overly broad roles.
  5. 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/read level verbs on OKE resources instead of manage all-resources).
    • In your IaC (Terraform or similar), update:
      • Any oci_identity_policy resources 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.
    • 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 (view or custom roles with only get, list, watch).
      • Remove or downgrade unnecessary cluster-admin/edit bindings.
  6. Re-verify effective access

    • From a sample read-only user’s environment, confirm access is limited:
      kubectl auth can-i get pods --all-namespaces
      kubectl auth can-i delete pods --all-namespaces
      kubectl 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.
    • Repeat for admin/maintainer roles to verify they still have the necessary permissions.
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.