Add a playbook which starts to capturing data using Sysdig and uploads capture to a s3 bucket (#414)

* Fix spec name

* Add a playbook for capturing stuff using sysdig in a container

* Add event-name to job name for avoid collisions among captures

* Implement job for starting container in Pod in Kubernetes Client

We are going to pick data for all Pod, not limited to one container

* Use sysdig/capturer image for capture and upload to s3 the capture

* There is a bug with environment string splitting in kubeless

https://github.com/kubeless/kubeless/issues/824

So here is a workaround which uses multiple --env flags, one for each
environment.

* Use shorter job name. Kubernetes limit is 64 characters.

* Add a deployable playbook with Kubeless for capturing stuff with Sysdig

* Document the integration with Sysdig capture

* Add Dockerfile for creating sysdig-capturer
This commit is contained in:
Néstor Salceda 2018-10-12 01:55:40 +02:00 committed by Mark Stemm
parent f746c4cd57
commit e4ffa55d58
9 changed files with 309 additions and 4 deletions

View File

@ -178,3 +178,24 @@ This playbook creates an incident in Demisto
* VERIFY_SSL: Verify SSL certificates for HTTPS requests. By default is enabled. * VERIFY_SSL: Verify SSL certificates for HTTPS requests. By default is enabled.
In this example, when Falco raises any kind of alert, the alert will be created in Demisto In this example, when Falco raises any kind of alert, the alert will be created in Demisto
### Start a capture using Sysdig
This playbook starts to capture information about pod using sysdig and uploads
to a s3 bucket.
```
$ ./deploy_playbook -p capture -e CAPTURE_DURATION=300 -e AWS_S3_BUCKET=s3://xxxxxxx -e AWS_ACCESS_KEY_ID=xxxxXXXxxXXxXX -e AWS_SECRET_ACCESS_KEY=xxXxXXxxxxXXX -t "falco.notice.terminal_shell_in_container"
```
#### Parameters:
* CAPTURE_DURATION: Captures data for this duration in seconds. By default is
120 seconds (2 minutes)
* AWS_S3_BUCKET: This is the bucket where data is going to be uploaded. Jobs
starts with sysdig- prefix and contain pod name and time where event starts.
* AWS_ACCESS_KEY_ID: This is the Amazon access key id.
* AWS_SECRET_ACCESS_KEY: This is the Amazon secret access key.
In this example, when we detect a shell in a container, we start to collect data
for 300 seconds. This playbook requires permissions for creating a new pod from
a Kubeless function.

View File

@ -0,0 +1,20 @@
import sys
import os.path
sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__))))
import os
import playbooks
from playbooks import infrastructure
playbook = playbooks.StartSysdigCaptureForContainer(
infrastructure.KubernetesClient(),
int(os.environ.get('CAPTURE_DURATION', 120)),
os.environ['AWS_S3_BUCKET'],
os.environ['AWS_ACCESS_KEY_ID'],
os.environ['AWS_SECRET_ACCESS_KEY']
)
def handler(event, context):
playbook.run(event['data'])

View File

