Skip to content

Reaching Noct from outside the house

Noct speaks plain HTTP and is meant to be put behind something that adds HTTPS when it leaves your own network: anyone who can see the traffic can read a password or a token. Sign-in is needed for everything (see security), so a server that’s reachable isn’t open.

Both configurations below were run, in front of a real server, with a browser’s requests made by curl: signing up and signing in with the cookie, adding and scanning a folder, a ranged read of a video (a 206 with its Content-Range), a media URL with a token, an HLS playlist and segment, and signing out. They passed through each.

  • Pass Host through unchanged, or say what it was in X-Forwarded-Host. Noct refuses a signed-in browser’s request to change something when the page’s Origin isn’t the host that was asked for, so that a page on another site can’t act as you. If the web app signs in but then everything it changes answers 403 that request came from another site, this is why.
  • Say X-Forwarded-Proto: https when the browser used HTTPS. Noct then marks its sign-in cookie Secure, so it is never sent over plain HTTP. Without it the cookie works, and isn’t Secure.
  • Not hold video back. Turn buffering off for it, or a long film is read into the proxy before it’s played. Noct answers ranges (Range), which is how a player seeks, and serves HLS (/api/hls/…), and neither needs anything beyond that.
  • Allow bodies of 20 MB or more, for uploading a poster.

Caddy does all of that without being told, and gets the certificate itself:

noct.example.com {
reverse_proxy 127.0.0.1:7356
}
server {
listen 443 ssl;
server_name noct.example.com;
# ssl_certificate and ssl_certificate_key: yours, or certbot's.
client_max_body_size 25m;
location / {
proxy_pass http://127.0.0.1:7356;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Video is sent as it's read, not kept first.
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 1h;
}
}

Host has to be one of those proxy_set_header lines: nginx’s own default is the address of Noct, not the one the browser asked for.

On a tailnet there’s nothing to open: reach Noct at the machine’s tailnet name or address from any device on it. The traffic between tailnet devices is encrypted, so plain HTTP is no weaker there.

To get HTTPS and a name like https://nas.your-tailnet.ts.net, use Tailscale Serve:

Terminal window
tailscale serve --bg 7356

I haven’t run this one. If the web app signs in and then refuses changes with that request came from another site, Serve isn’t passing the original host on, as above.

Don’t use Tailscale Funnel, which makes the server public to the whole internet, unless that is what you want: Noct has no limit on requests, only on guesses at a password.

What you see Why
Signing in works, changes answer 403 … another site The proxy isn’t passing Host or X-Forwarded-Host
Signs in, and is asked to sign in again at once, over plain HTTP The proxy says X-Forwarded-Proto: https for a browser that used HTTP, so the cookie is Secure and the browser never sends it back
Video stalls, or starts only after a long wait The proxy is buffering the response
413 uploading a picture The proxy’s body limit is under 20 MB