mirror of
https://github.com/tale/headplane.git
synced 2026-08-28 16:07:07 +00:00
feat: add support for https
This commit is contained in:
@@ -35,6 +35,14 @@ try {
|
|||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((config.server.tls_cert_path || config.server.tls_key_path) && !config.server.cookie_secure) {
|
||||||
|
log.warn(
|
||||||
|
"server",
|
||||||
|
"TLS is enabled but `server.cookie_secure` is false; forcing it to true (browsers reject Secure-less cookies over HTTPS)",
|
||||||
|
);
|
||||||
|
config.server.cookie_secure = true;
|
||||||
|
}
|
||||||
|
|
||||||
const ctx = await createAppContext(config);
|
const ctx = await createAppContext(config);
|
||||||
ctx.startServices();
|
ctx.startServices();
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ const serverConfig = type({
|
|||||||
cookie_secure: "boolean = true",
|
cookie_secure: "boolean = true",
|
||||||
cookie_domain: "string.lower?",
|
cookie_domain: "string.lower?",
|
||||||
cookie_max_age: "number.integer = 86400",
|
cookie_max_age: "number.integer = 86400",
|
||||||
|
|
||||||
|
// TLS termination. When both `tls_cert_path` and `tls_key_path`
|
||||||
|
// are provided, Headplane serves HTTPS on `server.port`. When
|
||||||
|
// either is set, `cookie_secure` is forced to `true`.
|
||||||
|
tls_cert_path: "string?",
|
||||||
|
tls_key_path: "string?",
|
||||||
});
|
});
|
||||||
|
|
||||||
const partialServerConfig = type({
|
const partialServerConfig = type({
|
||||||
@@ -55,6 +61,9 @@ const partialServerConfig = type({
|
|||||||
cookie_secure: "boolean?",
|
cookie_secure: "boolean?",
|
||||||
cookie_domain: "string.lower?",
|
cookie_domain: "string.lower?",
|
||||||
cookie_max_age: "number.integer?",
|
cookie_max_age: "number.integer?",
|
||||||
|
|
||||||
|
tls_cert_path: "string?",
|
||||||
|
tls_key_path: "string?",
|
||||||
});
|
});
|
||||||
|
|
||||||
const headscaleConfig = type({
|
const headscaleConfig = type({
|
||||||
|
|||||||
+26
-1
@@ -8,19 +8,44 @@
|
|||||||
// Vite, and the dev-only `runtime/vite-plugin.ts` dispatches requests
|
// Vite, and the dev-only `runtime/vite-plugin.ts` dispatches requests
|
||||||
// straight to `./app`'s default export.
|
// straight to `./app`'s default export.
|
||||||
|
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
import { dirname, resolve } from "node:path";
|
import { dirname, resolve } from "node:path";
|
||||||
|
import { exit } from "node:process";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
import { composeListener, startHttpServer } from "../../runtime/http";
|
import log from "~/utils/log";
|
||||||
|
|
||||||
|
import { type StartOptions, composeListener, startHttpServer } from "../../runtime/http";
|
||||||
import requestListener, { config, dispose } from "./app";
|
import requestListener, { config, dispose } from "./app";
|
||||||
|
|
||||||
// `import.meta.url` resolves to `build/server/index.js`; the built
|
// `import.meta.url` resolves to `build/server/index.js`; the built
|
||||||
// client lives next to it at `build/client/`.
|
// client lives next to it at `build/client/`.
|
||||||
const clientDir = resolve(dirname(fileURLToPath(import.meta.url)), "../client");
|
const clientDir = resolve(dirname(fileURLToPath(import.meta.url)), "../client");
|
||||||
|
|
||||||
|
let tls: StartOptions["tls"];
|
||||||
|
const { tls_cert_path: certPath, tls_key_path: keyPath } = config.server;
|
||||||
|
if (certPath || keyPath) {
|
||||||
|
if (!certPath || !keyPath) {
|
||||||
|
log.error(
|
||||||
|
"server",
|
||||||
|
"TLS misconfigured: both `server.tls_cert_path` and `server.tls_key_path` must be provided",
|
||||||
|
);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [cert, key] = await Promise.all([readFile(certPath), readFile(keyPath)]);
|
||||||
|
tls = { cert, key };
|
||||||
|
} catch (err) {
|
||||||
|
log.error("server", "Failed to read TLS material: %s", err);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
startHttpServer({
|
startHttpServer({
|
||||||
host: config.server.host,
|
host: config.server.host,
|
||||||
port: config.server.port,
|
port: config.server.port,
|
||||||
|
tls,
|
||||||
listener: composeListener({
|
listener: composeListener({
|
||||||
basename: __PREFIX__,
|
basename: __PREFIX__,
|
||||||
staticRoot: clientDir,
|
staticRoot: clientDir,
|
||||||
|
|||||||
@@ -1,18 +1,36 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// hp_healthcheck pings the Headplane /healthz endpoint. It's invoked
|
||||||
|
// from the Docker HEALTHCHECK directive, so it must work whether the
|
||||||
|
// container is serving plain HTTP or has TLS termination enabled.
|
||||||
|
//
|
||||||
|
// Configuration (via env, all optional):
|
||||||
|
// - HEADPLANE_HEALTHCHECK_URL full URL, takes precedence over the
|
||||||
|
// pieces below (default: http://localhost:3000/admin/healthz)
|
||||||
|
// - HEADPLANE_HEALTHCHECK_TLS "true" to use https://
|
||||||
|
// - HEADPLANE_HEALTHCHECK_HOST default: localhost
|
||||||
|
// - HEADPLANE_HEALTHCHECK_PORT default: 3000
|
||||||
|
// - HEADPLANE_HEALTHCHECK_PATH default: /admin/healthz
|
||||||
func main() {
|
func main() {
|
||||||
|
url := healthcheckURL()
|
||||||
|
|
||||||
client := http.Client{
|
client := http.Client{
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
// We just care that the server is alive and with us
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.Get("http://localhost:3000/admin/healthz")
|
resp, err := client.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Health check failed: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Health check failed: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -27,3 +45,31 @@ func main() {
|
|||||||
fmt.Println("Health check passed.")
|
fmt.Println("Health check passed.")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func healthcheckURL() string {
|
||||||
|
if v := os.Getenv("HEADPLANE_HEALTHCHECK_URL"); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
scheme := "http"
|
||||||
|
if os.Getenv("HEADPLANE_HEALTHCHECK_TLS") == "true" {
|
||||||
|
scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
host := os.Getenv("HEADPLANE_HEALTHCHECK_HOST")
|
||||||
|
if host == "" {
|
||||||
|
host = "localhost"
|
||||||
|
}
|
||||||
|
|
||||||
|
port := os.Getenv("HEADPLANE_HEALTHCHECK_PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "3000"
|
||||||
|
}
|
||||||
|
|
||||||
|
path := os.Getenv("HEADPLANE_HEALTHCHECK_PATH")
|
||||||
|
if path == "" {
|
||||||
|
path = "/admin/healthz"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s://%s:%s%s", scheme, host, port, path)
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,8 +17,19 @@ server:
|
|||||||
# Whether cookies should be marked as Secure
|
# Whether cookies should be marked as Secure
|
||||||
# * Should be false if running without HTTPs
|
# * Should be false if running without HTTPs
|
||||||
# * Should be true if running behind a reverse proxy with HTTPs
|
# * Should be true if running behind a reverse proxy with HTTPs
|
||||||
|
# When `tls_cert_path`/`tls_key_path` are set below this is forced
|
||||||
|
# to true automatically.
|
||||||
cookie_secure: true
|
cookie_secure: true
|
||||||
|
|
||||||
|
# Optional TLS termination. When both `tls_cert_path` and
|
||||||
|
# `tls_key_path` are set, Headplane serves HTTPS on `server.port`.
|
||||||
|
# Both files must be PEM encoded.
|
||||||
|
#
|
||||||
|
# If you are terminating TLS at a reverse proxy (recommended for most
|
||||||
|
# deployments) leave these unset.
|
||||||
|
# tls_cert_path: "/var/lib/headplane/tls/fullchain.pem"
|
||||||
|
# tls_key_path: "/var/lib/headplane/tls/privkey.pem"
|
||||||
|
|
||||||
# The maximum age of the session cookie in seconds
|
# The maximum age of the session cookie in seconds
|
||||||
cookie_max_age: 86400 # 1 day in seconds
|
cookie_max_age: 86400 # 1 day in seconds
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,40 @@ To enable debug logging, set the **`HEADPLANE_DEBUG_LOG=true`** environment vari
|
|||||||
This will enable all debug logs for Headplane, which could fill up log space very quickly.
|
This will enable all debug logs for Headplane, which could fill up log space very quickly.
|
||||||
This is not recommended in production environments.
|
This is not recommended in production environments.
|
||||||
|
|
||||||
|
## TLS Termination
|
||||||
|
|
||||||
|
Headplane can terminate TLS itself when both `server.tls_cert_path` and
|
||||||
|
`server.tls_key_path` are set in the configuration file. Both must point to
|
||||||
|
PEM-encoded files that Headplane can read.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
port: 443
|
||||||
|
tls_cert_path: "/var/lib/headplane/tls/fullchain.pem"
|
||||||
|
tls_key_path: "/var/lib/headplane/tls/privkey.pem"
|
||||||
|
```
|
||||||
|
|
||||||
|
When TLS is configured Headplane serves HTTPS/1.1 on `server.port`. HTTP/2
|
||||||
|
and HTTP/3 are intentionally not supported in-process — terminate those at a
|
||||||
|
reverse proxy (e.g. Caddy or Traefik) and forward to Headplane over HTTP/1.1
|
||||||
|
if you need them today.
|
||||||
|
|
||||||
|
`server.cookie_secure` is forced to `true` whenever TLS is enabled (browsers
|
||||||
|
refuse `Secure`-less cookies over HTTPS); a warning is logged if your config
|
||||||
|
had it set to `false`.
|
||||||
|
|
||||||
|
For most deployments we still recommend terminating TLS at a reverse proxy
|
||||||
|
([see below](#reverse-proxying)) so you can share certificates with Headscale
|
||||||
|
and other services. Built-in TLS is meant for the simpler "Headplane on a
|
||||||
|
single box" scenarios.
|
||||||
|
|
||||||
|
If you're running Headplane in Docker with TLS enabled, the bundled
|
||||||
|
healthcheck binary needs to know it should use HTTPS. Set the following
|
||||||
|
environment variables on the container:
|
||||||
|
|
||||||
|
- `HEADPLANE_HEALTHCHECK_TLS=true`
|
||||||
|
- `HEADPLANE_HEALTHCHECK_PORT` (if you've changed it from the default `3000`)
|
||||||
|
|
||||||
## Reverse Proxying
|
## Reverse Proxying
|
||||||
|
|
||||||
Reverse proxying is very common when deploying web applications. Headscale and
|
Reverse proxying is very common when deploying web applications. Headscale and
|
||||||
|
|||||||
Reference in New Issue
Block a user