Isolierte Prozessausführung (Sandbox)
auf Basis von »Linux Containers (LXC)«
Wesentlich effizienter als VMs/Hypervisoren
Portables Format für Container mit
Versionierung
Leichtgewichtige Laufzeit- und Packaging-Tools
Cloud-Repository für Container-Vorlagen
**Build** Once, **Configure** Once and **Run Anywhere**
(Some) Key Objectives
für die Pflege von Container Images
1. Performance & Size
1. Security & Update-Management
1. Diagnostik & Robustheit
1. Einhaltung von Best Practices
1. Sicherer Betrieb
# Image-Diät
Images bauen: Dockerfile
FROM openjdk:slim
MAINTAINER Inspector Gadget
# Kommando im Container ausführen
RUN apt-get update && apt-get install unzip -y && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# JAR und Config-File in Image aufnehmen
WORKDIR /opt
ADD target/rest-microservice-1.0.0.jar app.jar
ADD src/main/resources/example.yml app.yml
# announce exported port 8080 and 8081
EXPOSE 8080 8081
# Wichtig: Separate Volumes für Daten
VOLUME ["/srv/"]
# JAR ausführen beim Start des Containers
ENTRYPOINT java -jar app.jar server
Layers & Union File System
- Filesystem aus gestapelten **Layern** (Union File System)
- **Images** sind schreibgeschützte Layers & Meta
- Schreibbarer Layer dazu ➱ **Container**.
- Dockerfile: Jeder Schritt = **neuer Layer** ➱ `docker history`
- **Versionierung:** Layer-ID = Container/Image ID ➱ `ac408c338`
### Weitere Mittel Gewicht zu verlieren…
* Größe des Basis-Image, z.B. **Google Project "Distroless"**
`openjdk:slim` → 426 MB
`gcr.io/distroless/java:11` → 228MB
* Tools wie `docker-slim` oder `jlink`
* Layers zusammenfassen mit `docker build --squash …`
* **Multistage Builds** für _Build_- vs. _Run_-Container
# Sicher & Aktuell
## Neue Challenges
Als Entwickler / DevOps Engineer Pflege-Verantwortung für **alle** Bestandteile des Images.
→ Prozess-Behandlung notwendig
* Regelmäßige (Security)-Releases
* (Daily) Dependency Scanning
z.B. via `renovatebot` oder _Gitlab SAST_
* (Daily) Container Scanning mit `trivy` oder `grype`
* Sorgfältige Auswahl des Basis-Image →
_Weniger ist mehr!_
## Logging & Konfiguration
* Logging per Default _unbuffered_ nach `STDOUT` und `STDERR`
* Abwägung: Logging als JSON
* Konfiguration über Umgebungsvariablen (zur Not: Volumes) bzw. _secrets_ und _configs_
```shell
$ docker run --env VAR1=val1 --env VAR2=val2 …
$ docker run --env-file env.list …
```
## Datenhaltung
* Keine Datenhaltung im Container
→ Container sind Wegwerfware & zustandslos
* Datenhaltung exklusiv in Volumes oder Services
* Idealfall: Filesystem des Containers ist r/o:
```
docker run … --read-only
--tmpfs /run:rw myservice
```
## Health-Checks
Endpunkte für Health (`/health`), Ready (`/ready`)
und ggf. Metriken (`/metrics`) anbieten.
Anbindung z.B. im `Dockerfile`:
```Docker
HEALTHCHECK --interval=60s --timeout=3s \
--start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/v1/health || exit 1
```
# Angreifer zähmen!
## Muss es immer `root` sein?
* Docker-Daemon: völlige Kontrolle über den Host
* Container: per Default viele Privilegien
Um den Impact einer RCE zu begrenzen, Applikationen im Container mittels `USER`
als _nicht-privilegiert, nicht `sudo`-fähig_ betreiben:
```
FROM alpine
# Create user and set ownership and permissions as required
RUN adduser -D myuser && chown -R myuser /myapp
USER myuser
ENTRYPOINT ["/myapp/app"]
```
## Augen auf bei der Image-Wahl!
* _verfified_ und _official_?
* Noch supported & regelmäßige Security Updates?
* ggf. `Dockerfile` der Quelle geprüft?
* Namen & Tags sind Schall & Rauch!
→ Trusted Sourcen oder SHA1 als Referenz
→ `latest` kann Pitfall sein
## PID 1, Signale & Zombie-Prozesse
* Sterben Kind-Prozesse im Container, müssen diese abgeräumt werden → _init_-Prozess
* `docker run --init …`
* Nur ein Prozess direkt als `ENTRYPOINT`
* Init-System wie _tini_ einbinden
* Dieser Prozess sollte auch Signale wie `SIGTERM` & Co. behandeln!
* Ideal: Ein Prozess pro Image
## Weitere Best-Practices
* `Dockerfile` linten,
z.B. via https://hadolint.github.io/hadolint/
* `.dockerignore` nutzen; `COPY` over `ADD` bevorzugen
* Docker Build-Cache verstehen, nutzen & steuern
* **Immutable** is key
* Konfiguration _vollständig_ extern halten
**Microservice Frameworks**
- **Quarkus.io**
- **Micronaut.io**
- **Helidon**
- Spring Fu
- Ktor
- Spark Framework
- Dropwizard
- Spring Boot
- Vert.x
## Docker Daemon & `docker.sock`
Zugriff auf `docker.socket` ist effektiv voller root-Zugriff
* möglichst Zugriff _nur_ für `127.0.0.1` und _nur_ für `root`
* Wenn's sein muss: TCP-Zugriff stabil absichern!
* Alternative _podman_ für Rootless-Betrieb
## Privilegien & Limits
By default weitreichende Privilegien & _Capabilities_ der Container über den Host!
Zusätzliches `--privileged` ist quasi völlige Sandbox-Aufgabe.
→ Es empfiehlt sich daher Container stets
mit **minimalen Capabilities** und **Ressource-Limits** zu starten:
```bash
$ docker run --cap-drop all --cap-add … \
--security-opt="no-new-privileges" \
--memory="1g" --memory-swap="1g" \
--cpus="2.5" --cpu-shares="2048"
```
## `docker-bench` kann Tipps geben
Im laufenden Betrieb z.B. Tools vie Docker-Bench nutzen,
die über Heuristiken Empfehlungen für das Hardening anbieten
```bash
$ sudo docker run --rm --net host --pid host \
--userns host --cap-add audit_control \
-v /etc:/etc:ro \
-v /lib/systemd/system:/lib/systemd/system:ro \
-v /usr/bin/containerd:/usr/bin/containerd:ro \
-v /usr/bin/runc:/usr/bin/runc:ro \
-v /usr/lib/systemd:/usr/lib/systemd:ro \
-v /var/lib:/var/lib:ro \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
--label docker_bench_security \
docker/docker-bench-security
```