Skip to content

Commit

Permalink
feat(conftest): associate Rego policies with K8s resources (#752)
Browse files Browse the repository at this point in the history
Signed-off-by: Daniel Pacak <pacak.daniel@gmail.com>
  • Loading branch information
danielpacak authored Oct 25, 2021
1 parent fa00227 commit 7ef9060
Show file tree
Hide file tree
Showing 14 changed files with 412 additions and 231 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ var (

//go:embed testdata/run_as_root.rego
runAsRootPolicy string
//go:embed testdata/service_with_external_ip.rego
serviceWithExternalIPPolicy string
)

func TestIntegrationOperatorWithConftest(t *testing.T) {
Expand Down Expand Up @@ -111,7 +113,10 @@ var _ = BeforeSuite(func() {
Data: map[string]string{
"conftest.imageRef": "docker.io/openpolicyagent/conftest:v0.25.0",

"conftest.policy.runs_as_root.rego": runAsRootPolicy,
"conftest.policy.runs_as_root.rego": runAsRootPolicy,
"conftest.policy.runs_as_root.kinds": "Workload",
"conftest.policy.service_with_external_ip.rego": serviceWithExternalIPPolicy,
"conftest.policy.service_with_external_ip.kinds": "Service",
},
}
err = kubeClient.Create(context.Background(), conftestCM)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
package main
package kubernetes.configaudit.run_as_root

deny[res] {
input.kind == "ReplicaSet"
not input.spec.template.spec.securityContext.runAsNonRoot
input.kind == "ReplicaSet"
not input.spec.template.spec.securityContext.runAsNonRoot

res := {
"msg": "Containers must not run as root",
"title": "Run as root"
}
res := {
"msg": "Containers must not run as root",
"title": "Run as root",
}
}

deny[res] {
input.kind == "Pod"
not input.spec.securityContext.runAsNonRoot
input.kind == "Pod"
not input.spec.securityContext.runAsNonRoot

res := {
"msg": "Containers must not run as root",
"title": "Run as root"
}
res := {
"msg": "Containers must not run as root",
"title": "Run as root",
}
}

deny[res] {
input.kind == "CronJob"
not input.spec.jobTemplate.spec.template.spec.securityContext.runAsNonRoot
input.kind == "CronJob"
not input.spec.jobTemplate.spec.template.spec.securityContext.runAsNonRoot

res := {
"msg": "Containers must not run as root",
"title": "Run as root"
}
res := {
"msg": "Containers must not run as root",
"title": "Run as root",
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kubernetes.configaudit.service_with_external_ip

deny[res] {
input.kind == "Service"
count(input.spec.externalIPs) > 0
res := {
"msg": "Service with external IP",
"title": "Service with external IP",
}
}
3 changes: 2 additions & 1 deletion itest/starboard/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ var (
Namespace: "starboard",
},
Data: map[string]string{
"conftest.imageRef": "docker.io/openpolicyagent/conftest:v0.25.0",
"conftest.imageRef": "docker.io/openpolicyagent/conftest:v0.25.0",
"conftest.policy.runs_as_root.kinds": "Workload",
"conftest.policy.runs_as_root.rego": `
package main
Expand Down
2 changes: 1 addition & 1 deletion pkg/configauditreport/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func (s *ScanJobBuilder) Get() (*batchv1.Job, []*corev1.Secret, error) {

jobSpec.Tolerations = append(jobSpec.Tolerations, s.tolerations...)

pluginConfigHash, err := s.plugin.GetConfigHash(s.pluginContext)
pluginConfigHash, err := s.plugin.ConfigHash(s.pluginContext, kube.Kind(s.object.GetObjectKind().GroupVersionKind().Kind))
if err != nil {
return nil, nil, err
}
Expand Down
10 changes: 5 additions & 5 deletions pkg/configauditreport/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,12 @@ type testPlugin struct {
configHash string
}

func (p *testPlugin) SupportsKind(_ kube.Kind) bool {
return true
func (p *testPlugin) SupportedKinds() []kube.Kind {
return []kube.Kind{}
}

func (p *testPlugin) IsReady(_ starboard.PluginContext) (bool, error) {
return true, nil
func (p *testPlugin) IsApplicable(_ starboard.PluginContext, _ client.Object) (bool, string, error) {
return true, "", nil
}

func (p *testPlugin) Init(_ starboard.PluginContext) error {
Expand All @@ -143,7 +143,7 @@ func (p *testPlugin) GetContainerName() string {
return ""
}

func (p *testPlugin) GetConfigHash(_ starboard.PluginContext) (string, error) {
func (p *testPlugin) ConfigHash(_ starboard.PluginContext, _ kube.Kind) (string, error) {
return p.configHash, nil
}

Expand Down
18 changes: 9 additions & 9 deletions pkg/configauditreport/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ type Plugin interface {
// to read logs from.
GetContainerName() string

// GetConfigHash returns hash of the plugin's configuration settings. The computed hash
// is used to invalidate v1alpha1.ConfigAuditReport object whenever configuration changes.
GetConfigHash(ctx starboard.PluginContext) (string, error)
// ConfigHash returns hash of the plugin's configuration settings. The computed hash
// is used to invalidate v1alpha1.ConfigAuditReport and v1alpha1.ClusterConfigAuditReport
// objects whenever configuration applicable to the specified resource kind changes.
ConfigHash(ctx starboard.PluginContext, kind kube.Kind) (string, error)

// SupportsKind returns true if the given resource kind is supported by
// this plugin, false otherwise.
SupportsKind(kind kube.Kind) bool
// SupportedKinds returns kinds supported by this plugin.
SupportedKinds() []kube.Kind

// IsReady returns true if the plugin is ready for reconciliation, false
// otherwise.
IsReady(ctx starboard.PluginContext) (bool, error)
// IsApplicable return true if the given object can be scanned by this
// plugin, false otherwise.
IsApplicable(ctx starboard.PluginContext, obj client.Object) (bool, string, error)
}
19 changes: 18 additions & 1 deletion pkg/configauditreport/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,22 @@ func NewScanner(
}

func (s *Scanner) Scan(ctx context.Context, partial kube.Object) (*ReportBuilder, error) {
if !s.plugin.SupportsKind(partial.Kind) {
if !s.supportsKind(partial.Kind) {
return nil, fmt.Errorf("kind %s is not supported by %s plugin", partial.Kind, s.pluginContext.GetName())
}
obj, err := s.objectResolver.GetObjectFromPartialObject(ctx, partial)
if err != nil {
return nil, err
}

applicable, reason, err := s.plugin.IsApplicable(s.pluginContext, obj)
if err != nil {
return nil, err
}
if !applicable {
return nil, fmt.Errorf("not applicable: %s", reason)
}

owner, err := s.objectResolver.ReportOwner(ctx, obj)
if err != nil {
return nil, err
Expand Down Expand Up @@ -127,3 +135,12 @@ func (s *Scanner) Scan(ctx context.Context, partial kube.Object) (*ReportBuilder
PluginConfigHash(pluginConfigHash).
Data(result), nil
}

func (s *Scanner) supportsKind(kind kube.Kind) bool {
for _, k := range s.plugin.SupportedKinds() {
if k == kind {
return true
}
}
return false
}
42 changes: 26 additions & 16 deletions pkg/operator/controller/configauditreport.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func (r *ConfigAuditReportReconciler) SetupWithManager(mgr ctrl.Manager) error {
}

for _, resource := range resources {
if !r.Plugin.SupportsKind(resource.kind) {
if !r.supportsKind(resource.kind) {
r.Logger.Info("Skipping unsupported kind", "pluginName", r.PluginContext.GetName(), "kind", resource.kind)
continue
}
Expand All @@ -94,7 +94,7 @@ func (r *ConfigAuditReportReconciler) SetupWithManager(mgr ctrl.Manager) error {
}

for _, resource := range clusterResources {
if !r.Plugin.SupportsKind(resource.kind) {
if !r.supportsKind(resource.kind) {
r.Logger.Info("Skipping unsupported kind", "pluginName", r.PluginContext.GetName(), "kind", resource.kind)
continue
}
Expand All @@ -120,23 +120,19 @@ func (r *ConfigAuditReportReconciler) SetupWithManager(mgr ctrl.Manager) error {
Complete(r.reconcileJobs())
}

func (r *ConfigAuditReportReconciler) supportsKind(kind kube.Kind) bool {
for _, k := range r.Plugin.SupportedKinds() {
if k == kind {
return true
}
}
return false
}

func (r *ConfigAuditReportReconciler) reconcileResource(resourceKind kube.Kind) reconcile.Func {
return func(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.Logger.WithValues("kind", resourceKind, "name", req.NamespacedName)

ready, err := r.Plugin.IsReady(r.PluginContext)
if err != nil {
return ctrl.Result{}, fmt.Errorf("checking whether plugin is ready: %w", err)
}
if !ready {
log.V(1).Info("Pushing back reconcile key",
"reason", "plugin not ready",
"pluginName", r.PluginContext.GetName(),
"retryAfter", r.ScanJobRetryAfter)
// TODO Introduce more generic param to retry processing a given key.
return ctrl.Result{RequeueAfter: r.Config.ScanJobRetryAfter}, nil
}

resourcePartial := kube.GetPartialObjectFromKindAndNamespacedName(resourceKind, req.NamespacedName)

log.V(1).Info("Getting resource from cache")
Expand Down Expand Up @@ -169,12 +165,26 @@ func (r *ConfigAuditReportReconciler) reconcileResource(resourceKind kube.Kind)
}
}

// Skip processing if plugin is not applicable to this object
applicable, reason, err := r.Plugin.IsApplicable(r.PluginContext, resource)
if err != nil {
return ctrl.Result{}, fmt.Errorf("checking whether plugin is applicable: %w", err)
}
if !applicable {
log.V(1).Info("Pushing back reconcile key",
"reason", reason,
"pluginName", r.PluginContext.GetName(),
"retryAfter", r.ScanJobRetryAfter)
// TODO Introduce more generic param to retry processing a given key.
return ctrl.Result{RequeueAfter: r.Config.ScanJobRetryAfter}, nil
}

resourceSpecHash, err := kube.ComputeSpecHash(resource)
if err != nil {
return ctrl.Result{}, fmt.Errorf("computing spec hash: %w", err)
}

pluginConfigHash, err := r.Plugin.GetConfigHash(r.PluginContext)
pluginConfigHash, err := r.Plugin.ConfigHash(r.PluginContext, kube.Kind(resource.GetObjectKind().GroupVersionKind().Kind))
if err != nil {
return ctrl.Result{}, fmt.Errorf("computing plugin config hash: %w", err)
}
Expand Down
Loading

0 comments on commit 7ef9060

Please sign in to comment.