Wire up X-Real-IP / X-Forwarded-For / True-Client-IP support for deployments fronted by a reverse proxy, while staying safe on flat-LAN deployments where a malicious speaker could spoof those headers directly. Two new fields on `datastore.Settings`: * TrustForwardedHeaders (bool, default false) — opt-in switch. * TrustedProxyCIDRs ([]string, default `["127.0.0.0/8", "::1/128"]`) — only requests whose immediate TCP peer falls in one of these blocks may have their source IP rewritten from forwarded headers. Loopback default matches the documented same-host nginx layout in docs/guides/HTTPS-SETUP.md. New middleware in `pkg/service/handlers/middleware_realip.go`: * TrustedRealIP wraps `chi/middleware.RealIP` with a trusted-peer gate. When the immediate TCP peer is in the allowlist, chi's parsing handles the actual header → IP rewrite. When it isn't (e.g. a speaker sending forwarded headers itself), we ignore the headers and r.RemoteAddr stays as-is. * ParseTrustedProxyCIDRs converts string CIDRs into *net.IPNet, applying the loopback default on empty input and erroring loudly on invalid entries. Server.TrustedRealIPMiddleware() returns the middleware (or nil) by reading the live settings; the router setup in cmd/soundtouch-service/main.go installs it as the very first middleware so SnapshotMiddleware and downstream handlers see the correct r.RemoteAddr. HandleMargePowerOn now prefers r.RemoteAddr over the body's self-reported `<IPAddress>` for outbound credential push: * The body field is treated as a hint only — a malicious LAN speaker could set it to any value; using it for outbound HTTP requests is the SSRF surface the previous zeroconf hardening was guarding against from the sink side. Fixing it at the source as well closes the gap entirely. * When body IP and TCP source disagree, a log line names both and the device ID so the discrepancy is investigable. * RemoteAddr is unparseable → fall back to the body so we don't silently drop the priming. docs/guides/HTTPS-SETUP.md gains a follow-up note next to the existing nginx snippet explaining the new flag, the loopback-only default, and the explicit warning against enabling the flag on a flat-LAN deployment without a real proxy. Eleven test cases in middleware_realip_test.go lock in the gate behaviour: trusted peers honoured for X-Real-IP / X-Forwarded-For / no-headers / IPv6, untrusted peers' headers ignored, garbage values rejected, ParseTrustedProxyCIDRs covers default / override / invalid. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.4 KiB
HTTPS & Custom CA Certificate
SoundTouch speakers communicate with cloud services over HTTPS. For the local service to work over HTTPS, speakers must trust the AfterTouch Root CA. The service manages this automatically — it generates a CA on first start and the web UI guides you through installing it on each speaker as part of the migration flow.
How TLS works in AfterTouch
The service includes a built-in HTTPS listener (default port 8443) that presents a certificate covering all Bose cloud hostnames. The certificate is signed by the AfterTouch Root CA, which is generated automatically on first start and stored in data/certs/.
Domain coverage — the certificate covers:
- Wildcard:
*.api.bose.io,*.api.bosecm.com - Specific:
streaming.bose.com,bmx.bose.com,stats.bose.com,updates.bose.com,worldwide.bose.com,bose-prod.apigee.net,media.bose.io,downloads.bose.com,voice.api.bose.io, and more
Note
: The hostname you configure as
HTTPS_SERVER_URL(e.g.https://soundtouch.fritz.box:8443) is also added as a Subject Alternative Name, ensuring valid TLS for direct browser or API access.
CA trust installation (via web UI)
The migration flow in the web UI includes a CA trust step that:
- Uploads the Root CA to the speaker via SSH
- Appends it to the speaker's shared trust store (
/etc/pki/tls/certs/ca-bundle.crt) - Verifies connectivity over HTTPS
This is handled automatically — you don't need to manage CA files manually unless you're doing an advanced or manual setup.
Downloading the CA certificate
You can download the Root CA for manual installation on other devices (phones, PCs, additional speakers):
http://<server>:8000/setup/ca.crt
Binding to port 443
Speakers expect HTTPS on the default port 443. Since binding to port 443 requires elevated privileges, you have three options:
- Port forwarding (recommended): Run the service on port 8443 and forward port 443 to it using
iptablesor your firewall/router. - Capabilities: Grant the binary permission to bind low ports:
sudo setcap 'cap_net_bind_service=+ep' ./soundtouch-service - Reverse proxy: Use Nginx or Caddy in front of the service (see below).
Reverse proxy (optional)
If you prefer to use Nginx or another proxy for TLS termination:
server {
listen 443 ssl;
server_name streaming.bose.com bmx.bose.com stats.bose.com updates.bose.com;
ssl_certificate /path/to/data/certs/server.crt;
ssl_certificate_key /path/to/data/certs/server.key;
ssl_protocols TLSv1.2;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305:AES128-GCM-SHA256:AES256-GCM-SHA384';
location / {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Tell the service to honour
X-Real-IP/X-Forwarded-For. When deploying behind a reverse proxy on the same host as above, set"trust_forwarded_headers": trueindata/settings.json. With that flag on, the service rewritesr.RemoteAddrfrom the proxy-supplied headers, so handlers that act on the source IP (e.g. the Spotify priming triggered by/marge/streaming/support/power_on) see the speaker's real address instead of the proxy's loopback peer.By default only
127.0.0.0/8and::1/128are trusted to set those headers. If your reverse proxy lives on a different host, list its CIDR(s) in"trusted_proxy_cidrs"(e.g.["10.0.0.0/8"]). Do not enabletrust_forwarded_headerson a flat LAN deployment without a proxy: a malicious speaker on the LAN can send the headers itself and spoof its source IP.
Manual CA injection (advanced)
If you need to inject the CA manually (e.g. without the web UI migration flow):
# Copy the CA to the speaker
scp data/certs/ca.crt root@<SPEAKER-IP>:/tmp/
# Make the filesystem writable and append the CA to the trust store
ssh root@<SPEAKER-IP> "(rw || mount -o remount,rw /) && cat /tmp/ca.crt >> /etc/pki/tls/certs/ca-bundle.crt"
TLS compatibility
SoundTouch speakers run OpenSSL 1.0.2, supporting up to TLS 1.2. The service is configured accordingly:
- Minimum TLS version: TLS 1.2
- Preferred cipher suites:
ECDHE-RSA-AES128-GCM-SHA256,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-RSA-CHACHA20-POLY1305 - Legacy support:
RSA-AES128-GCM-SHA256,RSA-AES256-GCM-SHA384