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
12 changes: 5 additions & 7 deletions experiments/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@ experiments:

## Available Experiments

| Type | Description | Framework |
|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|-----------|
| [privileged-container](run-privileged-container.yaml) | This experiment attempts to run a privileged container in a namespace | MITRE |
| [host-path-mount](host-path-mount.yaml) | This experiment attempts to mount a sensitive host filesystem path into a container | MITRE |
| [cluster-admin-binding](cluster-admin-binding.yaml) | This experiment attempts to create a container with the cluster-admin role binding attached | MITRE |
| [remote-execute-api](remote-execute-api.yaml) | This experiment attempts to create a deployment with a configurable image and verifies based off of API calls to the image | MITRE |
| [execute-api](execute-api.yaml) | This experiment attempts to call a service with a payload | MITRE |
For a list of available experiments run `secops-chaos experiment`. You can then generate a example template to get started:

```sh
secops-chaos experiment snippet -e <experiment-type>
```

## Implementing a new Experiment

Expand Down
18 changes: 12 additions & 6 deletions experiments/privileged-container.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ experiments:
type: privileged-container
namespace: default
parameters:
image: "alpine:latest"
command: [ "sh", "-c", "while true; do :; done"]
privileged: true
hostPid: true
hostNetwork: true
runAsRoot: true
experiment:
image: "alpine:latest"
command: [ "sh", "-c", "while true; do :; done"]
privileged: true
hostPid: true
hostNetwork: true
runAsRoot: true
verifier:
deployed: true
command:
- cat
- "/tmp/malicious-activity-log"
4 changes: 2 additions & 2 deletions internal/experiments/experiments.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func NewRunner(ctx context.Context, experimentFiles []string) *Runner {
func (r *Runner) Run() {
for _, e := range r.experimentsConfig {
experiment := r.experiments[e.Metadata.Type]
output.WriteInfo("Running experiment %s\n", e.Metadata.Name)
output.WriteInfo("Running experiment %s", e.Metadata.Name)
if err := experiment.Run(r.ctx, r.client, e); err != nil {
output.WriteError("Experiment %s failed with error: %s", e.Metadata.Name, err)
}
Expand Down Expand Up @@ -146,7 +146,7 @@ func (r *Runner) RunVerifiers(outputFormat string) {
// Cleanup cleans up all experiments in the Runner
func (r *Runner) Cleanup() {
for _, e := range r.experimentsConfig {
output.WriteInfo("Cleaning up experiment %s\n", e.Metadata.Name)
output.WriteInfo("Cleaning up experiment %s", e.Metadata.Name)
experiment := r.experiments[e.Metadata.Type]
if err := experiment.Cleanup(r.ctx, r.client, e); err != nil {
output.WriteError("Experiment %s cleanup failed: %s", e.Metadata.Name, err)
Expand Down
103 changes: 61 additions & 42 deletions internal/experiments/experiments_privileged_container.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@ type PrivilegedContainerExperimentConfig struct {

// PrivilegedContainer is an experiment that creates a deployment with a privileged container
type PrivilegedContainer struct {
Image string `yaml:"image"`
Command []string `yaml:"command"`
Privileged bool `yaml:"privileged"`
HostPid bool `yaml:"hostPid"`
HostNetwork bool `yaml:"hostNetwork"`
RunAsRoot bool `yaml:"runAsRoot"`
Experiment struct {
Image string `yaml:"image"`
Command []string `yaml:"command"`
Privileged bool `yaml:"privileged"`
HostPid bool `yaml:"hostPid"`
HostNetwork bool `yaml:"hostNetwork"`
RunAsRoot bool `yaml:"runAsRoot"`
} `yaml:"experiment"`
Verifier struct {
DeployedSuccessfully bool `yaml:"deployed"`
Command []string `yaml:"command"`
} `yaml:"verifier"`
}

func (p *PrivilegedContainerExperimentConfig) Type() string {
Expand Down Expand Up @@ -62,9 +68,10 @@ func (p *PrivilegedContainerExperimentConfig) Run(ctx context.Context, client *k
return err
}

if config.Parameters.Image == "" && len(config.Parameters.Command) == 0 {
config.Parameters.Image = "alpine:latest"
config.Parameters.Command = []string{
params := config.Parameters.Experiment
if params.Image == "" && len(params.Command) == 0 {
params.Image = "alpine:latest"
params.Command = []string{
"sh",
"-c",
"while true; do :; done",
Expand Down Expand Up @@ -95,22 +102,21 @@ func (p *PrivilegedContainerExperimentConfig) Run(ctx context.Context, client *k
},
},
Spec: corev1.PodSpec{
HostNetwork: config.Parameters.HostNetwork,
HostPID: config.Parameters.HostPid,
HostNetwork: params.HostNetwork,
HostPID: params.HostPid,
Containers: []corev1.Container{
{
Name: config.Metadata.Name,
Image: config.Parameters.Image,
Image: params.Image,
ImagePullPolicy: corev1.PullAlways,
Command: config.Parameters.Command,
Command: params.Command,
},
},
},
},
},
}

params := config.Parameters
container := deployment.Spec.Template.Spec.Containers[0]
securityContext := &corev1.SecurityContext{}
if params.RunAsRoot {
Expand Down Expand Up @@ -151,48 +157,61 @@ func (p *PrivilegedContainerExperimentConfig) Verify(ctx context.Context, client
config.Technique(),
)

if params.HostPid {
verifier.Fail("HostPID")
if deployment.Spec.Template.Spec.HostPID {
verifier.Success("HostPID")
}
}

if params.HostNetwork {
verifier.Fail("HostNetwork")
if deployment.Spec.Template.Spec.HostNetwork {
verifier.Success("HostNetwork")
}
}

// Find the container by name, as it may not be the first container in the list due to sidecar injection
container, err := k8s.FindContainerByName(deployment.Spec.Template.Spec.Containers, config.Metadata.Name)
container, err := client.FindContainerByName(deployment.Spec.Template.Spec.Containers, config.Metadata.Name)
if err != nil {
return nil, err
}

if params.RunAsRoot {
verifier.Fail("RunAsRoot")
if container.SecurityContext != nil {
if container.SecurityContext.RunAsUser != nil {
if *container.SecurityContext.RunAsUser == 0 {
verifier.Success("RunAsRoot")
if config.Parameters.Verifier.DeployedSuccessfully {
verifier.Success("Deployed")
if params.Experiment.HostPid {
if !deployment.Spec.Template.Spec.HostPID {
verifier.Fail("Deployed")
}
}

if params.Experiment.HostNetwork {
if !deployment.Spec.Template.Spec.HostNetwork {
verifier.Fail("Deployed")
}
}

if params.Experiment.RunAsRoot {
if container.SecurityContext != nil {
if container.SecurityContext.RunAsUser != nil {
if *container.SecurityContext.RunAsUser != 0 {
verifier.Fail("Deployed")
}
}
}
}
}

if params.Privileged {
verifier.Fail("Privileged")
if container.SecurityContext != nil {
if container.SecurityContext.Privileged != nil {
if *container.SecurityContext.Privileged {
verifier.Success("Privileged")
if params.Experiment.Privileged {
if container.SecurityContext != nil {
if container.SecurityContext.Privileged != nil {
if !*container.SecurityContext.Privileged {
verifier.Fail("Deployed")
}
}
}
}
}

if len(params.Verifier.Command) > 0 {
verifier.Success("Command")
pods, err := client.GetDeploymentsPods(ctx, config.Metadata.Namespace, deployment)
if err != nil {
return nil, err
}
for _, pod := range pods {
_, _, err := client.ExecuteRemoteCommand(ctx, config.Metadata.Namespace, pod.Name, container.Name, config.Parameters.Verifier.Command)
if err != nil {
verifier.Fail("Command")
}
}
}

return verifier.GetOutcome(), nil
}

Expand Down
28 changes: 27 additions & 1 deletion internal/k8s/helpers.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,46 @@
package k8s

import (
"context"
"errors"

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// ErrContainerNotFound is returned when a container is not found
var ErrContainerNotFound = errors.New("container not found")

// FindContainerByName returns a container by name from a list of containers
func FindContainerByName(containers []corev1.Container, containerName string) (corev1.Container, error) {
func (c *Client) FindContainerByName(containers []corev1.Container, containerName string) (corev1.Container, error) {
for _, container := range containers {
if container.Name == containerName {
return container, nil
}
}
return corev1.Container{}, ErrContainerNotFound
}

// GetDeploymentsPods gets the pods belonging to the provided deployment
func (c *Client) GetDeploymentsPods(ctx context.Context, namespace string, deployment *appsv1.Deployment) ([]corev1.Pod, error) {
rsList, err := c.Clientset.AppsV1().ReplicaSets(namespace).List(ctx, metav1.ListOptions{
LabelSelector: metav1.FormatLabelSelector(deployment.Spec.Selector),
})
if err != nil {
return nil, err
}
var podsList []corev1.Pod
for _, rs := range rsList.Items {
pods, err := c.Clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
LabelSelector: metav1.FormatLabelSelector(rs.Spec.Selector),
})
if err != nil {
return nil, err
}
for _, pod := range pods.Items {
podsList = append(podsList, pod)
}
}
return podsList, nil
}
3 changes: 2 additions & 1 deletion internal/k8s/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ func TestFindContainerByName(t *testing.T) {

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := FindContainerByName(test.containers, test.containerName)
client := Client{}
result, err := client.FindContainerByName(test.containers, test.containerName)

assert.Equal(t, test.expectedResult, result)
assert.Equal(t, test.expectedError, err)
Expand Down