Minimize Cluster Access To Read Only
More Info:
Configure the Cluster Service Account with read-only access to the container registry.
Risk Level
Medium
Address
Security
Compliance Standards
- APRA CPS 234 (Australia)
- BSI C5 (Germany)
- Brazil LGPD
- CCPA / CPRA (California)
- CIS Critical Security Controls v8
- CIS OKE
- CMMC 2.0
- CSA Cloud Controls Matrix v4
- DPDPA
- Digital Operational Resilience Act (EU)
- Essential 8
- ISO/IEC 27017
- ISO/IEC 27018
- ISO/IEC 27701
- KSA PDPL
- MAS Technology Risk Management (Singapore)
- MITRE ATT&CK (Cloud)
- NIS2 Directive
- NIST SP 800-171
- NYDFS 23 NYCRR 500
- SWIFT Customer Security Controls Framework
- Sarbanes-Oxley IT General Controls
- UK NCSC Cyber Assessment Framework
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
Identify all principals that can pull images from your container registry
- In OCI Console: go to Developer Services → Container Registry (OCIR) → Repositories → [your region/compartment], and list all repos used by the cluster.
- For each repository, note its OCID, compartment, and any visibility setting (private/public).
-
Review IAM policies granting image-pull (read) access
- In OCI Console: Identity & Security → Policies, filter by the compartments where your registries and clusters reside.
- Look for statements like:
allow dynamic-group <name> to read repos in compartment <name>allow group <name> to read repos in compartment <name>
- On any machine with OCI CLI configured, you can list policies for a compartment (replace with your actual OCID):
oci iam policy list --compartment-id ocid1.compartment.oc1..exampleuniqueID --all
- Save the output and identify which groups/dynamic groups map to your Kubernetes nodes or cluster workloads.
-
Verify cluster/node identities mapped to registry access
- In OCI Console: Identity & Security → Dynamic Groups, open each dynamic group used by your cluster nodes or OKE worker instances.
- Check the matching rules (e.g. instance OCID, tags) and confirm these dynamic groups are the ones referenced in policies from step 2.
- Ensure dynamic groups used for registry read access represent only the intended cluster/node instances and not a broader set of resources.
-
Assess least-privilege vs. “read-only” requirements
- For each policy that grants
read repos/read artifactsfor registry compartments, decide if it can be narrowed:- Restrict to specific compartments instead of
tenancy. - Restrict to specific repos or namespaces where possible.
- Ensure the dynamic group only includes cluster nodes that must pull images, not all instances.
- Restrict to specific compartments instead of
- Document which policies are overly broad (too many groups/dynamic groups, too wide compartment scope).
- For each policy that grants
-
Adjust IAM policies to enforce read-only, minimal scope
- In OCI Console, edit or create policies so that:
- Only the dynamic group for OKE worker nodes (and any required CI/CD principal) has
readaccess to the registry compartments used by the cluster. - Avoid any
manage reposormanage artifactspermissions for the cluster principals if they only need to pull images.
- Only the dynamic group for OKE worker nodes (and any required CI/CD principal) has
- Example of a more restrictive policy (adapt in console/IaC, not as a shell command):
allow dynamic-group oke-workers to read repos in compartment k8s-images-compartment
- In OCI Console, edit or create policies so that:
-
Verify effective access after changes
- From a worker node (or an instance in the dynamic group) with
dockerorcrictlconfigured, try pulling an image that should be allowed and one that should be denied, and confirm behavior matches your policy intent. - From an identity that should not have registry access (e.g., a test instance not in the dynamic group), attempt an image pull and confirm it fails with an authorization error.
- Optionally re-run your security/benchmark tooling to confirm the finding is resolved or appropriately justified.
- From a worker node (or an instance in the dynamic group) with
Using kubectl
kubectl cannot be used to configure the cluster’s read‑only access to the container registry, because this control is managed in the Oracle Cloud Infrastructure / managed control plane configuration, not via Kubernetes API objects. Please follow the guidance in the Manual Steps section to review and adjust the cluster’s registry access in the cloud provider console, CLI, or IaC.
Automation
#!/usr/bin/env bash
#
# Audit script for CIS OKE 5.1.3 – Minimize Cluster Access To Read Only
#
# Runs with: any machine with kubectl access to the cluster context you want to audit.
# Requires: kubectl, jq
set -euo pipefail
# Helper: check for required tools
for bin in kubectl jq; do
if ! command -v "$bin" >/dev/null 2>&1; then
echo "ERROR: $bin not found in PATH" >&2
exit 1
fi
done
echo "=== 1. Report cluster-scoped RBAC that can affect registry access ==="
echo
echo "# ClusterRoles that might relate to registry/image access (broad permissions):"
kubectl get clusterroles -o json \
| jq -r '
.items[]
| select(
.rules != null
and (
any(.rules[]?.resources[]?; test("secrets|pods|deployments|replicasets|daemonsets|statefulsets|jobs|cronjobs|configmaps"))
or any(.rules[]?.verbs[]?; . == "*" or . == "create" or . == "update" or . == "delete" or . == "patch")
)
)
| [
.metadata.name,
( .rules[]
| {resources, verbs}
)
]' \
| sed 's/^/ /'
echo
echo "# ClusterRoleBindings showing who gets those ClusterRoles:"
kubectl get clusterrolebindings -o json \
| jq -r '
.items[]
| {
name: .metadata.name,
role: .roleRef,
subjects: .subjects
}' \
| sed 's/^/ /'
echo
echo "=== 2. Identify service accounts that can pull private images (imagePullSecrets) ==="
echo
echo "# Namespaces and service accounts that use imagePullSecrets:"
kubectl get sa --all-namespaces -o json \
| jq -r '
.items[]
| select(.imagePullSecrets != null)
| [
.metadata.namespace,
.metadata.name,
( .imagePullSecrets[]?.name // "" )
]
| @tsv' \
| awk 'BEGIN { printf "NAMESPACE\tSERVICEACCOUNT\tIMAGEPULLSECRET\n" } { print }'
echo
echo "=== 3. Show details of imagePullSecrets (type and registry URL if dockerconfigjson) ==="
echo
echo "# For each imagePullSecret, show secret type and registry endpoints (if detectable):"
kubectl get sa --all-namespaces -o json \
| jq -r '
.items[]
| select(.imagePullSecrets != null)
| {ns: .metadata.namespace, sa: .metadata.name, ips: .imagePullSecrets[].name}
| @tsv' \
| while IFS=$'\t' read -r NS SA SECRET; do
echo "Namespace: $NS ServiceAccount: $SA imagePullSecret: $SECRET"
# Get secret type
TYPE=$(kubectl get secret "$SECRET" -n "$NS" -o jsonpath='{.type}' 2>/dev/null || echo "NOT_FOUND")
echo " Secret type: $TYPE"
if [ "$TYPE" = "kubernetes.io/dockerconfigjson" ]; then
# Extract registry endpoints from .dockerconfigjson
DOCKERCFG=$(kubectl get secret "$SECRET" -n "$NS" -o jsonpath='{.data..dockerconfigjson}' 2>/dev/null || true)
if [ -n "$DOCKERCFG" ]; then
echo "$DOCKERCFG" | base64 -d 2>/dev/null \
| jq -r '
.auths
| keys[]
' 2>/dev/null \
| sed 's/^/ Registry endpoint: /'
fi
fi
echo
done
echo "=== 4. Detect non-read-only permissions commonly used by automation / build SAs ==="
echo
echo "# ServiceAccounts bound to roles/clusterroles that allow write-level verbs:"
echo
# Build a quick index of RoleBindings and ClusterRoleBindings by subject
echo "## RoleBindings with write permissions:"
kubectl get rolebindings --all-namespaces -o json \
| jq -r '
.items[]
| . as $rb
| (
.roleRef.kind as $kind
| .roleRef.name as $rname
| .metadata.namespace as $ns
| $rb.subjects[]?
| select(.kind == "ServiceAccount")
| {rb: $rb.metadata.name, ns: $ns, san: .name, sakns: (.namespace // $ns), roleKind: $kind, roleName: $rname}
)
| @tsv' \
| while IFS=$'\t' read -r RBNS RB_SA_NS SA_NS SA_NAME ROLE_KIND ROLE_NAME; do
NS="$RBNS"
RB_NAME="$SA_NS"
SA_NAMESPACE="$SA_NS"
SA="$SA_NAME"
RK="$ROLE_KIND"
RN="$ROLE_NAME"
# Fetch rules for this Role or ClusterRole
if [ "$RK" = "Role" ]; then
RULES_JSON=$(kubectl get role "$RN" -n "$NS" -o json 2>/dev/null || true)
else
RULES_JSON=$(kubectl get clusterrole "$RN" -o json 2>/dev/null || true)
fi
if [ -z "$RULES_JSON" ]; then
continue
fi
# Check for write verbs
HAS_WRITE=$(printf '%s\n' "$RULES_JSON" \
| jq -e '
.rules[]
| select(
any(.verbs[]?; . == "*" or . == "create" or . == "update" or . == "patch" or . == "delete" or . == "deletecollection")
)
' >/dev/null 2>&1 && echo "yes" || echo "no")
if [ "$HAS_WRITE" = "yes" ]; then
echo "Namespace: $NS RoleBinding: $RB_NAME"
echo " ServiceAccount: ${SA_NAMESPACE}/${SA}"
echo " Bound to: ${RK}/${RN}"
echo " NOTE: This ServiceAccount has write-level verbs. Review if it should only have read-only access to the registry and related resources."
echo
fi
done
echo "## ClusterRoleBindings with write permissions:"
kubectl get clusterrolebindings -o json \
| jq -r '
.items[]
| . as $crb
| (
.roleRef.kind as $kind
| .roleRef.name as $rname
| $crb.subjects[]?
| select(.kind == "ServiceAccount")
| {crb: $crb.metadata.name, san: .name, sakns: (.namespace // "default"), roleKind: $kind, roleName: $rname}
)
| @tsv' \
| while IFS=$'\t' read -r CRB_NAME SA_NS SA_NAME ROLE_KIND ROLE_NAME; do
SA_NAMESPACE="$SA_NS"
SA="$SA_NAME"
RK="$ROLE_KIND"
RN="$ROLE_NAME"
RULES_JSON=$(kubectl get clusterrole "$RN" -o json 2>/dev/null || true)
if [ -z "$RULES_JSON" ]; then
continue
fi
HAS_WRITE=$(printf '%s\n' "$RULES_JSON" \
| jq -e '
.rules[]
| select(
any(.verbs[]?; . == "*" or . == "create" or . == "update" or . == "patch" or . == "delete" or . == "deletecollection")
)
' >/dev/null 2>&1 && echo "yes" || echo "no")
if [ "$HAS_WRITE" = "yes" ]; then
echo "ClusterRoleBinding: $CRB_NAME"
echo " ServiceAccount: ${SA_NAMESPACE}/${SA}"
echo " Bound to: ${RK}/${RN}"
echo " NOTE: This ServiceAccount has write-level verbs. Review whether it should only require read-only access (e.g., for pulling images)."
echo
fi
done
echo "=== 5. How to interpret the results ==="
cat <<'EOF'
Interpretation / What indicates a problem:
1. imagePullSecrets:
- Any ServiceAccount using an imagePullSecret means those pods can pull from the referenced registry.
- Review if those ServiceAccounts are used for workloads that should have *only* read-only access to the registry.
- If secret type is kubernetes.io/dockerconfigjson, verify in your registry/provider console that:
* The underlying registry credentials are restricted to pull/read-only.
* They do not allow image push, delete, or repository admin.
2. Broad RBAC (ClusterRoles / Roles and bindings):
- In sections "RoleBindings with write permissions" and "ClusterRoleBindings with write permissions":
* Any ServiceAccount listed here has write-level verbs (create/update/patch/delete/deletecollection or *),
which almost always exceeds a strict "read-only registry access" requirement.
- Pay special attention to:
* ServiceAccounts used by CI/CD, build, or automation jobs that interact with your registries.
* ServiceAccounts in shared namespaces that many teams use.
3. Problem indicators:
- ServiceAccounts used *only* to pull images that:
* Are bound to Roles/ClusterRoles with write or wildcard (*) verbs.
* Use registry credentials that have push/delete/admin on the registry side.
- Cluster-wide "automation" or "default" ServiceAccounts bound to powerful ClusterRoles
but also used to pull images, rather than a minimal read-only pull SA.
Next steps are manual by design:
- Use the above output to:
* Identify ServiceAccounts that should be limited to read-only image pulls.
* In your cloud/registry provider console or IaC, ensure the credentials in imagePullSecrets
are restricted to read-only.
* Adjust Kubernetes RBAC (via IaC or controlled change process) so those ServiceAccounts
have least-privilege and do not hold cluster-wide write capabilities unrelated to image pulls.
EOF