Jednorázové tajné údaje v Docker Swarm pomocí FIFO kanálů
Docker Swarm připojuje tajné údaje v /run/secrets/ jako read-only tmpfs. Kdokoli s přístupem k docker exec je může kdykoli přečíst. Řešením je použití pojmenovaných kanálů (FIFO) pro jednorázový přenos tajných údajů při spuštění kontejneru.
Problémy standardního mechanismu
Tajné údaje jsou uloženy v Raft logu manažerských uzlů, přenášeny přes mTLS na workery a připojeny do kontejneru. Definice v docker-compose.yml:
version: "3.8"
services:
app:
image: myapp:latest
secrets:
- db_password
- api_key
secrets:
db_password:
external: true
api_key:
external: true
Čtení v aplikaci:
with open("/run/secrets/db_password") as f:
db_password = f.read().strip()
Přístup přes docker exec:
$ docker exec -it <container_id> cat /run/secrets/db_password
SuperSecretPassword123
Přístup na hostiteli:
sudo cat /var/lib/docker/containers/<container_id>/mounts/secrets/ioz4pc6q8fjsfhem4pwsr35u6
SuperSecretPassword123
Proč standardní metody nefungují
- rm /truncate/chmod: read-only tmpfs
- umount: vyžaduje CAP_SYS_ADMIN
- Proměnné prostředí: viditelné v
/proc/1/environ, děděné podřízenými procesy - Externí správci (Vault): dodatečná infrastruktura
Princip fungování FIFO
Pojmenovaný kanál:
- Zápis je blokován bez čtečky
- Čtení je blokováno bez zapisovače
- Data jsou spotřebována při prvním čtení
- Opakované čtení zůstane zablokováno
Demonstrace:
$ mkfifo /tmp/my_pipe
echo "secret_value" > /tmp/my_pipe # blokuje se
cat /tmp/my_pipe # secret_value, data jsou odstraněna
cat /tmp/my_pipe # blokuje se
Architektura řešení
Dva kontejnery, sdílený tmpfs svazek s FIFO:
- secret-injector: vytvoří FIFO, zapíše tajné údaje a ukončí se
- app: čte z FIFO jednorázově, tajné údaje pouze v paměti
Kontejner app nemá /run/secrets/.
Implementace
docker-compose.yml
version: "3.8"
services:
secret-injector:
image: alpine:3.19
secrets:
- db_password
- api_key
volumes:
- secrets_pipe:/shared
entrypoint: ["sh", "-c", "
mkfifo /shared/db_password /shared/api_key &&
cat /run/secrets/db_password > /shared/db_password &
cat /run/secrets/api_key > /shared/api_key &
wait
"]
deploy:
restart_policy:
condition: none
resources:
limits:
memory: 16M
app:
image: myapp:latest
volumes:
- secrets_pipe:/shared:ro
entrypoint: ["sh", "-c", "
DB_PASSWORD=$(cat /shared/db_password) &&
API_KEY=$(cat /shared/api_key) &&
exec myapp
"]
depends_on:
- secret-injector
volumes:
secrets_pipe:
driver: local
driver_opts:
type: tmpfs
device: tmpfs
o: size=1m,noexec,nosuid
secrets:
db_password:
external: true
api_key:
external: true
Posloupnost provedení
- secret-injector:
- mkfifo /shared/db_password /shared/api_key
- cat /run/secrets/... > /shared/... (blokuje se)
- wait udržuje kontejner
- app:
- DB_PASSWORD=$(cat /shared/db_password) → odblokuje zapisovače
- Data jsou spotřebována z FIFO
- secret-injector se ukončí (
restart_policy: none)
- app běží s tajnými údaji v paměti
Výsledek pro útočníka
V app:
$ docker exec -it <app_container> sh
$ ls /run/secrets/
ls: /run/secrets/: No such file or directory
$ cat /shared/db_password # blokuje se
^C
V secret-injector:
$ docker exec -it <injector_container> sh
Error: container is not running
Zvýšení bezpečnosti: souborové deskriptory
Vyhýbejte se proměnným prostředí. Přenášejte přes FD:
Entrypoint:
#!/bin/sh
exec 3< /shared/db_password
exec 4< /shared/api_key
exec myapp --db-password-fd=3 --api-key-fd=4
Python:
import os
def read_secret_from_fd(fd_num: int) -> str:
with os.fdopen(fd_num, 'r') as f:
return f.read().strip()
db_password = read_secret_from_fd(3)
api_key = read_secret_from_fd(4)
Go:
package main
import (
"fmt"
"io"
"os"
)
func readSecretFromFD(fd int) (string, error) {
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd-%d", fd))
if f == nil {
return "", fmt.Errorf("invalid file descriptor: %d", fd)
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return "", err
}
return string(data), nil
}
Tajné údaje existují pouze v paměti procesu.
Co je důležité
- Tajné údaje jsou přečteny jednou přes FIFO, data jsou spotřebována
- Kontejner
secret-injectorse ukončí,docker execnení možný - App-kontejner nemá
/run/secrets/ - Používejte souborové deskriptory místo proměnných prostředí
- Řešení funguje bez externích služeb
— Editorial Team
Zatím žádné komentáře.