diff --git a/scripts/audit-namespace-roles/README.md b/scripts/audit-namespace-roles/README.md new file mode 100644 index 000000000..eaae8bdef --- /dev/null +++ b/scripts/audit-namespace-roles/README.md @@ -0,0 +1,62 @@ +This script may be used to audit the namespace-scoped Roles/RoleBindings that are created by the GitOps operator's 'applications in any namespace/applicationsets in any namespace' features. +(The 'apps/applications in any namespace' features are not enabled by default. They are enabled via `ArgoCD` CR `.spec.sourceNamespaces` and `.spec.applicationSet.sourceNamespaces`.) + +This is a simple script that will look for Roles/RoleBindings across ALL namespaces that meet ALL of the following criteria: +- A) The Role allows access to `argoproj.io/Application` resource +- B) The Role has label `app.kubernetes.io/part-of: argocd` +- C) The RoleBinding references a service-account in another namespace (cross-namespace access) + +This criteria ensures that the Role/RoleBinding was likely created by GitOps operator, and that an Argo CD instance on the cluster has (or had) access to that namespace. + +## Procedure: +1) Ensure that `jq` and `oc` executables are installed and on path. +2) Ensure that you are logged into cluster via `oc` or `kubectl` CLI. +3) Execute `./audit-operator-roles.sh` +4) Examine the output list of Roles/RoleBindings. + +For each Role/RoleBinding that is listed: +- If a Role/RoleBinding is listed, that means another namespace on the cluster has access to the namespace containing the Role/RoleBinding +- Verify that it is correct for the namespace containing the Role/RoleBinding to be accessed by the namespace listed in subject field of the RoleBinding. + - For example, it is correct if you need an Argo CD instance (installed in the namespace listed in subject field of the RoleBinding) to deploy to the namespace containing the RoleBinding. + - In contrast, it is likely not correct if there exist Roles/RoleBindings in namespaces that Argo CD is not explicitly deploying to. +- If a Role/RoleBinding exists that is not required, delete them. + - NOTE: They will be recreated by the operator if there exists an `ArgoCD` CR that references the namespace via the `.spec.sourceNamespaces` or `.spec.applicationSet.sourceNamespaces`. + - If this is the case, first remove the namespace from these fields, then delete the Role/RoleBinding. + + +Example: + +In this example, the script indicates that the `my-argocd` namespace has access to the `app-ns` namespaces via multiple GitOps-operator-created Roles/RoleBindings: + +``` +========================================================= +SEARCH CRITERIA (Must match ALL): + 1. API/Resource: argoproj.io / applications + 2. Label: app.kubernetes.io/part-of=argocd + 3. Scope: Cross-namespace only +========================================================= + +Scanning Cluster (this may take a moment)... + +Roles with cross-namespace access: + • Role: app-ns/example-my-argocd-applicationset + • Role: app-ns/example_app-ns + +Cross-namespace bindings detail: +-------------------------------------------------- +BINDING: app-ns / example-my-argocd-applicationset +ROLE REF: example-my-argocd-applicationset +SUBJECTS (cross-namespace only): + • ServiceAccount: example-applicationset-controller (ns: my-argocd) + +• Namespace my-argocd has access to app-ns + +-------------------------------------------------- +BINDING: app-ns / example_app-ns +ROLE REF: example_app-ns +SUBJECTS (cross-namespace only): + • ServiceAccount: example-argocd-server (ns: my-argocd) + • ServiceAccount: example-argocd-application-controller (ns: my-argocd) + +• Namespace my-argocd has access to app-ns +``` diff --git a/scripts/audit-namespace-roles/audit-operator-roles.sh b/scripts/audit-namespace-roles/audit-operator-roles.sh new file mode 100755 index 000000000..0a93c9436 --- /dev/null +++ b/scripts/audit-namespace-roles/audit-operator-roles.sh @@ -0,0 +1,140 @@ +#!/bin/bash + +# --------------------------------------------------------- +# Pre-flight Check: Verify jq is installed +# --------------------------------------------------------- +if ! command -v jq &> /dev/null; then + printf "Error: 'jq' is not installed.\n" + printf "This script requires jq to parse Kubernetes JSON output.\n" + exit 1 +fi + +# --------------------------------------------------------- +# CONFIGURATION +# --------------------------------------------------------- +TARGET_API="argoproj.io" +TARGET_RESOURCE="applications" +TARGET_LABEL_KEY="app.kubernetes.io/part-of" +TARGET_LABEL_VAL="argocd" + +printf "=========================================================\n" +printf "SEARCH CRITERIA (Must match ALL):\n" +printf " 1. API/Resource: %s / %s\n" "$TARGET_API" "$TARGET_RESOURCE" +printf " 2. Label: %s=%s\n" "$TARGET_LABEL_KEY" "$TARGET_LABEL_VAL" +printf " 3. Scope: Cross-namespace only\n" +printf "=========================================================\n" + +printf "\nScanning Cluster (this may take a moment)...\n" + +# --------------------------------------------------------- +# STEP 1: FIND CANDIDATE ROLES +# --------------------------------------------------------- +CANDIDATE_ROLES_JSON=$(oc get roles -A -o json -l "${TARGET_LABEL_KEY}=${TARGET_LABEL_VAL}" | jq -r --arg API "$TARGET_API" \ + --arg RES "$TARGET_RESOURCE" \ + --arg L_KEY "$TARGET_LABEL_KEY" \ + --arg L_VAL "$TARGET_LABEL_VAL" ' + [ + .items[] | + select( + (.metadata.labels?[$L_KEY] == $L_VAL) + and + ( + .rules[]? | + ( (.apiGroups[]? == $API) or (.apiGroups[]? == "*") ) and + ( (.resources[]? == $RES) or (.resources[]? == "*") ) + ) + ) | + "\(.metadata.namespace)/\(.metadata.name)" + ] | unique +') + +# If no candidate roles exist, we can exit early +if [ "$CANDIDATE_ROLES_JSON" == "[]" ]; then + printf " • No Roles found matching label/rule criteria.\n" + exit 0 +fi + +# --------------------------------------------------------- +# FIND BINDINGS +# --------------------------------------------------------- +# We process ALL bindings, but filter down to only those that: +# a) Point to a "Candidate Role" found in Step 1 +# b) Have at least one Subject in a DIFFERENT namespace +# We save this filtered JSON array to a variable. +TARGET_BINDINGS_JSON=$(oc get rolebindings -A -o json -l "${TARGET_LABEL_KEY}=${TARGET_LABEL_VAL}" | jq --argjson TARGET_ROLES "$CANDIDATE_ROLES_JSON" ' + [ + .items[] | + (.metadata.namespace + "/" + .roleRef.name) as $localRef | + .metadata.namespace as $binding_ns | + + # Filter A: Must reference one of our Candidate Roles + select( + .roleRef.kind == "Role" and + ($localRef as $ref | $TARGET_ROLES | index($ref)) + ) | + + # Filter B: Must have at least one cross-namespace ServiceAccount + select( + [ + .subjects[]? | + select(.kind == "ServiceAccount" and .namespace != $binding_ns) + ] | length > 0 + ) + ] +') + +# --------------------------------------------------------- +# OUTPUT ROLES +# --------------------------------------------------------- +printf "\nRoles with cross-namespace access:\n" + +# We extract the unique list of roles strictly from the OFFENDING bindings. +VERIFIED_ROLES=$(echo "$TARGET_BINDINGS_JSON" | jq -r ' + [ .[] | "\(.metadata.namespace)/\(.roleRef.name)" ] | unique +') + +if [ "$VERIFIED_ROLES" == "[]" ]; then + printf " • No cross-namespace bindings found for the candidate roles.\n" + printf "Scan Complete.\n" + exit 0 +else + echo "$VERIFIED_ROLES" | jq -r '.[] | " • Role: \((.))"' +fi + +# --------------------------------------------------------- +# OUTPUT BINDINGS +# --------------------------------------------------------- +printf "\nCross-namespace bindings detail:\n" + +echo "$TARGET_BINDINGS_JSON" | jq -r ' + .[] | + .metadata.namespace as $binding_ns | + + # Calculate aggregate list of external namespaces for summary + ( + [ + .subjects[]? | + select(.kind == "ServiceAccount" and .namespace != $binding_ns) | + .namespace + ] + | unique + | join(", ") + ) as $external_namespaces | + + "--------------------------------------------------", + "BINDING: \(.metadata.namespace) / \(.metadata.name)", + "ROLE REF: \(.roleRef.name)", + "SUBJECTS (cross-namespace only):", + ( + .subjects[]? | + # Print only external service accounts + if (.kind == "ServiceAccount" and .namespace != $binding_ns) then + " • \(.kind): \(.name) (ns: \(.namespace))" + else + empty + end + ), + "", + "• Namespace \($external_namespaces) has access to \(.metadata.namespace)", + "" +'