feat: add Cloudflare tunnel hosting
This commit is contained in:
parent
e6c1b82679
commit
23e087ae4b
15 changed files with 839 additions and 30 deletions
|
|
@ -9,6 +9,7 @@
|
|||
{
|
||||
imports = [
|
||||
(modulesPath + "/virtualisation/digital-ocean-config.nix")
|
||||
./public-gateway.nix
|
||||
./observability.nix
|
||||
];
|
||||
|
||||
|
|
@ -46,8 +47,10 @@
|
|||
];
|
||||
};
|
||||
networking.firewall = {
|
||||
allowedTCPPorts = [
|
||||
22
|
||||
# SSH is intentionally Tailscale-only via the trusted tailscale0 interface.
|
||||
# Keep direct HTTP(S) open until the Cloudflare Tunnel is enabled, then all
|
||||
# public web traffic enters through the tunnel instead of the droplet IP.
|
||||
allowedTCPPorts = lib.optionals (!config.services.noisebell-public-gateway.enable) [
|
||||
80
|
||||
443
|
||||
];
|
||||
|
|
@ -74,6 +77,7 @@
|
|||
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
openFirewall = false;
|
||||
settings = {
|
||||
PasswordAuthentication = false;
|
||||
PermitRootLogin = "prohibit-password";
|
||||
|
|
@ -98,17 +102,39 @@
|
|||
security.sudo.wheelNeedsPassword = false;
|
||||
|
||||
age.identityPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
|
||||
age.secrets.noisebell-cloudflare-api-token = lib.mkIf config.services.noisebell-public-gateway.enable {
|
||||
file = ../../secrets/cloudflare-api-token.age;
|
||||
mode = "0400";
|
||||
};
|
||||
age.secrets.noisebell-cloudflare-tunnel-secret = lib.mkIf config.services.noisebell-public-gateway.enable {
|
||||
file = ../../secrets/cloudflare-tunnel-secret.age;
|
||||
mode = "0400";
|
||||
};
|
||||
|
||||
services.tailscale.enable = true;
|
||||
|
||||
services.caddy = {
|
||||
enable = true;
|
||||
openFirewall = false;
|
||||
email = "postmaster@extremist.software";
|
||||
globalConfig = lib.mkIf config.services.noisebell-public-gateway.enable ''
|
||||
auto_https off
|
||||
'';
|
||||
};
|
||||
|
||||
# Reconciles the Cloudflare Tunnel and public DNS through the Cloudflare API.
|
||||
services.noisebell-public-gateway = {
|
||||
enable = true;
|
||||
accountId = "9f7c0277922ab28c45cb85bf4e7838af";
|
||||
zoneId = "710e3255f43066c4a6bb4081b05a6c3f";
|
||||
apiTokenFile = lib.mkIf config.services.noisebell-public-gateway.enable config.age.secrets.noisebell-cloudflare-api-token.path;
|
||||
tunnelSecretFile = lib.mkIf config.services.noisebell-public-gateway.enable config.age.secrets.noisebell-cloudflare-tunnel-secret.path;
|
||||
};
|
||||
|
||||
services.noisebell-cache = {
|
||||
enable = true;
|
||||
domain = "noisebell.extremist.software";
|
||||
httpOnly = config.services.noisebell-public-gateway.enable;
|
||||
piAddress = "http://noisebell-pi";
|
||||
outboundWebhooks = [
|
||||
{
|
||||
|
|
@ -130,7 +156,8 @@
|
|||
|
||||
services.noisebell-rss = {
|
||||
enable = true;
|
||||
domain = "rss.noisebell.extremist.software";
|
||||
domain = "rss-noisebell.extremist.software";
|
||||
httpOnly = config.services.noisebell-public-gateway.enable;
|
||||
};
|
||||
|
||||
zramSwap = {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,89 @@ let
|
|||
prometheusPort = 9090;
|
||||
lokiPort = 3100;
|
||||
grafanaPort = 3030;
|
||||
grafanaDomain = "grafana-noisebell.extremist.software";
|
||||
nodeExporterPort = 9100;
|
||||
blackboxExporterPort = 9115;
|
||||
publicDashboardToken = "6e6f69736562656c6c7075626c696330";
|
||||
|
||||
sharePublicDashboard = pkgs.writeShellApplication {
|
||||
name = "noisebell-grafana-share-public-dashboard";
|
||||
runtimeInputs = [
|
||||
pkgs.coreutils
|
||||
pkgs.curl
|
||||
pkgs.jq
|
||||
];
|
||||
text = ''
|
||||
set -euo pipefail
|
||||
|
||||
base_url=http://127.0.0.1:${toString grafanaPort}
|
||||
dashboard_uid=noisebell-public
|
||||
public_uid=${publicDashboardToken}
|
||||
access_token=${publicDashboardToken}
|
||||
password=$(tr -d '\r\n' < /var/lib/grafana/admin_password)
|
||||
|
||||
ready=0
|
||||
for _ in $(seq 1 60); do
|
||||
if curl -fsS -u "admin:$password" "$base_url/api/health" >/dev/null; then
|
||||
ready=1
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [ "$ready" -ne 1 ]; then
|
||||
echo "Grafana did not become ready at $base_url" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dashboard_ready=0
|
||||
for _ in $(seq 1 60); do
|
||||
if curl -fsS -u "admin:$password" "$base_url/api/dashboards/uid/$dashboard_uid" >/dev/null; then
|
||||
dashboard_ready=1
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [ "$dashboard_ready" -ne 1 ]; then
|
||||
echo "Grafana dashboard '$dashboard_uid' was not provisioned" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
existing=$(curl -fsS -u "admin:$password" \
|
||||
"$base_url/api/dashboards/uid/$dashboard_uid/public-dashboards/" 2>/dev/null || true)
|
||||
existing_uid=""
|
||||
existing_token=""
|
||||
if [ -n "$existing" ]; then
|
||||
existing_uid=$(jq -r '.uid // empty' <<<"$existing")
|
||||
existing_token=$(jq -r '.accessToken // empty' <<<"$existing")
|
||||
fi
|
||||
|
||||
if [ -n "$existing_uid" ] && { [ "$existing_uid" != "$public_uid" ] || [ "$existing_token" != "$access_token" ]; }; then
|
||||
curl -fsS -u "admin:$password" \
|
||||
-X DELETE \
|
||||
"$base_url/api/dashboards/uid/$dashboard_uid/public-dashboards/$existing_uid" >/dev/null
|
||||
existing_uid=""
|
||||
fi
|
||||
|
||||
if [ -n "$existing_uid" ]; then
|
||||
body=$(jq -cn '{timeSelectionEnabled:true,isEnabled:true,annotationsEnabled:false,share:"public"}')
|
||||
curl -fsS -u "admin:$password" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-X PATCH \
|
||||
--data "$body" \
|
||||
"$base_url/api/dashboards/uid/$dashboard_uid/public-dashboards/$existing_uid" >/dev/null
|
||||
else
|
||||
body=$(jq -cn \
|
||||
--arg uid "$public_uid" \
|
||||
--arg accessToken "$access_token" \
|
||||
'{uid:$uid,accessToken:$accessToken,timeSelectionEnabled:true,isEnabled:true,annotationsEnabled:false,share:"public"}')
|
||||
curl -fsS -u "admin:$password" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-X POST \
|
||||
--data "$body" \
|
||||
"$base_url/api/dashboards/uid/$dashboard_uid/public-dashboards/" >/dev/null
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
blackboxConfig = pkgs.writeText "noisebell-blackbox.yml" ''
|
||||
modules:
|
||||
|
|
@ -78,7 +159,7 @@ let
|
|||
|
||||
dashboard = pkgs.writeText "noisebell-dashboard.json" (builtins.toJSON {
|
||||
uid = "noisebell";
|
||||
title = "Noisebell DO + Pi";
|
||||
title = "Noisebell Full Debug";
|
||||
tags = [
|
||||
"noisebell"
|
||||
"prometheus"
|
||||
|
|
@ -297,9 +378,136 @@ let
|
|||
];
|
||||
});
|
||||
|
||||
publicDashboard = pkgs.writeText "noisebell-public-dashboard.json" (builtins.toJSON {
|
||||
uid = "noisebell-public";
|
||||
title = "Noisebell Public";
|
||||
tags = [
|
||||
"noisebell"
|
||||
"public"
|
||||
"prometheus"
|
||||
];
|
||||
timezone = "browser";
|
||||
schemaVersion = 39;
|
||||
version = 1;
|
||||
refresh = "30s";
|
||||
time = {
|
||||
from = "now-6h";
|
||||
to = "now";
|
||||
};
|
||||
panels = [
|
||||
(prometheusPanel {
|
||||
id = 1;
|
||||
title = "Door State";
|
||||
type = "stat";
|
||||
x = 0;
|
||||
y = 0;
|
||||
w = 6;
|
||||
h = 6;
|
||||
targets = [ (promTarget "A" "noisebell_cache_status" "{{status}}") ];
|
||||
})
|
||||
(prometheusPanel {
|
||||
id = 2;
|
||||
title = "Public Status Endpoint";
|
||||
type = "stat";
|
||||
x = 6;
|
||||
y = 0;
|
||||
w = 6;
|
||||
h = 6;
|
||||
targets = [ (promTarget "A" "probe_success{job=\"noisebell-http-probes\",instance=\"https://noisebell.extremist.software/status\"}" "status endpoint") ];
|
||||
})
|
||||
(prometheusPanel {
|
||||
id = 3;
|
||||
title = "Noisebell Service Health";
|
||||
type = "stat";
|
||||
x = 12;
|
||||
y = 0;
|
||||
w = 6;
|
||||
h = 6;
|
||||
targets = [ (promTarget "A" "up{job=~\"noisebell-(cache|pi-app|pi-relay)\"}" "{{job}}") ];
|
||||
})
|
||||
(prometheusPanel {
|
||||
id = 4;
|
||||
title = "Cache Poll Health";
|
||||
x = 0;
|
||||
y = 6;
|
||||
w = 12;
|
||||
targets = [
|
||||
(promTarget "A" "noisebell_cache_poll_consecutive_failures" "consecutive failures")
|
||||
(promTarget "B" "rate(noisebell_cache_poll_failure_total[5m])" "failure rate")
|
||||
(promTarget "C" "rate(noisebell_cache_poll_success_total[5m])" "success rate")
|
||||
(promTarget "D" "noisebell_cache_poll_last_duration_seconds" "last duration")
|
||||
];
|
||||
})
|
||||
(prometheusPanel {
|
||||
id = 5;
|
||||
title = "Last Poll Result";
|
||||
type = "stat";
|
||||
x = 12;
|
||||
y = 6;
|
||||
w = 12;
|
||||
targets = [
|
||||
(promTarget "A" "noisebell_cache_poll_last_result" "result {{result}}")
|
||||
(promTarget "B" "time() - noisebell_cache_poll_last_attempt_timestamp_seconds" "seconds since attempt")
|
||||
(promTarget "C" "time() - noisebell_cache_poll_last_success_timestamp_seconds" "seconds since success")
|
||||
];
|
||||
})
|
||||
(prometheusPanel {
|
||||
id = 6;
|
||||
title = "Poll Failure Types";
|
||||
x = 0;
|
||||
y = 14;
|
||||
w = 12;
|
||||
targets = [
|
||||
(promTarget "A" "rate(noisebell_cache_poll_http_error_total[5m])" "http error")
|
||||
(promTarget "B" "rate(noisebell_cache_poll_request_timeout_total[5m])" "timeout")
|
||||
(promTarget "C" "rate(noisebell_cache_poll_request_connect_total[5m])" "connect")
|
||||
(promTarget "D" "rate(noisebell_cache_poll_request_other_total[5m])" "request other")
|
||||
(promTarget "E" "rate(noisebell_cache_poll_parse_failure_total[5m])" "parse")
|
||||
];
|
||||
})
|
||||
(prometheusPanel {
|
||||
id = 7;
|
||||
title = "Pi App Delivery";
|
||||
x = 12;
|
||||
y = 14;
|
||||
w = 12;
|
||||
targets = [
|
||||
(promTarget "A" "rate(noisebell_pi_notify_success_total[5m])" "success")
|
||||
(promTarget "B" "rate(noisebell_pi_notify_attempt_failure_total[5m])" "attempt failures")
|
||||
(promTarget "C" "rate(noisebell_pi_notify_failure_total[5m])" "final failures")
|
||||
];
|
||||
})
|
||||
(prometheusPanel {
|
||||
id = 8;
|
||||
title = "Relay Delivery";
|
||||
x = 0;
|
||||
y = 22;
|
||||
w = 12;
|
||||
targets = [
|
||||
(promTarget "A" "rate(noisebell_relay_forwarded_total[5m])" "forwarded")
|
||||
(promTarget "B" "rate(noisebell_relay_attempt_failure_total[5m])" "attempt failures")
|
||||
(promTarget "C" "rate(noisebell_relay_failed_total[5m])" "final failures")
|
||||
(promTarget "D" "noisebell_relay_last_duration_seconds" "last duration")
|
||||
];
|
||||
})
|
||||
(prometheusPanel {
|
||||
id = 9;
|
||||
title = "Pi Hardware Summary";
|
||||
x = 12;
|
||||
y = 22;
|
||||
w = 12;
|
||||
targets = [
|
||||
(promTarget "A" "noisebell_pi_temperature_celsius" "temperature C")
|
||||
(promTarget "B" "noisebell_pi_throttled_flags" "throttled flags")
|
||||
];
|
||||
})
|
||||
];
|
||||
});
|
||||
|
||||
dashboardDir = pkgs.runCommand "noisebell-grafana-dashboards" { } ''
|
||||
mkdir -p "$out"
|
||||
cp ${dashboard} "$out/noisebell.json"
|
||||
cp ${publicDashboard} "$out/noisebell-public.json"
|
||||
'';
|
||||
|
||||
blackboxRelabels = [
|
||||
|
|
@ -608,22 +816,26 @@ in
|
|||
enable = true;
|
||||
settings = {
|
||||
server = {
|
||||
http_addr = "0.0.0.0";
|
||||
http_addr = "127.0.0.1";
|
||||
http_port = grafanaPort;
|
||||
domain = "noisebell-do";
|
||||
root_url = "http://noisebell-do:${toString grafanaPort}/";
|
||||
domain = grafanaDomain;
|
||||
root_url = "https://${grafanaDomain}/";
|
||||
};
|
||||
analytics.reporting_enabled = false;
|
||||
metrics.enabled = true;
|
||||
security = {
|
||||
admin_user = "admin";
|
||||
admin_password = "$__file{/var/lib/grafana/admin_password}";
|
||||
secret_key = "$__file{/var/lib/grafana/secret_key}";
|
||||
disable_initial_admin_creation = true;
|
||||
disable_initial_admin_creation = false;
|
||||
cookie_secure = true;
|
||||
strict_transport_security = true;
|
||||
strict_transport_security_max_age_seconds = 31536000;
|
||||
};
|
||||
auth.disable_login_form = true;
|
||||
auth.disable_login_form = false;
|
||||
users.allow_sign_up = false;
|
||||
"auth.anonymous" = {
|
||||
enabled = true;
|
||||
org_role = "Viewer";
|
||||
enabled = false;
|
||||
};
|
||||
};
|
||||
provision = {
|
||||
|
|
@ -670,5 +882,21 @@ in
|
|||
umask 077
|
||||
${pkgs.coreutils}/bin/head -c 64 /dev/urandom | ${pkgs.coreutils}/bin/base64 --wrap=0 > /var/lib/grafana/secret_key
|
||||
fi
|
||||
if [ ! -s /var/lib/grafana/admin_password ]; then
|
||||
umask 077
|
||||
${pkgs.coreutils}/bin/head -c 36 /dev/urandom | ${pkgs.coreutils}/bin/base64 --wrap=0 > /var/lib/grafana/admin_password
|
||||
fi
|
||||
'';
|
||||
|
||||
systemd.services.noisebell-grafana-public-dashboard = {
|
||||
description = "Ensure deterministic Noisebell public Grafana dashboard share";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "grafana.service" ];
|
||||
wants = [ "grafana.service" ];
|
||||
restartTriggers = [ dashboardDir ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${sharePublicDashboard}/bin/noisebell-grafana-share-public-dashboard";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
244
hosts/noisebell-do/public-gateway.nix
Normal file
244
hosts/noisebell-do/public-gateway.nix
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.services.noisebell-public-gateway;
|
||||
optionalSecretPath = path: if path == null then "" else toString path;
|
||||
optionalString = value: if value == null then "" else value;
|
||||
hostnames = [
|
||||
cfg.cacheDomain
|
||||
cfg.rssDomain
|
||||
cfg.grafanaDomain
|
||||
];
|
||||
quotedHostnames = lib.concatMapStringsSep " " lib.escapeShellArg hostnames;
|
||||
reconcileScript = pkgs.writeShellApplication {
|
||||
name = "noisebell-cloudflare-tunnel-reconcile";
|
||||
runtimeInputs = [
|
||||
pkgs.coreutils
|
||||
pkgs.curl
|
||||
pkgs.jq
|
||||
];
|
||||
text = ''
|
||||
set -euo pipefail
|
||||
|
||||
api=https://api.cloudflare.com/client/v4
|
||||
account_id=${lib.escapeShellArg (optionalString cfg.accountId)}
|
||||
zone_id=${lib.escapeShellArg (optionalString cfg.zoneId)}
|
||||
tunnel_name=${lib.escapeShellArg cfg.tunnelName}
|
||||
api_token_file=${lib.escapeShellArg (optionalSecretPath cfg.apiTokenFile)}
|
||||
tunnel_secret_file=${lib.escapeShellArg (optionalSecretPath cfg.tunnelSecretFile)}
|
||||
credentials_file=${lib.escapeShellArg (toString cfg.credentialsFile)}
|
||||
hostnames=(${quotedHostnames})
|
||||
|
||||
api_token=$(tr -d '\r\n' < "$api_token_file")
|
||||
tunnel_secret=$(tr -d '\r\n' < "$tunnel_secret_file")
|
||||
|
||||
cloudflare_request() {
|
||||
local method=$1
|
||||
local path=$2
|
||||
local data=''${3:-}
|
||||
local response
|
||||
|
||||
if [ -n "$data" ]; then
|
||||
response=$(curl -sS \
|
||||
--request "$method" \
|
||||
--header "Authorization: Bearer $api_token" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "$data" \
|
||||
"$api$path")
|
||||
else
|
||||
response=$(curl -sS \
|
||||
--request "$method" \
|
||||
--header "Authorization: Bearer $api_token" \
|
||||
--header 'Content-Type: application/json' \
|
||||
"$api$path")
|
||||
fi
|
||||
|
||||
if ! jq -e '.success == true' >/dev/null <<<"$response"; then
|
||||
jq -r '.errors[]?.message // "Cloudflare API request failed"' <<<"$response" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "$response"
|
||||
}
|
||||
|
||||
encoded_tunnel_name=$(jq -rn --arg value "$tunnel_name" '$value | @uri')
|
||||
tunnels=$(cloudflare_request GET "/accounts/$account_id/cfd_tunnel?name=$encoded_tunnel_name&is_deleted=false")
|
||||
tunnel=$(jq -c --arg name "$tunnel_name" '[.result[] | select(.name == $name and (.deleted_at == null))][0] // empty' <<<"$tunnels")
|
||||
|
||||
if [ -z "$tunnel" ]; then
|
||||
payload=$(jq -cn \
|
||||
--arg name "$tunnel_name" \
|
||||
--arg secret "$tunnel_secret" \
|
||||
'{name:$name, config_src:"local", tunnel_secret:$secret}')
|
||||
tunnel=$(cloudflare_request POST "/accounts/$account_id/cfd_tunnel" "$payload" | jq -c '.result')
|
||||
fi
|
||||
|
||||
tunnel_id=$(jq -r '.id' <<<"$tunnel")
|
||||
remote_config=$(jq -r '.remote_config // false' <<<"$tunnel")
|
||||
config_src=$(jq -r '.config_src // "local"' <<<"$tunnel")
|
||||
|
||||
if [ -z "$tunnel_id" ] || [ "$tunnel_id" = "null" ]; then
|
||||
echo "Cloudflare tunnel '$tunnel_name' did not return an id" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$remote_config" = "true" ] || [ "$config_src" = "cloudflare" ]; then
|
||||
echo "Cloudflare tunnel '$tunnel_name' exists but is remotely managed; expected config_src=local" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install -d -m 0700 "$(dirname "$credentials_file")"
|
||||
tmp_credentials=$(mktemp)
|
||||
jq -cn \
|
||||
--arg account "$account_id" \
|
||||
--arg id "$tunnel_id" \
|
||||
--arg name "$tunnel_name" \
|
||||
--arg secret "$tunnel_secret" \
|
||||
'{AccountTag:$account,TunnelID:$id,TunnelName:$name,TunnelSecret:$secret}' > "$tmp_credentials"
|
||||
install -m 0400 "$tmp_credentials" "$credentials_file"
|
||||
rm -f "$tmp_credentials"
|
||||
|
||||
target="$tunnel_id.cfargotunnel.com"
|
||||
for hostname in "''${hostnames[@]}"; do
|
||||
encoded_hostname=$(jq -rn --arg value "$hostname" '$value | @uri')
|
||||
records=$(cloudflare_request GET "/zones/$zone_id/dns_records?name=$encoded_hostname")
|
||||
record_id=$(jq -r --arg target "$target" '.result[] | select(.type == "CNAME" and .content == $target) | .id' <<<"$records" | head -n 1)
|
||||
payload=$(jq -cn \
|
||||
--arg name "$hostname" \
|
||||
--arg content "$target" \
|
||||
'{type:"CNAME", name:$name, content:$content, proxied:true, ttl:1}')
|
||||
|
||||
jq -r --arg keep "$record_id" '.result[] | select(.id != $keep) | .id' <<<"$records" | while IFS= read -r old_record_id; do
|
||||
if [ -n "$old_record_id" ]; then
|
||||
cloudflare_request DELETE "/zones/$zone_id/dns_records/$old_record_id" >/dev/null
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$record_id" ]; then
|
||||
cloudflare_request PUT "/zones/$zone_id/dns_records/$record_id" "$payload" >/dev/null
|
||||
else
|
||||
cloudflare_request POST "/zones/$zone_id/dns_records" "$payload" >/dev/null
|
||||
fi
|
||||
done
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
options.services.noisebell-public-gateway = {
|
||||
enable = lib.mkEnableOption "Noisebell public Cloudflare Tunnel gateway";
|
||||
|
||||
tunnelName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "noisebell-do";
|
||||
description = "Cloudflare Tunnel name.";
|
||||
};
|
||||
|
||||
accountId = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = "Cloudflare account ID that owns the tunnel.";
|
||||
};
|
||||
|
||||
zoneId = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = "Cloudflare zone ID for the public Noisebell hostnames.";
|
||||
};
|
||||
|
||||
apiTokenFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
description = "Path to a Cloudflare API token with tunnel and DNS edit permissions.";
|
||||
};
|
||||
|
||||
tunnelSecretFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
description = "Path to the base64-encoded local tunnel secret used to create the tunnel.";
|
||||
};
|
||||
|
||||
credentialsFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
default = "/var/lib/noisebell-public-gateway/credentials.json";
|
||||
description = "Path where the reconciler writes the Cloudflare Tunnel credentials JSON file.";
|
||||
};
|
||||
|
||||
cacheDomain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "noisebell.extremist.software";
|
||||
};
|
||||
|
||||
rssDomain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "rss-noisebell.extremist.software";
|
||||
};
|
||||
|
||||
grafanaDomain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "grafana-noisebell.extremist.software";
|
||||
};
|
||||
|
||||
grafanaPort = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 3030;
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.accountId != null;
|
||||
message = "services.noisebell-public-gateway.accountId must be set.";
|
||||
}
|
||||
{
|
||||
assertion = cfg.zoneId != null;
|
||||
message = "services.noisebell-public-gateway.zoneId must be set.";
|
||||
}
|
||||
{
|
||||
assertion = cfg.apiTokenFile != null;
|
||||
message = "services.noisebell-public-gateway.apiTokenFile must point to the Cloudflare API token secret.";
|
||||
}
|
||||
{
|
||||
assertion = cfg.tunnelSecretFile != null;
|
||||
message = "services.noisebell-public-gateway.tunnelSecretFile must point to the Cloudflare Tunnel secret.";
|
||||
}
|
||||
];
|
||||
|
||||
systemd.services.noisebell-cloudflare-tunnel-reconcile = {
|
||||
description = "Reconcile Noisebell Cloudflare Tunnel and DNS routes";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network-online.target" ];
|
||||
wants = [ "network-online.target" ];
|
||||
before = [ "cloudflared-tunnel-${cfg.tunnelName}.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${reconcileScript}/bin/noisebell-cloudflare-tunnel-reconcile";
|
||||
StateDirectory = "noisebell-public-gateway";
|
||||
StateDirectoryMode = "0700";
|
||||
};
|
||||
};
|
||||
|
||||
services.cloudflared = {
|
||||
enable = true;
|
||||
tunnels.${cfg.tunnelName} = {
|
||||
credentialsFile = cfg.credentialsFile;
|
||||
edgeIPVersion = "auto";
|
||||
default = "http_status:404";
|
||||
ingress = {
|
||||
${cfg.cacheDomain} = "http://127.0.0.1:80";
|
||||
${cfg.rssDomain} = "http://127.0.0.1:80";
|
||||
${cfg.grafanaDomain} = "http://127.0.0.1:${toString cfg.grafanaPort}";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services."cloudflared-tunnel-${cfg.tunnelName}" = {
|
||||
after = [ "noisebell-cloudflare-tunnel-reconcile.service" ];
|
||||
requires = [ "noisebell-cloudflare-tunnel-reconcile.service" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue