REDTEAM.md
Rama main · réplica de lectura del repositorio original.
# Red Team Security Testing
Automated security testing suite for artifact-keeper. Run on-demand during major changes to validate the security posture of the registry.
**Tracking:** [Discussion #50](https://github.com/orgs/artifact-keeper/discussions/50) | [Issue #51](https://github.com/artifact-keeper/artifact-keeper/issues/51)
## Quick Start
### Automated (full suite)
```bash
docker compose -f docker-compose.test.yml --profile redteam up --build --abort-on-container-exit
```
Results are written to the `redteam-results` Docker volume as `redteam-report.json`.
### Run a single test
```bash
docker compose -f docker-compose.test.yml --profile redteam up -d postgres backend setup pki
docker compose -f docker-compose.test.yml --profile redteam run --rm redteam bash /scripts/run-all.sh --test cors
```
### Interactive exploration
```bash
# Start infrastructure
docker compose -f docker-compose.test.yml --profile redteam up -d postgres backend setup pki
# Drop into the redteam container
docker compose -f docker-compose.test.yml --profile redteam run --rm --entrypoint bash redteam
```
Inside the container you have access to:
```bash
# Network scanning
nmap -sV backend
# Web scanning
nikto -host http://backend:8080
nuclei -target http://backend:8080 -severity critical,high
# gRPC probing
grpcurl -plaintext backend:9090 list
grpcurl -plaintext backend:9090 describe
# SQL injection testing
sqlmap -u "http://backend:8080/api/v1/search?q=test" --batch --level=1 \
--auth-type=Basic --auth-cred="admin:admin123"
# Manual HTTP testing
curl -s http://backend:8080/health | jq .
curl -sI -H "Origin: https://evil.com" http://backend:8080/api/v1/repositories
```
### Cleanup
```bash
docker compose -f docker-compose.test.yml --profile redteam down -v --remove-orphans
```
## Test Suite
| # | Test | Severity | What it checks |
|---|------|----------|----------------|
| 01 | recon | MEDIUM | Port scanning, unexpected services |
| 02 | security-headers | MEDIUM | X-Frame-Options, CSP, HSTS, X-Content-Type-Options |
| 03 | cors | CRITICAL | CORS `AllowOrigin::Any` in non-dev mode |
| 04 | auth-bypass | HIGH | Protected endpoints accessible without auth |
| 05 | default-credentials | CRITICAL | admin:admin123, peer API key, Meilisearch key |
| 06 | grpc-unauth | CRITICAL | gRPC services callable without authentication |
| 07 | oci-dos | CRITICAL | OCI V2 unlimited body size (no `DefaultBodyLimit`) |
| 08 | path-traversal | HIGH | `../` in 30+ format handler paths |
| 09 | sql-injection | MEDIUM | SQLi in search, repo creation, backup endpoints |
| 10 | rate-limit | HIGH | Rate limiting middleware not wired to routes |
| 11 | wasm-plugin | HIGH | Plugin install from arbitrary URLs, no signing |
| 12 | information-disclosure | MEDIUM | /metrics exposed, error verbosity, version leak |
## Results
The JSON report at `/results/redteam-report.json` has this structure:
```json
{
"timestamp": "2026-02-07T00:00:00Z",
"target": "http://backend:8080",
"findings": [
{
"severity": "CRITICAL",
"test": "cors",
"description": "CORS allows any origin",
"evidence": "Access-Control-Allow-Origin: *"
}
],
"summary": {
"pass": 15,
"fail": 8,
"warn": 3,
"info": 2,
"findings": 1
}
}
```
The counts are totals for the whole run and the document is guaranteed to
parse: `run-all.sh` runs `jq` over the report before it exits and fails the run
if it does not. Both were broken until #3491 — each test script runs in its own
shell, so the counters reset per script (a run printed "Passed: 1 checks" after
dozens of PASS lines, and the exit-code gate never fired) and findings written
by different scripts were concatenated with no separating comma. The counters
now live in `${RESULTS_DIR}/.state`, which is also where the run's single
shared admin token is cached — the suite authenticates once rather than once
per script, so the login rate limiter that tests 04 and 10 exercise on purpose
no longer starves the later tests of a token.
`scripts/ci/test-redteam-report.sh` exercises all of that against a stub
registry, offline, in about a second; it runs in the Shell Tests CI job.
### gRPC checks and reflection
Server reflection is disabled, which is correct and is what test 06's first two
checks assert. That also means `grpcurl` cannot build a request for the
unauthenticated method probes on its own, so the container mounts
`backend/proto` at `/protos` (`PROTO_DIR`) and the probes invoke methods from
the descriptors. If the protos are not available the probes report themselves
as SKIPPED: an unmeasurable control is reported as unmeasured, never as a pass
and never as a finding.
## Container Tools
| Tool | Version | Purpose |
|------|---------|---------|
| nmap | 7.94 | Port scanning, service discovery |
| nikto | 2.5.0 | Web server misconfiguration scanning |
| nuclei | 3.7.0 | Template-based vulnerability scanning |
| grpcurl | 1.9.3 | gRPC service probing and enumeration |
| sqlmap | 1.10.2 | SQL injection detection |
| curl + jq | - | HTTP testing, JSON parsing |
| openssl | - | TLS certificate inspection |
## HTML application UI (RT-15)
The suite above and `docker-compose.test.yml` start the backend only. A
red-team rig built from them reaches the JSON API and the opt-in Swagger UI,
and **no HTML application**. The repository, catalog, capability, curation
and audit views are a separate component with its own releases (#2708):
| | |
| --- | --- |
| Source | [`artifact-keeper/artifact-keeper-web`](https://github.com/artifact-keeper/artifact-keeper-web) |
| Image | `ghcr.io/artifact-keeper/artifact-keeper-web` (`artifactkeeper/web` on Docker Hub) |
| Deployment | the `web` service of [`docker-compose.yml`](docker-compose.yml), pinned by `ARTIFACT_KEEPER_WEB_VERSION` (#4481), behind Caddy |
| Stored-content rendering tests | `e2e/suites/interactions/security/rt15-mcp-stored-content.spec.ts` in that repository |
A raw-JSON or Swagger check therefore cannot show that stored content stays
inert in the UI (#5472). #5472 named `artifact-keeper-test` as the primary
regression destination, but the rendering case lives in the web repository's
Playwright suite instead. The test repository's release gate
(`release-gate.yml`, which the web image publish calls) runs no browser
suite, and it does not run this one.
The suite stores hostile MCP content through a live backend: a publish, a CI
capability report, a status change, a Remote synced from a hostile upstream
registry, an unprivileged reader's approval request and an administrator's
approval reason. It then opens the following views in Chromium:
- the MCP repository views
- the catalog
- server detail, each tab and each capability panel
- curation
- the audit log
Each view is opened as an administrator and as an unprivileged reader, with
and without the web server's Content-Security-Policy. The suite fails on
script execution, active nodes, active-scheme URLs or requests to an attacker
origin. artifact-keeper/artifact-keeper-web#1106 lists the views it covers
and the stored fields the UI does not render at all.
The suite runs in the web repository's `E2E Interactions` CI job against the
backend `dev` image. That job is **advisory**: E2E is not a required check
there, so a red run does not block a merge and someone has to read it. To run
the suite by hand, see that repository's `docker-compose.e2e-mcp-upstream.yml`.
Two #5472 criteria are covered elsewhere, not by this suite:
- **Terminal controls in logs:** the red-team rig's DEBUG-log analysis, plus
the backend test `telemetry::tests::test_json_request_line_has_siem_fields_top_level`
(JSON logs never carry ANSI).
- **Authorization unaffected by display text:**
`mcp_capabilities::db_tests::capability_reports_run_the_namespace_gate` and
`mcp_namespaces::tests::namespace_rule_grants_nothing_outside_the_mcp_gate`.
The suite adds its own checks that a reader is shown no lint findings,
audit rows or approval queue.
When a release assessment covers the UI, record the web revision and image
digest next to the backend's, and deploy that exact image rather than a tag:
```bash
skopeo inspect --format '{{.Digest}} {{index .Labels "org.opencontainers.image.revision"}}' \
docker://ghcr.io/artifact-keeper/artifact-keeper-web:<version>
# then ARTIFACT_KEEPER_WEB_VERSION=<version>@sha256:<digest> docker compose up -d web
```
Web `1.11.2` (`sha256:c17f1bff9a8bc1646efded313c107a003c383f7c03b213b389cdf789302267a5`,
revision `6a43afc`) predates the MCP UI: it serves no MCP views and does not
proxy `/mcp/`. The first web release with MCP views will be the next one,
1.12.0, which contains `b65f961` and artifact-keeper/artifact-keeper-web#1106.
The suite has passed against `b65f961` plus #1106, built from source, with
backend `dev` revision `ad20810`
(`sha256:a55b5d560c18e66efdac3107e13d4239cbae59c15fcbad4eccd88365ace27c5f`).
`b65f961` is the `artifact-keeper-web:dev` image
(`sha256:e05124d427420f81fbc2656bbad1cd50a5e3333a5cfae27859a61c6f1812e5ff`).
That build is not the release. RT-15 is marked PASS only after the suite is
rerun against the 1.12.0 web image, deployed by its digest, and that digest
is recorded here.
## When to Run
- Before any release
- After changes to authentication, middleware, or route configuration
- After adding new package format handlers
- After changes to gRPC services
- After changes to plugin system
- When onboarding new infrastructure services