Skip to content

Commit 2a3ef18

Browse files
committed
chore: add operator role audit script
Signed-off-by: Jonathan West <[email protected]>
1 parent 4c10981 commit 2a3ef18

File tree

2 files changed

+202
-0
lines changed

2 files changed

+202
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
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.
2+
(The 'apps/applications in any namespace' features are not enabled by default. They are enabled via `ArgoCD` CR `.spec.sourceNamespaces` and `.spec.applicationSet.sourceNamespaces`.)
3+
4+
This is a simple script that will look for Roles/RoleBindings across ALL namespaces that meet ALL of the following criteria:
5+
- A) The Role allows access to `argoproj.io/Application` resource
6+
- B) The Role has label `app.kubernetes.io/part-of: argocd`
7+
- C) The RoleBinding references a service-account in another namespace (cross-namespace access)
8+
9+
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.
10+
11+
## Procedure:
12+
1) Ensure that `jq` and `oc` executables are installed and on path.
13+
2) Ensure that you are logged into cluster via `oc` or `kubectl` CLI.
14+
3) Execute `./audit-operator-roles.sh`
15+
4) Examine the output list of Roles/RoleBindings.
16+
17+
For each Role/RoleBinding that is listed:
18+
- If a Role/RoleBinding is listed, that means another namespace on the cluster has access to the namespace containing the Role/RoleBinding
19+
- 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.
20+
- 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.
21+
- In contrast, it is likely not correct if there exist Roles/RoleBindings in namespaces that Argo CD is not explicitly deploying to.
22+
- If a Role/RoleBinding exists that is not required, delete them.
23+
- 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`.
24+
- If this is the case, first remove the namespace from these fields, then delete the Role/RoleBinding.
25+
26+
27+
Example:
28+
29+
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:
30+
31+
```
32+
=========================================================
33+
SEARCH CRITERIA (Must match ALL):
34+
1. API/Resource: argoproj.io / applications
35+
2. Label: app.kubernetes.io/part-of=argocd
36+
3. Scope: Cross-namespace only
37+
=========================================================
38+
39+
Scanning Cluster (this may take a moment)...
40+
41+
Roles with cross-namespace access:
42+
• Role: app-ns/example-my-argocd-applicationset
43+
• Role: app-ns/example_app-ns
44+
45+
Cross-namespace bindings detail:
46+
--------------------------------------------------
47+
BINDING: app-ns / example-my-argocd-applicationset
48+
ROLE REF: example-my-argocd-applicationset
49+
SUBJECTS (cross-namespace only):
50+
• ServiceAccount: example-applicationset-controller (ns: my-argocd)
51+
52+
• Namespace my-argocd has access to app-ns
53+
54+
--------------------------------------------------
55+
BINDING: app-ns / example_app-ns
56+
ROLE REF: example_app-ns
57+
SUBJECTS (cross-namespace only):
58+
• ServiceAccount: example-argocd-server (ns: my-argocd)
59+
• ServiceAccount: example-argocd-application-controller (ns: my-argocd)
60+
61+
• Namespace my-argocd has access to app-ns
62+
```
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#!/bin/bash
2+
3+
# ---------------------------------------------------------
4+
# Pre-flight Check: Verify jq is installed
5+
# ---------------------------------------------------------
6+
if ! command -v jq &> /dev/null; then
7+
printf "Error: 'jq' is not installed.\n"
8+
printf "This script requires jq to parse Kubernetes JSON output.\n"
9+
exit 1
10+
fi
11+
12+
# ---------------------------------------------------------
13+
# CONFIGURATION
14+
# ---------------------------------------------------------
15+
TARGET_API="argoproj.io"
16+
TARGET_RESOURCE="applications"
17+
TARGET_LABEL_KEY="app.kubernetes.io/part-of"
18+
TARGET_LABEL_VAL="argocd"
19+
20+
printf "=========================================================\n"
21+
printf "SEARCH CRITERIA (Must match ALL):\n"
22+
printf " 1. API/Resource: %s / %s\n" "$TARGET_API" "$TARGET_RESOURCE"
23+
printf " 2. Label: %s=%s\n" "$TARGET_LABEL_KEY" "$TARGET_LABEL_VAL"
24+
printf " 3. Scope: Cross-namespace only\n"
25+
printf "=========================================================\n"
26+
27+
printf "\nScanning Cluster (this may take a moment)...\n"
28+
29+
# ---------------------------------------------------------
30+
# STEP 1: FIND CANDIDATE ROLES
31+
# ---------------------------------------------------------
32+
CANDIDATE_ROLES_JSON=$(oc get roles -A -o json | jq -r --arg API "$TARGET_API" \
33+
--arg RES "$TARGET_RESOURCE" \
34+
--arg L_KEY "$TARGET_LABEL_KEY" \
35+
--arg L_VAL "$TARGET_LABEL_VAL" '
36+
[
37+
.items[] |
38+
select(
39+
(.metadata.labels?[$L_KEY] == $L_VAL)
40+
and
41+
(
42+
.rules[]? |
43+
( (.apiGroups[]? == $API) or (.apiGroups[]? == "*") ) and
44+
( (.resources[]? == $RES) or (.resources[]? == "*") )
45+
)
46+
) |
47+
"\(.metadata.namespace)/\(.metadata.name)"
48+
] | unique
49+
')
50+
51+
# If no candidate roles exist, we can exit early
52+
if [ "$CANDIDATE_ROLES_JSON" == "[]" ]; then
53+
printf " • No Roles found matching label/rule criteria.\n"
54+
exit 0
55+
fi
56+
57+
# ---------------------------------------------------------
58+
# FIND BINDINGS
59+
# ---------------------------------------------------------
60+
# We process ALL bindings, but filter down to only those that:
61+
# a) Point to a "Candidate Role" found in Step 1
62+
# b) Have at least one Subject in a DIFFERENT namespace
63+
# We save this filtered JSON array to a variable.
64+
TARGET_BINDINGS_JSON=$(oc get rolebindings -A -o json | jq --argjson TARGET_ROLES "$CANDIDATE_ROLES_JSON" '
65+
[
66+
.items[] |
67+
(.metadata.namespace + "/" + .roleRef.name) as $localRef |
68+
.metadata.namespace as $binding_ns |
69+
70+
# Filter A: Must reference one of our Candidate Roles
71+
select(
72+
.roleRef.kind == "Role" and
73+
($localRef as $ref | $TARGET_ROLES | index($ref))
74+
) |
75+
76+
# Filter B: Must have at least one cross-namespace ServiceAccount
77+
select(
78+
[
79+
.subjects[]? |
80+
select(.kind == "ServiceAccount" and .namespace != $binding_ns)
81+
] | length > 0
82+
)
83+
]
84+
')
85+
86+
# ---------------------------------------------------------
87+
# OUTPUT ROLES
88+
# ---------------------------------------------------------
89+
printf "\nRoles with cross-namespace access:\n"
90+
91+
# We extract the unique list of roles strictly from the OFFENDING bindings.
92+
VERIFIED_ROLES=$(echo "$TARGET_BINDINGS_JSON" | jq -r '
93+
[ .[] | "\(.metadata.namespace)/\(.roleRef.name)" ] | unique
94+
')
95+
96+
if [ "$VERIFIED_ROLES" == "[]" ]; then
97+
printf " • No cross-namespace bindings found for the candidate roles.\n"
98+
printf "Scan Complete.\n"
99+
exit 0
100+
else
101+
echo "$VERIFIED_ROLES" | jq -r '.[] | " • Role: \((.))"'
102+
fi
103+
104+
# ---------------------------------------------------------
105+
# OUTPUT BINDINGS
106+
# ---------------------------------------------------------
107+
printf "\nCross-namespace bindings detail:\n"
108+
109+
echo "$TARGET_BINDINGS_JSON" | jq -r '
110+
.[] |
111+
.metadata.namespace as $binding_ns |
112+
113+
# Calculate aggregate list of external namespaces for summary
114+
(
115+
[
116+
.subjects[]? |
117+
select(.kind == "ServiceAccount" and .namespace != $binding_ns) |
118+
.namespace
119+
]
120+
| unique
121+
| join(", ")
122+
) as $external_namespaces |
123+
124+
"--------------------------------------------------",
125+
"BINDING: \(.metadata.namespace) / \(.metadata.name)",
126+
"ROLE REF: \(.roleRef.name)",
127+
"SUBJECTS (cross-namespace only):",
128+
(
129+
.subjects[]? |
130+
# Print only external service accounts
131+
if (.kind == "ServiceAccount" and .namespace != $binding_ns) then
132+
" • \(.kind): \(.name) (ns: \(.namespace))"
133+
else
134+
empty
135+
end
136+
),
137+
"",
138+
"• Namespace \($external_namespaces) has access to \(.metadata.namespace)",
139+
""
140+
'

0 commit comments

Comments
 (0)