Skip to content

Repository files navigation

signal-cli-gateway

Build

Configurable, security-hardened Docker container for signal-cli daemon. Designed for Hermes Agent integration and other automation use cases.

Quick Start

# 1. Clone repo
git clone https://github.com/libre-7/signal-cli-gateway.git
cd signal-cli-gateway

# 2. (Optional) Build the image locally
docker build -t signal-cli-gateway .

# 3. Link your phone (one-time setup)
mkdir -p signal-cli-data

# With Docker Compose (recommended):
cp .env.example .env       # then set SIGNAL_ACCOUNT
docker compose run --rm link

# Or with a plain docker run:
docker run --rm -it \
  -v "$(pwd)/signal-cli-data:/opt/signal-cli-data" \
  -e DEVICE_NAME=HermesAgent \
  signal-cli-gateway \
  bash /scripts/link-account.sh

# Scan the QR code from Signal → Settings → Linked Devices

# 4. Configure Hermes
# In your Hermes .env:
#   SIGNAL_HTTP_URL=http://127.0.0.1:8880
#   SIGNAL_ACCOUNT=+123****7890
# (Hermes connects to the proxy — IP allowlist bypasses auth)

# 5. Edit SIGNAL_ACCOUNT in compose.yaml, then start
docker compose up -d

Security Modes

Mode Env Var signal-cli Exposed Auth Network Use Case
Loopback loopback 127.0.0.1:8080 ❌ None Host Hermes on host networking, trusted LAN — ✅ Shipped default
Loopback + Proxy loopback-proxy 127.0.0.1:8080 0.0.0.0:8880 (proxy, ipFilter: local-only) Bearer + IP allowlist + ipFilter Host Add auth on a single host when another client needs access
Exposed + Proxy exposed-proxy 127.0.0.1:8080 0.0.0.0:8880 (proxy) Bearer + IP allowlist Bridge or Host Multi-host, Kubernetes, cloud
UNIX Socket unix /var/run/signal-cli/socket Socat bridge 127.0.0.1:8080 File perms Host Maximum process isolation

Network note: loopback, loopback-proxy, and unix require --network host — those modes bind signal-cli to 127.0.0.1, which is unreachable from outside a bridge network. exposed-proxy is the one mode that works on a bridge network, because the proxy binds 0.0.0.0:8880 and you publish it with a port mapping:

docker run -d --name signal-cli-gateway --restart unless-stopped \
  -p 8880:8880:8880 \
  -v "$(pwd)/signal-cli-data:/opt/signal-cli-data" \
  -e SIGNAL_ACCOUNT=+123****7890 \
  -e SECURITY_MODE=exposed-proxy \
  -e SECURITY_PROXY_TOKEN=your-fixed-token-here \
  signal-cli-gateway

With exposed-proxy on a bridge, the proxy is reachable from any host that can route to the published port — put a Bearer token on it and keep SECURITY_PROXY_ALLOWED_IPS narrow.

Platform note: this image is amd64-only. signal-cli publishes no arm64 build of its native Linux tarball, so the CI workflow builds linux/amd64 and no arm64 image is produced.

Why loopback is the shipped default (and when to move to loopback-proxy)

compose.yaml and the Unraid deploy script default to loopback: signal-cli binds to 127.0.0.1 and nothing else is exposed, which is safe on a trusted single-host setup with host networking.

  1. signal-cli is never exposed — binds to 127.0.0.1, unreachable from other containers
  2. No ports opened at all in loopback mode — smallest possible attack surface
  3. Upgrade path: set SECURITY_MODE=loopback-proxy when other containers or remote clients need access — then every proxied request requires a Bearer token or Basic Auth, and an ipFilter restricts the proxy to local callers
  4. IP allowlist for Hermes — trusted IPs bypass auth so Hermes' signal.py adapter works unpatched
  5. Auto-generated random token — if you don't set SECURITY_PROXY_TOKEN, one is generated and logged at startup

Architecture

loopback mode:
  signal-cli daemon → 127.0.0.1:8080 (loopback, TCP)

loopback-proxy mode:
  signal-cli daemon → 127.0.0.1:8080 (loopback, unreachable)
       ↑ (proxied, secured)
  secured-signal-api → 0.0.0.0:8880 (Bearer auth + IP allowlist + ipFilter: local-only)
       ↑
  Hermes → 127.0.0.1:8880 (IP is trusted, no auth needed)

exposed-proxy mode:
  signal-cli daemon → 127.0.0.1:8080 (loopback, unreachable)
       ↑ (proxied, secured)
  secured-signal-api → 0.0.0.0:8880 (Bearer auth + IP allowlist)
       ↑
  Any client → <host>:8880 (Bearer token required)

unix mode:
  signal-cli daemon → /var/run/signal-cli/socket
       ↑ (socat bridge)
  socat → 127.0.0.1:8080 (loopback, TCP)

Environment Variables

Docker Compose: .env handling

