42 Commits
Author SHA1 Message Date
Bastien Chanot 2891be5898 Merge release/1.1.0 into main 2026-07-23 15:22:56 +02:00
Bastien Chanot 3f6bc571d3 chore(release): 1.1.0 — zone Loire-Atlantique · Vendée · Morbihan 2026-07-23 15:22:56 +02:00
Bastien Chanot 3c4dab88a5 Merge chore/cv-update into develop 2026-07-23 15:22:20 +02:00
Bastien Chanot aceb7e2c70 chore(cv): restructured CV + general variant, zone aligned
Owner-authored CV rework: technical skills promoted above experience,
'Centres d'intérêt' dropped, markup trimmed 656 -> 424 lines. Header
carries the Loire-Atlantique · Vendée · Morbihan zone and the presence
hierarchy (full remote, hybrid or on-site in the zone, Paris capped at
1-2 days/month).

Adds CV_Bastien_CHANOT_General.{html,pdf} as the general variant kept
alongside the served CV. Not linked from the landing and not in the
Dockerfile COPY whitelist, so it ships in the repo only, not to prod.

Both HTML/PDF pairs verified in sync via weasyprint re-render (text
identical, 2 pages each). New files normalized to mode 644.
2026-07-23 15:22:13 +02:00
Bastien Chanot e2f7ea4546 Merge chore/zone-loire-atlantique into develop 2026-07-23 14:56:24 +02:00
Bastien Chanot e13a4b1bb5 chore(profile): align geography to Loire-Atlantique · Vendée · Morbihan
Landing was still advertising Yerres (91) + a planned Nantes relocation,
and a presence rule that omitted on-site work in the target zone.

- about paragraph + callout: zone replaces 'Localisation actuelle';
  presence stated in order of preference — full remote, then hybrid or
  on-site in the zone, Paris only as a fallback at 1-2 days/month max
- ZenQuality timeline entry: drop the 'Yerres' location, keep 'Full remote'
- CLAUDE.md content rules: geography note rewritten to match, with an
  explicit 'never mention Yerres' guard

