Verified Commit 85ebfdbc authored by Kourser's avatar Kourser
Browse files

Composition : images épinglées, secret hors ligne de commande, jeton en lecture

Le secret de LiveKit était passé en argument « --keys », donc lisible par
« docker inspect », par le « ps » de l'hôte et par tout processus voisin qui
sait lire /proc. Il ouvre les salles de crise : il passe désormais par
l'environnement.

Toutes les images étaient sur des étiquettes mouvantes, trois sur « latest » :
deux démarrages à un mois d'écart ne partaient pas de la même image, et rien
ne disait laquelle tournait. Chacune porte maintenant sa version explicite et
l'empreinte de son manifeste multi-architecture — une empreinte propre à une
plateforme aurait cassé l'autre.

Seul le service applicatif interdisait l'élévation de privilèges ; les sept la
refusent. Celui d'encodage garde SYS_ADMIN, que le bac à sable de Chrome
exige et qu'on ne peut pas lui retirer — il est au moins encadré.

Trouvé au passage : « bitnami/minio:latest » a disparu de Docker Hub, Bitnami
ayant retiré ses images publiques. Le travail de vérification ne pouvait plus
tirer son image. Il utilise l'image officielle, lancée en étape parce que
celle-ci exige « server /data » en argument et que le bloc « services » de
GitHub Actions ne permet pas de passer de commande.

Et le jeton de la forge n'a plus que la lecture : aucun de ces travaux n'écrit
dans le dépôt.

Co-Authored-By: Claude (RCA)
Signed-off-by: default avatarJordan Grossemy <jordan.grossemy@rca.fr>
parent 0ca714c1
Loading
Loading
Loading
Loading
+40 −15
Original line number Diff line number Diff line
@@ -7,12 +7,18 @@ on:
    branches: [main]
  pull_request:

# Aucun de ces travaux n'écrit dans le dépôt : le jeton n'a donc pas à pouvoir
# le faire. Sans ce bloc, il hérite du réglage du dépôt — souvent « lecture et
# écriture », ce qui donne à toute action tierce de quoi pousser un commit.
permissions:
  contents: read

jobs:
  garanties:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:17-alpine
        image: postgres:17.11-alpine3.24@sha256:18cfe3ef5e6815560c98237d6216d1e5119702fb0f3894c8785dd58b8bbe5d73
        env:
          POSTGRES_USER: kastell
          POSTGRES_PASSWORD: verification
@@ -22,28 +28,47 @@ jobs:
          --health-interval 5s --health-timeout 3s --health-retries 20
        ports: ["5432:5432"]
      redis:
        image: redis:7-alpine
        image: redis:7.4.11-alpine3.21@sha256:ff02b58f971e7d7d156a1267e283fcbbeee91773b6aa36c49dac28ecfe28eadf
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 5s --health-timeout 3s --health-retries 20
        ports: ["6379:6379"]
      minio:
        image: bitnami/minio:latest
        env:
          MINIO_ROOT_USER: kastell
          MINIO_ROOT_PASSWORD: verification-minio
        ports: ["9000:9000"]

    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262   # v4.4.0
      - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1   # v4.3.0
      - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020   # v4.4.0
        with:
          node-version: 24
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm typecheck

      # Le stockage objet est lancé en étape et non comme « service ».
      #
      # Deux raisons, et la première est un incident : le travail utilisait
      # « bitnami/minio:latest », que Bitnami a retiré de Docker Hub en 2025 —
      # l'image ne se tirait plus, le travail ne pouvait plus démarrer. Ce qui
      # subsiste est « bitnamilegacy », une archive déclarée non maintenue :
      # y renvoyer la construction reviendrait à figer une image qui ne sera
      # plus corrigée.
      #
      # La seconde : l'image officielle exige « server /data » en argument, et
      # le bloc « services » ne permet pas de passer une commande. D'où
      # « docker run », qui le permet — et qui donne au passage la maîtrise de
      # l'attente de disponibilité.
      - name: Stockage objet pour la vérification
        run: |
          docker run -d --name minio -p 9000:9000 \
            -e MINIO_ROOT_USER=kastell -e MINIO_ROOT_PASSWORD=verification-minio \
            minio/minio:RELEASE.2025-09-07T16-13-09Z@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e \
            server /data
          for _ in $(seq 1 40); do
            curl -fsS http://localhost:9000/minio/health/live >/dev/null && exit 0
            sleep 1
          done
          echo "MinIO ne répond pas." >&2; docker logs minio >&2; exit 1

      - name: Les 400 et quelques garanties
        env:
          KASTELL_ENV: developpement
