# TLS front for OPNexus. Certificates come from Caddy's own internal CA, so
# they work for bare IP addresses and for names that exist in no DNS at all.
# Clients trust it once by importing the root certificate served at
# /opnexus-ca.crt.
#
# OPNEXUS_SITES is a comma-separated list of every address OPNexus is reached
# by, e.g. "10.0.0.5" or "10.0.0.5, opnexus.lan".
# OPNEXUS_DEFAULT_SNI is the primary one (normally the IP).
{
	admin off
	local_certs
	# Clients that connect by bare IP send no SNI, and behind Docker the proxy
	# cannot see which host IP was dialled - so name the certificate to use then.
	default_sni {$OPNEXUS_DEFAULT_SNI}
	# Redirect plain http://IP to https, but never serve anything over http.
	auto_https disable_redirects
}

http:// {
	redir https://{host}{uri} permanent
}

{$OPNEXUS_SITES} {
	encode zstd gzip

	header {
		Strict-Transport-Security "max-age=31536000"
		X-Content-Type-Options "nosniff"
		X-Frame-Options "DENY"
		Referrer-Policy "no-referrer"
		Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()"
		Cross-Origin-Opener-Policy "same-origin"
		Cross-Origin-Resource-Policy "same-origin"
		# Next.js hydrates with inline scripts and React sets inline styles, hence
		# 'unsafe-inline'. Everything else is locked to this origin: no third-party
		# scripts, no framing, no plugins, the API is same-origin.
		Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'"
		-Server
		-X-Powered-By
	}

	# Root certificate of the internal CA, for importing into browsers/OS.
	handle /opnexus-ca.crt {
		root * /data/caddy/pki/authorities/local
		rewrite * /root.crt
		header Content-Type "application/x-x509-ca-cert"
		header Content-Disposition `attachment; filename="opnexus-root-ca.crt"`
		file_server
	}

	handle /api/* {
		# Every legitimate API body is small JSON; refuse anything bigger before
		# it is buffered by the application.
		request_body {
			max_size 2MB
		}
		reverse_proxy backend:8000
	}

	handle {
		reverse_proxy frontend:3000
	}
}

# GUI remote access (optional, compose profile "gui"): every slot port is its
# own browser origin for one firewall web interface, served by the gui-gateway
# container. OPNEXUS_GUI_SITES lists the slot origins, e.g.
# "https://10.0.0.5:9441, https://10.0.0.5:9442". Unset, a localhost
# placeholder keeps the Caddyfile valid; without the gateway it answers 502.
# Certificates come from the same internal CA (bare IPs work).
{$OPNEXUS_GUI_SITES:https://localhost:9441} {
	# The gateway sets CSP, X-Frame-Options, no-store etc. itself; no `encode`
	# (it passes the firewall's own content encoding through) and no access log
	# (the redeem URL carries a one-time ticket in its query string).
	header {
		Strict-Transport-Security "max-age=31536000"
		-Server
		-X-Powered-By
	}

	# The backend's internal gateway API is never reachable from outside, and
	# the gateway would not forward it either; refuse it here as well.
	handle /internal/* {
		respond 404
	}

	handle {
		# Config restore uploads go through the firewall GUI: same limit as the
		# gateway (64 MB).
		request_body {
			max_size 64MB
		}
		reverse_proxy gui-gateway:8080 {
			# The slot is the port this connection actually arrived on (not the
			# client's Host header); any client-sent X-Onx-Slot is overwritten.
			header_up X-Onx-Slot {http.request.local.port}
		}
	}
}