Verified: no 'Yerres' left outside the negative rule, CSP script hash
unchanged, headless render checked at 375px and 1440px.
2026-07-23 14:39:43 +02:00
Bastien Chanot d8ffa983b5 Merge release/1.0.0 into main 2026-07-06 01:14:47 +02:00
Bastien Chanot 1c01cb20eb Merge release/1.0.0 into develop 2026-07-06 01:14:47 +02:00
Bastien Chanot 6b0ea8494b chore(release): 1.0.0 — version.txt + CHANGELOG (lineage starts) 2026-07-06 01:12:53 +02:00
Bastien Chanot 1d0b7ee8e7 Merge chore/tour-2026-07-05-3 into develop 2026-07-06 01:07:37 +02:00
Bastien Chanot 9af6aa4be8 chore(gitignore): ignore .gstack/ (browse daemon session state, contains tokens) 2026-07-06 01:07:26 +02:00
Bastien Chanot 136e1df5e3 docs(tour): follow-up — 10 residuals closed 2026-07-06 01:06:54 +02:00
Bastien Chanot c8c72c24aa chore(memory): capitalize — LRN-004, EVAL-001, BDR-006 update note, journal 2026-07-06 2026-07-06 01:06:54 +02:00
Bastien Chanot dd8c327162 docs(claude): document white family + CV typography exception (tour J1/J2) 2026-07-06 01:06:54 +02:00
Bastien Chanot 84288c5c58 fix(nginx+compose): dotfile block first, no pdf gzip, single healthcheck (tour J4/N4/J5)
Dotfile location moved above caching regex locations (first match wins).
application/pdf out of gzip_types (already flate-compressed). Compose
healthcheck block removed — image HEALTHCHECK is the single source,
inherited. Oracles: nginx -t, dotfiles 404, PDF no Content-Encoding,
HTML still gzipped, headers 5/5, inherited health = healthy.
2026-07-06 01:06:54 +02:00
Bastien Chanot a589b99878 perf(index): trim unused Google Fonts faces, drop contact-grid no-ops (tour N1/N3)
Fraunces 0,300/0,500/0,700 + DM Sans 300 unused (all serif-300 usages
are italic -> served by 1,400; no strong/em inside serif elements).
.contact-grid grid props no-op around single child. Verified headless
Chromium 375px + 1440px: real italic renders, layout intact, zero
console errors. Inline script untouched, CSP hash unchanged.
2026-07-06 01:06:54 +02:00
Bastien Chanot b86a5129f0 style(cv): french date chips, pill radius, font trim (tour N1/N2/J3)
Chips: avr./mars/fév + en-dash, mirrors landing wording. Tags radius
10px -> 999px (true pills). Fonts URL drops Fraunces 0,300/0,600 +
DM Sans 300 — trim proven render-identical (per-page hash == baseline)
BEFORE the intended chip/radius changes; PDF regenerated, 2 pages
eyeballed.
2026-07-06 01:06:54 +02:00
Bastien Chanot cc65225b3d docs(tour): report — run 2026-07-05-3 (converged, 3 iterations) 2026-07-06 00:51:17 +02:00
Bastien Chanot 2f5e51a1b4 docs: sync README base-image reference (1.28 -> 1.30-alpine) 2026-07-05 22:57:48 +02:00
Bastien Chanot 613bfc0d49 chore(clean): dedup CV/index CSS, drop dead directives (tour F1-F8)
CV: shared block for xp/project/edu headers + date chips + roles + tags,
2 identical inline style attrs -> .inline-link class, no-op body margin/
padding removed, stray blank collapsed. Proven behavior-preserving: PDF
text-hash + per-page render-hash + full byte-identity vs committed PDF.
index: .stack-note/.theme-list code grouped, 2 no-op .formation overrides
removed. nginx.conf: dead 'deny all' after return 404 removed (nginx -t +
dotfile-404 oracle PASS). .dockerignore: phantom nginx.conf.bak entry.
Snippet comment: CV style attrs no longer exist. CSP hash unchanged.
2026-07-05 22:52:39 +02:00
Bastien Chanot 1aa97f0af0 fix(security): bump base to nginx-unprivileged 1.30-alpine — CVE-2026-42945 (tour SEC-1)
1.28 stable branch retired; 2026-05-13 nginx security batch (rewrite-module
buffer overflow, fixed 1.30.1+) never backported to 1.28.x. New digest pin
carries nginx/1.30.3. Verified: build, nginx -t, uid 101, hardened run,
5/5 security headers + HTTP 200 on /, .html, .pdf, favicon.
2026-07-05 22:41:58 +02:00
Bastien Chanot 7967afff08 Merge chore/tour-2026-07-05-2 into develop 2026-07-05 22:20:32 +02:00
Bastien Chanot 7984a7d2df docs(memory): capitalize tour residuals — BDR-006/007, LRN-003, journal, TOUR follow-up
BDR-006 supersedes BDR-004 infra detail (hardened container: nginx-unprivileged
:1.28 / port 8080 / uid 101) — closes reconcile REC-1.
BDR-007 supersedes BDR-003 geo (canonical = Nantes relocation) — records the
CLN-9 owner decision.
LRN-003: prove CSS cleanup behavior-preserving via before/after PDF render-hash.
journal 2026-07-05; TOUR.md follow-up documenting all 5 residuals closed.
2026-07-05 21:28:17 +02:00
Bastien Chanot f5158758b2 content: align landing geo to CV — Nantes relocation (tour CLN-9)
CLAUDE.md requires the profile/job-search state to stay consistent across
index.html and the CV. The CV stated a concrete Nantes relocation + a
hybride-Nantes option the landing lacked. Per owner decision, the CV is
canonical: propagate those facts into the landing (about paragraph +
callout) and update CLAUDE.md's geography note to match. No invented claims
— mirrors what the CV already states. CV unchanged (no PDF regen).
2026-07-05 21:25:16 +02:00
Bastien Chanot ede75765cd style(palette): map 5 off-palette colors to palette tokens (tour CLN-7/8)
Brings both files back inside the CLAUDE.md palette (any color outside the
6 brand hex + documented neutrals is a violation). Nearest-allowed mappings,
minimal visual delta (verified by rendering the CV):
- index .footer bg #061008 -> var(--dark) #0d1b12 (the documented footer color).
- CV .tag border #a8d4bc -> var(--g300) (nearest visible green; keeps the pill outline).
- CV body+print texture rgba(26,71,48,.05) -> rgba(27,94,59,.05) (--g700 green primary).
- CV body+print gradient stops #edeadf/#f2efe6 -> var(--tag)/var(--page).
PDF regenerated (renders 2 pages, layout intact).
2026-07-05 21:24:08 +02:00
Bastien Chanot 607124aa70 fix(a11y): aria-hidden on 2 decorative CTA arrows (tour CLN-6)
The 'Me contacter' and 'Voir le CV' arrow SVGs were missing the
aria-hidden the sibling download arrow already carries; they are purely
decorative, so screen readers should skip them. Visual output unchanged.
2026-07-05 21:22:28 +02:00
Bastien Chanot 7b3d9bec4c docs(tour): report chore/tour-2026-07-05-2 — CONVERGED (2 it., 1 clean fix commit, 5 suggestions) 2026-07-05 21:05:43 +02:00
Bastien Chanot 30b0e44a45 chore(clean): remove dead CSS + normalize whitespace (tour CLN)
index.html: drop unused .reveal.d6 rule (markup uses d1-d5 only).

