Zpět na domů

FIFO pro jednorázové sekrety Docker Swarm

Článek popisuje metodu ochrany sekretů Docker Swarm přes pojmenované kanály FIFO. Sekrety se předávají jednorázově do aplikace, vylučuje přístup přes docker exec. Implementace na bázi dvou kontejnerů se sdíleným tmpfs svazkem.

Jednorázové sekrety Docker Swarm: FIFO proti docker exec
Advertisement 728x90

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:

Google AdInline article slot
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:

Google AdInline article slot
  • 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/.

Google AdInline article slot

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-injector se ukončí, docker exec není 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

Advertisement 728x90

Číst dál