Merge release/1.0.0 into main

This commit is contained in:
Bastien Chanot
2026-07-06 01:14:47 +02:00
21 changed files with 590 additions and 249 deletions
+261
View File
@@ -0,0 +1,261 @@
# TOUR — audit & fix log (append-only)
## Tour 2026-07-05 — REPORT-ONLY — 1 iteration — no branch, zero fixes
Mode: `--report-only` (first real run of /tour). All findings `open`/`suggested`
— nothing was modified. Checks detected: NONE (no tests/lint/build — static
site, no package manager; report line INF-1).
| ID | Axis | File | Sev | Finding | Status |
|----|------|------|-----|---------|--------|
| SEC-1 | security | Dockerfile:27 | high | no `USER` directive — nginx master runs as root in container (semgrep `missing-user`, BLOCK-class). Compose hardening (read_only, cap_drop ALL, no-new-privileges, 127.0.0.1 bind) shrinks blast radius but root master remains. Fix: `FROM nginxinc/nginx-unprivileged:1.29-alpine` (uid 101, port 8080) + adjust EXPOSE/ports/healthcheck | open |
| SEC-2 | security | nginx.conf:46-62 | med | **add_header inheritance trap**: location blocks (.html/.pdf/images) set their own `Cache-Control` → ALL 5 server-level security headers (CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy) dropped on real responses. Confirmed live: pages send zero security headers, only 404 path carries them. Fix: repeat the 5 add_header in each location block (or `include headers.conf`) | open |
| SEC-3 | security | nginx.conf | med | HSTS missing end-to-end — delegated to outer TLS proxy but live site doesn't send it. Fix at the front proxy (VPS) or add here behind X-Forwarded-Proto check | open |
| SEC-4 | security | Dockerfile:4 | med | base `nginx:1.27-alpine` = retired mainline (no security fixes since 2025-04), tag-pinned without digest. Fix: bump to current stable + digest pin | open |
| SEC-5 | security | nginx.conf:26 | low | `set_real_ip_from 0.0.0.0/0` trusts X-Forwarded-For from anywhere — safe only while the 127.0.0.1 bind holds. Fix: restrict to the front proxy IP | open |
| SEC-6 | security | nginx.conf | low | no `server_tokens off` in this config; live front proxy also leaks `nginx/1.24.0 (Ubuntu)` (host-level, outside repo — VPS action) | open |
| SEC-7 | security | nginx.conf:22 | info | CSP `unsafe-inline` script/style — inherent to the documented single-file convention; script hash possible if wanted. Google Fonts = only external dep (conforms; GDPR self-host note). mailto/tel in clear = deliberate for a CV | open/accepted |
| CLN-1 | clean | index.html + CV html | - | 5 × pure-white bg (`#fff`) in `.stack-card`, `.project-card`, `.theme-card`, `.methode-item`, CV `body` — forbidden by project CLAUDE.md → `var(--page)` `#f5f3ec` | suggested |
| CLN-2 | clean | CV_Bastien_Chanot.html | - | dead CSS rule `.screen-label` (no matching element) | suggested |
| CLN-3 | clean | index.html | - | 4 card components duplicate ~80% of base+hover styles (~421 redundant lines) — collapsible into a shared `.card` base class | suggested |
| CLN-4 | clean | index.html | - | 8 colors beyond the strict 6-hex palette (`--dark-mid`, `--g900`, `--g050`, text neutrals…) — likely intentional neutrals; JUDGMENT CALL, not auto-fixable | suggested |
| CLN-5 | clean | index.html | - | CSS transitions stay active under `prefers-reduced-motion: reduce` (only animations disabled) — stricter conformity would zero transitions too | suggested |
| REC-1 | reconcile | .claude/* | - | ZERO drift. Oracles: 1369d27 exists ✓, PDF=HTML same commit 1ae73e0 (declared invariant holds) ✓, develop==origin ✓, BLK-001 resolved AND live-confirmed (favicon HTTP 200 in prod — VPS rebuild done) ✓. Open TODO items (OG image, favicon mirror into CV, mobile QA, WCAG contrast) verified genuinely open, not drift | consistent |
| DOC-1 | doc | README.md | - | Contents table omits `.githooks/` (active gitflow guard since 195188f, predates last README edit) + no clone note `git config core.hooksPath .githooks` | suggested |
| DOC-2 | doc | README.md | - | Contents table omits `.gitignore`/`.dockerignore` — conventionally skipped, low value | suggested |
| INF-1 | infra | - | - | no checks configured (tests/lint/build) — nothing to run in re-verify phase; acceptable for a zero-dependency static site | reported |
### Iterations
1. **It1 (report-only)** — 4 parallel read-only audits: security-auditor
(semgrep 1.168.0, pinned rulesets, 91 rules / 18 files → VERDICT BLOCK(1)),
cso posture (0 crit / 0 high / 3 med / 2 low / 5 info; secrets sweep of tree
+ full git history clean), clean audit (10 findings, config files clean),
doc drift (2 drifts; README otherwise accurate; README-only judged right
for this repo — DEPLOY.md split not warranted). Reconcile inline: zero
drift. Report-only ⇒ zero fixes by design ⇒ single iteration = full
picture; convergence loop N/A.
### Residuals (all — nothing fixed by design)
SEC-1 high (root in container), SEC-2/3/4 med (headers dropped / HSTS / EOL
base image), SEC-5/6 low, CLN-1..5, DOC-1/2. Highest-value single fix:
**SEC-2** (nginx add_header inheritance — live site currently serves zero
security headers).
### Suggested next step
`/tour ~/Documents/bchanot-cv` (auto mode) to fix on a `chore/tour-*` branch —
SEC-1/2/4 + CLN-1/2 are mechanical; SEC-3 needs the VPS side; CLN-3 is a
larger refactor worth its own pass; CLN-4 is the owner's judgment call.
Commits: 1 (this report — `.claude/**`, hook-exempt; no code touched).
Scratch reports (.tour-semgrep/.tour-cso/.tour-clean/.tour-doc) folded here
then deleted (STEP 3.2).
## Tour 2026-07-05 — AUTO — branch chore/tour-2026-07-05 — 2 iterations — CONVERGED
Fix pass over the 2026-07-05 report-only findings (user GO + 3 scope answers:
fix Docker path / strict palette conformity / prod vhost provided).
| ID | Axis | File | Sev | Finding | Status |
|----|------|------|-----|---------|--------|
| SEC-1 | security | Dockerfile | high | root master in container | fixed ba13d69 — `nginxinc/nginx-unprivileged:1.28-alpine` digest-pinned, uid 101 (verified `id` in container), `USER root` scoped to the one `rm`, cap_add dropped — **BREAKING**: container port 80 → 8080 (compose mapping/healthcheck updated same commit; VPS `.env PORT=2937` unaffected: mapping is `127.0.0.1:${PORT}:8080`) |
| SEC-2 | security | nginx.conf | med | add_header inheritance dropped all security headers | fixed ba13d69 — shared `nginx-security-headers.conf` snippet re-included in every location; live-style oracle in hardened container: 5/5 headers on `/`, `.html`, `.pdf`, favicon |
| SEC-3 | security | VPS vhost | med | HSTS missing end-to-end | fixed IN PROD by owner (front vhost patch) — live-verified `strict-transport-security: max-age=31536000` on bchanot.fr + www |
| SEC-4 | security | Dockerfile | med | EOL base image, tag-only pin | fixed ba13d69 (1.28-alpine stable + digest) |
| SEC-5 | security | nginx.conf | low | trust-all set_real_ip_from | fixed ba13d69 (→ 127.0.0.1, matches compose bind) |
| SEC-6 | security | nginx.conf + VPS vhost | low | server version leak | fixed ba13d69 (`server_tokens off` in-repo) + IN PROD by owner (front) — live-verified `server: nginx` |
| SEC-7 | security | snippet:12 | low/info | CSP `unsafe-inline` | open/accepted — documented convention, static no-input site (it2 semgrep sole non-blocking note) |
| CLN-1 | clean | index.html + CV | - | 5× `background:#fff` | fixed 7e7bd66 → `var(--page)` (user chose strict conformity; visual change: cards blend with parchment, borders kept) |
| CLN-2 | clean | CV html | - | dead `.screen-label` | fixed 7e7bd66 |
| CLN-5 | clean | index.html | - | transitions alive under reduced-motion | fixed 7e7bd66 (universal kill rule) |
| CLN-3 | clean | index.html | - | ~421-line card CSS duplication | open — refactor worth its own pass |
| CLN-4 | clean | index.html | - | 8 neutrals beyond strict palette | open — owner judgment call |
| REC-1 | reconcile | TODO/BDR-004 | - | prod topology CONFIRMED = BDR-004 as declared (native front proxy → container on 2937); earlier "native, no docker" premise was the misunderstanding — container IS the content server | consistent |
| DOC-1 | doc | README.md | - | .githooks row + hooksPath note; deploy section synced (unprivileged image, snippet, front/container split) | fixed 840632a |
| INV-1 | invariant | CV pdf | - | PDF regenerated with the HTML (weasyprint, same commit 7e7bd66) | held |
### Iterations
1. **It1** — fixes from the same-day report-only audit (tree unchanged since):
security ba13d69 (docker build + in-container `nginx -t` + hardened run +
4-location header oracle ALL PASS), clean 7e7bd66 (+PDF regen), doc
840632a (via doc-commit.sh). Prod side: owner applied front vhost patch
(HSTS + server_tokens), live-verified from here.
2. **It2 (convergence)** — fresh semgrep full scan: VERDICT PASS, 0 blocking
(prior Dockerfile BLOCK resolved), 1 LOW reported (SEC-7 accepted); fresh
clean re-audit: CONVERGED-CLEAN yes, prior findings resolved, zero new
(CSS braces balanced, README↔infra aligned). Zero fixes → CONVERGED.
### Residuals (open)
SEC-7 (accepted CSP convention), CLN-3 (dedup refactor), CLN-4 (palette
judgment). Prod content headers (CSP/XCTO/XFO…) appear once the fixed
container is redeployed: merge → VPS `git pull && docker compose up -d
--build` → verify `curl -sI https://bchanot.fr/ | grep -i x-content`.
Commits: 4 (fix/clean/docs + this report). BREAKING: 1 (SEC-1, container
port — compose covered). Branch left UNMERGED — `gitflow finish` on GO.
## Follow-up 2026-07-05 — residuals closed (chore/tour-residuals, user GO)
| ID | Resolution |
|----|-----------|
| CLN-3 | Card CSS deduplicated via grouped selectors (shared chrome/hover/head/title/tag blocks + per-class specifics), zero HTML change, cascade-order verified (no interfering same-specificity rules between shared and specific blocks), braces 195/195. Honest correction: the audited "~421 redundant lines" was overstated — real net dedup ≈ 60 lines. |
| CLN-4 | Norm aligned with reality: the 8 functional neutrals (inks, rule/tag, 2 green intermediates) are now DOCUMENTED as allowed in CLAUDE.md (+ README pointer). "Any color outside the two lists is a violation" keeps the norm enforceable. |
| SEC-7 | script-src hardened: `unsafe-inline` replaced by the sha256 hash of the single inline script (index has zero style/script attributes). style-src keeps `unsafe-inline` (CV carries 2 style attributes + single-file convention) — documented. NEW INVARIANT in CLAUDE.md: recompute the hash after any inline-JS edit (stale hash = JS silently blocked in prod). |
| INF-2 | CORRECTION: false positive in the 2026-07-05 report-only run — `.gitignore` exists (549B) and covers the expected classes. No action was ever needed. |
## Tour 2026-07-05-2 — AUTO — branch chore/tour-2026-07-05-2 — 2 iterations — CONVERGED
Re-run of /tour on develop (d7256ff) after the day's earlier tours merged. Goal:
verify no regression + catch anything new. Branch suffixed `-2` to keep this
header distinct from the earlier converged run. gstack OFF → optional It1 cso
posture add-on not run; the security floor (security-auditor + pinned semgrep)
ran BOTH iterations, not degraded. No package.json/Makefile → no automated
tests/lint/build; project checks = domain invariants (CSP-hash, PDF↔HTML).
| ID | Axis | File | Sev | Finding | Status |
|----|------|------|-----|---------|--------|
| SEC-1 | security | (full tree) | - | Fresh semgrep both iterations → VERDICT PASS, 0 blocking. Sole note: `style-src 'unsafe-inline'` (accepted single-file convention, It1==It2). No regression from the prior SEC fixes; script-src hash still matches inline script | pass |
| CLN-1 | clean | index.html:347 | - | dead `.reveal.d6` rule (markup uses reveal d1–d5 only) | fixed 30b0e44 |
| CLN-2 | clean | CV:408 | - | dead `position: running(siteFooter)` — no `element(siteFooter)` consumer; `.footer-bar` is `display:none` in @media print (@page auto-numbered footer replaces it); on screen running() is an invalid position value, ignored | fixed 30b0e44 |
| CLN-3 | clean | CV:438 | - | no-op `box-shadow: none` on `.page` (`.page` sets a shadow nowhere; weasyprint ignores box-shadow entirely — confirmed by its own warning) | fixed 30b0e44 |
| CLN-4 | clean | CV:315 | - | dead `.skills-grid { font-size: 8.4pt }` — every direct child is a `.skill-label`(9.5pt)/`.skill-values`(10pt) div; no bare text node, no em-dependency → never renders | fixed 30b0e44 |
| CLN-5 | clean | CV:46-48,54,316 | - | stray blank lines (triple blank before `.page`, blanks inside `.page`/`.skills-grid`) | fixed 30b0e44 |
| CLN-6 | a11y | index.html:1003,1007 | - | 2 decorative `.arrow` SVGs miss the `aria-hidden="true"` the sibling download arrow (1011) has. NOT auto-fixed: adding it changes the a11y tree → outside the clean phase's behavior-preserving scope (belongs to an a11y pass; cf. TODO "WCAG AA contrast") | open (suggested) |
| CLN-7 | norm | index.html:931 | - | `.footer` bg `#061008` is off-palette (darker than `--dark #0d1b12`, `--g900 #0e3320`). Design system documents footer = `#0d1b12`. Fix changes rendering (slightly lighter footer) → owner decision, not auto-fixed | open (suggested) |
| CLN-8 | norm | CV:252,135-136,434-435 | - | off-palette colors: `#a8d4bc` tag border (252), gradient stops `#edeadf`/`#f2efe6` (136/435), texture fill `rgba(26,71,48,0.05)` (135/434). All rendering-changing → owner decision, not auto-fixed | open (suggested) |
| CLN-9 | content | index.html vs CV:507-508 | - | profile-state wording drift: landing "Pays de la Loire / remote or 1–2j Paris" vs CV "région nantaise / hybride Nantes / 1–2j Paris" (not contradictory — Nantes ∈ PdL — but CV adds "hybride Nantes"). CLAUDE.md requires cross-file consistency → owner picks canonical wording, not auto-fixed | open (suggested) |
| REC-1 | reconcile | .claude/memory/decisions.md | - | **BDR-004 stale**: text says `nginx:1.27-alpine` / container port 80 / "HSTS omitted at container", but the real Dockerfile+compose (post 2026-07-05 SEC-1 fix) = `nginxinc/nginx-unprivileged:1.28-alpine` / port 8080 / uid 101. That tour never added a superseding decision (index stops at BDR-005). Append-only registry + tour-read-only → SUGGEST a superseding **BDR-006**. README deploy section is already correct | suggested |
| REC-2 | reconcile | .claude/memory/decisions.md | - | BDR-002 "Warnings connus: `box-shadow:none` ignoré par weasyprint" — that declaration was removed this tour (CLN-3), so the documented warning no longer fires. Minor note to add when BDR-002 is next touched | suggested |
| REC-3 | reconcile | TODO.md + registries | - | ZERO false-done. Oracles: 1369d27 exists ✓; `og:image` absent = TODO item genuinely open ✓; CV favicon-block not mirrored = open, matches BDR-005 note ✓; WCAG-contrast + real-mobile-QA open ✓; develop==origin/develop (d7256ff), branch +1 unmerged ✓; BLK-001 resolved, favicon assets present ✓ | consistent |
| DOC-1 | doc | README.md | - | doc-syncer automatic mode → `PATCHED_FILES: (none)`. Deploy section already reflects unprivileged image/port 8080 (prior tour sync); file table matches root inventory; cleanup touched nothing user-facing | no-op |
| INV-1 | invariant | index.html / CV pdf | - | CSP hash `sha256-Al1M34KxI6Ye5Viu6aO//7CYyaLzqtpG9GX95FFlSOY=` recomputed == pinned (inline `<script>` untouched) ✓; PDF regenerated byte-identical to the pre-edit baseline (text sha256 083055…96a8 + per-page PNG @150dpi render hash all match) → PDF=HTML invariant holds, PDF file unchanged | held |
### Iterations
1. **It1** — security-auditor fresh semgrep (94 rules / 25 files → VERDICT PASS,
1 accepted LOW) + read-only clean audit (13 findings: C1–C8, N1–N5). Applied
behavior-preserving fixes CLN-1..5 (commit 30b0e44); proven behavior-preserving
(PDF renders pixel-identical, CSP hash unchanged). Fresh `analyzer` re-verify:
RE-VERIFY PASS, braces balanced, zero new. Reconcile (report-only): BDR-004
drift + BDR-002 note + zero false-done. Doc: no drift.
2. **It2 (convergence)** — fresh full-tree semgrep: VERDICT PASS, identical to It1,
0 new blocking. Clean stability: 4 removed selectors GONE, braces balanced
(index 204/204, CV 68/68), known-open findings (CLN-6..9) persist = NOT new,
zero new introduced. Zero fixes → CONVERGED.
### Residuals (open — all require owner judgment, none auto-fixable behavior-preservingly)
CLN-6 (arrows aria-hidden — a11y pass), CLN-7/8 (off-palette colors — design
decision), CLN-9 (profile-state wording — copy canonicalization), REC-1
(superseding BDR-006 for the hardened container), REC-2 (BDR-002 warning note).
SEC accepted-LOW (`style-src 'unsafe-inline'`) unchanged from prior runs.
Checks: semgrep PASS (both it.), CSP-hash MATCH, PDF↔HTML byte-identical render,
CSS braces balanced. No automated tests/lint/build (static site).
Commits: 2 (clean 30b0e44 + this report). BREAKING: 0. Branch left UNMERGED.
Scratch reports (.tour-semgrep, .tour-clean, .tour-semgrep-it2) folded here then
deleted (STEP 3.2).
## Follow-up 2026-07-05-2 — all 5 residuals closed (chore/tour-2026-07-05-2, owner GO)
| ID | Resolution |
|----|-----------|
| CLN-6 | `aria-hidden="true"` added to the 2 decorative CTA arrows (match sibling download arrow). Visual identical, a11y-tree only. Commit `607124a`. |
| CLN-7 | `.footer` bg `#061008` → `var(--dark)` #0d1b12 (the design-system footer color). Commit `ede7576`. |
| CLN-8 | CV off-palette → tokens: `.tag` border `#a8d4bc` → `var(--g300)` (nearest visible green); body+print texture `rgba(26,71,48,.05)` → `rgba(27,94,59,.05)` (--g700); gradient stops `#edeadf`/`#f2efe6` → `var(--tag)`/`var(--page)`. PDF regenerated, render verified (2 pages, layout intact, page-1 eyeballed). Commit `ede7576`. |
| CLN-9 | Owner chose the CV wording as canonical (Option B): landing about-para + callout aligned to "installation région nantaise prévue" + "hybride Nantes"; `CLAUDE.md` geography note updated to match. CV unchanged. Commit `f515875` → BDR-007. |
| REC-1 | BDR-004 drift resolved by superseding entry **BDR-006** (nginx-unprivileged:1.28 / port 8080 / uid 101). |
| REC-2 | BDR-002 "box-shadow warning" note now historical (declaration removed in `30b0e44`) — left as-is (append-only registry), noted here. |
Checks: CSP-hash MATCH, braces balanced (index 204/204, CV 68/68), PDF 2 pages.
Commits: 3 fixes (`607124a`/`ede7576`/`f515875`) + capitalize (BDR-006/007, LRN-003,
journal) + this follow-up. Branch finished → develop + pushed on owner GO.
## Tour 2026-07-05-3 — AUTO — branch chore/tour-2026-07-05-3 — 3 iterations — CONVERGED
Third run of the day, on develop 7967aff (all prior tour residuals merged).
gstack ON → cso posture add-on ran it1 (it was OFF for run -2) — and caught the
run's only HIGH. Session-limit pause mid-it3 (2026-07-05→06); both it3 agents
resumed from transcript, tree unchanged, no audit gap.
| ID | Axis | File | Sev | Finding | Status |
|----|------|------|-----|---------|--------|
| SEC-1 | security | Dockerfile:5 | high | base `nginx-unprivileged:1.28-alpine` (correct this morning) now on a RETIRED stable branch — 2026-05-13 nginx batch (CVE-2026-42945, rewrite-module buffer overflow, RCE/DoS-class) fixed only in 1.30.1+/1.31.1+, never backported to 1.28.x; digest pin froze the vulnerable build | fixed 1aa97f0 — `1.30-alpine@fd3314e3…` (nginx/1.30.3). Verified: build, `nginx -t`, uid 101, hardened run 5/5 headers + HTTP 200 on /, .html, .pdf, favicon. Not BREAKING (same port/contract); prod needs rebuild+redeploy after merge |
| SEC-2 | security | (full tree) | - | semgrep floor: fresh scan ALL 3 iterations → VERDICT PASS, 0 findings (94 rules; 23→28 files as scratch reports accrued). cso it1: all same-day fixes hold; secrets sweep tree + 36-commit history clean | pass |
| CLN-1 | clean | CV:490 | - | leftover double blank after `<body>` (run -2's CLN-5 went triple→double) | fixed 613bfc0 |
| CLN-2 | clean | nginx.conf:67 | - | dead `deny all;` — `return 404` fires at rewrite phase, access phase never reached | fixed 613bfc0 — dotfile-404 oracle PASS |
| CLN-3 | clean | .dockerignore:14 | - | phantom `nginx.conf.bak` (never existed in tree or history) | fixed 613bfc0 |
| CLN-4 | clean | CV | - | duplicated rules: `.xp/.project/.edu-header`, 3 date chips (5/7 shared), `.xp-role`≡`.edu-degree`, `.lang-item`≡`.interest-tag` → shared block + per-class overrides | fixed 613bfc0 — PDF text-hash + per-page render-hash + full byte-identity vs committed PDF (LRN-003) |
| CLN-5 | clean | CV:528,585 | - | 2 byte-identical inline `style=` attrs → `.inline-link` class; snippet comment ("style attributes in the CV") synced | fixed 613bfc0 — CV now zero style attrs |
| CLN-6 | clean | index:505/761 | - | `.stack-note code`≡`.theme-list code` minus padding → grouped | fixed 613bfc0 |
| CLN-7 | clean | CV:43-44,430 | - | no-op body `margin/padding` (universal reset covers) | fixed 613bfc0 |
| CLN-8 | clean | index:700-701 | - | 2 no-op `.formation .timeline*` overrides restating base values (also removed a latent same-specificity override of the `.current` ring) | fixed 613bfc0 |
| J1 | norm | index+CV (12 sites) | - | `#fff`/rgba-white family (text-on-dark + 4% overlay) outside BOTH documented palette lists — de-facto accepted, undocumented | open — document as 3rd allowed family in CLAUDE.md, or map to tokens (visible change) |
| J2 | norm | CV headings | - | CV section titles/roles mono/sans vs CLAUDE.md "Fraunces = section titles, role headings" — deliberate compact-CV style, unflagged by all prior passes | open — document CV exception (recommended) or restyle |
| J3 | norm | CV:204 | - | `border-radius: 10px` on lang/interest tags — >6px unless counted as pills | open — owner call |
| J4 | config | nginx.conf | - | regex-location order lets hypothetical `/.foo.html` hit the caching block before the dotfile block (both still 404 — file absent; defense-in-depth only) | open — optional reorder |
| J5 | config | Dockerfile+compose | - | healthcheck duplicated (compose fully overrides image HEALTHCHECK, identical params — one always inert) | open — owner call (image stays self-checking without compose) |
| N1 | clean | both font URLs | info | unused Google Fonts faces (index: Fraunces 0,300/0,500/0,700 + DM Sans 300; CV: Fraunces 0,300/0,600 + DM Sans 300) | open — CV half provable via render-hash; index half needs a visual oracle (browser) → owner GO |
| N2 | content | CV:485/498/517 | - | date chips in English (`Apr 2026 - present`…) vs French-copy rule + hyphen/en-dash inconsistency | open — content change, owner call |
| N3 | clean | index:863-869 | info | `.contact-grid` grid declarations no-op around single child — removing `display:grid` can alter margin-collapsing, no render oracle | open |
| N4 | config | nginx.conf:39 | info | `application/pdf` in gzip_types — compressing already-flate-compressed format; removal changes observable response header | open — owner call |
| REC-1 | reconcile | TODO + registries | - | ZERO pre-existing drift. Oracles: CSP hash pinned==computed ✓, PDF↔HTML same commit ede7576 + byte-identical render ✓, BDR-006 (unprivileged/8080) + BDR-007 (Nantes wording ×2 index, ×1 CV) match tree ✓, BLK-001 COPY line holds ✓, og:image + CV-favicon TODO items genuinely open ✓, develop==origin ✓ | consistent |
| REC-2 | reconcile | decisions.md BDR-006 | - | SEC-1 bump makes BDR-006's "1.28-alpine" version detail stale (decision itself — unprivileged base + 8080 — unchanged). Registry read-only for tour | suggested — annotate via /reconcile or /capitalize |
| DOC-1 | doc | README.md:91 | - | stale `1.28-alpine` ref after SEC-1 | fixed 2f5e51a (doc-syncer automatic, single-line) |
| INV-1 | invariant | CSP + PDF | - | CSP hash MATCH all iterations (script byte-untouched); post-clean PDF byte-identical to committed (text sha256 083055…96a8 + both page render-hashes) → PDF file unchanged, nothing to regen | held |
### Iterations
1. **It1** — parallel: security-auditor (semgrep PASS 0 findings) + cso posture
(gstack ON, it1-only: 0c/1h/0m/0l/6i — the HIGH = SEC-1, sourced
endoflife.date + nginx advisories) + clean audit (8 fixable / 5 judgment).
Fixes: 1aa97f0 (security, oracle-verified) + 613bfc0 (clean F1–F8, 5 files,
net −54 lines, render-hash proof). Re-verify (fresh analyzer): PASS — cascade
safety, zero dead selectors, commits scoped. Reconcile: zero drift + REC-2.
Doc-syncer automatic: README 1.28→1.30 (2f5e51a via scoped fallback commit).
2. **It2** — fresh semgrep PASS; clean stability: it1 fixes hold (braces
203/203, 67/67), but 4 NEW info/judgment findings (N1–N4). Fix policy: none
provably behavior-preserving with available oracles (N1-index/N3 need a
browser render; N2/N4 owner calls) → 0 applied, all catalogued open. New
findings appeared → iteration 3 required.
3. **It3 (convergence, at bound)** — fresh semgrep PASS (0 findings); fresh
clean sweep against the full catalogue: stability PASS, borderline items
considered and rejected below threshold, ZERO new. Zero fixes + zero new →
CONVERGED.
### Residuals (open — all owner-judgment, none auto-fixable with available oracles)
J1 (document white family — recommended), J2 (document CV typography
exception — recommended), J3 (10px radius), J4 (dotfile regex order), J5
(healthcheck dup), N1 (font trim — CV provable, index needs eyeball), N2
(English date chips), N3 (.contact-grid), N4 (gzip pdf), REC-2 (BDR-006
version note). Standing accepted/TODO: SEC-7 CSP style-src, og:image, CV
favicon block, WCAG contrast, real-mobile QA.
### Prod follow-up
SEC-1 lands in prod only after merge: VPS `git pull && docker compose up -d
--build` → verify `curl -sI https://bchanot.fr/ | grep -i server` + container
`nginx -v` = 1.30.3.
Checks: semgrep PASS ×3, docker build + nginx -t + hardened-run 4-location
header oracle PASS, CSP-hash MATCH, PDF byte-identical, braces 203/203 + 67/67.
No automated tests/lint/build (static site). Commits: 4 (fix/clean/docs + this
report). BREAKING: 0. Branch left UNMERGED — `gitflow finish` on owner GO.
Scratch reports (.tour-semgrep ×3, .tour-cso, .tour-clean ×3) folded here then
deleted (STEP 3.2).
## Follow-up 2026-07-06 — all 10 residuals closed (chore/tour-2026-07-05-3, owner GO)
| ID | Resolution |
|----|-----------|
| N1 | Google Fonts trimmed: index drops Fraunces 0,300/0,500/0,700 + DM Sans 300 (keeps 0,600 + 1,400 / 400;500;600); CV drops Fraunces 0,300/0,600 + DM Sans 300 (keeps 0,700 + 1,300 / 400;500). CV PROVEN render-identical (per-page hash == baseline). index: font-matching analysis (zero strong/em inside serif elements beyond handled cases: hero-name em → 1,400; about/tsrit strong = sans with explicit weights) + headless-browser check 375px & 1440px — real Fraunces italic renders, zero console errors. |
| N2 | CV date chips → French + en-dash: `avr. 2026 – présent`, `mars 2019 – mars 2025`, `fév. 2017 – nov. 2017` (mirrors landing wording; edu chips already en-dash). |
| J3 | `.lang-item`/`.interest-tag` radius 10px → 999px (true pill treatment, matches landing `--r-pill`). |
| N3 | `.contact-grid` no-op grid declarations dropped (single child + universal reset ⇒ no margin-collapse delta); `position`/`z-index` kept. Browser-verified both widths. |
| J4 | dotfile `location ~ /\.` moved ABOVE the caching regex locations (first regex match wins). Oracle: `/.hidden` + `/.foo.html` → 404, pages 200, headers 5/5. |
| N4 | `application/pdf` dropped from `gzip_types`. Oracle: PDF response carries no Content-Encoding under `Accept-Encoding: gzip`; HTML still gzipped. |
| J5 | compose `healthcheck:` block removed — image HEALTHCHECK is the single definition, inherited by compose. Oracle: compose-less hardened run → `docker inspect` Health = `healthy`. |
| J1 | White family documented in CLAUDE.md allowed lists (text/hover on dark + ≤5% overlays; never a background). |
| J2 | CV typography exception documented in CLAUDE.md (mono section titles/company names, sans roles; Fraunces = header name + accroche; main mapping = landing). |
| REC-2 | BDR-006 annotated: 1.30-alpine bump (CVE-2026-42945), decision itself unchanged. |
CV PDF regenerated (weasyprint, 2 pages, both eyeballed: chips one line, layout
intact). Checks: docker build + nginx -t PASS, header/dotfile/gzip/healthcheck
oracles PASS, CSP hash MATCH (inline script untouched), index verified headless
at 375px + 1440px. Capitalize: LRN-004, EVAL-001, BDR-006 note, journal
2026-07-06. Branch → develop on owner GO (this session).
+10
View File
@@ -0,0 +1,10 @@
# Deploy incidents (append-only) — DEP-NNN
<!-- One entry per incident. Next ID = grep '^## DEP-' | max+1. Mirrors blockers.md. -->
<!-- Resolution = the commit that adds this entry (atomic patch+incident). Recover: git log -S 'DEP-NNN' -- .claude/deploy/INCIDENTS.md -->
<!-- ## DEP-NNN — <step> failed
- date: YYYY-MM-DD
- step: <runbook step + label>
- error: `<verbatim error>`
- cause: <root cause>
- fix: <what changed in PROCEDURE.md> -->
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# === deploy runbook (reference) — NOT run directly. Instantiated to NEXT.sh per delta. ===
# Fixed steps run every deploy; # @delta: steps re-instantiate from the delta.
# @config push_deploy_tags=false
# Static site baked into the nginx image (COPY whitelist): any content or
# infra change needs a rebuild; docs/.claude-only deltas skip it.
# Front: VPS native nginx (TLS, HSTS) → proxy_pass 127.0.0.1:$PORT → container.
# Style: one command per line, as typed in an interactive session — step 1 opens
# the ssh session, later steps run ON the box; local steps say "(from your machine)".
# 1) connect + pull the desired branch (fixed)
ssh "$DEPLOY_HOST"
cd "$APP_DIR"
git pull # VERIFY: HEAD == target sha
# @delta:rebuild when=index.html,CV_Bastien_Chanot.*,favicon*,apple-touch-icon.png,Dockerfile,docker-compose*.yml,nginx*.conf
# 2) rebuild + restart the container (content is baked into the image)
docker compose up -d --build # VERIFY: docker compose ps → healthy
# 3) smoke test (from your machine)
curl -fsS -o /dev/null -w '%{http_code}\n' https://bchanot.fr/ # VERIFY: 200
curl -sI https://bchanot.fr/ | grep -i 'x-content-type-options' # VERIFY: nosniff
# ROLLBACK: on the VPS — git checkout deploy/<date-précédent> && docker compose up -d --build
+6
View File
@@ -0,0 +1,6 @@
{
"deployed_sha": "b24c58b8a467811719ff197f4b008d2f991b80d0",
"deployed_at": "2026-07-05T16:55:00+02:00",
"outcome": "ok",
"tag": "deploy/2026-07-05-2"
}
+25
View File
@@ -27,6 +27,8 @@ rules:
| BDR-003 | 2026-05-15 | Position pro: CDI prioritaire, freelance parallèle | accepted |
| BDR-004 | 2026-05-15 | Containerize site with nginx:alpine behind reverse proxy | accepted |
| BDR-005 | 2026-05-17 | Favicon: SVG primary + PIL raster fallback | accepted |
| BDR-006 | 2026-07-05 | Hardened container: nginx-unprivileged + port 8080 | accepted (supersedes BDR-004 infra detail) |
| BDR-007 | 2026-07-05 | Profile geo canonical: Nantes relocation | accepted (supersedes BDR-003 geo) |
---
@@ -95,3 +97,26 @@ rules:
- Online favicon generator — external dep, opaque rendering, no source control.
- **CV HTML**: not modified (user's WIP M state). Browser auto-fetches `/favicon.ico` from root → CV tab still shows icon. Link block mirror logged in `.claude/tasks/TODO.md` for later.
- **Reference**: `favicon.svg`, `favicon-32.png`, `favicon.ico`, `apple-touch-icon.png`, `index.html` head, commit `ef31fb3`.
---
## BDR-006 — Hardened container: nginx-unprivileged base + port 8080
- **Date**: 2026-07-05
- **Status**: accepted — supersedes the base-image/port detail of BDR-004
- **Decision**: Container base = `nginxinc/nginx-unprivileged:1.28-alpine` (digest-pinned), runs as uid 101, listens on **8080** (not 80). Compose maps `127.0.0.1:${PORT}:8080`; `USER root` scoped to the one build-time `rm` only; `cap_add` dropped; `server_tokens off`; `set_real_ip_from 127.0.0.1`; `nginx-security-headers.conf` re-included per `location`; CSP `script-src` hash-pinned.
- **Why**: SEC-1 tour finding — stock `nginx:*-alpine` runs its master as root inside the container. Unprivileged image + port 8080 removes the root master; the rest shrinks blast radius. BDR-004's "port 80 / nginx:1.27-alpine / HSTS omitted at container" no longer matched the tree.
- **Supersedes**: BDR-004 — topology unchanged (native front proxy → container on loopback); only the base image, internal port, and uid change.
- **Reference**: `Dockerfile`, `docker-compose.yml`, `nginx.conf`, `nginx-security-headers.conf`. Fix commit `ba13d69`; drift caught by tour REC-1 (`.claude/audits/TOUR.md`, run 2026-07-05-2).
- **Update 2026-07-06**: base bumped `1.28-alpine` → `1.30-alpine` digest-pinned (nginx/1.30.3) — 1.28 branch retired, CVE-2026-42945 fixed 1.30.1+ only, no backport. Decision unchanged (unprivileged base, 8080, uid 101). Commit `1aa97f0`, tour 2026-07-05-3 REC-2.
---
## BDR-007 — Profile geo canonical: Nantes relocation
- **Date**: 2026-07-05
- **Status**: accepted — supersedes the geography detail of BDR-003
- **Decision**: Canonical profile geo = "Yerres (91) now; installation région nantaise prévue à moyen terme; full remote, hybride possible sur Nantes, ou 1–2 j/mois Paris." CV was the source of truth; `index.html` + `CLAUDE.md` aligned to it.
- **Why**: tour CLN-9 found the landing ("mobilité Pays de la Loire") drifting from the CV ("installation région nantaise" + "hybride Nantes"). Owner chose the CV wording as truth — more current/specific, and Nantes ∈ Pays de la Loire so not contradictory. Cross-file profile-state consistency is a CLAUDE.md content rule.
- **Alternatives rejected**: align CV down to the landing (would delete real, more-specific relocation info).
- **Reference**: `index.html` (about para + about-callout), `CV_Bastien_Chanot.html`, `CLAUDE.md` geography note. Commit `f515875`.
+9
View File
@@ -21,6 +21,15 @@ rules:
| ID | Date | Output | Action |
|----|------|--------|--------|
| EVAL-001 | 2026-07-06 | /tour run 2026-07-05-3 (3 it., converged) + residual closure | keep |
## EVAL-001 — /tour run 2026-07-05-3 + residual closure pass
- **Date**: 2026-07-06
- **Output**: 3-iteration tour (security/clean/reconcile/doc, converged at bound) + closure of all 10 residuals on owner GO. Commits `1aa97f0`/`613bfc0`/`2f5e51a` + follow-up.
- **Method**: oracle-based — semgrep ×3 (deterministic PASS), PDF render-hash (LRN-003) for behavior-preserving proofs, docker oracles (build, nginx -t, header/dotfile/gzip/healthcheck curls), headless-browser screenshots 375+1440 (index font trim), brace counts, CSP-hash pinned==computed.
- **Anomalies**: (1) cso add-on caught a HIGH (base-image CVE) two same-day semgrep-only tours missed — gstack was OFF then → LRN-004. (2) Fresh clean sweeps surfaced new info-tier nits each iteration (N1–N4 at it2) — convergence needed explicit reporting threshold in it3 prompt; bound of 3 did its job. (3) Session limit killed both it3 agents mid-flight — SendMessage transcript-resume recovered both, zero re-audit gap.
- **Action**: keep
<!-- Append entries below. Template:
+14
View File
@@ -33,3 +33,17 @@ rules:
- Favicon set added (commit `ef31fb3`): SVG primary + PIL-generated PNG/ICO/apple-touch. Brand pulse-dot translated to icon. BDR-005 + LRN-002 logged.
- CV files (`CV_Bastien_Chanot.html`, `.pdf`) untouched — user's WIP M state, off-scope per brief.
- User pushed + reported favicon 404 in prod. Root cause: Dockerfile selective `COPY` whitelist never included favicon files. Fix shipped (commit `f1e4392`): COPY line appended + nginx long-cache rule for image assets. BLK-001 logged. VPS rebuild required.
## 2026-07-05
- Grouped tours (security+clean+reconcile+doc): container hardened (SEC-1..7 → nginx-unprivileged:1.28 / port 8080 / uid 101, per-location security headers, `server_tokens off`, CSP script-src hash-pinned), palette + dead-code clean, README synced. Merged via chore/tour + chore/tour-residuals. Detail in `.claude/audits/TOUR.md`.
- Re-run tour (chore/tour-2026-07-05-2) CONVERGED 2 it: fresh semgrep PASS, dead CSS removed (`30b0e44`, render-hash proven behavior-preserving → LRN-003), reconcile caught BDR-004 drift, doc no-drift.
- Closed all 5 residuals on owner GO: CLN-6 aria-hidden CTA arrows (`607124a`), CLN-7/8 palette conformance (5 off-palette colors → tokens, PDF regen render-verified, `ede7576`), CLN-9 geo aligned landing→CV = Nantes relocation (`f515875`).
- Decided: BDR-006 (hardened container, supersedes BDR-004 infra), BDR-007 (geo canonical = Nantes, supersedes BDR-003 geo).
- Branch chore/tour-2026-07-05-2 finished → develop + pushed.
## 2026-07-06
- Tour 2026-07-05-3 finished (session-limit pause mid-it3, agents transcript-resumed): CONVERGED 3 it. SEC-1 HIGH fixed — base 1.28→1.30-alpine, CVE-2026-42945 (`1aa97f0`); clean F1-F8 (`613bfc0`, −54 lines, render-hash proven); README synced (`2f5e51a`).
- All 10 residuals closed on owner GO: Google Fonts trimmed both files (CV render-hash identical, index browser-verified 375+1440), CV date chips French + en-dash, CV tags → 999px pills, contact-grid no-ops dropped, nginx dotfile block reordered first, PDF gzip dropped, compose healthcheck deduped (image healthcheck verified healthy), CLAUDE.md white-family + CV-typography exception documented, BDR-006 annotated (1.30 bump).
- LRN-004 (pinned base = frozen CVE exposure) + EVAL-001 (tour verdict) logged. Branch chore/tour-2026-07-05-3 → develop on owner GO (this session).
+20
View File
@@ -21,6 +21,8 @@ rules:
|----|------|---------|------------|
| LRN-001 | 2026-05-15 | certbot --nginx matches `server_name`, not filename | nginx + certbot on multi-site VPS |
| LRN-002 | 2026-05-17 | PIL supersample ×8 + Lanczos = clean icon antialiasing | Python stdlib icon generation |
| LRN-003 | 2026-07-05 | Prove CSS cleanup behavior-preserving via before/after PDF render-hash | weasyprint / paged-media PDF projects |
| LRN-004 | 2026-07-06 | Digest-pinned base image = frozen CVE exposure; SAST can't see it | any Dockerfile with pinned FROM |
---
@@ -39,3 +41,21 @@ rules:
- **Pattern**: Render icon at 8× target size via `ImageDraw.rounded_rectangle` + `ellipse` on RGBA canvas, then `Image.resize((target, target), Image.LANCZOS)`. Output rivals `rsvg-convert` / `inkscape` for simple geometric shapes. Crisp at 16×16 favicon scale, no visible jaggies.
- **Context**: Generated `favicon-32.png`, `apple-touch-icon.png` (180×180), `favicon.ico` (multi-size 16/24/32/48) for `bchanot.fr` from scratch — no `rsvg-convert` / `inkscape` / `ImageMagick` on host. Single PIL script, ~20 lines.
- **Future application**: Any project needing a PNG/ICO icon set with a stdlib-only Python toolchain. Skip if shape is complex (text rendering, gradients, curves) — use `rsvg-convert` or commit a finalized PNG instead.
---
## LRN-003 — Prove CSS cleanup is behavior-preserving via before/after PDF render-hash
- **Date**: 2026-07-05
- **Pattern**: To confirm a CSS/HTML edit is truly behavior-preserving on a project whose deliverable is a weasyprint PDF: render a baseline PDF from the pre-edit HTML, apply the edit, regenerate, then compare (a) `pdftotext | sha256` and (b) per-page `pdftoppm -r 150 -png | sha256`. Text-hash alone misses `font-size`/color changes — the render-hash catches them. Identical render-hash = provably no visual change; and since weasyprint output is deterministic, an unchanged render yields a byte-identical PDF → nothing new to commit.
- **Context**: tour clean phase on `bchanot-cv` removed dead CSS (`.reveal.d6`, `position:running()`, no-op `box-shadow`, dead `.skills-grid font-size`). Render-hash matched on both pages → proven before commit `30b0e44`. The same tooling later confirmed the intentional palette edit DID change the render (expected), distinguishing dead-code removal from real visual change.
- **Future application**: Any weasyprint / paged-media project where you must tell "dead code removal" (must render identically) apart from "intended visual change". General trick: verify a refactor by hashing the rendered artifact, not the source.
---
## LRN-004 — Digest-pinned base image = frozen CVE exposure; SAST can't see it
- **Date**: 2026-07-06
- **Pattern**: Digest pin freezes image bytes → also freezes vulnerabilities. Pin correct at audit time can be HIGH same day: upstream retires stable branch, security batch lands only on newer branches, no backport. semgrep/SAST floor scans code, blind to base-image CVE freshness. Complementary posture pass required: base branch EOL status (endoflife.date) + vendor security advisories, every audit.
- **Context**: bchanot-cv tour 2026-07-05-3. `nginx-unprivileged:1.28-alpine` digest-pinned as SEC fix in morning run; same evening cso posture add-on flagged HIGH — 1.28 branch retired, CVE-2026-42945 (rewrite-module overflow) fixed 1.30.1+/1.31.1+ only. Two intervening semgrep-only tours saw nothing (gstack OFF → no cso). Bump commit `1aa97f0`.
- **Future application**: Any Dockerfile `FROM x@sha256:…` → security audit must include EOL + advisory check on the pinned branch, not just SAST. gstack ON → cso add-on covers it; OFF → manual endoflife.date + vendor advisory check.
-1
View File
@@ -11,7 +11,6 @@ docker-compose.yml
.dockerignore
.env
.env.example
nginx.conf.bak
# Editor / OS noise
*.swp
+1
View File
@@ -32,3 +32,4 @@ graphify-out/
.claude/gstack/
.claude/deploy/PENDING.json
.claude/deploy/NEXT.sh
.gstack/
+41
View File
@@ -0,0 +1,41 @@
# Changelog
All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.0.0] — 2026-07-06
First tagged release. Landing page + CV live at https://bchanot.fr, served by
a hardened nginx container behind the host reverse proxy.
### Security
- Container runs unprivileged: `nginxinc/nginx-unprivileged` digest-pinned,
uid 101, internal port 8080; base at 1.30-alpine (nginx/1.30.3) covering
CVE-2026-42945. Compose hardening: `read_only`, `cap_drop: ALL`,
`no-new-privileges`, loopback-bound port.
- Security headers (CSP, X-Content-Type-Options, X-Frame-Options,
Referrer-Policy, Permissions-Policy) re-included in every nginx location
(add_header inheritance trap closed); `server_tokens off`; CSP `script-src`
pinned to the inline script's sha256 hash; dotfile requests 404 ahead of
the caching locations.
### Changed
- Palette strictly tokenized: 6 brand hexes + documented functional neutrals
+ white-on-dark family; off-palette colors mapped to tokens.
- CSS deduplicated via grouped selectors (landing cards, CV headers / date
chips / roles / tags); dead rules and no-op declarations removed.
- Unused Google Fonts faces trimmed from both pages (CV proven
render-identical; landing verified at 375 px and 1440 px).
- CV date chips in French with en-dashes; language/interest tags as true
pills; profile geography aligned landing ↔ CV (Nantes relocation).
- `prefers-reduced-motion` disables transitions as well as animations;
decorative CTA arrows removed from the accessibility tree.
- PDF served uncompressed (already flate-compressed); single container
healthcheck (image `HEALTHCHECK`, inherited by compose).
### Added
- `version.txt` and this CHANGELOG — the release lineage starts here.
+24 -3
View File
@@ -66,7 +66,7 @@ The PDF must match the latest HTML before pushing or sending.
## Design system (non-negotiable)
Palette — exact hex:
Palette — exact hex (brand colors):
- `#0d1b12` — dark forest (nav, dark sections, footer)
- `#1b5e3b` — green primary (links, section titles on light bg)
- `#2d7a4f` — green accent (borders, dots, separators)
@@ -74,11 +74,25 @@ Palette — exact hex:
- `#dff0e7` — green tint (pill bg)
- `#f5f3ec` — parchment (page bg)
Functional neutrals (allowed, intentional — layering + text, NOT brand):
- `#183325` (`--dark-mid`), `#0e3320` (`--g900`), `#eef7f1` (`--g050`) —
green-scale intermediates for dark layering and light block bg
- `#111111` / `#1e1e1e` / `#636363` (`--ink-1/2/3`) — text hierarchy
- `#d8d4c8` (`--rule`), `#e6e2d8` (`--tag`) — separators, generic tags
- `#ffffff` text + `rgba(255,255,255,…)` alphas — text/hover on dark bg and
low-alpha (≤5%) overlays only; never as a background color
Any color outside these lists is a violation.
Typography:
- `Fraunces` (serif) — display: hero name, section titles, role headings
- `JetBrains Mono` (mono) — eyebrows, badges, tech pills, nav, contact rows
- `DM Sans` (sans) — body text
CV exception (`CV_Bastien_Chanot.html`, compact print style): section titles
and company/school names are mono, roles/degrees are sans; Fraunces is
reserved for the header name and the accroche. The mapping above applies to
the landing.
Forbidden:
- Pure white background (`#ffffff`)
- `border-radius` > 6px except pills
@@ -108,8 +122,9 @@ Forbidden:
- Profile state, including job search context, must stay consistent across
index.html and CV. Currently: looking for **CDI** in embedded / systems
software first; freelance missions (ZenQuality) in parallel.
- Geography: Yerres (91) currently; targeting Pays de la Loire mid-term;
full remote preferred or hybrid 1–2 days/month if Paris.
- Geography: Yerres (91) currently; relocation to the Nantes area (Pays de
la Loire) planned mid-term; full remote preferred, hybrid possible in
Nantes, or hybrid 1–2 days/month if Paris.
---
@@ -124,6 +139,12 @@ None — global rules apply.
- Edits to `index.html` or `CV_Bastien_Chanot.html` must preserve the
palette + typography + structure unless explicitly asked to change them.
- After editing `CV_Bastien_Chanot.html`, regenerate the PDF.
- After editing index.html's inline `<script>`, recompute the CSP hash and
update `nginx-security-headers.conf` (script-src is hash-pinned — a stale
hash silently disables the JS in prod):
```bash
python3 -c "import hashlib,base64,re;h=base64.b64encode(hashlib.sha256(re.search(r'<script>(.*?)</script>',open('index.html',encoding='utf-8').read(),re.S).group(1).encode()).digest()).decode();print('sha256-'+h)"
```
- Never add external dependencies beyond Google Fonts.
- Never add tracking, analytics, cookie banners or third-party scripts.
- Always test in mobile width (375px) and desktop (1440px) before claiming done.
+52 -100
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bastien Chanot — CV</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Fraunces:ital,wght@0,300;0,600;0,700;1,300&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Fraunces:ital,wght@0,700;1,300&family=DM+Sans:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
/* Zones sombres (header, footer) */
@@ -37,21 +37,16 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #fff;
background: var(--page);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
margin: 0;
padding: 0;
}
.page {
width: 210mm;
max-width: 100%;
margin: 0 auto;
background: var(--page);
overflow: hidden;
}
@@ -132,8 +127,8 @@
.cv-body {
padding: 10px 20mm 12px;
background:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6'%3E%3Ccircle cx='1' cy='1' r='0.6' fill='rgba(26,71,48,0.05)'/%3E%3C/svg%3E"),
linear-gradient(160deg, #f5f3ec 0%, #edeadf 55%, #f2efe6 100%);
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6'%3E%3Ccircle cx='1' cy='1' r='0.6' fill='rgba(27,94,59,0.05)'/%3E%3C/svg%3E"),
linear-gradient(160deg, var(--page) 0%, var(--tag) 55%, var(--page) 100%);
}
.accroche {
@@ -177,15 +172,46 @@
background: var(--rule);
}
/* ── XP ── */
.xp-block { margin-bottom: 6px; }
.xp-header {
/* ── SHARED (xp/project/edu headers, date chips, roles, tags, links) ── */
.xp-header, .project-header, .edu-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 1px;
}
.xp-header, .project-header { margin-bottom: 1px; }
.xp-dates, .project-dates, .edu-dates {
font-family: var(--mono);
color: var(--ink-3);
background: var(--tag);
padding: 1px 6px;
border-radius: 2px;
}
.xp-role, .edu-degree {
font-size: 10pt;
font-weight: 500;
color: var(--g700);
margin-bottom: 3px;
}
.lang-item, .interest-tag {
font-family: var(--mono);
font-size: 8.5pt;
color: var(--g900);
background: var(--g100);
padding: 2px 8px;
border-radius: 999px;
}
.inline-link {
color: var(--g500);
text-decoration: none;
border-bottom: 1px solid var(--g300);
}
/* ── XP ── */
.xp-block { margin-bottom: 6px; }
.xp-company {
font-family: var(--mono);
@@ -194,22 +220,7 @@
color: var(--ink-1);
}
.xp-dates {
font-family: var(--mono);
font-size: 8.5pt;
color: var(--ink-3);
white-space: nowrap;
background: var(--tag);
padding: 1px 6px;
border-radius: 2px;
}
.xp-role {
font-size: 10pt;
font-weight: 500;
color: var(--g700);
margin-bottom: 3px;
}
.xp-dates { font-size: 8.5pt; white-space: nowrap; }
.xp-loc {
font-size: 7.5pt;
@@ -249,7 +260,7 @@
.tag {
display: inline-block;
background: var(--g100);
border: 1px solid #a8d4bc;
border: 1px solid var(--g300);
font-family: var(--mono);
font-size: 6.5pt;
padding: 1px 5px;
@@ -262,13 +273,6 @@
/* ── PROJECTS ── */
.project-block { margin-bottom: 5px; }
.project-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 1px;
}
.project-name {
font-family: var(--mono);
font-size: 10.5pt;
@@ -283,15 +287,7 @@
font-style: italic;
}
.project-dates {
font-family: var(--mono);
font-size: 8pt;
color: var(--ink-3);
white-space: nowrap;
background: var(--tag);
padding: 1px 6px;
border-radius: 2px;
}
.project-dates { font-size: 8pt; white-space: nowrap; }
.project-desc {
font-size: 10pt;
@@ -312,8 +308,6 @@
display: grid;
grid-template-columns: 115px 1fr;
gap: 1px 10px;
font-size: 8.4pt;
}
.skill-label {
@@ -333,12 +327,6 @@
/* ── EDU ── */
.edu-block { margin-bottom: 5px; }
.edu-header {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.edu-school {
font-family: var(--mono);
font-size: 11.5pt;
@@ -346,21 +334,7 @@
color: var(--ink-1);
}
.edu-dates {
font-family: var(--mono);
font-size: 7.2pt;
color: var(--ink-3);
background: var(--tag);
padding: 1px 6px;
border-radius: 2px;
}
.edu-degree {
font-size: 10pt;
font-weight: 500;
color: var(--g700);
margin-bottom: 3px;
}
.edu-dates { font-size: 7.2pt; }
.edu-detail {
font-size: 10pt;
@@ -378,15 +352,6 @@
.lang-row { display: flex; gap: 4px; flex-wrap: wrap; }
.lang-item {
font-family: var(--mono);
font-size: 8.5pt;
color: var(--g900);
background: var(--g100);
padding: 2px 8px;
border-radius: 10px;
}
.lang-item .level {
color: var(--ink-3);
font-size: 7.5pt;
@@ -394,18 +359,8 @@
.interests { display: flex; flex-wrap: wrap; gap: 4px; }
.interest-tag {
font-family: var(--mono);
font-size: 8.5pt;
color: var(--g900);
background: var(--g100);
padding: 2px 8px;
border-radius: 10px;
}
/* ── FOOTER ── */
.footer-bar {
position: running(siteFooter);
background: var(--dark);
padding: 5px 20mm;
display: flex;
@@ -431,12 +386,10 @@
@media print {
body {
background:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6'%3E%3Ccircle cx='1' cy='1' r='0.6' fill='rgba(26,71,48,0.05)'/%3E%3C/svg%3E"),
linear-gradient(160deg, #f5f3ec 0%, #edeadf 55%, #f2efe6 100%);
padding: 0;
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6'%3E%3Ccircle cx='1' cy='1' r='0.6' fill='rgba(27,94,59,0.05)'/%3E%3C/svg%3E"),
linear-gradient(160deg, var(--page) 0%, var(--tag) 55%, var(--page) 100%);
}
.screen-label { display: none; }
.page { box-shadow: none; background: transparent; }
.page { background: transparent; }
.cv-header { padding: 18px 14mm 14px; }
.cv-body { padding: 10px 14mm 12px; background: transparent; }
@@ -495,7 +448,6 @@
</head>
<body>
<div class="page">
<div class="cv-header">
@@ -530,9 +482,9 @@
<div class="xp-block">
<div class="xp-header">
<span class="xp-company">ZenQuality</span>
<span class="xp-dates">Apr 2026 - present</span>
<span class="xp-dates">avr. 2026 – présent</span>
</div>
<div class="xp-role">Développeur indépendant <span class="xp-loc">· Yerres · <a href="https://zenquality.fr" style="color:var(--g500);text-decoration:none;border-bottom:1px solid var(--g300);">zenquality.fr</a></span></div>
<div class="xp-role">Développeur indépendant <span class="xp-loc">· Yerres · <a href="https://zenquality.fr" class="inline-link">zenquality.fr</a></span></div>
<ul class="bullets">
<li>Mission SEO et conformité légale RGPD pour PME service (Île-de-France) — audit technique Core Web Vitals + Schema.org + NAP, refonte CGV B2B/B2C, RGPD, mentions légales, mise en conformité médiateur CM2C. Plan d'action 12 sprints.</li>
<li>Site vitrine WordPress (<span class="tag">Gutenverse</span>) pour PME esthétique — conception, intégration, déploiement et support continu (hébergement client).</li>
@@ -543,7 +495,7 @@
<div class="xp-block">
<div class="xp-header">
<span class="xp-company">CareGame</span>
<span class="xp-dates">Mar 2019 - Mar 2025</span>
<span class="xp-dates">mars 2019 – mars 2025</span>
</div>
<div class="xp-role">Développeur logiciel — Systèmes &amp; Backend <span class="xp-loc">· Paris · Full remote dès 2020</span></div>
<ul class="bullets">
@@ -562,7 +514,7 @@
<div class="xp-block break-before-page">
<div class="xp-header">
<span class="xp-company">Deewee</span>
<span class="xp-dates">Feb 2017 - Nov 2017</span>
<span class="xp-dates">fév. 2017 – nov. 2017</span>
</div>
<div class="xp-role">Développeur C — Système embarqué <span class="xp-loc">· Ivry-sur-Seine</span></div>
<div class="xp-contract">Stage 42 (6 mois) puis CDD (4 mois)</div>
@@ -589,7 +541,7 @@
<div class="project-desc">
Configuration Claude Code, dotfiles, projets bas-niveau (42, expérimentations C/Rust) — accessibles publiquement. Mirror automatique vers GitHub via push hook.
</div>
<div class="project-link"><a href="https://git.bchanot.fr/bchanot" style="color:var(--g500);text-decoration:none;border-bottom:1px solid var(--g300);">git.bchanot.fr/bchanot</a></div>
<div class="project-link"><a href="https://git.bchanot.fr/bchanot" class="inline-link">git.bchanot.fr/bchanot</a></div>
</div>
<div class="project-block">
Binary file not shown.
+10 -8
View File
@@ -1,27 +1,29 @@
# Static site for bchanot.fr
# nginx:alpine serves index.html + CV (HTML + PDF).
# nginx-unprivileged serves index.html + CV (HTML + PDF) as uid 101 —
# no root master process in the container (tag + digest pinned).
FROM nginx:1.27-alpine
FROM nginxinc/nginx-unprivileged:1.30-alpine@sha256:fd3314e343bad2de4e1127ef58be122abbfa7e09572fa46ae62fcddb6b3f21c5
# Custom nginx config (gzip, cache, security headers).
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY nginx-security-headers.conf /etc/nginx/snippets/security-headers.conf
# Site assets.
# Site assets — clean the default content, then copy ours.
WORKDIR /usr/share/nginx/html
USER root
RUN rm -rf ./*
USER nginx
COPY index.html ./
COPY CV_Bastien_Chanot.html ./
COPY CV_Bastien_Chanot.pdf ./
COPY favicon.svg favicon-32.png favicon.ico apple-touch-icon.png ./
# Non-root hardening: nginx:alpine already drops privileges to "nginx" user
# for worker processes. Master runs as root only to bind port 80 inside
# the container — fine because the host port is the one exposed.
EXPOSE 80
# nginx-unprivileged listens on 8080 (>1024, no NET_BIND_SERVICE needed).
EXPOSE 8080
# Basic healthcheck: nginx must serve index.html.
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO- http://127.0.0.1/ >/dev/null || exit 1
CMD wget -qO- http://127.0.0.1:8080/ >/dev/null || exit 1
CMD ["nginx", "-g", "daemon off;"]
+18 -5
View File
@@ -15,10 +15,18 @@ Static single-page site (no framework, no build step). Lives at https://bchanot.
| `.claude/` | Memory registries, tasks, audits |
| `Dockerfile` | Container image build — copies static assets into nginx |
| `docker-compose.yml` | Service def — host port, hardening (read-only, cap_drop), tmpfs |
| `nginx.conf` | In-container nginx — security headers, CSP, gzip, cache |
| `nginx.conf` | In-container nginx — CSP, gzip, cache rules |
| `nginx-security-headers.conf` | Shared security-headers snippet, re-included per location (nginx `add_header` inheritance is all-or-nothing) |
| `.env.example` | Sample env — `PORT` for the host bind |
| `.githooks/` | Versioned git hooks — pre-commit blocks direct code commits on `main`/`develop` (gitflow) |
| `favicon.*`, `apple-touch-icon.png` | Favicon set — SVG primary + ICO/PNG + 180×180 apple-touch |
After cloning, wire the versioned hooks once:
```bash
git config core.hooksPath .githooks
```
## Local preview
```bash
@@ -65,6 +73,9 @@ Strict palette (non-negotiable):
| `#dff0e7` | Green tint — pill background |
| `#f5f3ec` | Parchment — page background |
Plus a documented set of functional neutrals (text inks, rules/tags, two
green-scale intermediates) — the exhaustive list lives in `CLAUDE.md`.
Typography:
- `Fraunces` — display (names, titles)
- `JetBrains Mono` — technical labels, badges, pills, nav, contact
@@ -75,10 +86,12 @@ WCAG AA contrast. Focus visible. Semantic HTML.
## Deploy
Production runs as a Docker container (`bchanot-web`, `nginx:1.27-alpine`)
behind the host's nginx reverse proxy, which terminates TLS and `proxy_pass`es
to it. The host port is set via `PORT` (default 8080) and bound to `127.0.0.1`,
so all traffic goes through the front proxy.
Production currently serves the static files directly from the VPS's native
nginx (which also terminates TLS). The repo additionally maintains a hardened
container path (`bchanot-web`, `nginxinc/nginx-unprivileged:1.30-alpine`,
digest-pinned, runs as uid 101 on port 8080) for when a containerized deploy
is preferred: the host port is set via `PORT` (default 8080) and bound to
`127.0.0.1`, so all traffic goes through the front proxy.
```bash
cp .env.example .env # optional: set PORT
+4 -14
View File
@@ -18,24 +18,14 @@ services:
container_name: bchanot-web
restart: unless-stopped
ports:
- "127.0.0.1:${PORT:-8080}:80"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1/"]
interval: 30s
timeout: 3s
retries: 3
start_period: 5s
- "127.0.0.1:${PORT:-8080}:8080"
# Healthcheck inherited from the image HEALTHCHECK (Dockerfile) — do not
# redeclare here, one definition only.
read_only: true
tmpfs:
- /var/cache/nginx
- /var/run
# nginx-unprivileged writes pid + temp files under /tmp only.
- /tmp
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- CHOWN
- SETGID
- SETUID
- NET_BIND_SERVICE
+31 -90
View File
@@ -13,7 +13,7 @@
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Fraunces:ital,wght@0,300;0,500;0,600;0,700;1,400&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Fraunces:ital,wght@0,600;1,400&family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
/* Palette — non négociable */
@@ -344,7 +344,6 @@
.reveal.d3 { animation-delay: .30s; }
.reveal.d4 { animation-delay: .42s; }
.reveal.d5 { animation-delay: .55s; }
.reveal.d6 { animation-delay: .68s; }
@keyframes rise {
to { opacity: 1; transform: translateY(0); }
}
@@ -352,6 +351,7 @@
.reveal { opacity: 1; transform: none; animation: none; }
.brand::before { animation: none; }
html { scroll-behavior: auto; }
*, *::before, *::after { transition: none !important; animation: none !important; }
}
/* ── ABOUT ── */
@@ -418,39 +418,49 @@
gap: 20px;
margin-top: 40px;
}
.stack-card {
background: #fff;
/* ── Shared card chrome (stack / project / theme cards + méthode items).
Per-class blocks below keep only their specifics; the cascade resolves
identically to the previous duplicated declarations. ── */
.stack-card, .project-card, .theme-card, .methode-item {
background: var(--page);
border: 1px solid var(--rule);
border-radius: var(--r-md);
padding: 24px;
transition: border-color .25s ease, transform .25s ease, box-shadow .25s ease;
}
.stack-card:hover {
.stack-card:hover, .project-card:hover, .theme-card:hover, .methode-item:hover {
border-color: var(--g300);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.stack-card-head {
.stack-card-head, .project-card-head, .theme-card-head {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px dashed var(--rule);
}
.stack-card h3 {
.stack-card h3, .project-card h3, .theme-card h4, .methode-body h3 {
font-family: var(--serif);
font-weight: 600;
font-size: 19px;
color: var(--ink-1);
letter-spacing: -0.01em;
}
.stack-card-tag {
.stack-card-tag, .project-card-tag, .theme-card-tag {
font-family: var(--mono);
font-size: 11px;
color: var(--g500);
letter-spacing: 0.1em;
}
.stack-card {
padding: 24px;
}
.stack-card-head {
margin-bottom: 16px;
padding-bottom: 12px;
}
.stack-card h3 {
font-size: 19px;
}
.pills {
display: flex;
flex-wrap: wrap;
@@ -492,16 +502,16 @@
gap: 8px;
flex-wrap: wrap;
}
.stack-note code {
.stack-note code, .theme-list code {
font-family: var(--mono);
font-size: 12px;
font-weight: 500;
color: var(--g700);
background: var(--g050);
border: 1px solid var(--g100);
padding: 2px 8px;
border-radius: var(--r-sm);
}
.stack-note code { padding: 2px 8px; }
@media (min-width: 768px) { .stack-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1200px) { .stack-grid { grid-template-columns: repeat(3, 1fr); } }
@@ -640,40 +650,19 @@
}
@media (min-width: 768px) { .projects-grid { grid-template-columns: repeat(2, 1fr); } }
.project-card {
background: #fff;
border: 1px solid var(--rule);
border-radius: var(--r-md);
padding: 24px;
transition: border-color .25s ease, transform .25s ease, box-shadow .25s ease;
display: flex;
flex-direction: column;
}
.project-card:hover {
border-color: var(--g300);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.project-card-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px dashed var(--rule);
}
.project-card h3 {
font-family: var(--serif);
font-weight: 600;
font-size: 20px;
color: var(--ink-1);
letter-spacing: -0.01em;
}
.project-card-tag {
font-family: var(--mono);
font-size: 11px;
color: var(--g500);
letter-spacing: 0.1em;
flex-shrink: 0;
white-space: nowrap;
}
@@ -708,8 +697,6 @@
/* ── FORMATION ── */
.formation { background: var(--g050); }
.formation .timeline { border-left-color: var(--g100); }
.formation .timeline-item::before { box-shadow: 0 0 0 4px var(--g050); }
.formation-school-desc {
font-family: var(--serif);
@@ -731,41 +718,20 @@
@media (min-width: 1200px) { .formation-themes { grid-template-columns: repeat(3, 1fr); } }
.theme-card {
background: #fff;
border: 1px solid var(--rule);
border-radius: var(--r-md);
padding: 22px;
transition: border-color .25s ease, transform .25s ease, box-shadow .25s ease;
display: flex;
flex-direction: column;
}
.theme-card:hover {
border-color: var(--g300);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.theme-card-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px dashed var(--rule);
}
.theme-card h4 {
font-family: var(--serif);
font-weight: 600;
font-size: 18px;
color: var(--ink-1);
letter-spacing: -0.01em;
line-height: 1.2;
}
.theme-card-tag {
font-family: var(--mono);
font-size: 11px;
color: var(--g500);
letter-spacing: 0.1em;
flex-shrink: 0;
}
.theme-quote {
@@ -790,16 +756,7 @@
line-height: 1.55;
color: var(--ink-2);
}
.theme-list code {
font-family: var(--mono);
font-size: 12px;
font-weight: 500;
color: var(--g700);
background: var(--g050);
border: 1px solid var(--g100);
padding: 1px 7px;
border-radius: var(--r-sm);
}
.theme-list code { padding: 1px 7px; }
.formation-tsrit-list {
list-style: none;
@@ -860,16 +817,7 @@
grid-template-columns: 56px 1fr;
gap: 20px;
align-items: start;
background: #fff;
border: 1px solid var(--rule);
border-radius: var(--r-md);
padding: 22px 24px;
transition: border-color .25s ease, transform .25s ease, box-shadow .25s ease;
}
.methode-item:hover {
border-color: var(--g300);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.methode-num {
font-family: var(--mono);
@@ -881,11 +829,7 @@
padding-top: 4px;
}
.methode-body h3 {
font-family: var(--serif);
font-weight: 600;
font-size: 19px;
color: var(--ink-1);
letter-spacing: -0.01em;
margin-bottom: 6px;
line-height: 1.25;
}
@@ -917,9 +861,6 @@
pointer-events: none;
}
.contact-grid {
display: grid;
grid-template-columns: 1fr;
gap: 32px;
position: relative;
z-index: 1;
}
@@ -972,7 +913,7 @@
/* ── FOOTER ── */
.footer {
background: #061008;
background: var(--dark);
color: rgba(223, 240, 231, 0.55);
padding: 32px 24px;
border-top: 1px solid rgba(106,185,138,0.1);
@@ -1044,11 +985,11 @@
<div class="hero-cta reveal d4">
<a class="btn btn-primary" href="#contact">
Me contacter
<svg class="arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="19" x2="19" y2="5"/><polyline points="8 5 19 5 19 16"/></svg>
<svg class="arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="5" y1="19" x2="19" y2="5"/><polyline points="8 5 19 5 19 16"/></svg>
</a>
<a class="btn btn-secondary" href="CV_Bastien_Chanot.html">
Voir le CV
<svg class="arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="19" x2="19" y2="5"/><polyline points="8 5 19 5 19 16"/></svg>
<svg class="arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="5" y1="19" x2="19" y2="5"/><polyline points="8 5 19 5 19 16"/></svg>
</a>
<a class="btn btn-secondary" href="CV_Bastien_Chanot.pdf" download>
Télécharger PDF
@@ -1075,7 +1016,7 @@
<p>Ce qui m'intéresse, c'est descendre jusqu'à ce qu'il n'y ait plus de magie — <strong>kernel, hardware, drivers</strong>. Là, soit ça marche, soit ça ne marche pas.</p>
<p>C'est ce confort-là que je cherche dans une équipe : <strong>systèmes, embarqué, backend bas niveau</strong>, sur une stack dont on peut lire le code source. Pas envie d'aller vers le buzzword-driven — microservices à tout prix, framework du mois, archi conçue pour le pitch deck.</p>
<p>Aujourd'hui indépendant sous la marque <strong>ZenQuality</strong>, mais avant tout en recherche d'un <strong>CDI en systèmes embarqués ou logiciel</strong> — les missions freelance se font en parallèle.</p>
<p>Côté présence : <strong>full remote</strong> idéalement, ou <strong>hybride 1 à 2 jours par mois</strong> si l'équipe est à Paris. Mobilité visée à moyen terme : <strong>Pays de la Loire</strong>.</p>
<p>Côté présence : <strong>full remote</strong> idéalement, <strong>hybride possible sur Nantes</strong>, ou <strong>1 à 2 jours par mois</strong> si l'équipe est à Paris. Installation en <strong>région nantaise</strong> prévue à moyen terme.</p>
</div>
<dl class="about-callout">
<dt>Recherche prioritaire</dt>
@@ -1083,9 +1024,9 @@
<dt>En parallèle</dt>
<dd>Missions freelance · ZenQuality</dd>
<dt>Localisation actuelle</dt>
<dd>Yerres (91) · mobilité Pays de la Loire</dd>
<dd>Yerres (91) · installation région nantaise prévue</dd>
<dt>Présence</dt>
<dd>Full remote · ou 1–2 j/mois si Paris</dd>
<dd>Full remote · hybride Nantes · ou 1–2 j/mois Paris</dd>
<dt>Site pro</dt>
<dd><a href="https://zenquality.fr" target="_blank" rel="noopener">zenquality.fr&nbsp;↗</a></dd>
</dl>
+15
View File
@@ -0,0 +1,15 @@
# Security headers for bchanot.fr — included at server level AND in every
# location that declares its own add_header: nginx add_header inheritance
# is all-or-nothing (one add_header in a location drops ALL inherited
# headers), so each such location must re-include this file.
# HSTS is intentionally NOT here — it belongs to the TLS-terminating proxy.
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()" always;
# CSP: inline CSS allowed (single-file convention: inline <style> element);
# the inline script is HASH-pinned (no script unsafe-inline). INVARIANT: after
# ANY edit to index.html's inline <script>, recompute the hash (command in
# CLAUDE.md) and update it here — a stale hash silently disables the JS.
add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self' 'sha256-Al1M34KxI6Ye5Viu6aO//7CYyaLzqtpG9GX95FFlSOY='; img-src 'self' data:; base-uri 'self'; form-action 'self'; frame-ancestors 'self'" always;
+25 -28
View File
@@ -1,29 +1,27 @@
# nginx server block for bchanot.fr static site.
# Container listens on port 80; host port is configured via docker-compose
# (PORT env var). A host-level reverse proxy (nginx, Traefik, Caddy) should
# terminate TLS and proxy_pass to http://127.0.0.1:${PORT}.
# Container (nginx-unprivileged) listens on 8080; host port is configured via
# docker-compose (PORT env var). A host-level reverse proxy (nginx, Traefik,
# Caddy) should terminate TLS and proxy_pass to http://127.0.0.1:${PORT}.
server {
listen 80;
listen [::]:80;
listen 8080;
listen [::]:8080;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Security headers. HSTS is intentionally NOT set here — leave it to the
# outer reverse proxy that terminates TLS, otherwise it may be sent over
# plain HTTP between proxy and container.
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()" always;
# CSP: inline CSS + JS are allowed (project convention), fonts from Google.
add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self' 'unsafe-inline'; img-src 'self' data:; base-uri 'self'; form-action 'self'; frame-ancestors 'self'" always;
# Don't advertise the nginx version.
server_tokens off;
# Forwarded headers — trust the upstream reverse proxy.
# Security headers — shared snippet. Re-included in every location that
# sets its own add_header (inheritance is all-or-nothing in nginx).
include /etc/nginx/snippets/security-headers.conf;
# Forwarded headers — trust only the local reverse proxy (the container
# port is bound to 127.0.0.1 in docker-compose).
real_ip_header X-Forwarded-For;
set_real_ip_from 0.0.0.0/0;
set_real_ip_from 127.0.0.1;
# Compression.
gzip on;
@@ -34,43 +32,42 @@ server {
gzip_types
text/plain
text/css
text/html
text/javascript
application/javascript
application/json
application/xml
application/pdf
image/svg+xml;
# Block access to dotfiles (defense-in-depth — none are shipped anyway).
# First regex location wins: keep this above the caching regex blocks so
# a hypothetical /.foo.html can't be served by them.
location ~ /\. {
return 404;
}
# Long cache for the PDF (regenerated rarely, content-hash not used).
location ~* \.pdf$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
include /etc/nginx/snippets/security-headers.conf;
}
# Short cache for HTML so content updates land fast.
location ~* \.html$ {
expires 1h;
add_header Cache-Control "public, max-age=3600, must-revalidate";
include /etc/nginx/snippets/security-headers.conf;
}
# Long cache for favicon + image assets (rarely change).
location ~* \.(?:ico|svg|png|jpg|jpeg|gif|webp)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
include /etc/nginx/snippets/security-headers.conf;
access_log off;
}
# Logs to stdout/stderr (default in nginx:alpine).
# Logs to stdout/stderr (default in nginx images).
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
# Block access to dotfiles (defense-in-depth — none are shipped anyway).
location ~ /\. {
deny all;
return 404;
}
# Default: serve files, fall back to 404.
location / {
try_files $uri $uri/ =404;
+1
View File
@@ -0,0 +1 @@
1.0.0