@@ -62,11 +87,11 @@ jobs:
  image:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-qemu-action@v3
      - uses: docker/setup-buildx-action@v3
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262   # v4.4.0
      - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130   # v3.7.0
      - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f   # v3.12.0
      - name: Construction multi-architecture (EX-25)
        uses: docker/build-push-action@v6
        uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8   # v6.19.2
        with:
          context: .
          file: docker/Dockerfile
+6 −2
Original line number Diff line number Diff line
@@ -3,7 +3,11 @@
# web dans un seul artefact, activés par variable d'environnement.
# Le même artefact sert le SaaS et l'auto-hébergement (EX-35).
# ═══════════════════════════════════════════════════════════════════════════
FROM node:24-alpine AS base
# Version explicite et empreinte du manifeste : « node:24-alpine » est une
# étiquette mouvante, deux constructions à un mois d'écart ne partaient pas de
# la même base. L'empreinte est celle du manifeste multi-architecture — la
# construction vise amd64 et arm64 (EX-25).
FROM node:24.19.0-alpine3.24@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43 AS base
RUN corepack enable
WORKDIR /app

@@ -27,7 +31,7 @@ RUN pnpm --filter @kastell/api build \
 && cp -r apps/web/dist apps/api/dist/web

# ── Exécution ──────────────────────────────────────────────────────────────
FROM node:24-alpine AS runtime
FROM node:24.19.0-alpine3.24@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43 AS runtime
ENV NODE_ENV=production
WORKDIR /app

+41 −9
Original line number Diff line number Diff line
@@ -12,7 +12,7 @@ name: kastell

services:
  postgres:
    image: postgres:17-alpine
    image: postgres:17.11-alpine3.24@sha256:18cfe3ef5e6815560c98237d6216d1e5119702fb0f3894c8785dd58b8bbe5d73
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
@@ -26,10 +26,14 @@ services:
      timeout: 3s
      retries: 20
    ports:
      - "127.0.0.1:5432:5432"   # exposé en local uniquement, pour l'outillage
      # Bornée à la boucle locale : l'outillage lancé depuis la machine
      # (« pnpm migrate », « pnpm verif ») en a besoin, l'internet non.
      - "127.0.0.1:5432:5432"
    security_opt:
      - no-new-privileges:true

  redis:
    image: redis:7-alpine
    image: redis:7.4.11-alpine3.21@sha256:ff02b58f971e7d7d156a1267e283fcbbeee91773b6aa36c49dac28ecfe28eadf
    restart: unless-stopped
    command: ["redis-server", "--save", "60", "1", "--appendonly", "no"]
    volumes:
@@ -40,10 +44,18 @@ services:
      timeout: 3s
      retries: 20
    ports:
      - "127.0.0.1:6379:6379"   # exposé en local uniquement, pour l'outillage
      # Idem, à une différence près : Postgres et MinIO demandent des
      # identifiants, Redis n'en demande aucun. Tout utilisateur de la machine
      # peut donc lire et vider ce cache — ce qui coûte la présence en ligne et
      # la fraîcheur des écrans, pas le journal, qui est en base. Fermer cela
      # demande « --requirepass », donc un secret de plus dans le .env, donc
      # une décision d'exploitation : elle n'est pas prise ici.
      - "127.0.0.1:6379:6379"
    security_opt:
      - no-new-privileges:true

  minio:
    image: minio/minio:latest
    image: minio/minio:RELEASE.2025-09-07T16-13-09Z@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
    restart: unless-stopped
    command: ["server", "/data", "--console-address", ":9001"]
    environment:
