vikingowl 5821547a73 feat(security): close audit waves 1-4 (C1-C6, H1, H2, H4, H11, H13, H14, H16)
Implements the remediation pass described in
planning/19-security-audit-2026-04-30.md. All Critical findings and the
Wave 1-4 High findings are closed; PoC tests added; full backend test
suite green; helm chart lints clean.

Wave 1 - Auth & identity
- C1 OAuth state nonce: PutOAuthState / ConsumeOAuthState (valkey,
  GETDEL single-use, 15min TTL); Callback rejects missing/forged/cross-
  provider state before token exchange.
- C2 OAuth identity linking: refuse silent linking to existing user
  unless info.EmailVerified is true. fetchGitHubUser now consults the
  /user/emails endpoint for the verified flag (no more hardcoded true);
  fetchFacebookUser sets EmailVerified=false (FB exposes no per-email
  verification flag).
- H1 Magic-link verify: replaced Get + MarkUsed with a single atomic
  UPDATE...RETURNING (ConsumeMagicLink) - TOCTOU-free.
- H2 TOTP code replay: MarkTOTPCodeConsumed (valkey SET NX, 120s TTL)
  prevents replay of a successfully validated code; fails closed on
  transient store errors.
- H3 Backup-code orphan: DisableTOTP now also wipes totp_backup_codes.

Wave 2 - Middleware & network
- C3 CORS/CSRF regex anchoring: NewCORSConfig wraps each pattern with
  \A...\z so substring spoofing of origins is impossible.
- H4 ClientIP: server reads APP_TRUSTED_PROXIES; gin SetTrustedProxies
  is called explicitly (empty default = no proxy trust).
- H11 Body limit + DisallowUnknownFields: BodyLimitBytes middleware
  (1 MiB default) wraps every request; validate.BindJSON now uses a
  json.Decoder with DisallowUnknownFields and rejects trailing tokens;
  413 envelope on body-limit overflow.
- H16 NetworkPolicy: backend.networkPolicy.enabled defaults to true;
  new web-networkpolicy.yaml restricts web pod ingress to nginx-gateway
  and egress to backend service + DNS + 443.

Wave 3 - Encryption at rest
- C4 TOTP secrets: CreateTOTPSecret writes encrypted secret_v2;
  GetTOTPSecret prefers v2 with legacy fallback.
- C5 OAuth tokens: migration 000033 adds *_v2 columns; CreateOAuthAccount
  and UpdateOAuthTokens write encrypted; GetOAuthAccount reads v2 with
  legacy fallback.
- M1 Domain separation: crypto.DeriveKeyFor(secret, purpose) replaces
  single-purpose DeriveKey; settings, totp, oauth each use a distinct
  HKDF-derived subkey. DeriveKey kept as back-compat alias for settings.

Wave 4 - Input & AI safety
- C6 SSRF: new pkg/safehttp refuses to dial RFC1918, loopback, link-
  local, ULA, multicast, unspecified, or cloud-metadata IPs; scheme
  allowlist (http/https). Wired into pkg/scrape, discovery LinkChecker,
  and imageURLReachable. NewForTesting opt-in for httptest.
- H13 PromptGuard German + Unicode: NFKC + Cf-class strip pre-pass
  closes zero-width and full-width-homoglyph bypasses; new German rules
  for ignoriere/missachte/vergiss/role-escalation/prompt-exfil/verbatim;
  Gemma-style and pipe-delimited chat-template tokens covered;
  source-fence rule prevents '=== Quelle:' splice in scraped text.
- H14 BudgetGate: new ai.BudgetGate interface; UsageRepo.CheckBudget
  reads today's SUM(estimated_cost_usd) (10s cache) and refuses calls
  when AI_DAILY_CAP_USD is exceeded; GeminiProvider.Chat checks the
  gate before contacting Gemini.

OAuth routes remain disabled in server/routes.go, so C1/C2 are not
actively reachable today; fixes ensure correctness when re-enabled.
2026-04-30 23:41:48 +02:00

Marktvogt

The central portal for medieval markets ("Mittelaltermaerkte") in the DACH region.

Marktvogt connects visitors, merchants, artists, camp groups, and organizers on one platform — replacing the patchwork of Facebook groups, phone calls, mailing lists, and scattered websites that the scene currently runs on.

The name comes from the historical Marktvogt — the keeper of order at medieval markets. Same job, different century.


The Problem

The medieval-market scene in the DACH region is large (hundreds of markets per year) but digitally fragmented:

  • Finding markets: Facebook groups, mittelalterkalender.info, private sites
  • Bookings (merchants, artists, camps): phone, e-mail, Facebook Messenger
  • Camp groups: organize almost exclusively on Facebook
  • Tickets: usually box-office-only or one-off organizer sites