CV_Bastien_Chanot.html:
- remove dead `position: running(siteFooter)` — no `element()` consumer,
  and .footer-bar is `display:none` in @media print (the @page
  auto-numbered footer replaces it); on screen running() is an invalid
  position value, ignored.
- remove no-op `box-shadow: none` on .page (weasyprint ignores box-shadow;
  .page sets a shadow nowhere).
- remove dead `.skills-grid { font-size: 8.4pt }` (every direct child is a
  .skill-label/.skill-values div that sets its own size; no bare text).
- normalize stray blank lines.

Behavior-preserving: PDF regenerated from the edited HTML is byte-identical
to the pre-edit baseline (text sha256 + per-page PNG render hash match),
so CV_Bastien_Chanot.pdf is unchanged and the PDF=HTML invariant holds.
2026-07-05 20:47:51 +02:00
Bastien Chanot d7256ffe0e chore(deploy): mark 2026-07-05-2 @ b24c58b 2026-07-05 20:25:07 +02:00
Bastien Chanot b24c58b8a4 Merge chore/tour-residuals into develop 2026-07-05 20:06:12 +02:00
Bastien Chanot ef7e2312c6 docs: legalize functional neutrals (CLN-4) + CSP-hash invariant + TOUR follow-up
CLAUDE.md palette now two enforceable lists (6 brand + 8 documented
neutrals — anything else is a violation); workflow gains the recompute-
CSP-hash-after-JS-edit invariant with the exact command. README points to
the neutrals list. TOUR.md follow-up: CLN-3/CLN-4/SEC-7 closed, INF-2
corrected (false positive — .gitignore exists).
2026-07-05 19:59:45 +02:00
Bastien Chanot c0632aefa8 fix(security): pin script-src to the inline script's sha256 hash (SEC-7)
unsafe-inline dropped for scripts (index has zero style/script attributes;
the single inline script is hash-pinned). style-src keeps unsafe-inline
(CV carries 2 style attributes + single-file convention). Verified in
hardened container: served-script hash == policy hash, JS executes.
2026-07-05 19:59:45 +02:00
Bastien Chanot d63a52ec50 chore(clean): dedup card CSS via grouped selectors (CLN-3)
Shared chrome/hover/head/title/tag blocks for stack/project/theme cards +
methode items; per-class blocks keep only specifics. Zero HTML change,
cascade-order verified (no interfering rules between shared and specific
blocks). Net -60 lines; the audit's ~421 estimate was overstated.
2026-07-05 19:59:45 +02:00
Bastien Chanot bd7f6e4984 docs(deploy): runbook style — one command per line, session style 2026-07-05 15:31:10 +02:00
Bastien Chanot 395c77b597 chore(deploy): mark 2026-07-05 @ 5fe8b41 2026-07-05 15:23:46 +02:00
Bastien Chanot 5fe8b4119b feat(deploy): bootstrap runbook 2026-07-05 15:17:00 +02:00
Bastien Chanot 7b2d033761 Merge chore/tour-2026-07-05 into develop 2026-07-05 15:07:30 +02:00
Bastien Chanot c335769e1a docs(tour): auto run 2026-07-05 — converged in 2 iterations, 10 fixed, 3 open 2026-07-05 14:19:44 +02:00
Bastien Chanot 840632a6f8 docs: .githooks + hooksPath clone note; deploy section synced (native-nginx prod, hardened container path, headers snippet) 2026-07-05 14:12:43 +02:00
Bastien Chanot 7e7bd66384 chore(clean): enforce palette + reduced-motion, drop dead CSS
- 5x background:#fff -> var(--page) (stack/project/theme/methode cards +
  CV body) per CLAUDE.md 'no pure white background' (user-approved strict
  conformity; visual change: cards now blend with parchment, border-kept)
- prefers-reduced-motion now also kills transitions (universal rule)
- dead .screen-label rule removed (no matching element)
- PDF regenerated via weasyprint (must match HTML invariant)
2026-07-05 14:12:01 +02:00
Bastien Chanot ba13d697a5 fix(security): unprivileged nginx + security headers on every location
- base image -> nginxinc/nginx-unprivileged:1.28-alpine, digest-pinned
  (BREAKING for the docker path: container port 80 -> 8080; compose
  mapping/healthcheck updated in the same change, cap_add dropped)
- nginx add_header inheritance fix: shared snippets file re-included in
  every location that sets Cache-Control -- previously ALL security
  headers were dropped on real responses (verified live before/after)
- server_tokens off; set_real_ip_from restricted to 127.0.0.1
- expires directives removed (duplicated Cache-Control); gzip_types
  text/html redundancy removed (nginx -t warn)
