| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
gosmee is a relay, not a firewall. It forwards webhook payloads from a public ingress point to clients running in private networks — it does not inspect, filter, or sanitize payload content beyond the controls described in this document.
internet → [gosmee server] → SSE stream → [gosmee client] → local service
What gosmee can protect:
What gosmee does not handle by itself:
| Threat | Relevant controls |
|---|---|
| Forged or tampered webhooks from untrusted senders | Signature validation, IP allowlisting |
| Eavesdropping on the SSE relay stream | End-to-end encryption |
| Unauthorized replay injection | --replay-token |
| Payload-based resource exhaustion (DoS) | --max-body-size, channel name length limit |
| Command injection via exec scripts | --exec hardening, signature validation, IP allowlisting |
| Unauthorized access to protected channels | Encrypted channels with public-key authentication |
If you do nothing else, apply these controls before deploying gosmee in production, ordered by impact:
IP allowlisting and signature validation are complementary controls. Use both where possible: IP restrictions are coarse-grained (network-level, easy to configure) and signatures are fine-grained (cryptographic, provider-verified). An attacker who spoofs a source IP still fails signature validation; an attacker who obtains a signature secret but sends from a blocked IP is still rejected.
If you know which IP ranges your webhooks will come from, restrict them with --allowed-ips. Requests from other IPs receive a 403 and are logged. The restriction applies only to POST requests — the web UI remains open.
The examples below use --trust-proxy because they assume gosmee sits behind a reverse proxy. Only enable --trust-proxy when gosmee is reachable exclusively through a trusted proxy that overwrites the forwarded headers (see the warning under Trusting Proxy Headers Safely below). If gosmee is directly reachable, drop --trust-proxy from these commands so the allowlist is enforced against the real connection address.
# Accept webhooks from GitHub's ranges only
gosmee server --trust-proxy \
--allowed-ips 192.30.252.0/22 \
--allowed-ips 185.199.108.0/22 \
--allowed-ips 140.82.112.0/20
# GitLab.com
gosmee server --trust-proxy \
--allowed-ips 35.231.145.151 \
--allowed-ips 34.74.90.64 \
--allowed-ips 34.74.226.93
# Bitbucket Cloud
gosmee server --trust-proxy \
--allowed-ips 34.199.54.113 \
--allowed-ips 34.232.119.183 \
--allowed-ips 34.236.25.177 \
--allowed-ips 35.171.175.212Use --trust-proxy when gosmee sits behind a reverse proxy so that X-Forwarded-For / X-Real-IP headers are used for the client IP. Both IPv4 and IPv6 addresses and CIDR ranges are supported. You can also set allowed IPs via the GOSMEE_ALLOWED_IPS environment variable (comma-separated) and enable proxy trust via GOSMEE_TRUST_PROXY.
Official IP range docs: GitHub · GitLab.com · Bitbucket Cloud
When --trust-proxy (or GOSMEE_TRUST_PROXY) is set, gosmee derives the client IP from the X-Forwarded-For (first value) and X-Real-IP request headers instead of the network connection's source address. These headers are trivially set by any HTTP client.
Warning: --trust-proxy makes --allowed-ips only as trustworthy as whatever sets those headers. If gosmee is reachable directly (not solely through your proxy), an attacker can send X-Forwarded-For: <an-allowed-ip> and bypass the allowlist entirely. The same spoofing also forges the client IP shown in logs.
Only enable --trust-proxy when both of these hold:
If you cannot guarantee both conditions, leave --trust-proxy off. Without it, gosmee uses the connection's source address, which cannot be spoofed by the client — though behind a proxy that means every request appears to come from the proxy, so --allowed-ips should then contain the proxy's address (or you should enforce source IPs at the proxy/firewall instead). Pair IP allowlisting with --webhook-signature so a spoofed or proxy-originated request still fails cryptographic validation.
Signature validation ensures that incoming webhooks are genuinely from your provider and haven't been tampered with. Enable it by passing one or more secrets:
gosmee server --webhook-signature=SECRET1 --webhook-signature=SECRET2gosmee automatically detects the provider from the request headers and validates accordingly:
| Provider | Header validated |
|---|---|
| GitHub | X-Hub-Signature-256 (HMAC-SHA256) |
| GitLab | X-Gitlab-Token (constant-time comparison) |
| Bitbucket Cloud/Server | X-Hub-Signature (HMAC-SHA256) |
| Gitea / Forgejo | X-Gitea-Signature (HMAC-SHA256) |
Requests with a missing or invalid signature are rejected with HTTP 401. When multiple secrets are configured, each is tried in turn — useful when migrating secrets or receiving webhooks from multiple sources. The overhead is negligible (~2 μs per request).
Secrets can also be set via GOSMEE_WEBHOOK_SIGNATURE (comma-separated).
The /replay/{channel} endpoint re-sends a captured event to all SSE subscribers on that channel. This is useful for debugging and incident replay, but it is also a write path into your relay stream.
Without authentication, anyone who can reach the server can POST to /replay/{channel} and inject payloads into any channel they can name.
Set --replay-token (or GOSMEE_REPLAY_TOKEN) to require bearer-token authentication:
gosmee server --replay-token=MY_SECRET_TOKENWhen set, replay requests must include:
Authorization: Bearer <token>
Missing or incorrect tokens are rejected with HTTP 401.
The web UI Replay button will prompt you to enter the token when you first attempt to replay an event. The token is stored in your browser's sessionStorage for convenience during the session.
If --replay-token is not set, the replay endpoint remains open for backward compatibility.
Once a webhook passes ingress checks (IP allowlisting, signature validation), it travels over the SSE stream to the client. End-to-end encryption protects this leg of the relay from eavesdropping, even if the SSE connection itself is unencrypted.
TLS for the connection is a complement, not a substitute — enable it at your reverse proxy to protect the transport layer.
gosmee uses NaCl box (Curve25519 + XSalsa20-Poly1305). For each SSE message on a protected channel, the server generates a fresh ephemeral Curve25519 keypair and a random 24-byte nonce, then seals the payload with box.Seal addressed to the recipient's public key. This gives per-message forward secrecy: even if a key is later compromised, past messages cannot be decrypted. The server never has access to plaintext after encryption.
The wire format is a JSON envelope:
{
"encrypted": true,
"version": 1,
"epk": "<base64 ephemeral public key>",
"nonce": "<base64 24-byte nonce>",
"ciphertext": "<base64 ciphertext>"
}On receipt, the client calls box.Open with its static private key and the ephemeral public key to recover the original payload.
Generate a keypair once and store it locally:
gosmee keygen --key-file ~/.config/gosmee/key.jsonThis writes a 0600-mode JSON file and prints the public key to stdout in base64 URL-safe format — paste that value into the server's channels config. Then pass the key file when starting the client:
gosmee client --encryption-key-file ~/.config/gosmee/key.json <server-url> <local-url>Keep the key file private. Anyone with the private key can decrypt messages addressed to that keypair.
Pass a channels config file to gosmee server:
gosmee server --encrypted-channels-file /etc/gosmee/channels.jsonThe config lists which channels are protected and which client public keys are authorized for each:
{
"channels": {
"my-channel": {
"allowed_public_keys": [
"<base64-url public key from gosmee keygen>"
]
}
}
}When a subscriber connects to a protected channel, the server checks their public key against the list. Unauthorized clients get a generic not-found response — no information about the channel is leaked. Channels not listed in the config remain normal plaintext channels.
Encryption covers the server-to-client SSE leg only. Incoming webhook POST bodies arrive at the server in plaintext, as does all web UI traffic.
| Encrypted | Not encrypted |
|---|---|
| SSE payload delivery to authorized clients | Incoming webhook POST bodies |
| Redis Stream payloads when --redis-url is set | |
| Unlisted (plaintext) channels | |
| Web UI and /new endpoint | |
| TLS transport (use a reverse proxy) |
Encryption requires gosmee's own server — smee.io is not supported.
If you run multiple server replicas with Redis Streams, Redis stores the payload before gosmee encrypts it for each SSE subscriber. Payloads remain in Redis until stream trimming removes them. Treat Redis as trusted private infrastructure, enable Redis authentication/TLS where needed, and size --redis-stream-maxlen according to your retention and memory requirements.
The SSE endpoint (/events/{channel}) sets Access-Control-Allow-Origin. By default, it is *.
That default means any website that knows a channel ID can open an EventSource connection and receive all payloads for that channel.
Use --cors-origin (or GOSMEE_CORS_ORIGIN) to control this header:
# Default (backward compatible): allow all origins
gosmee server --cors-origin="*"
# Restrict browser access to one origin
gosmee server --cors-origin="https://dashboard.example.com"
# Omit the header entirely (same-origin browser access only)
gosmee server --cors-origin=""The default * preserves backward compatibility for existing deployments.
These controls protect server availability against large or malformed payloads.
gosmee enforces a 25 MB limit on incoming webhook bodies by default, matching GitHub's maximum. Raise or lower it with --max-body-size (in bytes):
gosmee server --max-body-size 10485760 # 10 MBOn the client side, the SSE receive buffer defaults to 1 MB. If you're forwarding large payloads, increase it to match:
gosmee client --sse-buffer-size 5242880 <SMEE_URL> <TARGET_URL> # 5 MBRaising these limits increases memory consumption proportionally. A server with a very high --max-body-size is also a more attractive DoS target. If you run gosmee in Kubernetes, update the memory requests and limits in your deployment manifests when you change these values, or Pods may be OOMKilled under load.
Channel names are capped at 64 characters across all endpoints. This guards against resource exhaustion from pathologically long names — no configuration is needed.
The --exec flag runs a shell command for each incoming webhook, with the payload written to $GOSMEE_PAYLOAD_FILE and headers to $GOSMEE_HEADERS_FILE. If you've already enabled signature validation and IP allowlisting, the scripts are much safer — but the payload content itself is still untrusted until your script validates it.
The risk: if your server accepts webhooks from untrusted sources and your exec script passes payload fields directly to shell commands (e.g. $(jq -r .field)), an attacker can craft a payload that executes arbitrary code.
Mitigations:
A restart is always required when rotating webhook secrets, regardless of whether you use --webhook-signature flags or the GOSMEE_WEBHOOK_SIGNATURE environment variable. The secret is read once at startup by the CLI flag parser and captured in a closure at route registration time — it is never re-read mid-run. In Kubernetes, kubectl set env on a Deployment triggers a pod restart, which picks up the new value; it is the restart doing the work, not live env-var re-reading.
The correct rotation procedure: add the new secret alongside the old one (gosmee accepts multiple values and tries each in turn, so in-flight webhooks signed with the old secret are not rejected), update the secret at your provider, then remove the old secret and restart once more.
Generate a new keypair with gosmee keygen, add the new public key to the server's channels config file, and redistribute the new key file to clients out-of-band. Once all clients have switched to the new keypair, remove the old public key from the config. There is no built-in key rotation — this process is manual.
When changing --max-body-size or --sse-buffer-size, update the memory requests and limits in your deployment manifests proportionally. Pods that exceed their memory limit are OOMKilled without warning.
Please report security issues by opening a GitHub issue. For sensitive disclosures, use GitHub's private vulnerability reporting feature on the Security tab.
| Back | FazBrowse Home | New Git URL |