Skip to content

Commit

Permalink
Merge pull request #6 from PaloAltoNetworks/auto-discover
Browse files Browse the repository at this point in the history
Add infra for discovering protections, identify LegacyServiceAccountToken feature gates
  • Loading branch information
yuvalavra authored Aug 30, 2022
2 parents fdfe13a + c89a15f commit b992f18
Show file tree
Hide file tree
Showing 9 changed files with 86 additions and 14 deletions.
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&outFile, "out-file", "o", "", "save results to file")
rootCmd.PersistentFlags().BoolVarP(&loudMode, "loud", "l", false, "loud mode, print results regardless of -o")
rootCmd.PersistentFlags().BoolVarP(&collectConfig.AllServiceAccounts, "all-serviceaccounts", "a", false, "collect data on all serviceAccounts, not only those assigned to a pod")
rootCmd.PersistentFlags().BoolVarP(&collectConfig.DiscoverProtections, "discover-protections", "w", false, "discover relevant control plane features gates and admission controller that protect against certain attacks, partly by emulating attacks via impersonation & dry-run write operations")
rootCmd.PersistentFlags().BoolVar(&collectConfig.IgnoreControlPlane, "ignore-controlplane", false, "don't collect data on control plane nodes and pods. Identified by either the 'node-role.kubernetes.io/control-plane' or 'node-role.kubernetes.io/master' labels. ServiceAccounts will not be linked to control plane components")
rootCmd.PersistentFlags().StringSliceVar(&collectConfig.NodeGroups, "node-groups", []string{"system:nodes"}, "treat nodes as part of these groups")
rootCmd.PersistentFlags().StringVar(&collectConfig.NodeUser, "node-user", "", "user assigned to all nodes, default behaviour assumes nodes users are compatible with the NodeAuthorizer")
Expand Down
6 changes: 5 additions & 1 deletion docs/policies.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ The above options are implemented by a Rego [wrapper](../lib/utils/wrapper.rego)
- Description: `SAs and nodes that can create or modify secrets in privileged namespaces can issue tokens for admin-equivalent SAs`
- Severity: `Critical`
- Violation types: `serviceAccounts, nodes`
### [list_secrets](../lib/list_secrets.rego)
- Description: `SAs and nodes that can list secrets cluster-wide may access confidential information, and in some cases serviceAccount tokens`
- Severity: `Low`
- Violation types: `serviceAccounts, nodes`
### [modify_node_status](../lib/modify_node_status.rego)
- Description: `SAs and nodes that can modify nodes' status can set or remove labels to affect scheduling constraints enforced via nodeAffinity or nodeSelectors`
- Severity: `Low`
Expand Down Expand Up @@ -114,7 +118,7 @@ The above options are implemented by a Rego [wrapper](../lib/utils/wrapper.rego)
- Description: `SAs and nodes that can update or patch pods or create pods/exec in unprivileged namespaces can execute code on existing pods`
- Severity: `Medium`
- Violation types: `serviceAccounts, nodes`
### [retrieve_secrets](../lib/retrieve_secrets.rego)
### [retrieve_token_secrets](../lib/retrieve_token_secrets.rego)
- Description: `SAs and nodes that can retrieve secrets in privileged namespaces can obtain tokens of admin-equivalent SAs`
- Severity: `Critical`
- Violation types: `serviceAccounts, nodes`
Expand Down
19 changes: 19 additions & 0 deletions lib/list_secrets.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package policy
import data.police_builtins as pb
import future.keywords.in

describe[{"desc": desc, "severity": severity}] {
desc := "SAs and nodes that can list secrets cluster-wide may access confidential information, and in some cases serviceAccount tokens"
severity := "Low"
}
checkServiceAccounts := true
checkNodes := true

