From c7e59b137cc1d4b0720a9bb734d5c2b0641a5b44 Mon Sep 17 00:00:00 2001 From: Aarnav Tale Date: Wed, 22 May 2024 12:43:42 -0400 Subject: [PATCH] docs: solidify DOCKER_SOCK and ROOT_API_KEY usage --- .env.example | 2 +- app/utils/config/headplane.ts | 38 +++++++++++++++++++++++++++++++---- app/utils/docker.ts | 27 +++++++++++++++---------- app/utils/oidc.ts | 3 +-- docs/Advanced-Integration.md | 8 ++++++-- docs/Basic-Integration.md | 3 +-- docs/Configuration.md | 10 ++++++++- 7 files changed, 68 insertions(+), 23 deletions(-) diff --git a/.env.example b/.env.example index 6ff946c..0d0f221 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -API_KEY=abcdefghijklmnopqrstuvwxyz +ROOT_API_KEY=abcdefghijklmnopqrstuvwxyz COOKIE_SECRET=abcdefghijklmnopqrstuvwxyz DISABLE_API_KEY_LOGIN=true HEADSCALE_CONTAINER=headscale diff --git a/app/utils/config/headplane.ts b/app/utils/config/headplane.ts index e69f204..4e3f061 100644 --- a/app/utils/config/headplane.ts +++ b/app/utils/config/headplane.ts @@ -25,7 +25,8 @@ export interface HeadplaneContext { } docker?: { - sock: string + url: string + sock: boolean container: string } @@ -163,19 +164,48 @@ async function checkAcl(config?: HeadscaleConfig) { } async function checkDocker() { - const path = process.env.DOCKER_SOCK ?? '/var/run/docker.sock' + const path = process.env.DOCKER_SOCK ?? 'unix:///var/run/docker.sock' + + let url: URL | undefined try { - await access(path, constants.R_OK) + url = new URL(path) } catch { return } + // The API is available as an HTTP endpoint + if (url.protocol === 'tcp:') { + url.protocol = 'http:' + } + + // Check if the socket is accessible + if (url.protocol === 'unix:') { + try { + await access(path, constants.R_OK) + } catch { + return + } + } + + if (url.protocol === 'http:') { + try { + await fetch(new URL('/v1.30/version', url).href) + } catch { + return + } + } + + if (url.protocol !== 'http:' && url.protocol !== 'unix:') { + return + } + if (!process.env.HEADSCALE_CONTAINER) { return } return { - sock: path, + url: url.href, + sock: url.protocol === 'unix:', container: process.env.HEADSCALE_CONTAINER, } } diff --git a/app/utils/docker.ts b/app/utils/docker.ts index 004bdb1..f9a8e1e 100644 --- a/app/utils/docker.ts +++ b/app/utils/docker.ts @@ -1,5 +1,3 @@ -/* eslint-disable no-await-in-loop */ -/* eslint-disable no-constant-condition */ import { setTimeout } from 'node:timers/promises' import { Client } from 'undici' @@ -13,9 +11,12 @@ export async function sighupHeadscale() { return } - const client = new Client('http://localhost', { - socketPath: context.docker.sock, - }) + // Supports the DOCKER_SOCK environment variable + const client = context.docker.sock + ? new Client('http://localhost', { + socketPath: context.docker.url, + }) + : new Client(context.docker.url) const response = await client.request({ method: 'POST', @@ -33,9 +34,12 @@ export async function restartHeadscale() { return } - const client = new Client('http://localhost', { - socketPath: context.docker.sock, - }) + // Supports the DOCKER_SOCK environment variable + const client = context.docker.sock + ? new Client('http://localhost', { + socketPath: context.docker.url, + }) + : new Client(context.docker.url) const response = await client.request({ method: 'POST', @@ -48,14 +52,15 @@ export async function restartHeadscale() { // Wait for Headscale to restart before continuing let attempts = 0 + // eslint-disable-next-line while (true) { try { - // Acceptable blank because API_KEY is not required - await pull('v1/apikey', process.env.API_KEY ?? '') + // Acceptable blank because ROOT_API_KEY is not required + await pull('v1/apikey', context.oidc?.rootKey ?? '') return } catch (error) { // This means the server is up but the API key is invalid - // This can happen if the user only uses API_KEY via cookies + // This can happen if the user only uses ROOT_API_KEY via cookies if (error instanceof HeadscaleError && error.status === 401) { break } diff --git a/app/utils/oidc.ts b/app/utils/oidc.ts index b2924f7..4b2482d 100644 --- a/app/utils/oidc.ts +++ b/app/utils/oidc.ts @@ -151,8 +151,7 @@ export async function finishOidc(oidc: OidcConfig, req: Request) { const keyResponse = await post<{ apiKey: string }>( 'v1/apikey', - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - process.env.API_KEY!, + oidc.rootKey, { expiration: expDate, }, diff --git a/docs/Advanced-Integration.md b/docs/Advanced-Integration.md index 5576b02..f7e17d4 100644 --- a/docs/Advanced-Integration.md +++ b/docs/Advanced-Integration.md @@ -30,10 +30,14 @@ The ACL file path is read from the following sources in order of priority: The Docker integration can be used to automatically reload the configuration or ACLs when they are changed. In order for this to work, you'll need to pass in the `HEADSCALE_CONTAINER` environment variable. -You'll also need to ensure that `/var/run/docker.sock` is mounted if Headplane is running in a container. This should be either the name or ID of the Headscale container (you can retrieve this using `docker ps`). If the other integrations aren't setup, then Headplane will automatically disable the Docker integration. +By default the integration will check for `/var/run/docker.sock`, however you can override this by +setting the `DOCKER_SOCK` environment variable if you use a different configuration than the default. +When setting `DOCKER_SOCK`, you'll need to include the protocol (e.g., `unix://` or `tcp://`). +Headplane currently does not support the HTTPS protocol for the Docker socket. + ## Deployment Requirements: @@ -85,7 +89,7 @@ services: # This NEEDS to be set with OIDC, regardless of what's in the config # This needs to be a very long-lived (999 day) API key used to create # shorter ones for OIDC and allow the OIDC functionality to work - API_KEY: 'abcdefghijklmnopqrstuvwxyz' + ROOT_API_KEY: 'abcdefghijklmnopqrstuvwxyz' ``` > For a breakdown of each configuration variable, please refer to the [Configuration](/docs/Configuration.md) guide. diff --git a/docs/Basic-Integration.md b/docs/Basic-Integration.md index 1cc745b..51858eb 100644 --- a/docs/Basic-Integration.md +++ b/docs/Basic-Integration.md @@ -31,8 +31,7 @@ services: COOKIE_SECRET: 'abcdefghijklmnopqrstuvwxyz' # These are all optional! - HEADSCALE_CONTAINER: 'headscale' - API_KEY: 'abcdefghijklmnopqrstuvwxyz' + ROOT_API_KEY: 'abcdefghijklmnopqrstuvwxyz' OIDC_CLIENT_ID: 'headscale' OIDC_ISSUER: 'https://sso.example.com' OIDC_CLIENT_SECRET: 'super_secret_client_secret' diff --git a/docs/Configuration.md b/docs/Configuration.md index 79c6c1c..e9a10a0 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -13,6 +13,14 @@ You can configure Headplane using environment variables. - **`PORT`**: The port to bind the server to (default: `3000`). - **`CONFIG_FILE`**: The path to the Headscale `config.yaml` (default: `/etc/headscale/config.yaml`). - **`ACL_FILE`**: The path to the ACL file (default: `/etc/headscale/acl_policy.json`, not needed if you have `acl_policy_path` in your config). + +#### Docker Integration +The Docker integration allows Headplane to manage the Headscale docker container. +You'll need to provide these variables if you want to use this feature. +Keep in mind that `DOCKER_SOCK` must start with a protocol (e.g., `unix://`). +Secure API is currently not supported. + +- **`DOCKER_SOCK`**: The protocol and path to the Docker socket (default: `unix:///var/run/docker.sock`). - **`HEADSCALE_CONTAINER`**: The name of the Headscale container (required for Docker integration). ### SSO/OpenID Connect @@ -23,7 +31,7 @@ If you use the Headscale configuration integration, these are not required. - **`OIDC_ISSUER`**: The issuer URL of your OIDC provider. - **`OIDC_CLIENT_ID`**: The client ID of your OIDC provider. - **`OIDC_CLIENT_SECRET`**: The client secret of your OIDC provider. -- **`API_KEY`**: An API key used to issue new ones for sessions (keep expiry fairly long). +- **`ROOT_API_KEY`**: An API key used to issue new ones for sessions (keep expiry fairly long). - **`DISABLE_API_KEY_LOGIN`**: If you want to disable API key login, set this to `true`. Here's what an example with Authelia would look like if you used the same client for both Headscale and Headplane.