Skip to content

Commit

Permalink
Add offline mode
Browse files Browse the repository at this point in the history
  • Loading branch information
yuvalavra authored and yuvalavra committed Sep 5, 2022
1 parent 7ab8552 commit 25dd22a
Show file tree
Hide file tree
Showing 21 changed files with 279 additions and 54 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ results/
rbac-police
lib/ignore
.DS_Store
.idea/
local_*/
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ go build
./rbac-police eval lib/
```

## Use Cases
## Usage
### Set severity threshold
Only evaluate policies with a severity equal to or higher than a threshold.
```
Expand All @@ -34,7 +34,7 @@ Only consider violations from service accounts that exist on all nodes. Useful f
./rbac-police eval lib/ --only-sas-on-all-nodes
```
### Discover protections
Improve accuracy by identifying security-related features gates and native admission controllers that can protect against certain attacks. Please note that [NodeRestriction](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#noderestriction) is identified by impersonating a node and *dry-run creating a pod*, which may be logged by some systems.
Improve accuracy by identifying security-related features gates and admission controllers that can protect against certain attacks. Please note that [NodeRestriction](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#noderestriction) is identified by impersonating a node and *dry-run creating a pod*, which may be logged by some systems.
```
./rbac-police eval lib/ -w
```
Expand Down
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func init() {
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")
rootCmd.PersistentFlags().StringVarP(&collectConfig.Namespace, "namespace", "n", "", "scope collection on serviceAccounts to a namespace")
rootCmd.PersistentFlags().StringVar(&collectConfig.OfflineDir, "local-dir", "", "offline mode, get cluster data from local files, see <rbac-police>/utils/get_cluster_data.sh")
}

// Prints and / or saves output to file
Expand Down
1 change: 1 addition & 0 deletions docs/collect.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Global Flags:
-a, --all-serviceaccounts collect data on all serviceAccounts, not only those assigned to a pod
-w, --discover-protections discover features gates and admission controllers that protect against certain attacks, partly by emulating the attacks via impersonation & dry-run write operations
--ignore-controlplane 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
--local-dir string offline mode, get cluster data from local files, see <rbac-police>/utils/get_cluster_data.sh
-l, --loud loud mode, print results regardless of -o
-n, --namespace string scope collection on serviceAccounts to a namespace
--node-groups strings treat nodes as part of these groups (default [system:nodes])
Expand Down
1 change: 1 addition & 0 deletions docs/eval.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Global Flags:
-a, --all-serviceaccounts collect data on all serviceAccounts, not only those assigned to a pod
-w, --discover-protections discover features gates and admission controllers that protect against certain attacks, partly by emulating the attacks via impersonation & dry-run write operations
--ignore-controlplane 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
--local-dir string offline mode, get cluster data from local files, see <rbac-police>/utils/get_cluster_data.sh
-l, --loud loud mode, print results regardless of -o
-n, --namespace string scope collection on serviceAccounts to a namespace
--node-groups strings treat nodes as part of these groups (default [system:nodes])
Expand Down
1 change: 1 addition & 0 deletions docs/expand.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Global Flags:
-a, --all-serviceaccounts collect data on all serviceAccounts, not only those assigned to a pod
-w, --discover-protections discover features gates and admission controllers that protect against certain attacks, partly by emulating the attacks via impersonation & dry-run write operations
--ignore-controlplane 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
--local-dir string offline mode, get cluster data from local files, see <rbac-police>/utils/get_cluster_data.sh
-l, --loud loud mode, print results regardless of -o
-n, --namespace string scope collection on serviceAccounts to a namespace
--node-groups strings treat nodes as part of these groups (default [system:nodes])
Expand Down
File renamed without changes.
22 changes: 11 additions & 11 deletions pkg/collect/cluster_db.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
"k8s.io/client-go/kubernetes"
)

// Builds a ClusterDb object by querying a cluster
func BuildClusterDb(clientset *kubernetes.Clientset, ns string, IgnoreControlPlane bool) *ClusterDb {
// buildClusterDb populates a ClusterDb object by querying a cluster
func buildClusterDb(clientset *kubernetes.Clientset, ns string, ignoreControlPlane bool) *ClusterDb {
var (
clusterDb ClusterDb
err error
Expand All @@ -28,16 +28,16 @@ func BuildClusterDb(clientset *kubernetes.Clientset, ns string, IgnoreControlPla
if err != nil {
return nil // error printed in getServiceAccounts
}
clusterDb.Nodes, err = getNodes(clientset, IgnoreControlPlane)
clusterDb.Nodes, err = getNodes(clientset, ignoreControlPlane)
if err != nil {
return nil // error printed in getPods
}
clusterDb.Pods, err = getPods(clientset, ns)
if err != nil {
return nil // error printed in getPods
}
if IgnoreControlPlane {
removePodsOnNonRelevantNodes(&clusterDb)
if ignoreControlPlane {
removePodsFromExcludedNodes(&clusterDb) // remove control plane pods if needed
}
return &clusterDb
}
Expand Down Expand Up @@ -107,21 +107,21 @@ func getRoleBindingsAndClusterRoleBindings(clientset *kubernetes.Clientset) ([]r
}

// Removes pods that have a NodeName which is not in cDb.Nodes
func removePodsOnNonRelevantNodes(cDb *ClusterDb) {
var relevantPods []v1.Pod
func removePodsFromExcludedNodes(cDb *ClusterDb) {
var includedPods []v1.Pod

for _, pod := range cDb.Pods {
if pod.Spec.NodeName == "" {
relevantPods = append(relevantPods, pod) // include non-scheduled pods
includedPods = append(includedPods, pod) // include non-scheduled pods
continue
}
for _, node := range cDb.Nodes {
if pod.Spec.NodeName == node.Name {
// Pod hosted on relevant node
relevantPods = append(relevantPods, pod)
// Pod hosted on included node
includedPods = append(includedPods, pod)
break
}
}
}
cDb.Pods = relevantPods
cDb.Pods = includedPods
}
69 changes: 41 additions & 28 deletions pkg/collect/collect.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,46 @@ import (
"strings"

log "github.com/sirupsen/logrus"

"k8s.io/client-go/kubernetes"
_ "k8s.io/client-go/plugin/pkg/client/auth" // in order to connect to clusters via auth plugins
"k8s.io/client-go/tools/clientcmd"
)

// Collects RBAC permissions in a k8s cluster
// Collect retrieves the RBAC settings in a k8s cluster
func Collect(collectConfig CollectConfig) *CollectResult {
// Init Kubernetes client
clientset, kubeConfig, err := initKubeClient()
if err != nil {
return nil // error printed in initKubeClient
var metadata *ClusterMetadata
var clusterDb *ClusterDb
var kubeConfig clientcmd.ClientConfig = nil

if collectConfig.OfflineDir == "" {
// Online mode, init Kubernetes client
clientset, kConfigTmp, err := initKubeClient()
kubeConfig = kConfigTmp
if err != nil {
return nil // error printed in initKubeClient
}
// Build metadata and clusterDb from remote cluster
metadata = buildMetadata(clientset, kubeConfig)
clusterDb = buildClusterDb(clientset, collectConfig.Namespace, collectConfig.IgnoreControlPlane)
} else {
// Offline mode, parse clusterDb and metadata from local files
clusterDb, metadata = parseLocalCluster(collectConfig)
}
metadata := getMetadata(clientset, kubeConfig)
clusterDb := BuildClusterDb(clientset, collectConfig.Namespace, collectConfig.IgnoreControlPlane)
if clusterDb == nil {
return nil // error printed in BuildClusterDb
return nil // error printed in buildClusterDb or in parseLocalCluster
}

rbacDb := BuildRbacDb(*clusterDb, collectConfig)
if rbacDb == nil {
return nil // error printed in BuildClusterDb
if collectConfig.DiscoverProtections {
discoverRelevantControlPlaneFeatures(collectConfig, kubeConfig, clusterDb, metadata)
}

if collectConfig.DiscoverProtections {
discoverRelevantControlPlaneFeatures(collectConfig, kubeConfig, clusterDb, &metadata)
rbacDb := buildRbacDb(*clusterDb, collectConfig)
if rbacDb == nil {
return nil // error printed in BuildClusterDb
}

return &CollectResult{
Metadata: metadata,
Metadata: *metadata,
ServiceAccounts: rbacDb.ServiceAccounts,
Nodes: rbacDb.Nodes,
Roles: rbacDb.Roles,
Expand All @@ -58,33 +68,36 @@ func initKubeClient() (*kubernetes.Clientset, clientcmd.ClientConfig, error) {
}

// Get cluster metadata
func getMetadata(clientset *kubernetes.Clientset, kubeConfig clientcmd.ClientConfig) ClusterMetadata {
versionInfo, err := clientset.Discovery().ServerVersion()
if err != nil {
log.Warnln("getMetadata: failed to get server version with", err)
return ClusterMetadata{}
func buildMetadata(clientset *kubernetes.Clientset, kubeConfig clientcmd.ClientConfig) *ClusterMetadata {
metadata := ClusterMetadata{
Features: []string{},
}

rawConfig, err := kubeConfig.RawConfig()
if err != nil {
log.Warnln("getMetadata: failed to get raw kubeconfig", err)
return ClusterMetadata{}
} else {
metadata.ClusterName = rawConfig.Contexts[rawConfig.CurrentContext].Cluster
}

return ClusterMetadata{
ClusterName: rawConfig.Contexts[rawConfig.CurrentContext].Cluster,
Platform: getPlatform(versionInfo.GitVersion),
Version: ClusterVersion{
versionInfo, err := clientset.Discovery().ServerVersion()
if err != nil {
log.Warnln("getMetadata: failed to get server version with", err)
} else {
metadata.Version = ClusterVersion{
Major: versionInfo.Major,
Minor: versionInfo.Minor,
GitVersion: versionInfo.GitVersion,
},
Features: []string{},
}
metadata.Platform = platformFromVersion(versionInfo.GitVersion)
}

return &metadata
}

// Identifies the underlying platform from a cluster's @version,
// supports EKS and GKE
func getPlatform(version string) string {
func platformFromVersion(version string) string {
if strings.Contains(version, "-eks-") {
return "eks"
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/collect/discover_protections.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ func discoverRelevantControlPlaneFeatures(collectConfig CollectConfig, kubeConfi
if legacyTokenSecretsReducted(clusterDb, collectConfig.Namespace) {
metadata.Features = append(metadata.Features, "LegacyTokenSecretsReducted")
}
// If NodeAuthorization is used, check for NodeRestriction
if collectConfig.NodeUser == "" {
// If NodeAuthorization is used, and we're not running in offline mode, check for NodeRestriction
if collectConfig.NodeUser == "" && collectConfig.OfflineDir == "" {
if NodeRestrictionEnabled(kubeConfig, clusterDb, metadata) {
metadata.Features = append(metadata.Features, "NodeRestriction")
// If the cluster's version >=1.17, populate NodeRestriction1.17
Expand All @@ -39,7 +39,7 @@ func discoverRelevantControlPlaneFeatures(collectConfig CollectConfig, kubeConfi

// Best effort test for whether serviceAccount tokens are stored as secrets
func legacyTokenSecretsReducted(clusterDb *ClusterDb, ns string) bool {
// If collection is scoped to ns, use it's default serviceAccount for testing,
// If collection is scoped to ns, use its default serviceAccount for testing,
// Otherwise use kube-system:replicaset-controller
saName := "default"
if ns == "" {
Expand Down
Loading

0 comments on commit 25dd22a

Please sign in to comment.