@ -21,7 +21,11 @@ EOF
exit 1 exit 1
} }
function join { local IFS="$1"; shift; echo "$*"; } function create_environment_flags {
for env in ${environment[*]}; do
echo "--env ${env} "
done
}
playbook="" playbook=""
environment=() environment=()
@ -54,7 +58,7 @@ zip "${playbook}".zip -r playbooks/*.py "${playbook}".py
kubeless function deploy --from-file "${playbook}".zip \ kubeless function deploy --from-file "${playbook}".zip \
--dependencies requirements.txt \ --dependencies requirements.txt \
--env "$(join , ${environment[*]})" \ $(create_environment_flags ${environment[*]}) \
--runtime python3.6 \ --runtime python3.6 \
--handler "${playbook}".handler \ --handler "${playbook}".handler \
falco-"${playbook}" falco-"${playbook}"

View File

@ -137,3 +137,24 @@ class CreateIncidentInDemisto:
'Informational': 5, 'Informational': 5,
'Debug': 5, 'Debug': 5,
} }
class StartSysdigCaptureForContainer:
def __init__(self, k8s_client, duration_in_seconds, s3_bucket,
aws_access_key_id, aws_secret_access_key):
self._k8s_client = k8s_client
self._duration_in_seconds = duration_in_seconds
self._s3_bucket = s3_bucket
self._aws_access_key_id = aws_access_key_id
self._aws_secret_access_key = aws_secret_access_key
def run(self, alert):
pod = alert['output_fields']['k8s.pod.name']
event_time = alert['output_fields']['evt.time']
self._k8s_client.start_sysdig_capture_for(pod,
event_time,
self._duration_in_seconds,
self._s3_bucket,
self._aws_access_key_id,
self._aws_secret_access_key)

View File

@ -14,6 +14,7 @@ class KubernetesClient:
config.load_incluster_config() config.load_incluster_config()
self._v1 = client.CoreV1Api() self._v1 = client.CoreV1Api()
self._batch_v1 = client.BatchV1Api()
def delete_pod(self, name): def delete_pod(self, name):
namespace = self._find_pod_namespace(name) namespace = self._find_pod_namespace(name)
@ -65,6 +66,152 @@ class KubernetesClient:
return self._v1.patch_namespaced_pod(name, namespace, body) return self._v1.patch_namespaced_pod(name, namespace, body)
def start_sysdig_capture_for(self, pod_name, event_time,
duration_in_seconds, s3_bucket,
aws_access_key_id, aws_secret_access_key):
job_name = 'sysdig-{}-{}'.format(pod_name, event_time)
node_name = self.find_node_running_pod(pod_name)
namespace = self._find_pod_namespace(pod_name)
body = self._build_sysdig_capture_job_body(job_name,
node_name,
duration_in_seconds,
s3_bucket,
aws_access_key_id,
aws_secret_access_key)
return self._batch_v1.create_namespaced_job(namespace, body)
def _build_sysdig_capture_job_body(self, job_name, node_name,
duration_in_seconds, s3_bucket,
aws_access_key_id, aws_secret_access_key):
return client.V1Job(
metadata=client.V1ObjectMeta(
name=job_name
),
spec=client.V1JobSpec(
template=client.V1PodTemplateSpec(
metadata=client.V1ObjectMeta(
name=job_name
),
spec=client.V1PodSpec(
containers=[client.V1Container(
name='capturer',
image='sysdig/capturer',
image_pull_policy='Always',
security_context=client.V1SecurityContext(
privileged=True
),
env=[
client.V1EnvVar(
name='AWS_S3_BUCKET',
value=s3_bucket
),
client.V1EnvVar(
name='CAPTURE_DURATION',
value=str(duration_in_seconds)
),
client.V1EnvVar(
name='CAPTURE_FILE_NAME',
value=job_name
),
client.V1EnvVar(
name='AWS_ACCESS_KEY_ID',
value=aws_access_key_id,
),
client.V1EnvVar(
name='AWS_SECRET_ACCESS_KEY',
value=aws_secret_access_key,
)
],
volume_mounts=[
client.V1VolumeMount(
mount_path='/host/var/run/docker.sock',
name='docker-socket'
),
client.V1VolumeMount(
mount_path='/host/dev',
name='dev-fs'
),
client.V1VolumeMount(
mount_path='/host/proc',
name='proc-fs',
read_only=True
),
client.V1VolumeMount(
mount_path='/host/boot',
name='boot-fs',
read_only=True
),
client.V1VolumeMount(
mount_path='/host/lib/modules',
name='lib-modules',
read_only=True
),
client.V1VolumeMount(
mount_path='/host/usr',
name='usr-fs',
read_only=True
),
client.V1VolumeMount(
mount_path='/dev/shm',
name='dshm'
)
]
)],
volumes=[
client.V1Volume(
name='dshm',
empty_dir=client.V1EmptyDirVolumeSource(
medium='Memory'
)
),
client.V1Volume(
name='docker-socket',
host_path=client.V1HostPathVolumeSource(
path='/var/run/docker.sock'
)
),
client.V1Volume(
name='dev-fs',
host_path=client.V1HostPathVolumeSource(
path='/dev'
)
),
client.V1Volume(
name='proc-fs',
host_path=client.V1HostPathVolumeSource(
path='/proc'
)
),
client.V1Volume(
name='boot-fs',
host_path=client.V1HostPathVolumeSource(
path='/boot'
)
),
client.V1Volume(
name='lib-modules',
host_path=client.V1HostPathVolumeSource(
path='/lib/modules'
)
),
client.V1Volume(
name='usr-fs',
host_path=client.V1HostPathVolumeSource(
path='/usr'
)
)
],
node_name=node_name,
restart_policy='Never'
)
)
)
)
class SlackClient: class SlackClient:
def __init__(self, slack_webhook_url): def __init__(self, slack_webhook_url):

View File

@ -1,8 +1,9 @@
from mamba import description, context, it, before from mamba import description, context, it, before
from expects import expect, be_false, be_true, start_with, equal, have_key from expects import expect, be_false, be_true, start_with, equal, have_key, be_none
import subprocess import subprocess
import os.path import os.path
import time
from playbooks import infrastructure from playbooks import infrastructure
@ -46,7 +47,7 @@ with description(infrastructure.KubernetesClient) as self:
expect(node.spec.taints[0].key).to(equal('playbooks')) expect(node.spec.taints[0].key).to(equal('playbooks'))
expect(node.spec.taints[0].value).to(equal('true')) expect(node.spec.taints[0].value).to(equal('true'))
with it('adds labels to a pod'): with it('adds label to a pod'):
self._create_nginx_pod() self._create_nginx_pod()
pod = self.kubernetes_client.add_label_to_pod('nginx', pod = self.kubernetes_client.add_label_to_pod('nginx',
@ -55,6 +56,18 @@ with description(infrastructure.KubernetesClient) as self:
expect(pod.metadata.labels).to(have_key('testing', 'true')) expect(pod.metadata.labels).to(have_key('testing', 'true'))
with it('starts sysdig capture for'):
self._create_nginx_pod()
job = self.kubernetes_client.start_sysdig_capture_for('nginx',
int(time.time()),
10,
'any s3 bucket',
'any aws key id',
'any aws secret key')
expect(job).not_to(be_none)
def _create_nginx_pod(self): def _create_nginx_pod(self):
current_directory = os.path.dirname(os.path.realpath(__file__)) current_directory = os.path.dirname(os.path.realpath(__file__))
pod_manifesto = os.path.join(current_directory, pod_manifesto = os.path.join(current_directory,

View File

@ -0,0 +1,40 @@
from mamba import description, it, before
from expects import expect
from doublex import Spy
from doublex_expects import have_been_called_with
from playbooks import infrastructure
import playbooks
with description(playbooks.StartSysdigCaptureForContainer) as self:
with before.each:
self.k8s_client = Spy(infrastructure.KubernetesClient)
self.duration_in_seconds = 'any duration in seconds'
self.s3_bucket = 'any s3 bucket url'
self.aws_access_key_id = 'any aws access key id'
self.aws_secret_access_key = 'any aws secret access key'
self.playbook = playbooks.StartSysdigCaptureForContainer(self.k8s_client,
self.duration_in_seconds,
self.s3_bucket,
self.aws_access_key_id,
self.aws_secret_access_key)
with it('add starts capturing job in same node than Pod alerted'):
pod_name = 'any pod name'
event_time = 'any event time'
alert = {'output_fields': {
'k8s.pod.name': pod_name,
'evt.time': event_time,
}}
self.playbook.run(alert)
expect(self.k8s_client.start_sysdig_capture_for)\
.to(have_been_called_with(pod_name,
event_time,
self.duration_in_seconds,
self.s3_bucket,
self.aws_access_key_id,
self.aws_secret_access_key))

View File

@ -0,0 +1,24 @@
FROM sysdig/sysdig:latest
MAINTAINER Néstor Salceda <nestor.salceda@sysdig.com>
RUN apt-get update \
&& apt-get --fix-broken install -y \
&& apt-get install -y --no-install-recommends \
s3cmd \
&& rm -rf /var/lib/apt/lists/*
# debian:unstable head contains binutils 2.31, which generates
# binaries that are incompatible with kernels < 4.16. So manually
# forcibly install binutils 2.30-22 instead.
RUN curl -s -o binutils_2.30-22_amd64.deb http://snapshot.debian.org/archive/debian/20180622T211149Z/pool/main/b/binutils/binutils_2.30-22_amd64.deb \
&& curl -s -o libbinutils_2.30-22_amd64.deb http://snapshot.debian.org/archive/debian/20180622T211149Z/pool/main/b/binutils/libbinutils_2.30-22_amd64.deb \
&& curl -s -o binutils-x86-64-linux-gnu_2.30-22_amd64.deb http://snapshot.debian.org/archive/debian/20180622T211149Z/pool/main/b/binutils/binutils-x86-64-linux-gnu_2.30-22_amd64.deb \
&& curl -s -o binutils-common_2.30-22_amd64.deb http://snapshot.debian.org/archive/debian/20180622T211149Z/pool/main/b/binutils/binutils-common_2.30-22_amd64.deb \
&& dpkg -i *binutils*.deb
ENV CAPTURE_DURATION 120
COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]

View File

@ -0,0 +1,15 @@
#!/bin/bash
set -exuo
echo "* Setting up /usr/src links from host"
for i in $(ls $SYSDIG_HOST_ROOT/usr/src)
do
ln -s $SYSDIG_HOST_ROOT/usr/src/$i /usr/src/$i
done
/usr/bin/sysdig-probe-loader
sysdig -S -M $CAPTURE_DURATION -pk -z -w $CAPTURE_FILE_NAME.scap.gz
s3cmd --access_key=$AWS_ACCESS_KEY_ID --secret_key=$AWS_SECRET_ACCESS_KEY put $CAPTURE_FILE_NAME.scap.gz $AWS_S3_BUCKET