Paste a secret, get a link, send it. The first person to open it and press
Reveal sees the secret; the link dies at that moment. The recipient needs a
browser and nothing else — no account, no client, no installed tooling.
The server cannot read what it stores. AES-256-GCM happens in the browser and
the key lives in the URL fragment, which browsers never transmit, so hushd
holds ciphertext and no key material. That is a property of where the key sits
rather than a promise about our conduct, which is why there is deliberately no
endpoint accepting a plaintext secret and no server-side-encryption fallback:
two guarantees behind one URL would be worse than one honest guarantee.
Three decisions carry the design:
* GET /s/{id} touches NO storage, not even to check existence. Slack, Teams,
WhatsApp, iMessage and Outlook Safe Links all fetch a URL before a human
sees it, so destroying on GET would destroy most secrets in transit and the
recipient's "already used" would be indistinguishable from interception.
Only POST /reveal consumes. Bot user-agent detection is an arms race;
removing the side effect from GET is not. Pinned by
TestGettingTheRevealPageNeverConsumesTheSecret.
* Destruction is one Redis GETDEL, which is atomic. GET-then-DEL has a window
where two simultaneous readers both win, and for a one-time secret that
window is the product. The store contract demands atomicity and the same
concurrency test runs against both implementations.
* Missing, already-revealed, expired and evicted are ONE indistinguishable
410. Separating them would confirm to a prober that a given link was real.
The secret id IS the capability, so secret.ID is a struct whose every
accidental path — %v, %s, String(), slog, json.Marshal — emits a redacted
handle or refuses, and the raw value needs an explicit Value(). The first
version tried to prevent leaks by implementing no String() at all; its own test
caught that Go's fmt prints unexported fields anyway, so forbidding the method
had removed the control rather than the leak.
Operationally: structured JSON on stdout in the fleet's wire format, which
Vector already collects with no annotation; six hush_* metrics on the chassis
registry with no id, IP or path in any label; five alert rules wired into
vmalert. The public Ingress enumerates /, /s/ and /api/ so /metrics, /healthz
and /readyz share the port but are unreachable from the internet — no
basic-auth middleware to maintain and get wrong.
Dependencies are vendored because go-chassis is private: the Woodpecker test
step and the in-cluster Kaniko build both run -mod=vendor with GOPROXY=off and
hold no git credential.
cmd/hush-mcp is a stdio MCP server doing the same client-side crypto locally,
so using hush from an agent preserves the same guarantee as using it from a
browser.
177 lines
4.6 KiB
YAML
177 lines
4.6 KiB
YAML
---
|
|
|
|
x-default-image: &default-image ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.10.0}
|
|
|
|
services:
|
|
redis:
|
|
image: *default-image
|
|
platform: linux/amd64
|
|
container_name: redis-standalone
|
|
environment:
|
|
- TLS_ENABLED=yes
|
|
- TLS_CLIENT_CNS=testcertuser
|
|
- TLS_AUTH_CLIENTS_USER=CN
|
|
- REDIS_CLUSTER=no
|
|
- PORT=6379
|
|
- TLS_PORT=6666
|
|
command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""}
|
|
ports:
|
|
- 6379:6379
|
|
- 6666:6666 # TLS port
|
|
volumes:
|
|
- "./dockers/standalone:/redis/work"
|
|
profiles:
|
|
- standalone
|
|
- sentinel
|
|
- all-stack
|
|
- all
|
|
- e2e
|
|
|
|
osscluster:
|
|
image: *default-image
|
|
platform: linux/amd64
|
|
container_name: redis-osscluster
|
|
environment:
|
|
- NODES=6
|
|
- PORT=16600
|
|
command: "--cluster-enabled yes"
|
|
ports:
|
|
- "16600-16605:16600-16605"
|
|
volumes:
|
|
- "./dockers/osscluster:/redis/work"
|
|
profiles:
|
|
- cluster
|
|
- all-stack
|
|
- all
|
|
|
|
cae-resp-proxy:
|
|
image: redislabs/client-resp-proxy:latest
|
|
container_name: cae-resp-proxy
|
|
environment:
|
|
- TARGET_HOST=redis
|
|
- TARGET_PORT=6379
|
|
- LISTEN_PORT=17000,17001,17002,17003 # 4 proxy nodes: initially show 3, swap in 4th during SMIGRATED
|
|
- LISTEN_HOST=0.0.0.0
|
|
- API_PORT=3000
|
|
- DEFAULT_INTERCEPTORS=cluster,hitless
|
|
ports:
|
|
- "17000:17000" # Proxy node 1 (host:container)
|
|
- "17001:17001" # Proxy node 2 (host:container)
|
|
- "17002:17002" # Proxy node 3 (host:container)
|
|
- "17003:17003" # Proxy node 4 (host:container) - hidden initially, swapped in during SMIGRATED
|
|
- "18100:3000" # HTTP API port (host:container)
|
|
depends_on:
|
|
- redis
|
|
profiles:
|
|
- e2e
|
|
- all
|
|
|
|
proxy-fault-injector:
|
|
build:
|
|
context: .
|
|
dockerfile: maintnotifications/e2e/cmd/proxy-fi-server/Dockerfile
|
|
container_name: proxy-fault-injector
|
|
ports:
|
|
- "15000:5000" # Fault injector API port (host:container)
|
|
depends_on:
|
|
- cae-resp-proxy
|
|
environment:
|
|
- PROXY_API_URL=http://cae-resp-proxy:3000
|
|
profiles:
|
|
- e2e
|
|
- all
|
|
|
|
osscluster-tls:
|
|
image: *default-image
|
|
platform: linux/amd64
|
|
container_name: redis-osscluster-tls
|
|
environment:
|
|
- NODES=6
|
|
- PORT=6430
|
|
- TLS_PORT=5430
|
|
- TLS_ENABLED=yes
|
|
- TLS_CLIENT_CNS=testcertuser
|
|
- TLS_AUTH_CLIENTS_USER=CN
|
|
- REDIS_CLUSTER=yes
|
|
- REPLICAS=1
|
|
command: "--tls-auth-clients optional --cluster-announce-ip 127.0.0.1"
|
|
ports:
|
|
- "6430-6435:6430-6435" # Regular ports
|
|
- "5430-5435:5430-5435" # TLS ports (set via TLS_PORT env var)
|
|
- "16430-16435:16430-16435" # Cluster bus ports (PORT + 10000)
|
|
volumes:
|
|
- "./dockers/osscluster-tls:/redis/work"
|
|
profiles:
|
|
- cluster-tls
|
|
- all
|
|
|
|
sentinel-cluster:
|
|
image: *default-image
|
|
platform: linux/amd64
|
|
container_name: redis-sentinel-cluster
|
|
network_mode: "host"
|
|
environment:
|
|
- NODES=3
|
|
- TLS_ENABLED=yes
|
|
- TLS_CLIENT_CNS=testcertuser
|
|
- TLS_AUTH_CLIENTS_USER=CN
|
|
- REDIS_CLUSTER=no
|
|
- PORT=9121
|
|
command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""}
|
|
#ports:
|
|
# - "9121-9123:9121-9123"
|
|
volumes:
|
|
- "./dockers/sentinel-cluster:/redis/work"
|
|
profiles:
|
|
- sentinel
|
|
- all-stack
|
|
- all
|
|
|
|
sentinel:
|
|
image: *default-image
|
|
platform: linux/amd64
|
|
container_name: redis-sentinel
|
|
depends_on:
|
|
- sentinel-cluster
|
|
environment:
|
|
- NODES=3
|
|
- REDIS_CLUSTER=no
|
|
- PORT=26379
|
|
command: ${REDIS_EXTRA_ARGS:---sentinel}
|
|
network_mode: "host"
|
|
#ports:
|
|
# - 26379:26379
|
|
# - 26380:26380
|
|
# - 26381:26381
|
|
volumes:
|
|
- "./dockers/sentinel.conf:/redis/config-default/redis.conf"
|
|
- "./dockers/sentinel:/redis/work"
|
|
profiles:
|
|
- sentinel
|
|
- all-stack
|
|
- all
|
|
|
|
ring-cluster:
|
|
image: *default-image
|
|
platform: linux/amd64
|
|
container_name: redis-ring-cluster
|
|
environment:
|
|
- NODES=3
|
|
- TLS_ENABLED=yes
|
|
- TLS_CLIENT_CNS=testcertuser
|
|
- TLS_AUTH_CLIENTS_USER=CN
|
|
- REDIS_CLUSTER=no
|
|
- PORT=6390
|
|
command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""}
|
|
ports:
|
|
- "6390:6390"
|
|
- "6391:6391"
|
|
- "6392:6392"
|
|
volumes:
|
|
- "./dockers/ring:/redis/work"
|
|
profiles:
|
|
- ring
|
|
- cluster
|
|
- all-stack
|
|
- all
|