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
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 Recommended for production
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: Modes that bind to 127.0.0.1 (loopback, loopback-proxy, unix) require --network host because 127.0.0.1 inside a bridge network is unreachable from outside the container. exposed-proxy works on bridge — the proxy binds 0.0.0.0:8880, so port mapping (-p 8880:8880) works, and signal-cli stays on loopback internally.

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

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. See secured-signal-api docs for the full configuration reference.

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" \
  -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.

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