There is no single portal that brings all participants together. Marktvogt is that portal.

The Solution

A two-sided marketplace:

  • Supply side — organizers list markets, open plots, programs, tickets
  • Demand side — visitors discover markets and buy tickets; merchants, artists, and camp groups apply for spots

Network effects: organizers attract applicants, applicants make the platform indispensable for organizers, and visitors follow the content.

Roles

Six roles, with the Veranstalter (organizer) as the central actor:

Role Description
Gast Anonymous visitor — browse markets, no account
User Registered visitor — tickets, favorites, profile
Haendler Merchant group applying for plots
Kuenstler Artist group applying for performance slots
Lager Reenactment camp group applying for camp space
Veranstalter Organizer — creates markets, reviews applications, sells tickets

All applicants (Haendler / Kuenstler / Lager) operate as groups. A solo merchant is just a one-person group. Mitarbeiter (staff) is a sub-role under Veranstalter with granular permissions.


Architecture

Monorepo. Components are plain directories, not git submodules.

Layer Stack
Backend Go REST API + WebSocket; PostgreSQL (PostGIS), Valkey/Dragonfly, S3
Web SvelteKit + Tailwind 4, SSR for SEO, runs on Bun
Mobile Flutter (Android + iOS)
Auth Custom (Go), e-mail+password / magic link / OAuth / 2FA
Payments Stripe Connect
LLM Google Gemini (gemini-2.5-flash-lite) for enrichment
CI/CD Woodpecker (ci.somegit.dev) — .gitlab-ci.yml retained as fallback
Hosting Kubernetes (itsh.dev), Helm chart at helm/marktvogt/
Monitoring Prometheus, Loki, Grafana, Sentry

Repo Layout

backend/    Go REST API + WebSocket chat
web/        SvelteKit frontend (SSR, Bun runtime)
app/        Flutter mobile app
helm/       Monolithic Helm chart (backend + web + Postgres + Dragonfly)
planning/   Vision, roadmap, MVP scope, feature specs (German, ASCII-only)
scripts/    Out-of-band tooling (e.g. K8s secret sync)
.woodpecker/ CI pipelines for somegit.dev

The admin dashboard lives in its own repo and will be added later.


Getting Started

The repo is driven by a Justfile. Run just for the full list.

# One-time setup: pre-commit hooks, web deps, golangci-lint
just hooks-install

# Backend
just backend-dev        # Postgres + Valkey via docker compose
just backend-run        # go run ./cmd/api
just backend-test
just backend-lint

# Web
just web-dev            # SvelteKit dev server
just web-check          # svelte-check (types)
just web-lint

# Everything
just lint
just test
just fmt

For component-specific details, see backend/README.md, web/README.md, and app/README.md.


Deployment

Single Helm release marktvogt in namespace tenant-2, deployed from helm/marktvogt/ (monolithic chart covering backend, web, Postgres, and Dragonfly). CI runs:

helm upgrade marktvogt --reuse-values \
  --set-string backend.image.tag=<sha> \
  --set-string web.image.tag=<sha>

--set-string is mandatory — all-digit short SHAs get float-coerced into scientific notation otherwise, which then fails K8s label validation.

K8s Secrets are pre-created out-of-band by scripts/k8s-secrets-sync.sh reading from .env.helm (gitignored). CI never touches secret values.

The container registry (registry.itsh.dev/vikingowl/marktvogt.de/{backend,web}) is Zot-backed and requires attestations on every pushed image. The Woodpecker pipelines use woodpeckerci/plugin-docker-buildx, which handles this by default.


Status

Active development as of 2026-04-29. backend/, web/, and app/ all contain working code (Go API scaffolding + auth, SvelteKit pages, Flutter skeleton).

  • Current phase scope: planning/15-mvp.md
  • Phased feature plan: planning/17-roadmap.md
  • Vision: planning/00-vision.md

Planning docs are in German and use ASCII only (no umlauts) for cross-platform compatibility.


Conventions

  • API design: REST for CRUD, WebSocket for real-time chat
  • Auth: custom-built using Go libraries — no external auth provider
  • Offline capability: required for QR-ticket validation and venue maps
  • Commits: conventional commit messages
  • Source of truth for product: planning/ — read it before adding features
Description
No description provided
Readme 1.7 MiB
Languages
Go 60.3%
Svelte 20.3%
Dart 11.1%
TypeScript 5%
PLpgSQL 1.1%
Other 2.1%