Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions scripts/audit-namespace-roles/README.md
Original file line number Diff line number Diff line change
@@ -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
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we'd like to include a safe scan example

$ ./audit-operator-roles.sh
=========================================================
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:
  • No cross-namespace bindings found for the candidate roles.
Scan Complete.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add this as a separate PR

140 changes: 140 additions & 0 deletions scripts/audit-namespace-roles/audit-operator-roles.sh
Original file line number Diff line number Diff line change
@@ -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)",
""
'