@@ -59,12 +71,14 @@ services:
    ports:
      - "127.0.0.1:9000:9000"   # idem
      - "127.0.0.1:9001:9001"
    security_opt:
      - no-new-privileges:true

  # Démarré par défaut. Au premier lancement il télécharge ses signatures —
  # plusieurs minutes pendant lesquelles il répond « injoignable ». Les dépôts
  # restent possibles : ils sont marqués « non analysée », jamais « saine ».
  clamav:
    image: clamav/clamav:stable
    image: clamav/clamav:1.5.4@sha256:0e85467cb0d6e7d860a45035707741cd5ffc032ffefc6002a3510c75b6d07027
    # Cisco ne publie ClamAV qu'en amd64 — aucune étiquette n'a d'arm64. Sur un
    # Mac Apple Silicon, Docker l'émule : plus lent à l'analyse, mais fidèle.
    #
@@ -74,6 +88,8 @@ services:
    # déplacer le problème plutôt qu'à le résoudre.
    platform: linux/amd64
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    volumes:
      - signatures:/var/lib/clamav
    healthcheck:
@@ -87,10 +103,17 @@ services:
  # Démarré par défaut. La clé et le secret viennent du .env, tiré au sort par
  # « pnpm demarrer » : les livrer en clair dans le dépôt donnerait à chaque
  # instance auto-hébergée les mêmes clés, et celles-ci ouvrent les salles.
  #
  # Ils passent par l'environnement et non par « --keys » : un argument de
  # ligne de commande est lisible par « docker inspect », par le « ps » de
  # l'hôte, et par tout processus voisin qui sait lire /proc. Un secret qui
  # ouvre les salles de crise n'a rien à faire là.
  livekit:
    image: livekit/livekit-server:latest
    image: livekit/livekit-server:v1.13.5@sha256:3497163e15c48fef6e7830c78716f9e9d5edc28abf7aa90b61c86e93bbc306b1
    restart: unless-stopped
    command: ["--config", "/etc/livekit.yaml", "--keys", "${LIVEKIT_CLE}: ${LIVEKIT_SECRET}"]
    command: ["--config", "/etc/livekit.yaml"]
    environment:
      LIVEKIT_KEYS: "${LIVEKIT_CLE}: ${LIVEKIT_SECRET}"
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:7880/"]
      interval: 10s
@@ -98,6 +121,8 @@ services:
      retries: 12
    volumes:
      - ../docker/livekit.yaml:/etc/livekit.yaml:ro
    security_opt:
      - no-new-privileges:true
    depends_on:
      redis: { condition: service_healthy }
    ports:
@@ -114,7 +139,7 @@ services:
  # promesse à moitié tenue. Au repos il ne consomme presque rien ; c'est
  # pendant un enregistrement qu'il travaille.
  egress:
    image: livekit/egress:latest
    image: livekit/egress:v1.14.1@sha256:bf2b648b947349c3e9ff7aa8c718f00378d5c06af7624652a3653318e00333ce
    restart: unless-stopped
    environment:
      EGRESS_CONFIG_BODY: |
@@ -126,7 +151,14 @@ services:
        insecure: true
    # L'encodage exige un navigateur embarqué et des capacités élevées ; il est
    # isolé pour que sa consommation ne dégrade jamais la main courante.
    #
    # SYS_ADMIN est demandé par le bac à sable de Chrome, et c'est la capacité
    # la plus proche de la racine de l'hôte que cette composition accorde. Elle
    # ne peut pas être retirée sans casser l'encodage — elle est donc au moins
    # encadrée : aucune élévation supplémentaire, et rien d'autre n'est ajouté.
    cap_add: ["SYS_ADMIN"]
    security_opt:
      - no-new-privileges:true
    depends_on:
      redis:   { condition: service_healthy }
      livekit: { condition: service_healthy }