# Setup — Serveur personnel

Référence : [[ADR-001-Mise en place d'un serveur personnel]]

**Stack décidée :** Ubuntu Server 24.04 LTS · SSH durci · Docker · Devcontainers · Traefik + Let's Encrypt · UFW + IPAllowList

---
name : vps-6f98b55e.vps.ovh.net
os : ubuntu 25.04
Adresse IPv4 : 51.77.147.135
Adresse IPv6 : 2001:41d0:404:200::53e0

## Phase 1 — Infrastructure de base

### 1.1 Installation Ubuntu Server 25.04 LTS

- [x] VPS installé par OVH

### 1.2 Configuration post-installation
sudo adduser paulnette
sudo usermod -aG sudo paulnette
sudo mkdir -p /home/paulnette/.ssh
sudo cp /home/ubuntu/.ssh/authorized_keys /home/paulnette/.ssh/
sudo chown -R paulnette:paulnette /home/paulnette/.ssh
sudo chmod 700 /home/paulnette/.ssh
sudo chmod 600 /home/paulnette/.ssh/authorized_keys


- [x] Mettre à jour le système : `sudo apt update && sudo apt upgrade -y`
- [ ] Désactiver le login root SSH (`PermitRootLogin no` dans `/etc/ssh/sshd_config`)
- [ ] Activer l'authentification par clé SSH uniquement (`PasswordAuthentication no`)
- [x] Configurer le hostname : `sudo hostnamectl set-hostname <nom-serveur>`
- [x] Configurer le fuseau horaire : `sudo timedatectl set-timezone Europe/Paris`
- [ ] Activer les mises à jour de sécurité automatiques : `sudo apt install unattended-upgrades`

### 1.3 Pare-feu UFW

- [x] sudo apt install fail2ban


> ⚠️ **Ordre impératif** : définir toutes les règles AVANT `ufw enable`.
> `ufw default deny incoming` ne coupe rien tant que UFW n'est pas activé.
> Si tu fais `ufw enable` sans avoir ajouté la règle SSH, tu perds l'accès distant — accès physique requis.

- [x] Installer UFW : `sudo apt install ufw`
- [x] Définir les politiques par défaut (UFW inactif à ce stade) :
  ```bash
  sudo ufw default deny incoming
  sudo ufw default allow outgoing
  ```
- [x] Autoriser SSH depuis les IPs fixes uniquement (**faire ceci avant `ufw enable`**) :
  ```bash
  sudo ufw allow from 80.11.215.231 to any port 22      Rennes
  sudo ufw allow from 82.66.18.183 to any port 22       Chateaugiron
  ```
- [x] Autoriser HTTP/HTTPS (pour Traefik / Let's Encrypt) :
  ```bash
  sudo ufw allow 80/tcp
  sudo ufw allow 443/tcp
  ```
- [x] Vérifier les règles avant activation : `sudo ufw show added`
- [x] Activer UFW (seulement après vérification) : `sudo ufw enable`
- [x] Vérifier l'état final : `sudo ufw status verbose`

### 1.4 Installation Docker

- [x] Installer Docker Engine (méthode officielle) :
  ```bash
  curl -fsSL https://get.docker.com | sudo sh
  sudo usermod -aG docker $USER
  ```
- [x] Installer Docker Compose plugin :
  ```bash
  sudo apt install docker-compose-plugin
  ```
- [x] Vérifier : `docker version` et `docker compose version`
- [x] Activer Docker au démarrage : `sudo systemctl enable docker`

---

## Phase 2 — Traefik + Let's Encrypt

> Décision : utiliser le challenge Let's Encrypt HTTP-01.
> Les ports `80/443` seront donc ouverts publiquement dans UFW.
> Les services sensibles resteront filtrés côté Traefik avec `IPAllowList`.

### 2.1 Préparation DNS et pare-feu

- [ ] Choisir le nom DNS du dashboard Traefik : `traefik.aulnette.eu`

### 2.2 Structure de fichiers

```
~/traefik/
├── docker-compose.yml
├── traefik.yml          # config statique
├── conf/
│   └── dynamic.yml      # config dynamique (middlewares, etc.)
└── letsencrypt/
    └── acme.json        # certificats (chmod 600 obligatoire)
```

### 2.3 Création des fichiers

- [ ] Créer l'arborescence Traefik :
  ```bash
  mkdir -p ~/traefik/conf ~/traefik/letsencrypt
  cd ~/traefik
  ```
- [x] Créer le fichier de stockage des certificats et verrouiller ses permissions :
  ```bash
  touch letsencrypt/acme.json
  chmod 600 letsencrypt/acme.json
  ```
- [x] Créer le réseau Docker partagé :
  ```bash
  docker network create traefik-public
  ```

### 2.4 Configuration statique Traefik

- [x] Créer `~/traefik/traefik.yml` :
  ```bash
  cd ~/traefik
  nano traefik.yml
  ```
  Coller le contenu ci-dessous, puis enregistrer avec `Ctrl+O`, `Entrée`, `Ctrl+X`.
  Il a été complété pour éviter les timeouts sur les commits git

  ```yaml
  api:
    dashboard: true
    insecure: false

  log:
    level: INFO

  accessLog: {}

  entryPoints:
    web:
      address: ":80"
      http:
        redirections:
          entryPoint:
            to: websecure
            scheme: https
    websecure:
      address: ":443"
      transport:
          respondingTimeouts:
            readTimeout: "10m"
            writeTimeout: "10m"
            idleTimeout: "10m"
  serversTransport:
    forwardingTimeouts:
      dialTimeout: "30s"
      responseHeaderTimeout: "10m"
      idleConnTimeout: "10m"
  providers:
    docker:
      endpoint: "unix:///var/run/docker.sock"
      network: traefik-public
      exposedByDefault: false
    file:
      directory: /conf
      watch: true

  certificatesResolvers:
    letsencrypt:
      acme:
        email: philippe.aulnette@6tm.com
        storage: /letsencrypt/acme.json
        httpChallenge:
          entryPoint: web
  ```

### 2.5 Middleware IPAllowList

- [ ] Créer `~/traefik/conf/dynamic.yml` :
  ```bash
  cd ~/traefik
  nano conf/dynamic.yml
  ```
  Coller le contenu ci-dessous, puis enregistrer avec `Ctrl+O`, `Entrée`, `Ctrl+X`.

  ```yaml
  http:
    middlewares:
      ip-allowlist:
        ipAllowList:
          sourceRange:
            - "80.11.215.231/32"
            - "82.66.18.183/32"
  ```

### 2.6 Docker Compose Traefik

- [ ] Créer `~/traefik/docker-compose.yml` :
  ```bash
  cd ~/traefik
  nano docker-compose.yml
  ```
  Coller le contenu ci-dessous, puis enregistrer avec `Ctrl+O`, `Entrée`, `Ctrl+X`.

  ```yaml
  services:
    traefik:
      image: traefik:v3.6.16
      container_name: traefik
      restart: unless-stopped

      ports:
        - "80:80"
        - "443:443"

      networks:
        - traefik-public

      volumes:
        - /var/run/docker.sock:/var/run/docker.sock:ro
        - ./traefik.yml:/etc/traefik/traefik.yml:ro
        - ./conf:/conf:ro
        - ./letsencrypt:/letsencrypt

      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.traefik.rule=Host(`traefik.aulnette.eu`)"
        - "traefik.http.routers.traefik.entrypoints=websecure"
        - "traefik.http.routers.traefik.tls=true"
        - "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
        - "traefik.http.routers.traefik.service=api@internal"
        - "traefik.http.routers.traefik.middlewares=ip-allowlist@file"

  networks:
    traefik-public:
      external: true
  ```

### 2.7 Démarrage et vérification

- [x] Valider la configuration Docker Compose :
  ```bash
  cd ~/traefik
  docker compose config
  ```
- [x] Démarrer Traefik :
  ```bash
  docker compose up -d
  ```
- [x] Vérifier que le container tourne :
  ```bash
  docker compose ps
  ```
- [x] Suivre les logs jusqu'à génération du certificat :
  ```bash
  docker compose logs -f traefik
  ```
- [x] Vérifier l'accès HTTPS depuis une IP autorisée :
  ```bash
  curl -I https://traefik.aulnette.eu
  ```
- [x] Vérifier que le dashboard est accessible uniquement depuis les IPs autorisées :
  ```text
  https://traefik.<domaine>
  ```
- [ ] Vérifier que `acme.json` contient un certificat :
  ```bash
  sudo ls -l ~/traefik/letsencrypt/acme.json
  sudo wc -c ~/traefik/letsencrypt/acme.json
  ```

### 2.8 Garde-fous

- [ ] Ne pas activer `api.insecure: true`
- [ ] Ne pas exposer un service sans label explicite `traefik.enable=true`
- [ ] Garder le socket Docker en lecture seule : `/var/run/docker.sock:/var/run/docker.sock:ro`
- [ ] Appliquer `ip-allowlist@file` à tous les services d'administration
- [ ] Documenter tout nouveau service exposé dans ce fichier avant mise en ligne

---

## Phase 3 — Services cœur (Forgejo + n8n)

> Objectif : déployer Forgejo (forge Git) et n8n (automatisation), tous deux derrière Traefik, avec stockage persistant et stratégie de sauvegarde.
>
> **Critère de sortie** : Forgejo et n8n accessibles en HTTPS, authentification active, données persistantes survivant à un `docker compose down`.

### 3.1 Préparation DNS

- [x] Créer les enregistrements DNS (A + AAAA) pointant vers le VPS :
  - `git.aulnette.eu` → `51.77.147.135` / `2001:41d0:404:200::53e0`
  - `flux.aulnette.eu` → `51.77.147.135` / `2001:41d0:404:200::53e0`
- [x] Vérifier la propagation : `dig +short git.aulnette.eu` et `dig +short flux.aulnette.eu`

### 3.2 Convention de stockage persistant

Tous les volumes applicatifs sont regroupés sous `/srv/docker/<service>/` pour faciliter la sauvegarde.

```
/srv/docker/
├── forgejo/
│   ├── data/           # dépôts Git, configuration, attachements
│   └── db/             # base PostgreSQL Forgejo
└── n8n/
    └── data/           # workflows, credentials, exécutions
```

- [x] Créer l'arborescence :
  ```bash
  sudo mkdir -p /srv/docker/forgejo/data /srv/docker/forgejo/db /srv/docker/n8n/data
  sudo chown -R 1000:1000 /srv/docker/forgejo /srv/docker/n8n
  ```

  > ⚠️ **Permissions importantes** : les containers Forgejo (user `git`) et n8n (user `node`) tournent en **uid 1000** par défaut. Sur ce VPS, l'utilisateur `paulnette` est en uid 1001 (l'uid 1000 est déjà pris par le user `ubuntu` créé par OVH). Il faut donc chown vers `1000:1000` (UID/GID *du container*), pas vers `$USER`. Sinon : `EACCES: permission denied, open '/home/node/.n8n/config'` au démarrage.
  >
  > Vérifier : `id paulnette` doit montrer `uid=1001`, et `ls -ln /srv/docker/n8n` doit montrer `1000 1000`.

### 3.3 Déploiement Forgejo

#### 3.3.1 Structure

```
~/forgejo/
└── docker-compose.yml
```

- [x] Créer le répertoire :
  ```bash
  mkdir -p ~/forgejo
  cd ~/forgejo
  ```

#### 3.3.2 Docker Compose

- [ ] Créer `~/forgejo/docker-compose.yml` :
  ```bash
  cd ~/forgejo
  nano docker-compose.yml
  ```
  Coller le contenu ci-dessous, puis enregistrer avec `Ctrl+O`, `Entrée`, `Ctrl+X`.

  ```yaml
  services:
    forgejo:
      image: codeberg.org/forgejo/forgejo:10
      container_name: forgejo
      restart: unless-stopped
      depends_on:
        - forgejo-db
      environment:
        - USER_UID=1000
        - USER_GID=1000
        - FORGEJO__database__DB_TYPE=postgres
        - FORGEJO__database__HOST=forgejo-db:5432
        - FORGEJO__database__NAME=forgejo
        - FORGEJO__database__USER=forgejo
        - FORGEJO__database__PASSWD=${FORGEJO_DB_PASSWORD}
        - FORGEJO__server__DOMAIN=git.aulnette.eu
        - FORGEJO__server__ROOT_URL=https://git.aulnette.eu/
        - FORGEJO__server__SSH_DOMAIN=git.aulnette.eu
        - FORGEJO__service__DISABLE_REGISTRATION=true
        - FORGEJO__security__INSTALL_LOCK=true
      volumes:
        - /srv/docker/forgejo/data:/var/lib/gitea
        - /etc/timezone:/etc/timezone:ro
        - /etc/localtime:/etc/localtime:ro
      networks:
        - traefik-public
        - forgejo-internal
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.forgejo.rule=Host(`git.aulnette.eu`)"
        - "traefik.http.routers.forgejo.entrypoints=websecure"
        - "traefik.http.routers.forgejo.tls=true"
        - "traefik.http.routers.forgejo.tls.certresolver=letsencrypt"
        - "traefik.http.routers.forgejo.middlewares=ip-allowlist@file"
        - "traefik.http.services.forgejo.loadbalancer.server.port=3000"
        - "traefik.docker.network=traefik-public"

    forgejo-db:
      image: postgres:16-alpine
      container_name: forgejo-db
      restart: unless-stopped
      environment:
        - POSTGRES_USER=forgejo
        - POSTGRES_PASSWORD=${FORGEJO_DB_PASSWORD}
        - POSTGRES_DB=forgejo
      volumes:
        - /srv/docker/forgejo/db:/var/lib/postgresql/data
      networks:
        - forgejo-internal

  networks:
    traefik-public:
      external: true
    forgejo-internal:
      driver: bridge
  ```

#### 3.3.3 Secrets et démarrage

- [x] Créer `~/forgejo/.env` (ne jamais commiter) :
  ```bash
  cd ~/forgejo
  echo "FORGEJO_DB_PASSWORD=$(openssl rand -base64 32)" > .env
  chmod 600 .env
  ```
- [x] Démarrer Forgejo :
  ```bash
  docker compose up -d
  docker compose logs -f forgejo
  ```
- [x] Vérifier l'accès : `curl -I https://git.aulnette.eu`
- [x] Créer le premier utilisateur administrateur (registration désactivée — passer par la CLI) :
  ```bash
  docker compose exec -u git forgejo forgejo admin user create \
    --username paulnette --admin \
    --email philippe.aulnette@6tm.com \
    --random-password
  ```

  > ⚠️ Le flag `-u git` est obligatoire. Sans lui, `docker compose exec` lance la commande en root et Forgejo refuse de démarrer (`Forgejo is not supposed to be run as root`).

- [x] Se connecter à `https://git.aulnette.eu`, changer le mot de passe, ajouter sa clé SSH

> **Garde-fou** : Forgejo est protégé par `ip-allowlist@file`. L'accès web ET les opérations Git via HTTPS (clone/push/pull) ne fonctionnent que depuis les IPs autorisées dans `~/traefik/conf/dynamic.yml`. Toute nouvelle IP (déplacement, partage avec un tiers) doit être ajoutée à l'allowlist.

> **Note SSH Forgejo** : le port SSH 22 du serveur est déjà occupé par OpenSSH. Pour activer le clone Git par SSH, exposer le SSH Forgejo sur un port alternatif (ex: 2222) et l'autoriser dans UFW depuis les mêmes IPs fixes. À défaut, utiliser HTTPS uniquement dans un premier temps.

### 3.4 Déploiement n8n

#### 3.4.1 Structure

```
~/n8n/
└── docker-compose.yml
```

- [x] Créer le répertoire :
  ```bash
  mkdir -p ~/n8n
  cd ~/n8n
  ```

#### 3.4.2 Docker Compose

- [x] Créer `~/n8n/docker-compose.yml` :
  ```bash
  cd ~/n8n
  nano docker-compose.yml
  ```
  Coller le contenu ci-dessous, puis enregistrer avec `Ctrl+O`, `Entrée`, `Ctrl+X`.

  ```yaml
  services:
    n8n:
      image: n8nio/n8n:latest
      container_name: n8n
      restart: unless-stopped
      environment:
        - N8N_HOST=flux.aulnette.eu
        - N8N_PORT=5678
        - N8N_PROTOCOL=https
        - WEBHOOK_URL=https://flux.aulnette.eu/
        - GENERIC_TIMEZONE=Europe/Paris
        - TZ=Europe/Paris
        - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
        - N8N_BLOCK_ENV_ACCESS_IN_NODE=true
        - N8N_RUNNERS_DISABLE_PYTHON=true
      volumes:
        - /srv/docker/n8n/data:/home/node/.n8n
      networks:
        - traefik-public
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.n8n.rule=Host(`flux.aulnette.eu`)"
        - "traefik.http.routers.n8n.entrypoints=websecure"
        - "traefik.http.routers.n8n.tls=true"
        - "traefik.http.routers.n8n.tls.certresolver=letsencrypt"
        - "traefik.http.routers.n8n.middlewares=ip-allowlist@file"
        - "traefik.http.services.n8n.loadbalancer.server.port=5678"
        - "traefik.docker.network=traefik-public"

  networks:
    traefik-public:
      external: true
  ```

> ⚠️ `N8N_ENCRYPTION_KEY` chiffre les credentials stockés. La perdre = perdre l'accès à tous les credentials configurés. La sauvegarder hors-serveur.

> **Notes sur les variables d'environnement n8n** :
> - `N8N_RUNNERS_ENABLED` n'est plus nécessaire (activé par défaut depuis n8n récent) — la mettre génère un warning de dépréciation.
> - `N8N_RUNNERS_DISABLE_PYTHON=true` supprime le warning au démarrage signalant l'absence de Python 3 dans l'image. **Choix retenu** : on ignore Python — les Code nodes seront écrits uniquement en JavaScript. Si un jour un besoin réel de Python émerge, il faudra construire une image custom basée sur `n8nio/n8n:latest` avec `apk add python3 py3-pip`.

#### 3.4.3 Secrets et démarrage

- [x] Créer `~/n8n/.env` :
  ```bash
  cd ~/n8n
  echo "N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)" > .env
  chmod 600 .env
  ```
- [x] **Sauvegarder la clé `N8N_ENCRYPTION_KEY` dans un gestionnaire de mots de passe externe** (Bitwarden / 1Password / KeePass)
- [ ] Démarrer n8n :
  ```bash
  docker compose up -d
  docker compose logs -f n8n
  ```
- [ ] Vérifier l'accès : `curl -I https://flux.aulnette.eu`
- [ ] Se connecter à `https://flux.aulnette.eu`, créer le compte propriétaire (premier utilisateur = owner)

> **Garde-fou** : n8n est protégé par `ip-allowlist@file` car il manipule des credentials sensibles et peut exécuter du code arbitraire. Aucun accès depuis l'extérieur des IPs autorisées.

### 3.5 Stratégie de sauvegarde

Approche : **dump applicatif + snapshot fichiers**, déposés dans `/srv/backups/` puis envoyés vers un stockage distant.

#### 3.5.1 Préparation

- [ ] Créer le répertoire local de sauvegarde :
  ```bash
  sudo mkdir -p /srv/backups/forgejo /srv/backups/n8n
  sudo chown -R $USER:$USER /srv/backups
  ```
- [ ] Installer les outils nécessaires :
  ```bash
  sudo apt install -y restic
  ```

#### 3.5.2 Script de sauvegarde quotidien

- [ ] Créer `/usr/local/bin/backup-services.sh` :
  ```bash
  sudo nano /usr/local/bin/backup-services.sh
  ```
  Coller le contenu ci-dessous :

  ```bash
  #!/usr/bin/env bash
  set -euo pipefail

  DATE=$(date +%Y%m%d-%H%M%S)
  BACKUP_DIR=/srv/backups

  # --- Forgejo ---
  # 1. Dump PostgreSQL
  docker exec forgejo-db pg_dump -U forgejo forgejo \
    | gzip > "${BACKUP_DIR}/forgejo/db-${DATE}.sql.gz"

  # 2. Dump applicatif Forgejo (dépôts + config)
  docker exec forgejo forgejo dump --type tar.gz --file /tmp/forgejo-dump.tar.gz
  docker cp forgejo:/tmp/forgejo-dump.tar.gz "${BACKUP_DIR}/forgejo/dump-${DATE}.tar.gz"
  docker exec forgejo rm /tmp/forgejo-dump.tar.gz

  # --- n8n ---
  # Arrêt court pour cohérence du SQLite interne
  docker compose -f /home/paulnette/n8n/docker-compose.yml stop n8n
  tar czf "${BACKUP_DIR}/n8n/data-${DATE}.tar.gz" -C /srv/docker/n8n data
  docker compose -f /home/paulnette/n8n/docker-compose.yml start n8n

  # --- Rotation locale (garder 7 jours) ---
  find "${BACKUP_DIR}" -type f -name "*.gz" -mtime +7 -delete
  ```

- [ ] Rendre exécutable :
  ```bash
  sudo chmod +x /usr/local/bin/backup-services.sh
  ```
- [ ] Tester manuellement :
  ```bash
  sudo /usr/local/bin/backup-services.sh
  ls -lh /srv/backups/forgejo /srv/backups/n8n
  ```

#### 3.5.3 Planification (cron)

- [ ] Ajouter au crontab root (sauvegarde quotidienne à 03:30) :
  ```bash
  sudo crontab -e
  ```
  Ajouter la ligne :
  ```
  30 3 * * * /usr/local/bin/backup-services.sh >> /var/log/backup-services.log 2>&1
  ```

#### 3.5.4 Externalisation (restic)

> Sans copie hors serveur, une sauvegarde locale ne protège ni d'une corruption du disque, ni d'une perte du VPS.

- [ ] Choisir une destination : OVH Object Storage (S3), Backblaze B2, ou autre VPS
- [ ] Initialiser le dépôt restic (exemple S3) :
  ```bash
  export RESTIC_REPOSITORY=s3:s3.gra.io.cloud.ovh.net/backup-vps-paulnette
  export RESTIC_PASSWORD=<phrase-secrete-forte>
  export AWS_ACCESS_KEY_ID=...
  export AWS_SECRET_ACCESS_KEY=...
  restic init
  ```
- [ ] **Sauvegarder `RESTIC_PASSWORD` dans le gestionnaire de mots de passe externe** (sans elle, restauration impossible)
- [ ] Ajouter un second cron (après la sauvegarde locale, 04:00) qui pousse `/srv/backups` vers restic et applique une rétention (par exemple 7 quotidiennes + 4 hebdomadaires + 6 mensuelles)

#### 3.5.5 Test de restauration

> Une sauvegarde non testée n'est pas une sauvegarde.

- [ ] Restaurer un dump Forgejo dans un container jetable et vérifier l'intégrité d'au moins un dépôt
- [ ] Restaurer le volume n8n dans un répertoire temporaire et vérifier la présence des workflows
- [ ] Documenter la procédure de restauration dans ce fichier (à compléter après le premier test réussi)

### 3.6 Critères de sortie Phase 3

- [ ] `https://git.aulnette.eu` accessible depuis IPs autorisées uniquement, certificat valide, login admin fonctionnel
- [ ] `https://flux.aulnette.eu` accessible depuis IPs autorisées uniquement, certificat valide, compte owner créé
- [ ] `docker compose down && docker compose up -d` sur chaque service ne fait perdre aucune donnée
- [ ] Script de sauvegarde exécuté avec succès au moins une fois, fichiers présents dans `/srv/backups/`
- [ ] Cron quotidien actif et journalisé dans `/var/log/backup-services.log`
- [ ] Sauvegarde externalisée vers stockage distant opérationnelle
- [ ] Au moins une restauration test effectuée et documentée
- [ ] `N8N_ENCRYPTION_KEY` et `RESTIC_PASSWORD` stockés hors serveur

---

## Phase 4 - Environnements de dev avec Codex

> Objectif : preparer un environnement de developpement distant utilisable par Codex, isole du compte d'administration principal, avec un utilisateur systeme dedie : `merlin`.
>
> Decision : `merlin` represente l'agent IA. Il doit pouvoir travailler sur des depots et lancer des environnements de dev, mais ne doit pas devenir un compte d'administration generaliste sans controle.

### 4.1 Principes de securite

- [ ] Creer un utilisateur Linux dedie `merlin`
- [ ] Ne pas donner `sudo` par defaut a `merlin`
- [ ] Authentification SSH par cle uniquement
- [ ] Acces SSH limite aux IPs autorisees par UFW
- [ ] Depots de travail sous `/home/merlin/dev/`
- [ ] Secrets jamais stockes en clair dans les depots
- [ ] Utiliser des `.env` locaux non commites pour les credentials de dev

### 4.2 Creation de l'utilisateur `merlin`

- [ ] Creer l'utilisateur :
  ```bash
  sudo adduser merlin
  ```
- [ ] Creer le dossier SSH :
  ```bash
  sudo mkdir -p /home/merlin/.ssh
  sudo nano /home/merlin/.ssh/authorized_keys
  sudo chown -R merlin:merlin /home/merlin/.ssh
  sudo chmod 700 /home/merlin/.ssh
  sudo chmod 600 /home/merlin/.ssh/authorized_keys
  ```
- [ ] Tester la connexion SSH :
  ```bash
  ssh merlin@vps-6f98b55e.vps.ovh.net
  ```

> Point d'attention : ne pas reutiliser la cle SSH personnelle d'administration si l'objectif est d'isoler les usages. Creer une cle specifique `merlin` cote poste local.

### 4.3 Arborescence de travail

```text
/home/merlin/dev/
+-- pkm-perso/
+-- seenaps/
+-- sandbox/
```

- [ ] Creer le dossier standard :
  ```bash
  sudo mkdir -p /home/merlin/dev
  sudo chown -R merlin:merlin /home/merlin/dev
  ```

> Choix initial : `/home/merlin/dev/`. C'est plus simple et suffisant tant qu'il n'y a pas plusieurs agents ou plusieurs utilisateurs.

### 4.4 Acces Git et Forgejo

- [ ] Creer une cle SSH Git pour `merlin` :
  ```bash
  sudo -u merlin ssh-keygen -t ed25519 -C "merlin@vps-codex" -f /home/merlin/.ssh/id_ed25519
  ```
- [ ] Ajouter la cle publique dans Forgejo :
  ```bash
  sudo cat /home/merlin/.ssh/id_ed25519.pub
  ```
- [ ] Configurer l'identite Git :
  ```bash
  sudo -u merlin git config --global user.name "Merlin"
  sudo -u merlin git config --global user.email "pa.ia@6tm.com"
  ```
- [ ] Tester un clone depuis Forgejo :
  ```bash
  sudo -u merlin git clone <url-du-repo> /home/merlin/dev/<repo>
  ```

> Convention : les commits produits par l'agent IA utilisent l'identite `Merlin <pa.ia@6tm.com>` pour rester auditables.

### 4.5 Outillage de base dev

- [ ] Installer les outils socle :
  ```bash
  sudo apt install -y git curl unzip build-essential
  ```
- [ ] Installer Node.js via une methode stable, par exemple `nvm` pour l'utilisateur `merlin`
- [ ] Installer Python et venv si necessaire :
  ```bash
  sudo apt install -y python3 python3-venv python3-pip
  ```
- [ ] Verifier les versions :
  ```bash
  git --version
  node --version
  npm --version
  python3 --version
  ```

### 4.6 Devcontainers et Docker

Deux options :

1. **Mode prudent** : `merlin` ne fait pas partie du groupe `docker`. Les builds Docker sont lances manuellement par `paulnette`.
2. **Mode autonome** : `merlin` est ajoute au groupe `docker`, ce qui donne un pouvoir quasi-root via Docker.

- [ ] Choisir explicitement le mode
- [ ] Si mode autonome, ajouter `merlin` au groupe Docker :
  ```bash
  sudo usermod -aG docker merlin
  ```
- [ ] Documenter le choix de securite retenu

> Position initiale : commencer en mode prudent. Ajouter `merlin` au groupe `docker` seulement si le besoin de devcontainers autonomes est reel.

### 4.7 Codex CLI / agent IA

- [ ] Definir le mode d'installation de Codex pour `merlin`
- [ ] Installer Codex dans l'environnement utilisateur `merlin`
- [ ] Creer un dossier de configuration :
  ```bash
  sudo -u merlin mkdir -p /home/merlin/.codex
  ```
- [ ] Stocker les credentials Codex selon le mecanisme officiel choisi, jamais dans un repo
- [ ] Tester une session Codex dans un repo de test

> Garde-fou : `merlin` doit etre capable de modifier un workspace, mais pas de reconfigurer le serveur, Traefik, Forgejo, n8n ou les sauvegardes sans intervention humaine.

### 4.8 Criteres de sortie Phase 4

- [ ] Connexion SSH `merlin` fonctionnelle par cle uniquement
- [ ] `merlin` n'a pas `sudo` par defaut
- [ ] Workspace standard cree et documente
- [ ] Identite Git `Merlin <pa.ia@6tm.com>` configuree
- [ ] Clone/pull/push testes sur un repo non critique
- [ ] Mode Docker/devcontainer choisi et documente
- [ ] Codex installe et teste dans un repo de test
- [ ] Aucun secret Codex, Git, n8n ou serveur stocke en clair dans un depot

---

## Phase 5 - Environnement de developpement Codex sous `paulnette`

> Objectif : tester rapidement Codex sur le serveur avec le compte existant `paulnette`, avant de decider si l'isolation par l'utilisateur `merlin` est necessaire.
>
> Decision temporaire : `paulnette` peut servir au premier essai, mais uniquement dans un workspace dedie. Ne pas lancer Codex dans les repertoires d'administration serveur.

  ```bash
  sudo apt install -y git
  git clone https://git.aulnette.eu/paulnette/pkm.git 
  ```

---

## Phase 6 - Mise en place de code-server

> Objectif : disposer d'un VS Code accessible depuis le navigateur, derriere Traefik, pour travailler sur les depots du serveur sans exposer directement le service.
>
> Decision initiale : deployer code-server en container Docker avec les droits de `paulnette` pour le premier usage. Reevaluer ensuite si l'environnement doit etre porte sous `merlin` pour isoler les usages IA/dev.

### 6.1 Principes de securite

- [ ] Ne pas exposer code-server sans HTTPS
- [ ] Proteger l'acces via `ip-allowlist@file` cote Traefik
- [ ] Conserver l'authentification native de code-server activee
- [ ] Utiliser un mot de passe long, stocke dans le gestionnaire de mots de passe
- [ ] Ne pas publier le port code-server directement sur l'hote
- [ ] Lancer le container avec l'UID/GID de l'utilisateur cible

### 6.2 Preparation DNS

- [ ] Creer l'enregistrement DNS :
  - `code.aulnette.eu` -> `51.77.147.135` / `2001:41d0:404:200::53e0`
- [ ] Verifier la propagation :
  ```bash
  dig +short code.aulnette.eu
  ```

### 6.3 Stockage persistant

- [ ] Verifier l'UID/GID de `paulnette` :
  ```bash
  id paulnette
  ```
- [ ] Creer l'arborescence code-server :
  ```bash
  sudo mkdir -p /srv/docker/code-server/config
  sudo chown -R 1001:1001 /srv/docker/code-server
  ```
- [ ] Definir le repertoire de travail a monter dans code-server :
  ```bash
  mkdir -p /home/paulnette/dev
  ```
- [ ] Cloner ou deplacer les depots dans ce repertoire :
  ```bash
  git clone https://git.aulnette.eu/paulnette/pkm.git /home/paulnette/dev/pkm
  ```

> Adapter `1001:1001` si `id paulnette` retourne une autre valeur.
>
> Convention retenue : stocker la configuration applicative sous `/srv/docker/code-server/`, comme les autres services Docker. Le dossier `/home/paulnette/dev` reste le workspace de travail monte dans code-server.

### 6.4 Secrets

- [ ] Creer `~/code-server/.env` :
  ```bash
  mkdir -p ~/code-server
  cd ~/code-server
  nano .env
  ```
- [ ] Renseigner les variables :
  ```bash
  PUID=1001
  PGID=1001
  TZ=Europe/Paris
  PASSWORD=<mot-de-passe-long>
  ```
- [ ] Verrouiller les permissions :
  ```bash
  chmod 600 .env
  ```

> Sauvegarder `PASSWORD` dans le gestionnaire de mots de passe externe.

### 6.5 Docker Compose code-server

> Convention : `~/code-server/` contient les fichiers de pilotage (`docker-compose.yml`, `.env`) ; `/srv/docker/code-server/` contient les donnees persistantes montees dans le container.

- [ ] Creer `~/code-server/docker-compose.yml` :
  ```bash
  cd ~/code-server
  nano docker-compose.yml
  ```
- [ ] Coller le contenu ci-dessous :
  ```yaml
  services:
    code-server:
      image: lscr.io/linuxserver/code-server:latest
      container_name: code-server
      restart: unless-stopped
      environment:
        PUID: ${PUID}
        PGID: ${PGID}
        TZ: ${TZ}
        PASSWORD: ${PASSWORD}
        DEFAULT_WORKSPACE: /workspace
      volumes:
        - /srv/docker/code-server/config:/config
        - /home/paulnette/dev:/workspace
      networks:
        - traefik-public
      labels:
        traefik.enable: "true"
        traefik.http.routers.code-server.rule: "Host(`code.aulnette.eu`)"
        traefik.http.routers.code-server.entrypoints: "websecure"
        traefik.http.routers.code-server.tls: "true"
        traefik.http.routers.code-server.tls.certresolver: "letsencrypt"
        traefik.http.routers.code-server.middlewares: "ip-allowlist@file"
        traefik.http.services.code-server.loadbalancer.server.port: "8443"
        traefik.docker.network: "traefik-public"
  networks:
    traefik-public:
      external: true
  ```
- [ ] Demarrer code-server :
  ```bash
  docker compose up -d
  ```

### 6.6 Verification

- [ ] Verifier que le container tourne :
  ```bash
  docker compose ps
  docker compose logs -f code-server
  ```
- [ ] Verifier l'acces HTTPS depuis une IP autorisee :
  ```bash
  curl -I https://code.aulnette.eu
  ```
- [ ] Verifier que l'acces est refuse depuis une IP non autorisee
- [ ] Se connecter dans le navigateur et ouvrir le workspace :
  ```text
  /workspace/pkm
  ```
- [ ] Installer uniquement les extensions VS Code necessaires
- [ ] Verifier qu'aucun secret n'est present dans le workspace

### 6.7 Criteres de sortie Phase 6

- [ ] `https://code.aulnette.eu` accessible en HTTPS depuis les IPs autorisees uniquement
- [ ] Authentification code-server active et mot de passe stocke hors serveur
- [ ] Aucun port code-server publie directement sur l'hote
- [ ] Container code-server actif apres redemarrage Docker
- [ ] Workspace PKM ouvert et utilisable
- [ ] Decision documentee : conserver sous `paulnette` ou migrer vers `merlin`
