all the changes
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 1m23s

This commit is contained in:
mats committed 2026-09-19 23:25:59 +02:00
1 parent 94f30165f9
commit f30adf05af
35 files changed
+2979 -1607

No files matched your search

+132
View File
@@ -0,0 +1,132 @@
# 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/clickhouse`
at `10.10.3.45` (native 9000, HTTP 8123). Set in `k8s/clickhouse.yaml` and
patched live. `docs/ML.md` still says "keep ClusterIP"; that is superseded.
- Credentials are in `.env` (gitignored, mode 600). Load with
`set -a; . ./.env; set +a`. `clickhouse-client` is installed locally.
- `.env` deliberately does not enable Needle; pass `-needle URL` explicitly.
## 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 your `expected` labels. Known
probe-like paths that still land in `stale_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.1`** because `net.peer.ip` is the TCP peer behind
the PROXY-protocol hop, so the tracker cannot see them. The ingress has
`enable-real-ip=true`, `forwarded-for-header=proxy_protocol`,
`proxy-real-ip-cidr=127.0.0.1/32`, so `block-cidrs` itself 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, CGNAT `100.64/10` (Tailscale), `192.0.0.0/24`,
`198.18/15`, `240/4`. The allowlist only adds exclusions.
## Before enabling enforcement
1. Deploy the new build (dry-run on). Watch `GET /api/blocker/status` for a day;
compare against `go run ./cmd/bad-actors -since ... -until ...`.
2. 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 the `otel-debug.k6n.net/blocked` annotation).
3. Then set `OTEL_BLOCKER_DRY_RUN=false`.
4. Do not `kubectl apply` the whole `k8s/deployment.yaml` blindly: the dry run
showed it would create a new `otel-sourcemaps` PVC not present in the cluster.
## Next steps (suggested order)
1. Deploy the collector so normalized rows stay current; verify with
`SELECT max(timestamp) FROM otel.normalized_events`.
2. Label `holdout.jsonl` (fill `expected`), run
`go 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.
3. Warm-up guard in `DetectSpikes`: require baseline data before alerting (the
first window after data begins alerts on everything as NEW).
4. Real client IP in nginx spans, to close the 127.0.0.1 gap (inspect the
ingress OTel config first; not looked at yet).
5. Alert delivery: run `DetectSpikes` on a timer in the collector and post
somewhere (UI panel / ntfy / Slack). Category chips and `same / different /
noise` + "mute pattern" in the UI, storing labels.
6. Reduce 401-poller noise: mute list or per-pattern rate limit;
`client_credential_issue` is the caller's fault, likely a lower severity.
7. 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
```bash
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.