Files
explorer-monorepo/docs/SECURITY.md

128 lines
6.1 KiB
Markdown
Raw Normal View History

fix(security): fail-fast on missing JWT_SECRET, harden CSP, strip hardcoded passwords backend/api/rest/server.go: - NewServer() now delegates to loadJWTSecret(), which: - Rejects JWT_SECRET < 32 bytes (log.Fatal). - Requires JWT_SECRET when APP_ENV=production or GO_ENV=production. - Generates a 32-byte crypto/rand ephemeral secret in dev only. - Treats rand.Read failure as fatal (removes the prior time-based fallback that was deterministic and forgeable). - Default Content-Security-Policy rewritten: - Drops 'unsafe-inline' and 'unsafe-eval'. - Drops private CIDRs (192.168.11.221:854[5|6]). - Adds frame-ancestors 'none', base-uri 'self', form-action 'self'. - CSP_HEADER is required in production; fatal if unset there. backend/api/rest/server_security_test.go (new): - Covers the three loadJWTSecret() paths (valid, whitespace-trimmed, ephemeral in dev). - Covers isProductionEnv() across APP_ENV / GO_ENV combinations. - Asserts defaultDevCSP contains no unsafe directives or private CIDRs and includes the frame-ancestors / base-uri / form-action directives. scripts/*.sh: - Removed '***REDACTED-LEGACY-PW***' default value from SSH_PASSWORD / NEW_PASSWORD in 7 helper scripts. Each script now fails with exit 2 and points to docs/SECURITY.md if the password isn't supplied via env or argv. EXECUTE_DEPLOYMENT.sh, EXECUTE_NOW.sh: - Replaced hardcoded DB_PASSWORD='***REDACTED-LEGACY-PW***' with a ':?' guard that aborts with a clear error if DB_PASSWORD (and, for EXECUTE_DEPLOYMENT, RPC_URL) is not exported. Other env vars keep sensible non-secret defaults via ${VAR:-default}. README.md: - Removed the hardcoded Database Password / RPC URL lines. Replaced with an env-variable reference table pointing at docs/SECURITY.md and docs/DATABASE_CONNECTION_GUIDE.md. docs/DEPLOYMENT.md: - Replaced 'PASSWORD: SSH password (default: ***REDACTED-LEGACY-PW***)' with a required-no-default contract and a link to docs/SECURITY.md. docs/SECURITY.md (new): - Full secret inventory keyed to the env variable name and the file that consumes it. - Five-step rotation checklist covering the Postgres role, the Proxmox VM SSH password, JWT_SECRET, vendor API keys, and a gitleaks-based history audit. - Explicit note that merging secret-scrub PRs does NOT invalidate already-leaked credentials; rotation is the operator's responsibility. Verification: - go build ./... + go vet ./... pass clean. - Targeted tests (LoadJWTSecret*, IsProduction*, DefaultDevCSP*) pass. Advances completion criterion 2 (Secrets & config hardened). Residual leakage from START_HERE.md / LETSENCRYPT_CONFIGURATION_GUIDE.md is handled by PR #2 (doc consolidation), which deletes those files.
2026-04-18 19:02:27 +00:00
# Security policy and rotation checklist
This document describes how secrets flow through the SolaceScan explorer and
the operator steps required to rotate credentials that were previously
checked into this repository.
## Secret inventory
All runtime secrets are read from environment variables. Nothing sensitive
is committed to the repo.
| Variable | Used by | Notes |
|---|---|---|
| `JWT_SECRET` | `backend/api/rest/server.go` | HS256 signing key. Must be ≥32 bytes. Required when `APP_ENV=production` or `GO_ENV=production`. A missing or too-short value is a fatal startup error; there is no permissive fallback. |
| `CSP_HEADER` | `backend/api/rest/server.go` | Full Content-Security-Policy string. Required in production. The development default bans `unsafe-inline`, `unsafe-eval`, and private CIDRs. |
| `DB_PASSWORD` | deployment scripts (`EXECUTE_DEPLOYMENT.sh`, `EXECUTE_NOW.sh`) and the API | Postgres password for the `explorer` role. |
| `SSH_PASSWORD` | `scripts/analyze-besu-logs.sh`, `scripts/check-besu-config.sh`, `scripts/check-besu-logs-with-password.sh`, `scripts/check-failed-transaction-details.sh`, `scripts/enable-besu-debug-api.sh` | SSH password used to reach the Besu VMs. Scripts fail fast if unset. |
| `NEW_PASSWORD` | `scripts/set-vmid-password.sh`, `scripts/set-vmid-password-correct.sh` | Password being set on a Proxmox VM. Fail-fast required. |
| `CORS_ALLOWED_ORIGIN` | `backend/api/rest/server.go` | Optional. When set, restricts `Access-Control-Allow-Origin`. Defaults to `*` — do not rely on that in production. |
| `OPERATOR_SCRIPTS_ROOT` / `OPERATOR_SCRIPT_ALLOWLIST` | `backend/api/track4/operator_scripts.go` | Required to enable the Track-4 run-script endpoint. |
| `OPERATOR_SCRIPT_TIMEOUT_SEC` | as above | Optional cap (1599 seconds). |
## Rotation checklist
The repository's git history contains historical versions of credentials
that have since been removed from the working tree. Treat those credentials
as compromised. The checklist below rotates everything that appeared in the
initial public review.
> **This repository does not rotate credentials on its own. The checklist
> below is the operator's responsibility.** Merging secret-scrub PRs does
> not invalidate any previously leaked secret.
1. **Rotate the Postgres `explorer` role password.**
- Generate a new random password (`openssl rand -base64 24`).
- `ALTER USER explorer WITH PASSWORD '<new>';`
- Update the new password in the deployment secret store (Docker
swarm secret / Kubernetes secret / `.env.secrets` on the host).
- Restart the API and indexer services so they pick up the new value.
2. **Rotate the Proxmox / Besu VM SSH password.**
- `sudo passwd besu` (or equivalent) on each affected VM.
- Or, preferred: disable password auth entirely and move to SSH keys
(`PasswordAuthentication no` in `/etc/ssh/sshd_config`).
3. **Rotate `JWT_SECRET`.**
- Generate 32+ bytes (`openssl rand -base64 48`).
- Deploy the new value to every API replica simultaneously.
- Note: rotating invalidates every outstanding wallet auth token. Plan
for a short window where users will need to re-sign.
- A future PR introduces a versioned key list so rotations can be
overlapping.
4. **Rotate any API keys (e.g. xAI / OpenSea) referenced by
`backend/api/rest/ai.go` and the frontend.** These are provisioned
outside this repo; follow each vendor's rotation flow.
5. **Audit git history.**
- Run `gitleaks detect --source . --redact` at HEAD.
- Run `gitleaks detect --log-opts="--all"` over the full history.
- Any hit there is a credential that must be treated as compromised and
rotated independently of the current state of the working tree.
- Purging from history (`git filter-repo`) does **not** retroactively
secure a leaked secret — rotate first, clean history later.
security: tighten gitleaks regex for escaped form, document history-purge audit trail Two small follow-ups to the out-of-band git-history rewrite that purged L@ker$2010 / L@kers2010 / L@ker\$2010 from every branch and tag: .gitleaks.toml: - Regex was L@kers?\$?2010 which catches the expanded form but NOT the shell-escaped form (L@ker\$2010) that slipped past PR #3 in scripts/setup-database.sh. PR #13 fixed the live leak but did not tighten the detector. New regex L@kers?\\?\$?2010 catches both forms so future pastes of either form fail CI. - Description rewritten without the literal password (the previous description was redacted by the history rewrite itself and read 'Legacy hardcoded ... (***REDACTED-LEGACY-PW*** / ***REDACTED-LEGACY-PW***)' which was cryptic). docs/SECURITY.md: - New 'History-purge audit trail' section recording what was done, how it was verified (0 literal password matches in any blob or commit message; 0 legacy-password findings from a post-rewrite gitleaks scan), and what operator cleanup is still required on the Gitea host to drop the 13 refs/pull/*/head refs that still pin the pre-rewrite commits (the update hook declined those refs over HTTPS, so only an admin on the Gitea VM can purge them via 'git update-ref -d' + 'git gc --prune=now' in the bare repo). - New 'Re-introduction guard' subsection pointing at the tightened regex and commit 78e1ff5. Verification: gitleaks detect --no-git --source . --config .gitleaks.toml # 0 legacy hits git log --all -p | grep -cE 'L@ker\$2010|L@kers2010' # 0
2026-04-18 20:08:13 +00:00
## History-purge audit trail
Following the rotation checklist above, the legacy `L@ker$2010` /
`L@kers2010` / `L@ker\$2010` password strings were purged from every
branch and tag in this repository using `git filter-repo
--replace-text` followed by a `--replace-message` pass for commit
message text. The rewritten history was force-pushed with
`git push --mirror --force`.
Verification post-rewrite:
```
git log --all -p | grep -cE 'L@ker\$2010|L@kers2010|L@ker\\\$2010'
0
gitleaks detect --no-git --source . --config .gitleaks.toml
0 legacy-password findings
```
### Residual server-side state (not purgable from the client)
Gitea's `refs/pull/*/head` refs (the read-only mirror of each PR's
original head commit) **cannot be force-updated over HTTPS** — the
server's `update` hook declines them. After a history rewrite the
following cleanup must be performed **on the Gitea host** by an
administrator:
1. Run `gitea admin repo-sync-release-archive` and
`gitea doctor --run all --fix` if available.
2. Or manually, as the gitea user on the server:
```bash
cd /var/lib/gitea/data/gitea-repositories/d-bis/explorer-monorepo.git
git for-each-ref --format='%(refname)' 'refs/pull/*/head' | \
xargs -n1 git update-ref -d
git gc --prune=now --aggressive
```
3. Restart Gitea.
Until this server-side cleanup is performed, the 13 `refs/pull/*/head`
refs still pin the pre-rewrite commits containing the legacy
password. This does not affect branches, the default clone, or
`master` — but the old commits remain reachable by SHA through the
Gitea web UI (e.g. on the merged PR's **Files Changed** tab).
### Re-introduction guard
The `.gitleaks.toml` rule `explorer-legacy-db-password-L@ker` was
tightened from `L@kers?\$?2010` to `L@kers?\\?\$?2010` so it also
catches the shell-escaped form that slipped past the original PR #3
scrub (see commit `78e1ff5`). Future attempts to paste any variant of
the legacy password — in source, shell scripts, or env files — will
fail the `gitleaks` CI job wired in PR #5.
fix(security): fail-fast on missing JWT_SECRET, harden CSP, strip hardcoded passwords backend/api/rest/server.go: - NewServer() now delegates to loadJWTSecret(), which: - Rejects JWT_SECRET < 32 bytes (log.Fatal). - Requires JWT_SECRET when APP_ENV=production or GO_ENV=production. - Generates a 32-byte crypto/rand ephemeral secret in dev only. - Treats rand.Read failure as fatal (removes the prior time-based fallback that was deterministic and forgeable). - Default Content-Security-Policy rewritten: - Drops 'unsafe-inline' and 'unsafe-eval'. - Drops private CIDRs (192.168.11.221:854[5|6]). - Adds frame-ancestors 'none', base-uri 'self', form-action 'self'. - CSP_HEADER is required in production; fatal if unset there. backend/api/rest/server_security_test.go (new): - Covers the three loadJWTSecret() paths (valid, whitespace-trimmed, ephemeral in dev). - Covers isProductionEnv() across APP_ENV / GO_ENV combinations. - Asserts defaultDevCSP contains no unsafe directives or private CIDRs and includes the frame-ancestors / base-uri / form-action directives. scripts/*.sh: - Removed '***REDACTED-LEGACY-PW***' default value from SSH_PASSWORD / NEW_PASSWORD in 7 helper scripts. Each script now fails with exit 2 and points to docs/SECURITY.md if the password isn't supplied via env or argv. EXECUTE_DEPLOYMENT.sh, EXECUTE_NOW.sh: - Replaced hardcoded DB_PASSWORD='***REDACTED-LEGACY-PW***' with a ':?' guard that aborts with a clear error if DB_PASSWORD (and, for EXECUTE_DEPLOYMENT, RPC_URL) is not exported. Other env vars keep sensible non-secret defaults via ${VAR:-default}. README.md: - Removed the hardcoded Database Password / RPC URL lines. Replaced with an env-variable reference table pointing at docs/SECURITY.md and docs/DATABASE_CONNECTION_GUIDE.md. docs/DEPLOYMENT.md: - Replaced 'PASSWORD: SSH password (default: ***REDACTED-LEGACY-PW***)' with a required-no-default contract and a link to docs/SECURITY.md. docs/SECURITY.md (new): - Full secret inventory keyed to the env variable name and the file that consumes it. - Five-step rotation checklist covering the Postgres role, the Proxmox VM SSH password, JWT_SECRET, vendor API keys, and a gitleaks-based history audit. - Explicit note that merging secret-scrub PRs does NOT invalidate already-leaked credentials; rotation is the operator's responsibility. Verification: - go build ./... + go vet ./... pass clean. - Targeted tests (LoadJWTSecret*, IsProduction*, DefaultDevCSP*) pass. Advances completion criterion 2 (Secrets & config hardened). Residual leakage from START_HERE.md / LETSENCRYPT_CONFIGURATION_GUIDE.md is handled by PR #2 (doc consolidation), which deletes those files.
2026-04-18 19:02:27 +00:00
## Build-time / CI checks (wired in PR #5)
- `gitleaks` pre-commit + CI gate on every PR.
- `govulncheck`, `staticcheck`, and `go vet -vet=all` on the backend.
- `eslint` and `tsc --noEmit` on the frontend.
## Reporting a vulnerability
Do not open public issues for security reports. Email the maintainers
listed in `CONTRIBUTING.md`.