7.5 KiB
Status: error triage, spike detection, IP blocking (2026-09-19)
Pick-up notes. Nothing below is committed, deployed or applied to the cluster unless stated. Greenfield, single user: radical changes are fine.
Access
- ClickHouse (otel instance) is now a LoadBalancer:
monitoring/clickhouseat10.10.3.45(native 9000, HTTP 8123). Set ink8s/clickhouse.yamland patched live.docs/ML.mdstill says "keep ClusterIP"; that is superseded. - Credentials are in
.env(gitignored, mode 600). Load withset -a; . ./.env; set +a.clickhouse-clientis installed locally. .envdeliberately does not enable Needle; pass-needle URLexplicitly.
What exists now
| Piece | Where | State |
|---|---|---|
| Normalized rows written on ingest | internal/storage/normalized.go, Append in clickhouse.go |
Code done, tests pass. Running collector is still the old build, so nothing new is written until deployed. |
| Live schema migration | EnsureNormalizedSchema (runs at collector start) |
Applied once by hand via backfill; issue_fingerprint etc. now exist. |
| 404/410 are not errors | isErrorStatus in internal/normalization/normalizer.go |
Done. Rows stored before this still count 404s as errors. |
| Backfill | cmd/normalized-backfill |
Ran 2026-09-19 12:00 to ~21:00 UTC. Queries need FINAL (duplicates until merge). |
| Spike rule | internal/features/spike.go |
Excess-error rule, trimmed-mean baseline, Suppressor (cooldown, 2x escalation). Tested. |
| Replay CLI | cmd/spike-alerts |
Replays a range; mutes non-actionable categories; Needle optional and off. |
| Cause categories | internal/triage |
Deterministic rules, first match wins. Tested. |
| Rules scoring | cmd/triage-eval |
Scores a labelled JSONL; also prepares rows for labelling. |
| Offender report | cmd/bad-actors |
Read-only. Ranks IPs from raw nginx spans, marks [BLOCK] / [INTERNAL]. |
| Tracker | internal/honeypot |
Now needs distinct paths (default 3), never tracks internal/reserved IPs, uses triage.IsProbePath. |
| Blocker | internal/blocker |
Writes ingress-nginx block-cidrs. Dry-run by default. Tested with a fake client. Wired into cmd/server. |
| RBAC + env | k8s/deployment.yaml |
Role limited to the one ConfigMap; blocker env set to enabled + dry-run. Not applied. |
Categories: scanner_probe, dependency_unreachable, client_credential_issue,
stale_reference, service_defect, capacity_saturation, expected_lifecycle,
needs_review. They are cause-level verdicts, not restatements of status.
Findings worth remembering
- Needle scored 10% on the 40-row golden set (about chance for 8 classes).
Reversing the tool order changed its answers wholesale (15%, 34/40
service_defect), so it picks by position, not content. Confidence is uncorrelated with correctness. Decision: Needle stays dormant. Intended later use: a gate that decides whether a larger model should act on an issue (affects anyone? fixable by a smart model?). Do not use it for categorization. - Rules scored 39/40 on the golden set, but that is a fit (written after seeing
the rows). A fresh 60-row holdout is in
cmd/needle-sidecar/eval/holdout.jsonl(from 2026-09-18, unseen); it still needs yourexpectedlabels. Known probe-like paths that still land instale_reference:/ssl/server.key,/_debugbar/open,/debug/pprof,/actuator/*,/graphql,/server-info,/.bash_profile,/settings.py,/__env.js. - The top error sources were noise: 404 probes and a 401 poller
(
/api/repos/available,/api/controller/instances, about 700 per hour, counted twice: nginx and the orchestrator). - IP data: 44 public IPs account for 63% of probe requests (12.5 h window).
34% show a peer of
127.0.0.1becausenet.peer.ipis the TCP peer behind the PROXY-protocol hop, so the tracker cannot see them. The ingress hasenable-real-ip=true,forwarded-for-header=proxy_protocol,proxy-real-ip-cidr=127.0.0.1/32, soblock-cidrsitself works on the real client address. - Internal addresses are a hard stop in both tracker and blocker
(
honeypot.Untrackable): loopback, RFC1918, ULA, link-local, multicast, unspecified, IPv4-mapped, CGNAT100.64/10(Tailscale),192.0.0.0/24,198.18/15,240/4. The allowlist only adds exclusions.
Before enabling enforcement
- Deploy the new build (dry-run on). Watch
GET /api/blocker/statusfor a day; compare againstgo run ./cmd/bad-actors -since ... -until .... - Put your own public IP(s) in
OTEL_BLOCKER_ALLOW. A blocked IP gets 403, so you would lock yourself out of the UI. Recovery:kubectl edit cm ingress-nginx-controller -n ingress-nginx(remove entries; managed ones are listed in theotel-debug.k6n.net/blockedannotation). - Then set
OTEL_BLOCKER_DRY_RUN=false. - Do not
kubectl applythe wholek8s/deployment.yamlblindly: the dry run showed it would create a newotel-sourcemapsPVC not present in the cluster.
Next steps (suggested order)
- Deploy the collector so normalized rows stay current; verify with
SELECT max(timestamp) FROM otel.normalized_events. - Label
holdout.jsonl(fillexpected), rungo run ./cmd/triage-eval -input cmd/needle-sidecar/eval/holdout.jsonl, fix the rule families that miss. Then re-export a fresh holdout so scoring stays honest. - Warm-up guard in
DetectSpikes: require baseline data before alerting (the first window after data begins alerts on everything as NEW). - Real client IP in nginx spans, to close the 127.0.0.1 gap (inspect the ingress OTel config first; not looked at yet).
- Alert delivery: run
DetectSpikeson a timer in the collector and post somewhere (UI panel / ntfy / Slack). Category chips andsame / different / noise+ "mute pattern" in the UI, storing labels. - Reduce 401-poller noise: mute list or per-pattern rate limit;
client_credential_issueis the caller's fault, likely a lower severity. - After 2+ weeks of data: time-of-day baseline, rollout proximity, then the
logistic regression / MLP from
docs/ML.md; the rule detector is what they must beat.
Commands
set -a; . ./.env; set +a
# replay the spike rule over a range (Needle off)
go run ./cmd/spike-alerts -since 2026-09-19T12:00:00Z -until 2026-09-19T20:30:00Z -baseline-windows 12
# add -show-muted to see probes / stale / lifecycle patterns too
# who would be blocked
go run ./cmd/bad-actors -since 2026-09-19T08:00:00Z -until 2026-09-19T20:30:00Z
# score the rules against labelled rows
go run ./cmd/triage-eval -input cmd/needle-sidecar/eval/holdout.jsonl
# Needle eval (needs the sidecar running; venv was in scratch and is gone)
pip install cactus-needle # python 3.12 recommended
NEEDLE_PORT=5005 NEEDLE_HOST=127.0.0.1 python cmd/needle-sidecar/server.py
python cmd/needle-sidecar/eval/run_eval.py
Backfill in small windows only (10 to 30 minutes over the LAN); one-hour windows
through a kubectl port-forward hung and dropped.
Git state
Everything is uncommitted. Files changed by this work: internal/{storage, normalization,features,triage,honeypot,blocker}, cmd/{spike-alerts, triage-eval,bad-actors,normalized-backfill,server,needle-sidecar},
k8s/{clickhouse,deployment}.yaml, .gitignore.
Not from this work; check before committing: AGENTS.md,
internal/collector/{collector_test,issue_extract}.go, otel-ui/src/**
(including a deleted GaugeMeter.tsx). Also tracked __pycache__/*.pyc files
under cmd/needle-sidecar show as modified; they should be untracked and
gitignored.