diff --git a/REPORT-mailer-source.md b/REPORT-mailer-source.md new file mode 100644 index 00000000..cadee116 --- /dev/null +++ b/REPORT-mailer-source.md @@ -0,0 +1,61 @@ +# REPORT — the contact mailer's lost source (R-902), 2026-10-08 + +**NOT FOUND.** Searched read-only on DooPlex, in the web editor, in all Gitea repositories and in Docker's build +records. The running binary is now kept in git; a replacement plan is written (Part D). + +> `REPORT-mailer-source.md`, not `REPORT.md`: another session has uncommitted work in this repo. + +## Part A — the program's clues (`documentation/audits/mailer-source-2026-10-08/`) + +- `go version -m`: module `github.com/felhom/contact-mailer (devel)`, **go1.23.12**, `CGO_ENABLED=0`, `-ldflags="-w -s"`. + **No `vcs.revision`/`vcs.time`/`vcs.modified`.** +- Build paths in the binary: **`/build/main.go`** only — one source file, built in a container folder. +- Image (containerd, `k3s ctr content get`): built **2026-02-05 10:40 +01:00** by BuildKit, two stages (builder at + `/build`; alpine 3.20.9, user 1000, `WORKDIR /app`, `COPY /build/contact-mailer .`). The SLSA provenance blob the + index names is not present in containerd. + +## Part B — places searched, each with its positive control + +| Place | Found | Control that proves the search worked | +|---|---|---| +| `/build` (from Part A) | does not exist on DooPlex | — | +| Web editor storage + its file history (through the running pod) | only `felhom.eu/manifests/contact-mailer.yaml` | `func main` found in agent code and in 2 History copies | +| All 10 Gitea repos, full history (`-G sendViaResend\|buildEmailHTML\|felhom/contact-mailer`) | manifests only (homelab-manifests c9648cd 2026-02-05; felhom.eu 0b144a4d) | the manifests are found by the same search | +| DooPlex disks, 55,548 `*.go`/`go.mod`/`Dockerfile*` files + name search, no depth limit | only `hub/cmd/hub/main.go` (×2, for `RESEND_API_KEY`) and the pod's logs | `RESEND_API_KEY` in the hub's main.go | +| Docker build cache (337 records) and build history | oldest record 2026-09-28; history 7 days | today's builds are listed | + +Excluded by name: kernel/runtime folders, Docker/containerd/k3s/kubelet/Longhorn/PBS stores, media folders — listed in +`SEARCH.md`. Not reachable: the operator's Windows workstation. + +**Side finding:** `homelab-manifests` history (c9648cd) holds an old Resend key literal. Compared by hash only, it is +not today's key (rotated 2026-06-29, felhom.eu feea0606). Whether the old key was also revoked at Resend is not visible +from here. No row. + +## Part D — the plan + +`documentation/audits/mailer-source-2026-10-08/PLAN-replacement.md`: endpoints (`/api/contact`, `/healthz`, +`/debug/test`), env, one mail per form with Reply-To, the limits read from the binary (5 files, 10 MB each, 20 MB +total, name ≤ 200, message ≤ 10 000, rate limit, honeypot), a stdlib-only replacement with a version-tagged image, +proof steps, switch-over and roll-back. + +**The binary is committed** (`contact-mailer.bin`, sha256 `775a23d5…30bb0`, no key inside — searched): a DooPlex +rebuild no longer loses the program. + +## Fixed without a row: the decoy suite now runs one at a time + +During this task the gates ran in this session and in Felhom.eu session 1 at the same moment. `scripts/test_gate_decoys.py` +edits real files and restores them from a copy taken when it plants; two runs at once left a planted fault behind — +`website/en/apps.html` lost its Radicale logo line, twice, in the local copy (never pushed; the live page was checked: +logo present). Restored from HEAD both times. The suite now takes an exclusive lock (`.git/decoy-suite.lock`) at start, +so a second run waits; two runs of mine started 2 s apart both finished, one after the other (the waiting one: „all 73 +decoys behaved"). The other session could not be messaged (not reachable from here). + +## Register + +Rows before **130**, after **130**, opened **0**, closed **0**. R-902 stays open, NARROWED (search done; owner operator). + +## One decision for you + +Is the February `contact-mailer` folder on your Windows workstation (for example under `e:\DooPlex Server\`)? +- **Pick: look there first.** If it is there, I commit it as it is and only a tagged rebuild is left. +- **If you do nothing:** the replacement is written from the plan in a later, attended task. The form keeps working. diff --git a/documentation/audits/mailer-source-2026-10-08/PLAN-replacement.md b/documentation/audits/mailer-source-2026-10-08/PLAN-replacement.md new file mode 100644 index 00000000..99cfa512 --- /dev/null +++ b/documentation/audits/mailer-source-2026-10-08/PLAN-replacement.md @@ -0,0 +1,44 @@ +# Plan — a replacement for the contact mailer (R-902) + +The February source is lost (`SEARCH.md`). The running program keeps working; this plan is for a later, attended +session. **Nothing here is built or deployed yet.** + +## What the running program does (read from the binary and the manifest — not from source) + +- **Endpoints:** `POST /api/contact` (the Ingress sends `felhom.eu/api/*` here), `GET /healthz` (the probes), + `POST /debug/test` (only when `DEBUG=true`; the manifest sets `false`). Listens on `LISTEN_ADDR`, default `:8080`. +- **Env:** `RESEND_API_KEY` (from `Secret/resend-api`), `FROM_EMAIL`, `TO_EMAIL`, `ALLOWED_ORIGIN`, `TZ`, `DEBUG`. +- **Per form, one mail** via `https://api.resend.com/emails` (Bearer key): from `FROM_EMAIL`, to `TO_EMAIL`, + `reply_to` = the visitor, HTML table (`buildEmailHTML`: header „Új üzenet a weboldalról", rows incl. Tárgy, + „Csatolmány … %d fájl"), attachments as base64. **Nothing is sent to the visitor.** +- **Checks, with Hungarian JSON errors:** CORS to `ALLOWED_ORIGIN`; a per-IP rate limiter („Túl sok kérés…"); a + honeypot field `website`; required fields („Kérlek, töltsd ki az összes kötelező mezőt."); name ≤ 200, message + ≤ 10 000 characters; e-mail format and length; at most 5 files, ≤ 10 MB each, ≤ 20 MB together; a malformed or + too-large request („A kérés mérete túl nagy vagy hibás formátum."). The page shows its own messages, not these. + +## The replacement + +One `main.go` (stdlib only, Go ≥ 1.23), same endpoints, env, limits and mail shape; the rate limit and the +template copied from the strings above. Committed to `felhom.eu/contact-mailer/` with `go.mod`, a two-stage +`Dockerfile` (the same shape the image record shows: builder at `/build`, alpine runtime, user 1000, `/app`), tests +for each refusal, and a `README.md`. The image is built **with a version tag** and pushed to the Gitea registry, +so `imagePullPolicy: Never` and the hand import end; the manifest names that tag. + +## How to prove it, before switching + +1. Unit tests: every refusal above, and the mail body built from a sample form (no network). +2. A second Deployment `contact-mailer-next` (no Ingress), port-forwarded: a real form with one small PDF → one mail + arrives at info@ with Reply-To = the sender and the attachment; an 11 MB file → refused; six files → refused. +3. Compare its mail with one from the running program (same fields, same subject line). + +## Switch-over (attended) + +Point the Deployment at the new tag, wait for Ready, send one form from `felhom.eu/kapcsolat` and one from +`/en/contact`, read both mails. Roll back = the old manifest line (the old image stays in containerd, and its binary +is kept in `documentation/audits/mailer-source-2026-10-08/contact-mailer.bin`). + +## One question for the operator + +Do you still have the February folder on your Windows workstation (`e:\DooPlex Server\…` or a „contact-mailer" +folder)? **Pick: look there first** — if it is there, it is committed as it is and this plan shrinks to „rebuild with +a version tag". **If you do nothing:** the replacement is written from this page in a later website/ops task. diff --git a/documentation/audits/mailer-source-2026-10-08/SEARCH.md b/documentation/audits/mailer-source-2026-10-08/SEARCH.md new file mode 100644 index 00000000..0a2d11e5 --- /dev/null +++ b/documentation/audits/mailer-source-2026-10-08/SEARCH.md @@ -0,0 +1,44 @@ +# The contact mailer's source — the search (2026-10-08) + +**Result: NOT FOUND.** Read only on DooPlex: no restart, no deploy, no image tag, no change to the pod. + +## Part A — the program's own clues + +| Clue | Value | Source | +|---|---|---| +| Module | `github.com/felhom/contact-mailer` `(devel)` | `go version -m` → `go-version-m.txt` | +| Go | go1.23.12, `CGO_ENABLED=0`, `-ldflags="-w -s"` | same | +| `vcs.revision` / `vcs.time` / `vcs.modified` | **absent** — not built inside a git folder (or with VCS stamping off) | same | +| Source file | **one file: `/build/main.go`** (the only non-stdlib `.go` path in the binary) | `strings` | +| Image | `docker.io/library/contact-mailer:latest`, index `sha256:fac420fd…`, built **2026-02-05 10:40 +01:00** by BuildKit; multi-stage: a builder stage with `/build`, then alpine 3.20.9 + `ca-certificates tzdata`, user `appuser` 1000, `WORKDIR /app`, `COPY /build/contact-mailer .` | `k3s ctr content get` → `image-config.json` | +| Provenance | the index names an SLSA provenance blob (`sha256:f4ad3f62…`) — **not present** in containerd (`ctr content get`: not found) | — | + +The binary itself is kept here (`contact-mailer.bin`, sha256 in `contact-mailer.bin.sha256`; no API key inside — a +search for `re_…`, `Bearer re_`, `sk-`, `ghp_` found 0): if DooPlex is rebuilt, this is the only copy of the program. + +## Part B — where it was searched (each with its positive control) + +| # | Place | Result | Positive control | +|---|---|---|---| +| 1 | Paths from Part A (`/build`) | none on DooPlex (`/build` does not exist; it was the build container's folder) | — | +| 2 | Web editor (`code-system/code-server`, Longhorn PVCs `code-server-workspace`/`-config`/`-local`, read through the running pod): `grep -r` for `sendViaResend`, `buildEmailHTML`, `felhom/contact-mailer` in `*.go`, `go.mod`, `Dockerfile*` under `/home/coder`; name search `*contact*mailer*`; the editor's file history `~/.local/share/code-server/User/History` (34 MB) without a type filter | only `felhom.eu/manifests/contact-mailer.yaml` | `func main` found in `felhom-agent/cmd/felhom-opsign/main.go` and in two History `.go` copies | +| 3 | Gitea: all 10 repositories the two tokens see (`admin/*`; no other owner visible), full history, `git log --all -G 'sendViaResend\|buildEmailHTML\|felhom/contact-mailer'` | hits only in manifests: `homelab-manifests` c9648cd (2026-02-05 „added mailer pod", deleted ee93b50) and `felhom.eu` 0b144a4d | the same search finds the manifests (which name `…/felhom/contact-mailer`) | +| 4 | DooPlex disks, whole (`/`, `/mnt/1_hdd`, `2_hdd`, `3_hdd`, `5_hdd`, `ssd_2`), `nice -n 19 ionice -c3`: name search `*contact-mailer*`/`*contact_mailer*`, no depth limit; content search of 55,548 `*.go`/`go.mod`/`Dockerfile*` files for `sendViaResend\|buildEmailHTML\|felhom/contact-mailer\|RESEND_API_KEY\|contact-mailer\|contact_mailer` (`search.sh`, `disk-search.txt`) | names: the manifest and the pod's own logs only; content: only `hub/cmd/hub/main.go` (×2) | `RESEND_API_KEY` found in the hub's `main.go` (both copies) | +| 5 | Docker build cache (`docker buildx du --verbose`, 337 records) and build history (`docker buildx history ls`) | oldest cache record 2026-09-28, history 7 days — the February build is gone; no dangling image | the lists hold today's builds | + +**Excluded by name** (fences or no source possible): `/proc /sys /dev /run`, `/var/lib/{docker,containerd, +containerd.pre-move-2026-08-05,rancher,kubelet,longhorn,proxmox-backup}`, `/mnt/ssd_2/{replicas,containerd}`, +`/mnt/backup-longhorn`, any `.chunks` (PBS), media folders (`Download Series Movies Cartoons Kids Audiobook Photos +Videos "Phone Backup"`, `/mnt/plex_media`), `lost+found`, the session scratchpad. Gitea's and the registry's own +storage live in k3s/Longhorn volumes and were not read directly (Gitea was read through its API). + +**Not reachable from here:** the operator's Windows workstation (`e:\DooPlex Server\` holds DooPlex deploy notes per +project memory). The manifest's own comment says `docker build -t contact-mailer:latest .`, then +`docker save … | sudo k3s ctr images import -` — a build on a machine with k3s, so DooPlex, from a folder that no +longer exists here. + +## A side finding + +`homelab-manifests` history (c9648cd, 2026-02-05) holds a Resend API key literal in the old manifest's comment. Compared +by hash only (never printed): it is **not** today's key (`Secret/resend-api`), which was rotated 2026-06-29 (felhom.eu +feea0606). No row: the key was already replaced. diff --git a/documentation/audits/mailer-source-2026-10-08/contact-mailer.bin b/documentation/audits/mailer-source-2026-10-08/contact-mailer.bin new file mode 100644 index 00000000..6d49f449 Binary files /dev/null and b/documentation/audits/mailer-source-2026-10-08/contact-mailer.bin differ diff --git a/documentation/audits/mailer-source-2026-10-08/contact-mailer.bin.sha256 b/documentation/audits/mailer-source-2026-10-08/contact-mailer.bin.sha256 new file mode 100644 index 00000000..e78b8547 --- /dev/null +++ b/documentation/audits/mailer-source-2026-10-08/contact-mailer.bin.sha256 @@ -0,0 +1 @@ +775a23d53c2b28040ae864de439c78d045fd46a046bcefea3b9f15f936830bb0 /mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/mailer-source-2026-10-08/contact-mailer.bin diff --git a/documentation/audits/mailer-source-2026-10-08/disk-search.txt b/documentation/audits/mailer-source-2026-10-08/disk-search.txt new file mode 100644 index 00000000..2210538d --- /dev/null +++ b/documentation/audits/mailer-source-2026-10-08/disk-search.txt @@ -0,0 +1,11 @@ +# name search +/mnt/5_hdd/felhom.eu/git/felhom.eu/manifests/contact-mailer.yaml +/var/log/containers/contact-mailer-5bb869b85b-pqtjt_felhom-system_contact-mailer-69fe37a42882d3860e5a2f0ce78332e5a6c8300000527559bb2bc457d7376c64.log +/var/log/pods/felhom-system_contact-mailer-5bb869b85b-pqtjt_94e8810a-d124-49b6-96e3-863c7966a343 +/var/log/pods/felhom-system_contact-mailer-5bb869b85b-pqtjt_94e8810a-d124-49b6-96e3-863c7966a343/contact-mailer +# content search (*.go, go.mod, Dockerfile*) +/mnt/5_hdd/felhom.eu/build/felhom-hub/workspace/cmd/hub/main.go +/mnt/5_hdd/felhom.eu/git/felhom.eu/hub/cmd/hub/main.go +# count of files content-searched +55548 +# done diff --git a/documentation/audits/mailer-source-2026-10-08/go-version-m.txt b/documentation/audits/mailer-source-2026-10-08/go-version-m.txt new file mode 100644 index 00000000..aad0cf52 --- /dev/null +++ b/documentation/audits/mailer-source-2026-10-08/go-version-m.txt @@ -0,0 +1,10 @@ +/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/mailer-source-2026-10-08/contact-mailer.bin: go1.23.12 + path github.com/felhom/contact-mailer + mod github.com/felhom/contact-mailer (devel) + build -buildmode=exe + build -compiler=gc + build -ldflags="-w -s" + build CGO_ENABLED=0 + build GOARCH=amd64 + build GOOS=linux + build GOAMD64=v1 diff --git a/documentation/audits/mailer-source-2026-10-08/image-config.json b/documentation/audits/mailer-source-2026-10-08/image-config.json new file mode 100644 index 00000000..7b0de2c6 --- /dev/null +++ b/documentation/audits/mailer-source-2026-10-08/image-config.json @@ -0,0 +1,67 @@ +{ + "created": "2026-02-05T10:40:02.940564676+01:00", + "config": { + "User": "appuser", + "ExposedPorts": { + "8080/tcp": {} + }, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Entrypoint": [ + "./contact-mailer" + ], + "WorkingDir": "/app" + }, + "history": [ + { + "created": "2026-01-28T01:18:16.507489867Z", + "created_by": "ADD alpine-minirootfs-3.20.9-x86_64.tar.gz / # buildkit", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2026-01-28T01:18:16.507489867Z", + "created_by": "CMD [\"/bin/sh\"]", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-02-05T10:39:46.98938696+01:00", + "created_by": "RUN /bin/sh -c apk add --no-cache ca-certificates tzdata # buildkit", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2026-02-05T10:39:47.416410394+01:00", + "created_by": "RUN /bin/sh -c adduser -D -u 1000 appuser # buildkit", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2026-02-05T10:39:47.511062104+01:00", + "created_by": "USER appuser", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-02-05T10:39:47.511062104+01:00", + "created_by": "WORKDIR /app", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2026-02-05T10:40:02.940564676+01:00", + "created_by": "COPY /build/contact-mailer . # buildkit", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2026-02-05T10:40:02.940564676+01:00", + "created_by": "EXPOSE [8080/tcp]", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + }, + { + "created": "2026-02-05T10:40:02.940564676+01:00", + "created_by": "ENTRYPOINT [\"./contact-mailer\"]", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + } + ] +} diff --git a/documentation/audits/mailer-source-2026-10-08/search.sh b/documentation/audits/mailer-source-2026-10-08/search.sh new file mode 100755 index 00000000..bf3b7d4a --- /dev/null +++ b/documentation/audits/mailer-source-2026-10-08/search.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# read-only whole-disk search for the contact-mailer source. Prints only paths. +P=( -path /proc -o -path /sys -o -path /dev -o -path /run -o -path /tmp/claude-1000 \ + -o -path /var/lib/docker -o -path /var/lib/containerd -o -path /var/lib/containerd.pre-move-2026-08-05 -o -path /var/lib/rancher \ + -o -path /var/lib/kubelet -o -path /var/lib/longhorn -o -path /var/lib/proxmox-backup -o -path /mnt/ssd_2/replicas -o -path /mnt/ssd_2/containerd \ + -o -path /mnt/backup-longhorn -o -name .chunks -o -name Download -o -name Series -o -name Movies -o -name Cartoons -o -name Kids \ + -o -name Audiobook -o -name Photos -o -name Videos -o -name "Phone Backup" -o -path /mnt/plex_media -o -name lost+found ) +echo "# name search" +find / \( "${P[@]}" \) -prune -o \( -iname '*contact-mailer*' -o -iname '*contact_mailer*' \) -print 2>/dev/null +echo "# content search (*.go, go.mod, Dockerfile*)" +find / \( "${P[@]}" \) -prune -o -type f \( -name '*.go' -o -name go.mod -o -name 'Dockerfile*' \) -size -2M -print0 2>/dev/null \ + | xargs -0 grep -l -E 'sendViaResend|buildEmailHTML|felhom/contact-mailer|RESEND_API_KEY|contact-mailer|contact_mailer' 2>/dev/null +echo "# count of files content-searched" +find / \( "${P[@]}" \) -prune -o -type f \( -name '*.go' -o -name go.mod -o -name 'Dockerfile*' \) -size -2M -print 2>/dev/null | wc -l +echo "# done" diff --git a/scripts/CHANGELOG.md b/scripts/CHANGELOG.md index a8e06c3d..6183cadd 100644 --- a/scripts/CHANGELOG.md +++ b/scripts/CHANGELOG.md @@ -1,3 +1,9 @@ +## gates — the decoy suite runs one at a time (2026-10-08, fixed without a row) + +- `test_gate_decoys.py` takes an exclusive `fcntl.flock` on `.git/decoy-suite.lock` before planting anything. Two runs at + once in the shared worktree (two sessions running the gates together) had left a decoy planted for good + (`website/en/apps.html` without its Radicale logo, local only, restored). A second run now waits for the first. + ## gates — the language globe (site_gates.py gates 3 and 13; 2026-10-08) - One language list, `LANGS = [("hu", "Magyar"), ("en", "English")]`, with `GLOBE_LABEL`. Gate 3 normalises the diff --git a/scripts/test_gate_decoys.py b/scripts/test_gate_decoys.py index f4640795..c7e9d579 100644 --- a/scripts/test_gate_decoys.py +++ b/scripts/test_gate_decoys.py @@ -34,6 +34,14 @@ import sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +# ONE RUN AT A TIME (2026-10-08). The decoys mutate REAL files and restore them from a backup taken at plant time. Two +# runs at once in the shared worktree (two sessions running the gates together) let run B back up a file run A had +# already mutated, and B's "restore" then wrote A's decoy back for good: website/en/apps.html was left without its +# Radicale logo, one step from a push. An exclusive lock makes a second run WAIT for the first. +import fcntl +_LOCK = open(os.path.join(ROOT, ".git", "decoy-suite.lock"), "w") +fcntl.flock(_LOCK, fcntl.LOCK_EX) + # ── WHAT THIS FILE COVERS ──────────────────────────────────────────────────────────────────────── # Read by scripts/decoy_coverage_gate.py, which AST-parses this literal rather than grepping for # gate names — a substring search for coverage would be the very shape this sweep exists to find.