Security and secrets runbook¶
DTED.org is a public-information terrain service. It is not authorized for Federal Contract Information, Controlled Unclassified Information, classified, export-controlled, proprietary, or operationally sensitive data. The CMMC-style notice on the public pages is a handling boundary, not a claim of CMMC certification.
Use a trusted operator shell. Never paste credential values into tickets, commits, command arguments, shell history, chat, screenshots, or logs.
Security boundaries¶
- Nginx is the only public listener. The portal binds to
127.0.0.1:8780. - The portal and worker run as an unprivileged host UID/GID with a read-only
root filesystem, all Linux capabilities dropped,
no-new-privileges, a PID limit, and an isolated temporary filesystem. - Dataset releases are immutable. Public and restricted activation links are separate.
- Preview hosts serve public-information release candidates by default and
carry a
noindexresponse policy. Restricted hosts deny clients outside the VPN allowlist. - The admin site uses TLS and central AndKit OIDC through the local OAuth2 proxy. Nginx forwards an HMAC-protected identity contract to the portal; DTED does not accept a local browser password.
Credential inventory and permissions¶
Expected root-managed files are:
| File | Owner and mode | Purpose |
|---|---|---|
/etc/andkit-auth/*.conf |
root:root 0644 (no credentials) |
Central forward-auth and OAuth2 locations |
/srv/dted/.env |
deployment owner 0600 |
Portal configuration including the proxy identity secret |
/etc/dted/usgs.env |
root:root 0600 |
USGS M2M account and application token |
/etc/dted/namecom.env |
root:root 0600 |
DNS API account and token |
/etc/dted/*.env for other providers |
root:root 0600 |
Source-specific credentials |
/etc/dted/tls/privkey.pem target |
root-readable | TLS private key |
/etc/dted/qa_allowlist.conf |
root:root 0644 |
Non-secret public-by-default QA network policy |
/etc/dted/vpn_allowlist.conf |
root:root 0644 |
Non-secret restricted-access CIDRs |
/srv/dted-data/dted.sqlite3 |
data owner/group 0640 |
Portal jobs, uploads, and dataset registry |
Audit names, owners, modes, and link targets without printing file contents:
sudo find /etc/dted -maxdepth 1 -type f \
-printf '%M %u:%g %p\n' | sort
sudo find /etc/dted/tls -maxdepth 1 -type l \
-printf '%p -> %l\n' | sort
sudo stat -Lc '%A %U:%G %n' /etc/dted/tls/privkey.pem
Create, disable, and assign operator roles through the central AndKit identity
service. Rotate the edge-to-portal identity secret using the AndKit auth runbook,
update DTED_PROXY_IDENTITY_SECRET, recreate the portal, and reload Nginx as one
coordinated change. A direct request to the loopback portal without valid signed
identity headers must continue to fail closed.
Rotate provider or DNS credentials in the provider console, atomically replace the root-only file, validate the minimum required operation, then revoke the old credential. Never print a credential to prove it works.
Repository and history secret scan¶
Run the pinned scanner with complete redaction. The small-target pass avoids spending time on ignored terrain binaries; scan large JSON receipts separately with the metadata-safe tool below.
cd /home/gba/Agents/DTED.org
GITLEAKS='zricethezav/gitleaks@sha256:c00b6bd0aeb3071cbcb79009cb16a60dd9e0a7c60e2be9ab65d25e6bc8abbb7f'
docker pull "$GITLEAKS"
docker run --rm -v "$PWD:/repo:ro" "$GITLEAKS" dir /repo \
--redact=100 --no-banner --max-target-megabytes 5 --max-archive-depth 0
docker run --rm -v "$PWD:/repo:ro" "$GITLEAKS" git /repo \
--redact=100 --no-banner
The receipt scanner reports only JSON locations and finding categories, never values:
python3 tools/scan_secret_metadata.py \
data/src \
/srv/dted-sources \
/srv/dted-data/source-manifests
A clean scan does not prove a credential was never disclosed through another channel. Rotate anything suspected of exposure.
Static, dependency, and image audit¶
Run application tests first:
make test
.venv/bin/mkdocs build --strict
git diff --check
Audit the exact built image's Python environment:
docker run --rm dted-portal python -m pip check
docker run --rm --entrypoint sh dted-portal -c \
'python -m pip install --no-cache-dir pip-audit==2.10.1 >/dev/null &&
pip-audit --skip-editable'
Scan OS and Python packages for fixed high or critical vulnerabilities:
TRIVY='aquasec/trivy@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f'
docker pull "$TRIVY"
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
"$TRIVY" image --severity HIGH,CRITICAL --ignore-unfixed \
--exit-code 1 dted-portal
Run Bandit at medium confidence and severity or higher. Any suppression must be narrow, next to the line, and explain why untrusted input cannot reach the flagged operation:
docker run --rm -v "$PWD:/src:ro" -w /src python:3.11-slim \
sh -c 'pip install --quiet bandit==1.9.4 &&
bandit -q -r dted_enclave tools -ll -ii'
Record scanner versions, image digests, scope, findings, remediation, and residual risks in a dated audit report. Do not record secrets or client IPs.
Deployment and external-surface checks¶
Deploy only a committed revision:
git status --short
git rev-parse HEAD
sudo tools/install_production.sh
Then verify:
cd /srv/dted
docker compose ps
docker inspect dted-portal-1 \
--format 'readonly={{.HostConfig.ReadonlyRootfs}} caps={{json .HostConfig.CapDrop}} security={{json .HostConfig.SecurityOpt}} pids={{.HostConfig.PidsLimit}}'
curl -fsS http://127.0.0.1:8780/healthz
sudo nginx -t
ss -ltnp
curl -sSI https://docs.dted.org/
curl -sSI https://admin.dted.org/
curl -sSI https://preview-ca-3dep.dted.org/
curl -sSI https://not-a-dataset.dted.org/
openssl s_client -connect dted.org:443 -servername dted.org \
-tls1_2 -verify_return_error </dev/null
Expected results are:
- portal health is successful only through loopback;
- admin redirects an unauthenticated browser to
auth.andkit.io; - a staged preview is
200, carriesX-Robots-Tag: noindex, nofollow, noarchive, and contains no sensitive data; - an unknown dataset is
404; - TLS verification succeeds;
- HTTPS responses omit the Nginx version and include HSTS,
nosniff, frame denial, a no-referrer policy, and restrictive browser permissions.
Use tools/smoke_test.py for every active state and candidate. The smoke test
also verifies ZIP signatures, byte ranges, TAK client hostnames, the SDS index,
and source isolation.
Backup and recovery¶
Protect and test backups of:
- SQLite through its online backup API or with services stopped;
/srv/dted-data/releases, activation links, source quality, promotion records, and evidence;/srv/dted-sourcesreceipts, archives, and content-addressed cache;/srv/dted-gridsand its hash manifest;- Nginx configuration, QA/VPN network-policy files, and certificates;
- root-managed secrets in a separate encrypted, access-controlled store.
Do not put secrets in the same broadly accessible archive as public terrain. Test restoration to an isolated host. Confirm database integrity, hashes, permissions, Nginx configuration, container hardening, public smoke tests, and restricted-dataset denial before returning service.
Incident response¶
- Preserve relevant timestamps, release IDs, image IDs, audit logs, and redacted request metadata. Do not copy secret values into the incident record.
- Contain the smallest affected boundary: close a preview window, remove a compromised CIDR, disable the affected provider credential, or stop Nginx if the public edge cannot be trusted.
- If a repository secret is suspected, rotate and revoke it before history rewriting. A force-push does not remove clones, caches, artifacts, or logs.
- Rebuild from a reviewed commit and pinned dependencies. Do not repair an immutable release in place.
- Validate scans, tests, TLS, permissions, health, and representative TAK clients before recovery.
- Record the cause, affected interval and assets, containment, rotations, verification evidence, and follow-up owner.
Report suspected security incidents to SNSTAC Mission Operations Support at
info@snstac.com.