evaluateRoles(roles, type) {
some role in roles
pb.notNamespaced(role)
some rule in role.rules
pb.valueOrWildcard(rule.resources, "secrets")
pb.valueOrWildcard(rule.verbs, "list")
pb.valueOrWildcard(rule.apiGroups, "")
}
11 changes: 8 additions & 3 deletions lib/obtain_token_weak_ns.rego
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,14 @@ ruleCanAcquireToken(rule) {
# Create - mannualy create a token secret (issue_token_secrets)
# Update & Patch - modfiy secret (issue_token_secrets), TODO: probably not exploitable if resourceNames is present?
canAbuseSecretsForToken(verbs) {
"list" in verbs
} {
"get" in verbs
pb.legacyTokenSecrets
listOrGet(verbs)
} {
pb.createUpdatePatchOrWildcard(verbs)
}

listOrGet(verbs) {
"list" in verbs
} {
"get" in verbs
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ checkServiceAccounts := true
checkNodes := true

evaluateRoles(roles, type) {
pb.legacyTokenSecrets
some role in roles
pb.affectsPrivNS(role)
some rule in role.rules
Expand All @@ -18,5 +19,3 @@ evaluateRoles(roles, type) {
pb.valueOrWildcard(rule.apiGroups, "")
not pb.hasKey(rule, "resourceNames")
}


7 changes: 7 additions & 0 deletions lib/utils/builtins.rego
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,11 @@ equalNamespaceIfExist(obj, other) {
} {
not hasKey(obj, "namespace")
not hasKey(other, "namespace")
}

# Check whether LegacyTokenSecretsReducted is disabled
legacyTokenSecrets := true {
metadata := object.get(input, "metadata", {})
features := object.get(metadata, "features", [])
not "LegacyTokenSecretsReducted" in features
}
5 changes: 5 additions & 0 deletions pkg/collect/collect.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ func Collect(collectConfig CollectConfig) *CollectResult {
return nil // error printed in BuildClusterDb
}

if collectConfig.DiscoverProtections {
discoverRelevantControlPlaneFeatures(clientset, clusterDb, &metadata)
}

return &CollectResult{
Metadata: metadata,
ServiceAccounts: rbacDb.ServiceAccounts,
Expand Down Expand Up @@ -70,6 +74,7 @@ func getMetadata(clientset *kubernetes.Clientset, kubeConfig clientcmd.ClientCon
ClusterName: rawConfig.Contexts[rawConfig.CurrentContext].Cluster,
Platform: getPlatform(versionInfo.GitVersion),
Version: versionInfo.GitVersion,
Features: []string{},
}
}

Expand Down
30 changes: 30 additions & 0 deletions pkg/collect/discover_protections.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package collect

import (
"k8s.io/client-go/kubernetes"
)

// Discover control plane feature gates and admission controllers that protect against certain attacks,
// and populate the cluster's metadata with them for policies to consume.
// NOTE: Uses impersonation and dry-run write operations, which won't affect the cluster, but may be logged / audited on.
func discoverRelevantControlPlaneFeatures(clientset *kubernetes.Clientset, clusterDb *ClusterDb, metadata *ClusterMetadata) {
if legacyTokenSecretsReducted(clusterDb) {
metadata.Features = append(metadata.Features, "LegacyTokenSecretsReducted")
}
}

// Best effort test for whether serviceAccount tokens are stored as secrets
func legacyTokenSecretsReducted(clusterDb *ClusterDb) bool {
for _, serviceAccount := range clusterDb.ServiceAccounts {
if serviceAccount.ObjectMeta.Namespace != "kube-system" {
continue
}
// Arbitrarily chose the replicaset-controller for testing
if serviceAccount.ObjectMeta.Name != "replicaset-controller" {
continue
}
// Return true if there are no auto-generated secrets for the serviceAccount
return len(serviceAccount.Secrets) == 0
}
return false
}
18 changes: 10 additions & 8 deletions pkg/collect/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import (

// Configuration for Collect()
type CollectConfig struct {
AllServiceAccounts bool
IgnoreControlPlane bool
NodeGroups []string
NodeUser string
Namespace string
AllServiceAccounts bool
IgnoreControlPlane bool
DiscoverProtections bool
NodeGroups []string
NodeUser string
Namespace string
}

// Outpot of Collect()
Expand Down Expand Up @@ -42,9 +43,10 @@ type RbacDb struct {
}

type ClusterMetadata struct {
ClusterName string `json:"cluster"`
Platform string `json:"platform"`
Version string `json:"version"`
ClusterName string `json:"cluster"`
Platform string `json:"platform"`
Version string `json:"version"`
Features []string `json:"features"`
}

// RBAC info of a serviceAccount
Expand Down

0 comments on commit b992f18

Please sign in to comment.