--- title: 'TimeTrackr Stack : PostgreSQL 17 + tunnel SSH + sync Odoo' url: https://blog.guigpap.com/fr/infrastructure/timetrackr-stack/ url_md: https://blog.guigpap.com/fr/infrastructure/timetrackr-stack.md category: infrastructure date: '2026-05-04' maturite: production techno: - postgresql - n8n - odoo application: - infrastructure - automation --- # TimeTrackr Stack : PostgreSQL 17 + tunnel SSH + sync Odoo > Pile de suivi du temps locale pour Windows, base PostgreSQL durcie, accès via tunnel SSH, sync vers les timesheets Odoo ## 1. Quoi ? — Définition et contexte La **TimeTrackr Stack** est la couche données du système de suivi du temps qui alimente les timesheets Odoo. Elle se distingue des autres stacks par le fait qu'elle n'expose **aucun port à Internet** : seul le client desktop, après ouverture d'un tunnel SSH dédié, peut atteindre la base. | Composant | Rôle | Localisation | |-----------|------|--------------| | **TimeTrackr.exe** | Client Windows desktop (timer + saisie d'entrées) | Poste utilisateur | | **Tunnel SSH** | Transport chiffré vers le VPS, restreint au port PG | Connexion utilisateur ↔ VPS | | **PostgreSQL 17** | Stockage local des entrées de temps | VPS (`timetrackr-stack`) | | **N8N webhooks** | Sync vers Odoo (projets en lecture, entrées en écriture) | VPS (`n8n-stack`) | | **Odoo** | Source des projets et destination des timesheets | VPS (`odoo-stack`) | > **Note - Pourquoi un système séparé** > > Odoo dispose d'un module timesheet natif, mais l'écran web n'est pas pratique pour démarrer/arrêter un chrono pendant la journée. TimeTrackr.exe vit dans la barre des tâches Windows, capture le temps en local, et synchronise par paquets. Postgres sert ici de buffer local fiable, pas de seconde source de vérité. ### Architecture visuelle ```mermaid flowchart TD Client["TimeTrackr.exe · Windows · barre des tâches"] SSH["SSH tunnel · timetrackr-tunnel@VPS:22"] subgraph VPS["VPS Hostinger"] direction TB subgraph Auth["sshd · Match User timetrackr-tunnel"] Force["ForceCommand /bin/false"] Permit["PermitOpen localhost:5433"] NoTTY["PermitTTY no"] end subgraph Stack["timetrackr-stack · timetrackr-internal"] PG["timetrackr-postgres · PG 17 · 127.0.0.1:5433"] SSL["SSL/TLS · SCRAM-SHA-256"] end subgraph N8N["n8n-stack"] Projects["Webhook · /webhook/timetrackr-projects"] Entries["Webhook · /webhook/timetrackr-entries"] Mapping["Data Table · timetrackr_user_mapping"] end subgraph Odoo["odoo-stack"] Project["project.project / project.task"] Timesheet["account.analytic.line"] end end Client -->|"port 22 · clé SSH"| Auth --> Stack Stack --> SSL Client -->|"HTTPS · X-TimeTrackr-Token"| Projects Client -->|"HTTPS · X-TimeTrackr-Token"| Entries Projects --> Project Entries --> Mapping Entries --> Timesheet ``` --- ## 2. Pourquoi ? — Enjeux et motivations ### Pourquoi pas exposer PostgreSQL directement ? | Approche | Pour | Contre | |----------|------|--------| | **Port 5432 public** | Simple, IPs autorisées par firewall | Surface d'attaque permanente, brute force, scans, fuite si firewall mal configuré | | **Tunnel SSH dédié** | Aucun port DB sur Internet, auth par clé SSH, restrictions sshd granulaires | Setup initial plus lourd (clé à déployer côté client) | | **VPN (WireGuard)** | Confort multi-services | Surcharge pour un seul port DB, gestion d'IPs/conf à entretenir | Le tunnel SSH gagne pour ce cas : un seul utilisateur, un seul port, et l'auth `timetrackr-tunnel` ne peut **rien faire d'autre** que forwarder vers `localhost:5433` (pas de shell, pas d'exécution). ### Pourquoi un buffer local plutôt qu'écrire direct dans Odoo ? > **Tip - Mode déconnecté** > > Le client doit fonctionner même quand le réseau lâche (déplacement, wifi instable). Postgres local permet de bufferiser les entrées et de re-synchroniser automatiquement quand la connectivité revient. Écrire en direct dans Odoo via XML-RPC bloquerait la saisie en cas d'incident. ### Pourquoi sync via N8N et pas connexion directe Odoo ? | Choix | Avantage | |-------|----------| | **N8N webhooks** | Logique de mapping (username TimeTrackr → employee Odoo) dehors du client, modifiable sans redéployer l'exe | | **Header Auth** | Token rotatable, indépendant des credentials Odoo | | **Pas de XML-RPC dans le client** | Le client ne connaît jamais les credentials Odoo | | **Audit centralisé** | Toutes les créations de timesheets sont visibles dans les exécutions N8N | --- ## 3. Comment ? — Mise en œuvre technique ### Le client TimeTrackr.exe Application desktop Windows qui vit dans la barre des tâches. Fonctionnalités principales : - **Timer projet/tâche** : démarrer / arrêter un chrono lié à une (projet, tâche) Odoo. - **Liste de projets dynamique** : récupérée au démarrage via `/webhook/timetrackr-projects`. - **Synchronisation par batch** : envoie les entrées accumulées via `/webhook/timetrackr-entries`. - **Mode déconnecté** : continue d'enregistrer en local (PostgreSQL), retente la sync au retour réseau. ### Configuration côté client (`.env`) ```env # Connexion DB (via tunnel SSH local) DB_HOST=127.0.0.1 DB_PORT=15433 # port forwardé localement (configurable) DB_NAME=timetrackr_db DB_USER=timetrackr_user DB_PASSWORD= DB_SSLMODE=require # Webhooks N8N (HTTPS public) PROJECTS_URL=https://n8n.guigpap.com/webhook/timetrackr-projects WEBHOOK_URL=https://n8n.guigpap.com/webhook/timetrackr-entries WEBHOOK_TOKEN= ``` ### Ouverture du tunnel SSH Le client (ou un service Windows associé) ouvre le tunnel avant d'accéder à la base : ```bash ssh -N -L 15433:localhost:5433 timetrackr-tunnel@85.31.237.23 -i ~/.ssh/timetrackr_key ``` - `-N` : pas de commande, juste le forward. - `-L 15433:localhost:5433` : le port local 15433 redirige vers `localhost:5433` côté VPS. - L'utilisateur `timetrackr-tunnel` est restreint à ce seul forward (voir bloc sshd ci-dessous). ### Restrictions sshd côté VPS Le fichier `sshd_timetrackr-tunnel.conf` est déployé dans `/etc/ssh/sshd_config.d/` : ```sshd Match User timetrackr-tunnel AllowTcpForwarding yes PermitOpen localhost:5433 ForceCommand /bin/false PermitTTY no X11Forwarding no AllowAgentForwarding no PermitTunnel no ``` | Directive | Effet | |-----------|-------| | `ForceCommand /bin/false` | L'utilisateur ne peut **pas** obtenir de shell | | `PermitOpen localhost:5433` | Aucun autre port forward autorisé | | `PermitTTY no` | Pas de terminal interactif | | `X11Forwarding no` / `AllowAgentForwarding no` / `PermitTunnel no` | Tous les autres types de forward désactivés | Le déploiement d'une clé client se fait via le script utilitaire : ```bash ./timetrackr-stack/setup.sh tunnel-user # création + reload sshd (one-time) ./timetrackr-stack/deploy_tunnel_key.sh client.pub # ajout d'une clé client ``` ### Configuration PostgreSQL ```yaml # timetrackr-stack/docker-compose.yaml (extrait) postgres: image: postgres:17-alpine container_name: timetrackr-postgres environment: POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256 --auth-local=scram-sha-256" TIMETRACKR_APP_USER: ${TIMETRACKR_APP_USER:-timetrackr_user} TIMETRACKR_APP_PASSWORD: ${TIMETRACKR_APP_PASSWORD:?...} volumes: - timetrackr-db:/var/lib/postgresql/data - ./postgresql.conf:/etc/postgresql/postgresql.conf:ro - ./pg_hba.conf:/etc/postgresql/pg_hba.conf:ro - ./init-ssl.sh:/usr/local/bin/init-ssl.sh:ro - ./init-app-user.sh:/docker-entrypoint-initdb.d/10-init-app-user.sh:ro - ./ssl:/etc/postgresql/ssl:ro entrypoint: ["/bin/bash", "/usr/local/bin/init-ssl.sh"] ports: - "127.0.0.1:5433:5432" # JAMAIS 0.0.0.0 deploy: resources: limits: memory: 512M reservations: { memory: 128M } ``` | Couche | Protection | |--------|------------| | **Réseau** | `127.0.0.1:5433` uniquement (pas d'exposition Internet) | | **Transport** | SSL/TLS obligatoire (`hostnossl reject` dans `pg_hba.conf`), AEAD + ECDHE seulement | | **Authentification** | SCRAM-SHA-256 (pas de MD5, pas de plaintext) | | **Autorisation** | App user limité à SELECT/INSERT/UPDATE/DELETE sur le schéma public | | **Quotas** | App user max 10 connexions concurrentes | | **Timeouts** | 60 s par statement, 300 s en idle-in-transaction, TCP keepalives | | **Conteneur** | `no-new-privileges:true`, mémoire plafonnée | ### init-ssl.sh — modes SSL L'entrypoint `init-ssl.sh` choisit dynamiquement comment monter les certificats : | Mode | Déclencheur | Cas d'usage | |------|-------------|-------------| | **Pré-monté** | `./ssl/server.crt` + `server.key` + `ca.crt` présents | `sslmode=verify-full` côté client (production) | | **Volume persistant** | Un certificat valide existe déjà dans le volume | Reprise après restart | | **Self-signed** | Aucun cert disponible | Génération à la volée (validité 10 ans), `sslmode=require` | ### init-app-user.sh — privilèges minimaux À la première initialisation, l'utilisateur applicatif est créé avec exactement ce qu'il faut : ```sql CREATE ROLE timetrackr_user WITH LOGIN PASSWORD '...'; ALTER ROLE timetrackr_user CONNECTION LIMIT 10; GRANT CONNECT ON DATABASE timetrackr_db TO timetrackr_user; GRANT USAGE, CREATE ON SCHEMA public TO timetrackr_user; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO timetrackr_user; ``` L'utilisateur `postgres` (admin) reste limité à `127.0.0.1` via `pg_hba.conf` — donc accessible uniquement depuis l'intérieur du container. ### Sync vers Odoo : les deux webhooks N8N Côté N8N, deux workflows actifs gèrent l'aller-retour : | Workflow | Endpoint | Méthode | Rôle | |----------|----------|---------|------| | **TimeTrackr - Projects** | `/webhook/timetrackr-projects` | GET | Liste des projets/tâches Odoo pour les menus du client | | **TimeTrackr - Receive Entries** | `/webhook/timetrackr-entries` | POST | Création des `account.analytic.line` dans Odoo | Les deux webhooks sont protégés par **Header Auth** (`X-TimeTrackr-Token`). Ils ne sont pas dans la liste de blocage Caddy, contrairement aux webhooks internes — donc accessibles depuis le poste client en HTTPS public. Le mapping `username TimeTrackr → employee Odoo` est stocké dans la Data Table N8N `timetrackr_user_mapping`. Aucun secret Odoo ne traverse le client, aucune connexion DB N8N → TimeTrackr. Voir [TimeTrackr → Odoo](/fr/workflows/timetrackr/) pour le détail des nodes, le format des payloads, et les règles de validation. ### Commandes d'exploitation ```bash # Stack management docker compose -f timetrackr-stack/docker-compose.yaml up -d docker compose -f timetrackr-stack/docker-compose.yaml logs -f # Vérifier que SSL est actif docker exec timetrackr-postgres psql -U postgres -c "SHOW ssl;" # Vérifier que le port n'est exposé qu'en local ss -tlnp | grep 5433 # doit montrer 127.0.0.1:5433, jamais 0.0.0.0 # Tester l'app user en local docker exec timetrackr-postgres psql -U timetrackr_user -d timetrackr_db -c "SELECT 1;" # Taille de la base docker exec timetrackr-postgres psql -U postgres -d timetrackr_db \ -c "SELECT pg_size_pretty(pg_database_size('timetrackr_db'));" # Vérifier qu'un client tunnel n'a pas de shell ssh timetrackr-tunnel@localhost # doit fermer immédiatement ``` --- ## 4. Et si ? — Perspectives et limites ### Limites actuelles | Limite | Impact | Mitigation | |--------|--------|------------| | **Mono-utilisateur effectif** | Une seule entrée dans `timetrackr_user_mapping` | OK pour mon usage actuel ; ajout d'employés via la Data Table | | **Client Windows uniquement** | Pas de macOS / Linux / mobile | À envisager si besoin (le protocole tunnel SSH + webhooks reste portable) | | **Self-signed par défaut** | Avertissement au premier démarrage du client | Monter des certs Let's Encrypt dans `./ssl/` pour `verify-full` | | **Pas de re-sync automatique côté N8N** | Si un POST échoue côté Odoo, l'entrée reste côté client | Le client retente ; un workflow de réconciliation reste à écrire | ### Scénarios d'évolution **Si je veux ouvrir à un deuxième utilisateur** : - Déployer une seconde clé via `deploy_tunnel_key.sh`. - Ajouter une ligne dans `timetrackr_user_mapping` (username, employee_id Odoo). - Le reste de la config (DB user, port forward) est partagé. **Si je veux remplacer le tunnel SSH** : - WireGuard à `wg.guigpap.com` exposerait un sous-réseau privé incluant Postgres. Plus confortable pour multi-services (admin Postgres en plus du seul client), mais coût opérationnel supérieur (gestion d'IPs, MTU, pair-config). **Si je veux changer le mapping de timesheet** : - Modifier directement le workflow N8N `TimeTrackr - Receive Entries` (par exemple ajouter une catégorie analytique, filtrer certaines tâches). - Aucun déploiement client requis — c'est tout l'intérêt de la sync via webhooks. ### Métriques à surveiller | Métrique | Source | Seuil d'attention | |----------|--------|-------------------| | Taille de la base | `pg_database_size('timetrackr_db')` | Croissance anormale = revoir la rétention locale côté client | | Connexions actives | `SELECT count(*) FROM pg_stat_activity` | Proche de 10 = limite app user atteinte | | Échecs sync N8N | Exécutions du workflow `TimeTrackr - Receive Entries` | Spike = vérifier le token, l'état Odoo, le mapping | | Tentatives SSH `timetrackr-tunnel` | logs `auth.log` | Brute force = revoir les IPs autorisées au niveau firewall | --- ## Pages liées ### Infrastructure - [Architecture VPS](/fr/infrastructure/architecture-vps/) — Vue d'ensemble - [Security Stack](/fr/infrastructure/security-stack/) — Caddy ne route pas TimeTrackr (tunnel SSH dédié) - [Odoo 18 Setup](/fr/infrastructure/odoo-18-setup/) — Modèle `account.analytic.line` cible ### Workflows - [TimeTrackr → Odoo](/fr/workflows/timetrackr/) — Workflows N8N de sync (Projects + Entries) ### Référence - [Glossaire](/fr/reference/glossary/) — SCRAM-SHA-256, SSH tunnel, timesheet ## Metadonnees agent - Cet article est issu du blog GuiGPaP Lab. - Contexte global du blog: https://blog.guigpap.com/llms.txt - Contact auteur: https://odoo.guigpap.com/mon-cv - Licence: CC-BY-SA 4.0