2026-07-05 14:10:55 +02:00
Bastien Chanot 5a813df015 docs(tour): report-only audit 2026-07-05 — 7 security, 5 clean, 2 doc findings; reconcile zero-drift 2026-07-05 13:09:42 +02:00
23 changed files with 1354 additions and 798 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/
+63
View File
@@ -0,0 +1,63 @@
# 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.1.0] — 2026-07-23
Profile geography moved to the Loire-Atlantique · Vendée · Morbihan zone, and
the CV restructured.
### Changed
- Landing profile block: `Zone` (Loire-Atlantique · Vendée · Morbihan) replaces
the former `Localisation actuelle`; every mention of Yerres and of the planned
Nantes relocation removed, including the ZenQuality timeline entry.
- Presence stated as an explicit order of preference on both surfaces — full
remote, then hybrid or on-site inside the zone, Paris only as a fallback and
capped at 1–2 days per month.
- CV restructured: technical skills promoted above professional experience,
"Centres d'intérêt" dropped, markup trimmed 656 → 424 lines.
- `CLAUDE.md` content rules: geography note rewritten to the zone model, with an
explicit guard against reintroducing Yerres.
### Added
- `CV_Bastien_CHANOT_General.{html,pdf}` — general CV variant kept beside the
served one. Repo-only: not linked from the landing, not in the Dockerfile
COPY whitelist.
## [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.
+25 -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,10 @@ 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: zone **Loire-Atlantique · Vendée · Morbihan**. Never mention
Yerres or the Essonne. Presence, in order of preference: full remote →
hybrid in the zone → on-site in the zone → Paris only as a fallback, and
only hybrid at 1–2 days per month maximum.
---
@@ -124,6 +140,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.
+424
View File
@@ -0,0 +1,424 @@
<!DOCTYPE html>
<!--
=====================================================================
CV MAÎTRE — Bastien Chanot
Personnalisation par offre : édite UNIQUEMENT les 3 zones marquées
▼ ZONE 1 : TITRE (la ligne sous le nom)
▼ ZONE 2 : ACCROCHE (2-3 lignes d'objectif, à adapter à l'entreprise)
▼ ZONE 3 : ORDRE (réordonne les <li> / blocs compétences selon l'offre)
Le reste (faits, dates, réalisations) ne bouge pas.
TODO une fois pour toutes : remplace les URLs LinkedIn / GitHub réelles
dans la ligne de contact (cherche "TODO-LIENS").
Palette reprise du CV v2.
=====================================================================
-->
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bastien Chanot — CV</title>
<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=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap" rel="stylesheet">
<style>
:root{
--ink:#14201a;
--paper:#eeebe0;
--band:#0d1b12;
--band2:#16301f;
--name:#ffffff;
--sage:#61ab7f;
--band-muted:#b7bbb8;
--green-deep:#1b5e3b;
--muted:#586b60;
--rule:#d6d2c3;
--chip-bg:#e6e2d4;
--chip-bd:#cfcabb;
--chip-ink:#1b5e3b;
}
*{ box-sizing:border-box; margin:0; padding:0; }
html{ background:#dcdfda; }
body{
font-family:"IBM Plex Sans", system-ui, "Segoe UI", Roboto, sans-serif;
color:var(--ink);
font-size:10.4pt;
line-height:1.42;
-webkit-font-smoothing:antialiased;
}
.page{
background:var(--paper);
width:210mm;
min-height:297mm;
margin:24px auto;
padding:0;
box-shadow:0 8px 40px rgba(0,0,0,.18);
overflow:hidden;
}
/* ---------- HEADER BAND (full-bleed) ---------- */
.band{
background:linear-gradient(125deg, var(--band) 0%, var(--band2) 100%);
padding:11mm 14mm 8mm;
}
.head{
display:flex;
justify-content:space-between;
align-items:flex-start;
gap:18px;
}
.name{
font-size:26pt;
font-weight:700;
letter-spacing:-.02em;
line-height:1;
color:var(--name);
}
.name em{ font-style:normal; color:var(--sage); }
.title{
font-family:"IBM Plex Mono", ui-monospace, monospace;
font-size:9.2pt;
font-weight:500;
letter-spacing:.05em;
color:var(--sage);
text-transform:uppercase;
margin-top:7px;
}
.contact{
text-align:right;
font-family:"IBM Plex Mono", ui-monospace, monospace;
font-size:8.1pt;
line-height:1.75;
color:var(--band-muted);
white-space:nowrap;
}
.contact a{ color:var(--sage); text-decoration:none; }
.contact .strong{ color:var(--name); font-weight:500; }
/* ---------- CONTENT WRAPPER ---------- */
.content{ padding:13px 14mm 0; }
.status{
font-family:"IBM Plex Mono", ui-monospace, monospace;
font-size:8.2pt;
font-weight:500;
letter-spacing:.04em;
color:var(--sage);
margin-top:6px;
}
.avail{
font-family:"IBM Plex Mono", ui-monospace, monospace;
font-size:8.3pt;
color:var(--muted);
margin:2px 0 12px 0;
}
.avail b{ color:var(--green-deep); font-weight:600; }
/* ---------- PROFIL ---------- */
.profil{ margin-top:13px;
font-size:10.2pt;
line-height:1.5;
color:#22302a;
border-left:3px solid var(--green-deep);
padding-left:12px;
margin:0 0 15px 0;
max-width:172mm;
}
.profil strong{ color:var(--green-deep); font-weight:600; }
/* ---------- SECTION HEADERS ---------- */
.sec{
display:flex;
align-items:center;
gap:10px;
margin:0 0 9px 0;
break-after:avoid;
}
.sec h2{
font-size:10.5pt;
font-weight:600;
letter-spacing:.13em;
text-transform:uppercase;
color:var(--ink);
white-space:nowrap;
}
.sec .tok{
font-family:"IBM Plex Mono", ui-monospace, monospace;
font-size:8pt;
color:var(--green-deep);
font-weight:500;
}
.sec .line{ flex:1; height:1px; background:var(--rule); }
section{ margin-bottom:14px; }
/* ---------- COMPÉTENCES ---------- */
.skills{ margin-left:2px; }
.skill-row{
display:grid;
grid-template-columns:34mm 1fr;
gap:8px;
padding:2.5px 0;
align-items:baseline;
}
.skill-row + .skill-row{ border-top:1px solid var(--rule); }
.skill-k{
font-family:"IBM Plex Mono", ui-monospace, monospace;
font-size:8.2pt;
font-weight:500;
color:var(--green-deep);
letter-spacing:.01em;
}
.skill-v{ font-size:9.6pt; color:#22302a; }
.skill-v .sep{ color:var(--chip-bd); padding:0 1px; }
/* ---------- EXPÉRIENCE ---------- */
.entry{ margin-bottom:11px; }
.entry-top, .entry-role{ break-after:avoid; }
.entry-top{
display:flex;
justify-content:space-between;
align-items:baseline;
gap:12px;
}
.entry-co{ font-size:12pt; font-weight:600; color:var(--ink); }
.entry-date{
font-family:"IBM Plex Mono", ui-monospace, monospace;
font-size:8.4pt;
color:var(--muted);
white-space:nowrap;
}
.entry-role{
font-size:9.7pt;
color:var(--green-deep);
font-weight:500;
margin:1px 0 5px;
}
.entry-role .meta{ color:var(--muted); font-weight:400; }
ul.bul{ list-style:none; orphans:2; widows:2; }
ul.bul li{
list-style:"▸ ";
margin-left:13px;
margin-bottom:3.5px;
font-size:9.7pt;
line-height:1.4;
color:#212d27;
break-inside:avoid;
}
ul.bul li b{ color:var(--ink); font-weight:600; }
code.k{
font-family:"IBM Plex Mono", ui-monospace, monospace;
font-size:8.4pt;
background:var(--chip-bg);
color:var(--chip-ink);
border:1px solid var(--chip-bd);
padding:.5px 4px;
border-radius:3px;
white-space:nowrap;
}
/* ---------- PROJETS / FORMATION grid ---------- */
.two{ display:grid; grid-template-columns:1fr 1fr; gap:9px 22px; }
.blk-h{ font-size:10.2pt; font-weight:600; color:var(--ink); }
.blk-sub{
font-family:"IBM Plex Mono", ui-monospace, monospace;
font-size:8pt; color:var(--muted); margin:1px 0 4px;
}
.blk p{ font-size:9.4pt; line-height:1.4; color:#28332d; }
.blk a{ color:var(--green-deep); text-decoration:none; }
.langs{ font-size:9.8pt; color:#22302a; margin-left:2px; }
.langs b{ color:var(--ink); font-weight:600; }
.langs .sep{ color:var(--chip-bd); padding:0 6px; }
/* ---------- SCREEN FOOTER BAND ---------- */
.screen-foot{
background:var(--band);
color:#7e8b84;
font-family:"IBM Plex Mono", ui-monospace, monospace;
font-size:7.6pt;
padding:7px 14mm;
display:flex;
justify-content:space-between;
margin-top:18px;
}
.screen-foot a, .pdf-foot a{ color:inherit; text-decoration:none; }
/* ---------- PDF BACKDROP + FOOTER ---------- */
.bg{ display:none; }
.pdf-foot{ display:none; }
/* ---------- PRINT / PDF ---------- */
/* marges : 14mm haut (sauf p.1) · 0 L/R (bleed) · 13mm bas (réserve footer) */
@page{ size:A4; margin:14mm 0 13mm 0; }
@page :first{ margin-top:0; }
@media print{
html{ background:#fff; }
.two{ display:block; }
.two .blk{ margin-bottom:7px; break-inside:avoid; }
.page{ width:auto; min-height:0; margin:0; box-shadow:none; overflow:visible; background:transparent; }
.screen-foot{ display:none; }
/* fond crème pleine feuille, répété chaque page (déborde dans les marges) */
.bg{
display:block; position:fixed;
top:-14mm; bottom:-13mm; left:0; right:0;
background:var(--paper);
z-index:-1;
}
.pdf-foot{
display:flex;
position:fixed;
bottom:-13mm; left:0; right:0;
height:9mm;
align-items:center;
justify-content:space-between;
padding:0 14mm;
background:var(--band);
color:#8a968f;
font-family:"IBM Plex Mono", monospace;
font-size:7.4pt;
}
}
</style>
</head>
<body>
<div class="page">
<div class="bg"></div>
<!-- ===== HEADER BAND ===== -->
<div class="band">
<header class="head">
<div>
<div class="name">Bastien <em>Chanot</em></div>
<!-- ▼ ZONE 1 : TITRE — adapte au poste visé (format court, séparé par ·) -->
<div class="title">Développeur Systèmes &amp; Backend · C · Rust · Linux</div>
<div class="status">CDI · disponible</div>
<!-- ▲ ZONE 1 -->
</div>
<div class="contact">
<a href="tel:+33778822297" class="strong">+33&nbsp;7&nbsp;78&nbsp;82&nbsp;22&nbsp;97</a><br>
<a href="mailto:bastien@bchanot.fr">bastien@bchanot.fr</a><br>
<a href="https://bchanot.fr">bchanot.fr</a> · <a href="https://git.bchanot.fr/bchanot">git.bchanot.fr</a><br>
<!-- TODO-LIENS : remplace par tes URLs réelles -->
<a href="https://www.linkedin.com/in/bastien-chanot-4075a0a3/">linkedin.com/in/bastien-chanot</a><br><a href="https://github.com/bchanot">github.com/bchanot</a><br>
Loire-Atlantique · Vendée · Morbihan<br>full remote ou hybride<br>Permis&nbsp;B&nbsp;&amp;&nbsp;A
</div>
</header>
</div>
<!-- ===== CONTENT ===== -->
<div class="content">
<!-- ▼ ZONE 2 : ACCROCHE — réécris 2-3 lignes orientées vers l'entreprise / le poste -->
<p class="profil">
Développeur <strong>systèmes &amp; backend</strong>, 7 ans en <strong>C et Rust sur Linux bare-metal</strong>. Du <strong>module kernel</strong> au backend temps réel : drivers, conteneurs (Docker / LXC / QEMU), exploitation d'une <strong>fleet GPU bare-metal</strong> en production, automatisation et CI/CD. Compréhension globale de la stack, du matériel au service.
</p>
<!-- ▲ ZONE 2 -->
<!-- ================= COMPÉTENCES ================= -->
<section>
<div class="sec"><span class="tok">~/</span><h2>Compétences techniques</h2><span class="line"></span></div>
<!-- ▼ ZONE 3a : réordonne les lignes selon l'offre -->
<div class="skills">
<div class="skill-row"><div class="skill-k">Langages</div><div class="skill-v">C <span class="sep">·</span> Rust <span class="sep">·</span> C++ <span class="sep">·</span> Bash <span class="sep">·</span> Python <span class="sep">·</span> Java (AOSP/Android)</div></div>
<div class="skill-row"><div class="skill-k">Systèmes &amp; Embarqué</div><div class="skill-v">Linux kernel drivers <span class="sep">·</span> AOSP <span class="sep">·</span> ARM / x86 <span class="sep">·</span> GPIO <span class="sep">·</span> NFC <span class="sep">·</span> cross-compilation GCC <span class="sep">·</span> systemd <span class="sep">·</span> SELinux</div></div>
<div class="skill-row"><div class="skill-k">Conteneurs / Virtu.</div><div class="skill-v">Docker <span class="sep">·</span> LXC / LXD <span class="sep">·</span> QEMU <span class="sep">·</span> cgroups <span class="sep">·</span> namespaces</div></div>
<div class="skill-row"><div class="skill-k">Cloud &amp; Infra</div><div class="skill-v">AWS (EC2, g4dn bare-metal, IAM, S3, CloudWatch) <span class="sep">·</span> Scaleway <span class="sep">·</span> OVH / Hetzner <span class="sep">·</span> Nginx</div></div>
<div class="skill-row"><div class="skill-k">DevOps &amp; Outils</div><div class="skill-v">Git <span class="sep">·</span> GitHub Actions <span class="sep">·</span> GitLab CI <span class="sep">·</span> CI/CD <span class="sep">·</span> Claude Code (agents/skills custom) <span class="sep">·</span> N8N</div></div>
</div>
<!-- ▲ ZONE 3a -->
</section>
<!-- ================= EXPÉRIENCE ================= -->
<section>
<div class="sec"><span class="tok">~/</span><h2>Expérience professionnelle</h2><span class="line"></span></div>
<div class="entry">
<div class="entry-top"><div class="entry-co">ZenQuality</div><div class="entry-date">2026 — présent</div></div>
<div class="entry-role">Développeur indépendant · Infra &amp; dév web <span class="meta">· zenquality.fr</span></div>
<ul class="bul">
<li>Déployé et opéré en production l'<b>infrastructure auto-hébergée</b> (uptime continu) en conteneurisant une stack <code class="k">Astro</code> <code class="k">React</code> <code class="k">PHP 8</code> <code class="k">PostgreSQL</code> sur VPS <code class="k">Scaleway</code>, avec pipeline de déploiement automatisé.</li>
<li>Livré <b>5 projets clients de bout en bout</b> depuis avril 2026 — conception, intégration, déploiement, hébergement et support continu.</li>
<li>Réalisé des <b>audits techniques</b> (Core Web Vitals, Schema.org) et la <b>mise en conformité légale</b> (RGPD, CGV B2B/B2C, médiateur CM2C) pour des PME — plan d'action sur 12 sprints.</li>
</ul>
</div>
<div class="entry">
<div class="entry-top"><div class="entry-co">CareGame</div><div class="entry-date">2019 — 2025</div></div>
<div class="entry-role">Développeur logiciel · Systèmes &amp; Backend <span class="meta">· Paris · full remote dès 2020</span></div>
<!-- ▼ ZONE 3b : remonte les bullets les plus alignés avec l'offre -->
<ul class="bul">
<li>Conçu et maintenu les <b>modules kernel Linux en C</b> (x86 / ARM) assurant la communication bidirectionnelle hôte ↔ instances AOSP conteneurisées — drivers d'interface, brique critique du pipeline d'exécution serveur.</li>
<li>Co-développé le <b>backend Rust</b> orchestrant le cycle de vie des conteneurs AOSP (<code class="k">WebSocket</code> temps réel clients ↔ instances, scheduling sur la fleet GPU, intégration <code class="k">Docker</code> + <code class="k">LXC</code>) — support de <b>plusieurs centaines de joueurs simultanés</b>.</li>
<li>Porté la densité de production à <b>32 sessions de jeu AAA stables par serveur</b> en concevant l'isolation <b>CPU/GPU par session</b> — adaptation de modules GPU <code class="k">Nvidia</code>, partitionnement 2 cœurs/session (physiques et émulés), sérialisation des accès concurrents sur zones GPU partagées.</li>
<li>Architecturé et exploité une <b>fleet GPU bare-metal</b> <code class="k">AWS g4dn.metal</code> (8× T4, 64 vCPU, ~20 serveurs en pic) servant plusieurs centaines de joueurs en parallèle — isolation 2 cœurs CPU/session, ramdisk I/O (Asphalt 9 : 3 sessions / T4).</li>
<li>Industrialisé jusqu'en production un <b>PoC LXD + Docker</b> issu d'une R&amp;D Nvidia — débogage kernel/conteneur, performance validée par les équipes Nvidia comme dépassant le scope initial du PoC.</li>
<li>Collaboré techniquement en <b>anglais</b> avec <b>Canonical</b> (Anbox, builds LXC/LXD non commerciaux, remontée bugs et feature requests) et <b>Ampere Computing</b> (benchmark de serveurs ARM pré-commerciaux pour évaluation de migration de fleet).</li>
<li>Automatisé l'installation des jeux AOSP et la persistance des sauvegardes (fusion Android Backup + scripts custom couvrant DRM et données externes) et développé un <b>outil d'orchestration Bash modulaire (1000+ lignes)</b> appelé par la CI et le backend Rust.</li>
<li>Atteint une <b>latence compatible gameplay AAA temps réel</b> en développant les virtual input devices AOSP en <code class="k">Java</code> (touchscreen, gamepad) sur le pipeline complet frontend → backend Rust → drivers hôtes → injection AOSP.</li>
</ul>
<!-- ▲ ZONE 3b -->
</div>
<div class="entry">
<div class="entry-top"><div class="entry-co">Deewee</div><div class="entry-date">2017</div></div>
<div class="entry-role">Développeur C · Système embarqué <span class="meta">· Ivry-sur-Seine · stage 42 (6 mois) puis CDD (4 mois)</span></div>
<ul class="bul">
<li>Développé en <b>C</b> le logiciel embarqué d'un boîtier connecté <code class="k">Orange Pi</code> (Debian ARM) interceptant le flux <b>ESC/POS</b> d'une imprimante thermique pour générer le PNG du ticket et le transférer en WiFi direct vers mobile.</li>
<li>Intégré le matériel : <b>GPIO</b> physique (bouton + timeout), hotspot WiFi embarqué avec appairage automatique par diffusion <b>NFC</b> des credentials.</li>
<li>Travaillé en cycle court sur un prototype fonctionnel — cross-compilation ARM, débogage sur cible, itérations rapides entre intégration matérielle et logiciel embarqué.</li>
</ul>
</div>
</section>
<!-- ================= PROJETS ================= -->
<section>
<div class="sec"><span class="tok">~/</span><h2>Projets &amp; réalisations</h2><span class="line"></span></div>
<div class="two">
<div class="blk">
<div class="blk-h">Homelab — infrastructure personnelle</div>
<div class="blk-sub">en continu</div>
<p>Auto-hébergement Git / DNS / VPN / SMB — NAS Asustor, WireGuard site-to-site, Pi-hole, segmentation réseau, hardening fail2ban, gocryptfs sur dossiers sensibles.</p>
</div>
<div class="blk">
<div class="blk-h">Code source &amp; projets persos</div>
<div class="blk-sub"><a href="https://git.bchanot.fr/bchanot">git.bchanot.fr/bchanot</a></div>
<p>Serveur Git auto-hébergé en production. Configuration Claude Code (agents/skills custom), dotfiles, projets bas-niveau C / Rust. Mirror automatique vers GitHub via push hook.</p>
</div>
</div>
</section>
<!-- ================= FORMATION ================= -->
<section>
<div class="sec"><span class="tok">~/</span><h2>Formation</h2><span class="line"></span></div>
<div class="two">
<div class="blk">
<div class="blk-h">École 42 — programmation informatique</div>
<div class="blk-sub">2015 — 2019 · Clichy</div>
<p>Kernel / systèmes (ft_linux, kfs-1, drivers, malloc), bas niveau (nm, 42sh POSIX, ft_ls), sécurité &amp; algorithmie (snow crash, ft_ssl_md5, lem-in).</p>
</div>
<div class="blk">
<div class="blk-h">TSRIT — Next Formation</div>
<div class="blk-sub">2013 — 2015 · Vincennes · Félicitations du jury</div>
<p>BTS Technicien Supérieur Réseaux &amp; Télécoms. Socle réseau (OSI, TCP/IP), administration Linux / Windows Server, virtualisation.</p>
</div>
</div>
</section>
<!-- ================= LANGUES ================= -->
<section>
<div class="sec"><span class="tok">~/</span><h2>Langues</h2><span class="line"></span></div>
<div class="langs"><b>Anglais</b> C2 <span class="sep">·</span> <b>Espagnol</b> B1 <span class="sep">·</span> <b>Français</b> natif</div>
</section>
</div><!-- /content -->
<div class="screen-foot"><a href="https://bchanot.fr">bchanot.fr</a><span>Bastien Chanot · CV 2026</span></div>
<div class="pdf-foot"><a href="https://bchanot.fr">bchanot.fr</a><span>Bastien Chanot · CV 2026</span></div>
</div>
</body>
</html>
Binary file not shown.
+367 -647
View File
File diff suppressed because it is too large Load Diff
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
+33 -92
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,17 +1016,17 @@
<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, sinon <strong>hybride ou présentiel</strong> en <strong>Loire-Atlantique · Vendée · Morbihan</strong>. Si l'équipe est à Paris, uniquement en <strong>hybride, 1 à 2 jours par mois maximum</strong>.</p>
</div>
<dl class="about-callout">
<dt>Recherche prioritaire</dt>
<dd>CDI systèmes embarqués / logiciel</dd>
<dt>En parallèle</dt>
<dd>Missions freelance · ZenQuality</dd>
<dt>Localisation actuelle</dt>
<dd>Yerres (91) · mobilité Pays de la Loire</dd>
<dt>Zone</dt>
<dd>Loire-Atlantique · Vendée · Morbihan</dd>
<dt>Présence</dt>
<dd>Full remote · ou 1–2 j/mois si Paris</dd>
<dd>Full remote · hybride ou présentiel dans la zone · Paris 1–2 j/mois max</dd>
<dt>Site pro</dt>
<dd><a href="https://zenquality.fr" target="_blank" rel="noopener">zenquality.fr&nbsp;↗</a></dd>
</dl>
@@ -1234,7 +1175,7 @@
<div class="timeline-meta">
<span class="period">avr.&nbsp;2026 — présent</span>
<span class="badge">En cours</span>
<span>Yerres · Full remote</span>
<span>Full remote</span>
</div>
<h3><a href="https://zenquality.fr" target="_blank" rel="noopener">ZenQuality</a></h3>
<p class="timeline-role">Développeur indépendant · Systèmes &amp; Backend</p>
+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.1.0