security: classify channel keys and baseline reviewed findings

This commit is contained in:
gadgethd
2026-09-23 11:46:06 +00:00
parent 153ddbaabf
commit 9ce8633e6d
7 changed files with 110 additions and 55 deletions
+9
View File
@@ -4,6 +4,8 @@ on:
push:
branches: ['**']
pull_request:
schedule:
- cron: '17 4 * * *'
permissions:
contents: read
@@ -14,6 +16,7 @@ concurrency:
jobs:
backend:
if: github.event_name != 'schedule'
name: Backend
runs-on: ubuntu-latest
services:
@@ -62,6 +65,7 @@ jobs:
- run: npm run build
frontend:
if: github.event_name != 'schedule'
name: Frontend
runs-on: ubuntu-latest
defaults:
@@ -81,6 +85,7 @@ jobs:
- run: npm run build
workers-and-compose:
if: github.event_name != 'schedule'
name: Workers and Compose
runs-on: ubuntu-latest
env:
@@ -407,3 +412,7 @@ jobs:
uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_VERSION: 8.30.0
- name: Scan full repository history on push and pull request
if: github.event_name != 'schedule'
run: gitleaks git --redact --no-banner --log-opts=--all
-35
View File
@@ -2,38 +2,3 @@ title = "UK Mesh gitleaks configuration"
[extend]
useDefault = true
[[rules]]
id = "generic-api-key"
[[rules.allowlists]]
description = "Documented MeshCore public-channel interoperability key"
paths = ['''^backend/backfill-transport-codes\.mjs$''']
regexTarget = "line"
regexes = ['''(?i)const DEFAULT_PUBLIC_CHANNEL_KEY\s*=\s*'[0-9a-f]{32}'\s*;''']
condition = "AND"
[[rules.allowlists]]
description = "Reviewed public/community MeshCore channel secrets (channelRegistry.ts VALIDATED_CHANNELS) — each entry verified to decrypt real UK Mesh group text; classified intentionally public in the channel registry"
paths = ['''^backend/src/mqtt/channelRegistry\.ts$''']
regexTarget = "line"
regexes = ['''\{ name: '[A-Za-z0-9_-]+',\s+secret: '[0-9a-f]{32}'\s*\},?''']
condition = "AND"
[[rules.allowlists]]
description = "Documented Public channel key in any file/historical commit (exact fingerprint)"
regexTarget = "secret"
regexes = ['''^8b3387e9c5cdea6ac9e5edbaa115cd72$''']
[[rules.allowlists]]
description = "Fake test fixtures (exact fingerprints)"
regexTarget = "secret"
regexes = [
'''^0123456789abcdef0123456789abcdef$''',
'''^abcdef0123456789$''',
]
[[rules.allowlists]]
description = "Deployed-commit SHA in live website manifests"
regexTarget = "secret"
regexes = ['''^bef003f072283fd5e3f14b29890f7610adf3fb9808e8c6e700bd48d3e7a8bc87$''']
+51
View File
@@ -0,0 +1,51 @@
# Exact reviewed Gitleaks fingerprints (commit:path:rule:line).
# Public MeshCore channel material; keep private channel keys in MESHCORE_CHANNEL_SECRETS.
8b902dfb3ff08128556f22de2d239c458f782949:backend/src/mqtt/channelRegistry.ts:generic-api-key:38
8b902dfb3ff08128556f22de2d239c458f782949:backend/src/mqtt/channelRegistry.ts:generic-api-key:42
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:30
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:31
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:32
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:33
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:34
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:35
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:36
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:37
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:38
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:39
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:42
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:43
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:46
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:47
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:48
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:49
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:50
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:51
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:52
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:53
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:54
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:55
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:56
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:57
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:58
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:59
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:60
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:61
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:62
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:63
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:65
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:66
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:67
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:68
5db382e181591b9bf736647eb903c699223cfdc8:backend/backfill-transport-codes.mjs:generic-api-key:35
be45a2e967ca51d909940e29ecfd531a4566d404:backend/backfill-transport-codes.mjs:generic-api-key:35
288c66a0b6e1037c74dea4392ad0a910525333df:backend/backfill-transport-codes.mjs:generic-api-key:35
cb713d6c1a469f634a5a11678a1b9a81fa6413a0:backend/src/mqtt/client.ts:generic-api-key:96
# Test-only credential and token fixtures.
1ebc496965ff61f398851dde81c9da6910ceb7e0:backend/src/security/operatorAuth.test.ts:generic-api-key:5
d278c5072891fcfc643e050c55fb6ace908fdaa3:backend/src/security/operatorAuth.test.ts:generic-api-key:5
93cc3e1b385fe2034876caf4af843d040da47cf1:backend/src/spam/sanitize.test.ts:generic-api-key:50
# Generated deployment manifests containing source revision identifiers.
cd9ee586364ed70c5298d36eba0bb92958725b2d:website-live-manifest.staging.json:generic-api-key:40
cd9ee586364ed70c5298d36eba0bb92958725b2d:website-live-manifest.json:generic-api-key:40
5210aba5e37b928db3744057beb78f2c17f92263:website-live-manifest.json:generic-api-key:38
ac4ee2afecc16ed67fdc09cf70953dc85d3a4c5a:website-live-manifest.staging.json:generic-api-key:38
+1 -1
View File
@@ -170,7 +170,7 @@ Copy `.env.example` to `.env` and fill in your values. All variables used by the
| `ALLOWED_ORIGINS` | `http://localhost:3001,http://localhost:3002` | Comma-separated browser origins allowed for CORS and WebSocket |
| `API_RATE_LIMIT_MAX` | `120` | Per-client public API requests/minute; raise only in an isolated load-test project |
| `VITE_APP_HOSTNAME` | *(blank — always shows dashboard)* | If set, only this hostname serves the analytics dashboard; all others serve the public website layout |
| `MESHCORE_CHANNEL_SECRETS` | *(blank)* | Comma-separated channel secrets for decrypting GroupText packets. Format: `name:hex` or bare hex. The default MeshCore public channel key is always included. |
| `MESHCORE_CHANNEL_SECRETS` | *(blank)* | Confidential channel keys for decrypting GroupText packets, managed through environment configuration. Format: `name:hex` or bare hex. The explicitly public `PUBLIC_CHANNELS` defaults are always included. |
| `OWNER_DATABASE_URL` | *(optional)* | Separate Postgres database URL for owner portal username → repeater mappings |
| `OWNER_COOKIE_SECRET` | *(optional but recommended)* | Secret used to encrypt/sign the owner session cookie |
| `OWNER_MQTT_USERNAME_MAP` | *(empty)* | Operator-managed owner grants in the format `user=nodeId1|nodeId2,...` |
+16
View File
@@ -0,0 +1,16 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { buildChannelEntries, PUBLIC_CHANNELS, VALIDATED_CHANNELS } from './channelRegistry.js';
test('committed channel material is explicitly classified as public', () => {
assert.strictEqual(VALIDATED_CHANNELS, PUBLIC_CHANNELS);
assert.ok(PUBLIC_CHANNELS.length > 0);
assert.ok(buildChannelEntries().every((entry) => entry.classification === 'public'));
});
test('environment-supplied channel keys are classified as confidential', () => {
const entry = buildChannelEntries(`private:${'a'.repeat(32)}`)
.find((channel) => channel.name === 'private');
assert.equal(entry?.classification, 'confidential');
});
+22 -12
View File
@@ -1,11 +1,9 @@
/**
* Channel registry — the single source of truth for MeshCore channel secrets.
* Channel registry — explicitly public defaults plus confidential environment keys.
*
* VALIDATED_CHANNELS are the default secrets baked into the service (all
* recovered/community-known channels, verified to decrypt real group text).
* MESHCORE_CHANNEL_SECRETS (env, 'name:hex' or bare 'hex', comma-separated)
* APPENDS extra channels on top — useful for community-shared secrets that
* shouldn't be committed.
* PUBLIC_CHANNELS contains only recovered/community-known material intentionally
* classified as public. MESHCORE_CHANNEL_SECRETS ('name:hex' or bare 'hex',
* comma-separated) adds confidential keys without committing their values.
*
* Also hosts the summary + channel-identification helpers so the ingest
* pipeline and offline tools (e.g. backfillDecrypt) share one implementation.
@@ -15,18 +13,21 @@ import type { GroupTextPayload } from '@michaelhart/meshcore-decoder';
import { decodePacketCompat } from './decodePacket.js';
import { BoundedTtlMap } from '../cache/boundedTtlMap.js';
export type ChannelClassification = 'public' | 'confidential';
export interface ChannelEntry {
classification: ChannelClassification;
name: string;
secret: string;
keyStore: ReturnType<typeof MeshCoreDecoder.createKeyStore>;
}
/**
* Default channels — recovered via the meshcore wordlist/derivation audit
* (2026-08-06) and validated: each key decrypts real human-readable group
* text (sane epoch timestamp + printable text) on the UK Mesh network.
* Every committed value is intentionally public: recovered/community-known
* channels from the 2026-08-06 derivation audit, validated against real UK Mesh
* group text. Confidential channel keys belong in MESHCORE_CHANNEL_SECRETS.
*/
export const VALIDATED_CHANNELS: ReadonlyArray<{ name: string; secret: string }> = [
export const PUBLIC_CHANNELS: ReadonlyArray<{ name: string; secret: string }> = [
{ name: 'Public', secret: '8b3387e9c5cdea6ac9e5edbaa115cd72' },
{ name: 'test', secret: '9cd8fcf22a47333b591d96a2b848b73f' },
{ name: 'bot', secret: 'eb50a1bcb3e4e5d7bf69a57c9dada211' },
@@ -70,9 +71,13 @@ export const VALIDATED_CHANNELS: ReadonlyArray<{ name: string; secret: string }>
{ name: 'uk', secret: '22b2eed34b5cc429ce1dc5e88635ff84' },
];
/** @deprecated Use PUBLIC_CHANNELS to make the source classification explicit. */
export const VALIDATED_CHANNELS = PUBLIC_CHANNELS;
/** Build the channel entry list: committed defaults + env extras (dedup by secret). */
export function buildChannelEntries(envValue?: string): ChannelEntry[] {
const entries: ChannelEntry[] = VALIDATED_CHANNELS.map(({ name, secret }) => ({
const entries: ChannelEntry[] = PUBLIC_CHANNELS.map(({ name, secret }) => ({
classification: 'public',
name,
secret,
keyStore: MeshCoreDecoder.createKeyStore({ channelSecrets: [secret] }),
@@ -84,7 +89,12 @@ export function buildChannelEntries(envValue?: string): ChannelEntry[] {
const secret = colon > 0 ? raw.slice(colon + 1) : raw;
if (seen.has(secret.toLowerCase())) continue;
seen.add(secret.toLowerCase());
entries.push({ name, secret, keyStore: MeshCoreDecoder.createKeyStore({ channelSecrets: [secret] }) });
entries.push({
classification: 'confidential',
name,
secret,
keyStore: MeshCoreDecoder.createKeyStore({ channelSecrets: [secret] }),
});
}
return entries;
}
+11 -7
View File
@@ -6,16 +6,20 @@ how historical traffic was retroactively decrypted.
## Key store
- `backend/src/mqtt/channelRegistry.ts` is the single source of truth:
- `VALIDATED_CHANNELS` — default secrets baked into the image (39 entries
incl. Public; recovered 2026-08-06 and validated to decrypt real
human-readable group text).
- `PUBLIC_CHANNELS` — 41 recovered/community-known values intentionally
classified as public and baked into the image. Each was validated against
real human-readable UK Mesh group text.
- `buildCombinedKeyStore()` — merges baked defaults with
`MESHCORE_CHANNEL_SECRETS` (env, comma-separated `name:hex` or bare hex;
dedupes by secret) for secrets that shouldn't be committed.
`MESHCORE_CHANNEL_SECRETS` (managed environment configuration, with
comma-separated `name:hex` or bare hex entries; dedupes by secret).
Environment-supplied entries are classified as confidential; never commit
their values or copy them into `PUBLIC_CHANNELS`.
- `buildSummary()` / `identifyChannel()` — shared by ingest and offline
tools.
- ⚠️ The repo is **public**: keys in `channelRegistry.ts` are public. Keep
non-derivable/community keys in the env var only.
- The repo is **public**. `PUBLIC_CHANNELS` is the explicit source classification;
the scanner baseline in `.gitleaksignore` records exact historical
commit/path/rule/line fingerprints for reviewed public values, test fixtures,
and generated revision identifiers. New findings remain blocking by default.
- Reading env at startup only — after a change:
`docker compose -f docker-compose.yml -f docker-compose.live.yml up -d --no-deps --force-recreate backend`.