Serve over wss://

Pass --ssl with a certificate and a key, and websocketd serves wss:// instead of ws://:

websocketd --port=443 --ssl \
    --sslcert=/etc/ssl/certs/example.com.crt \
    --sslkey=/etc/ssl/private/example.com.key \
    /opt/myapp/myscript.sh

Clients connect to wss://example.com/. All three flags go together: give all of them or none. --sslcert and --sslkey without --ssl are rejected at startup.

The certificate file must contain the server certificate followed by any intermediates, in PEM form. Browsers reject a chain they cannot complete.

websocketd negotiates TLS 1.2 at the lowest. A client that offers nothing above TLS 1.1 fails the handshake.

Requiring client certificates

--sslca names a CA certificate file, and clients must then present a certificate that CA signed:

websocketd --port=443 --ssl \
    --sslcert=/etc/ssl/certs/example.com.crt \
    --sslkey=/etc/ssl/private/example.com.key \
    --sslca=/etc/ssl/certs/client-ca.crt \
    /opt/myapp/myscript.sh

Every connection is now verified against that CA, and an unverified client is refused during the handshake, before any HTTP request exists.

--sslca requires --ssl. Drop --ssl from that command and websocketd refuses to start, exiting with code 1 and a message naming --sslca and --ssl on stderr, because mutual TLS has no handshake to verify a client certificate in without a TLS listener.

The file --sslca points at holds the CA certificates in PEM form. If it contains no parseable certificate, websocketd fails to start rather than starting without verification.

Terminating TLS at a proxy instead

Most deployments do this. The proxy holds the certificate on 443, and websocketd runs plain on loopback behind it:

websocketd --port=8080 --address=127.0.0.1 /opt/myapp/myscript.sh

You get certificate renewal, HTTP/2 to the browser, and one place to manage TLS for every service on the host. See nginx , Apache and HAProxy for the proxy side.

The trap: --sameorigin behind a TLS-terminating proxy

If you terminate TLS at a proxy, do not use --sameorigin. It rejects every upgrade with 403 Forbidden.

--sameorigin compares the browser's Origin header against the Host header of the request websocketd itself received, filling in a default port on each side from the scheme that side arrived under. Behind a TLS-terminating proxy the two sides disagree:

  • The browser is on https://example.com, so the origin side resolves to host example.com, port 443.
  • websocketd was reached over plain HTTP. Even with proxy_set_header Host $host; preserving the name, the Host it sees carries no port, so the request side resolves to host example.com, port 80.

Ports differ, so the check fails. Preserving the original Host name is not enough on its own, because it is the port that mismatches.

Use --origin instead, naming the public origin exactly:

websocketd --port=8080 --address=127.0.0.1 --origin=https://example.com /opt/myapp/myscript.sh

--origin is matched against the Origin header alone. It does not care what Host the proxy forwards, and it stays strict about scheme, host and port. https://example.com with no port matches only port 443.

If you would rather keep --sameorigin, make the proxy forward a Host that carries the public port, for example nginx's proxy_set_header Host $host:$server_port; on a listen 443 ssl server. Both sides then resolve to 443 and the check passes. --origin is the simpler answer.

--sameorigin is a good fit when the browser talks to websocketd directly: local development, or serving the client page from the same websocketd with --staticdir.

Redirecting plain HTTP

--redirport opens a second, plain-HTTP port that answers every request with a permanent redirect to the canonical address:

websocketd --port=443 --ssl --sslcert=... --sslkey=... --redirport=80 /opt/myapp/myscript.sh

A browser sent to http://example.com/ is redirected to https://example.com/. It is only useful when websocketd itself terminates TLS; a proxy in front already handles this.

Next