Copy .env.example to .env before docker compose up. Compose reads .env in two distinct ways, and this repo uses both:

  1. Variable interpolation — ${SIGNAL_ACCOUNT:?set SIGNAL_ACCOUNT in .env} in compose.yaml. The gateway refuses to start if .env is missing or the variable unset (fail-fast instead of a silently empty account).
  2. env_file: — passes all .env entries into the container so the entrypoint sees SECURITY_MODE, PROXY_PORT, etc.

Note that compose does not auto-inject a .env file into an environment: list; the mechanisms above are how it is wired in. The healthcheck ports use ${PROXY_PORT:-8880} / ${SIGNAL_CLI_PORT:-8080} interpolation, so they follow custom ports set in .env.

Variable Default Description
Required
SIGNAL_ACCOUNT — Your Signal number in E.164 format (e.g. +123****7890)
Security
SECURITY_MODE loopback One of: loopback, loopback-proxy, exposed-proxy, unix
SECURITY_PROXY_TOKEN auto-generated Bearer token for proxy authentication
SECURITY_PROXY_ALLOWED_IPS 127.0.0.1 CIDR ranges that bypass proxy auth
Ports
SIGNAL_CLI_PORT 8080 signal-cli daemon port
PROXY_PORT 8880 secured-signal-api proxy port
signal-cli options
SIGNAL_CLI_TRUST_NEW_IDENTITIES on-first-use on-first-use, always, never
Account linking
DEVICE_NAME SignalGateway Name shown in Signal's linked device list
Proxy config
CUSTOM_PROXY_CONFIG true true (default): a config.yml mounted at /config/config.yml is used as-is and never overwritten. false: always regenerate the built-in default.
LOG_LEVEL info Proxy log level: info, debug, warn, error

Hermes Agent Integration

Once the gateway is running, add these to your Hermes .env:

# If gateway uses host networking:
SIGNAL_HTTP_URL=http://127.0.0.1:8880
SIGNAL_ACCOUNT=+123****7890
SIGNAL_HOME_CHANNEL=+123****7890

# Or if gateway is a separate container on a Docker network:
# SIGNAL_HTTP_URL=http://signal-cli-gateway:8880

Hermes connects to the proxy on port 8880. If Hermes' container IP is in the SECURITY_PROXY_ALLOWED_IPS list, no Bearer token is needed — the proxy trusts the connection source. All other callers must authenticate.

No Hermes adapter patches needed. The built-in gateway/platforms/signal.py works as-is when the IP is allowlisted.

Advanced: Custom Proxy Configuration

Mount your own config.yml to /config/config.yml to override default settings — add rate limiting, field policies, message templates, etc. The container detects a mounted file and uses it as-is instead of generating its default; your config is never overwritten at startup. See secured-signal-api docs for the full configuration reference.

Mount it read-only (-v "$(pwd)/custom-config.yml:/config/config.yml:ro") so the container cannot write to it. Set CUSTOM_PROXY_CONFIG=false if you want the built-in default regenerated over the top.

docker run -d --name signal-cli-gateway --restart unless-stopped \
  --network host \
  -v "$(pwd)/signal-cli-data:/opt/signal-cli-data" \
  -v "$(pwd)/custom-config.yml:/config/config.yml:ro" \
  -e SIGNAL_ACCOUNT=+123****7890 \
  -e SECURITY_MODE=loopback-proxy \
  signal-cli-gateway

Linking Your Phone (Step-by-Step)

mkdir -p /mnt/user/appdata/signal-cli-data

# Run link in interactive mode
docker run --rm -it \
  -v /mnt/user/appdata/signal-cli-data:/opt/signal-cli-data \
  -e DEVICE_NAME=HermesAgent \
  signal-cli-gateway \
  bash /scripts/link-account.sh

This prints a URI like:

sgnl://linkdevice?uuid=XXXX&pub_key=YYYY

On a headless system, the link script prints an ANSI QR code itself (offline, via qrencode — no third-party service is contacted). To render one manually:

echo 'sgnl://linkdevice?uuid=XXXX&pub_key=YYYY' | \
  docker run --rm -i signal-cli-gateway qrencode -t ANSI256

Scan from your phone: Signal → Settings → Linked Devices → +.

Build Options

The Dockerfile builds secured-signal-api from source. To skip the proxy (leaner image, no Go build needed):

docker build --target signal-cli-builder -t signal-cli-gateway:no-proxy .
# Copy from stage 1 into a final Ubuntu base

Design

See DESIGN.md for the full security analysis, threat model, and comparison of all approaches considered.

Audit

AUDIT.md holds the most recent comprehensive code review (2026-09-27): 16 findings, all fixed and verified on real Docker, with the full verification record. It also carries forward the 2026-08-21 review's disposition and a list of suspicions that were investigated and refuted, so they aren't re-audited.

License

GNU General Public License v3.0

About

Configurable secure Docker container for signal-cli daemon with multiple security modes — designed for Hermes Agent integration and other automation use cases

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages