Compare commits

...

2 commits

Author SHA1 Message Date
f62a612758 Replace Caddy with Newt service config
- Remove Caddy config files for multiple hosts
- Add Newt service configuration to cloud, komo, and sock
2025-06-14 01:18:26 -04:00
39f0026f52 Replace caddy proxies with Pangolin and Newt; initial work in proxy
- Adds new container services for pangolin and newt
- Removes caddy configuration for proxy
2025-06-14 01:17:25 -04:00
16 changed files with 549 additions and 235 deletions

View file

@ -17,6 +17,7 @@ in
(map lib.custom.relativeToRoot [ (map lib.custom.relativeToRoot [
"modules/global" "modules/global"
"modules/nixos"
]) ])
]; ];

View file

@ -1,20 +0,0 @@
{
services.caddy = {
enable = true;
virtualHosts = {
## Filerun ##
"drive.ryot.foo" = {
useACMEHost = "ryot.foo";
extraConfig = ''
reverse_proxy http://localhost:8181 {
header_up Host {host}
# header_up X-Forwarded-For {remote}
# header_up X-Forwarded-Proto {scheme}
# header_up X-Forwarded-Protocol {scheme}
# header_up X-Forwarded-Port {server_port}
}
'';
};
};
};
}

View file

@ -0,0 +1,7 @@
{
services.newt = {
enable = true;
id = "v0d4o5oras85zq8";
secret = "zyqht58kymdv4iij6t4no4ldnr7djg7wbfec95olnsg8jzf2";
};
}

View file

@ -31,7 +31,6 @@ in
"hosts/global/core" "hosts/global/core"
## Optional Configs ## ## Optional Configs ##
"hosts/global/common/acme"
"hosts/global/common/docker.nix" "hosts/global/common/docker.nix"
]) ])
]; ];

View file

@ -1,119 +0,0 @@
{
services.caddy = {
enable = true;
virtualHosts = {
## TOPH.CC ##
"blog.toph.cc" = {
useACMEHost = "toph.cc";
extraConfig = ''
reverse_proxy localhost:2368
'';
};
## RYOT.FOO ##
"auth.ryot.foo" = {
useACMEHost = "ryot.foo";
extraConfig = ''
reverse_proxy localhost:9000 {
header_up Host {host}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-Protocol {scheme}
header_up X-Forwarded-Port {server_port}
}
'';
};
"frp.ryot.foo" = {
useACMEHost = "ryot.foo";
extraConfig = ''
route {
# 1) Proxy all outpost requests back to Authentik
reverse_proxy /outpost.goauthentik.io/* localhost:9000
# 2) Protect everything else via forward_auth
forward_auth localhost:9000 {
uri /outpost.goauthentik.io/auth/caddy
# copy user info headers from Authentik
copy_headers X-Authentik-Username X-Authentik-Groups \
X-Authentik-Entitlements X-Authentik-Email \
X-Authentik-Name X-Authentik-Uid \
X-Authentik-Jwt X-Authentik-Meta-Jwks \
X-Authentik-Meta-Outpost X-Authentik-Meta-Provider \
X-Authentik-Meta-App X-Authentik-Meta-Version
trusted_proxies private_ranges
}
# 3) If authenticated, proxy to your FRP UI
reverse_proxy localhost:4041 {
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-Port {server_port}
}
}
'';
};
"grafana.ryot.foo" = {
useACMEHost = "ryot.foo";
extraConfig = ''
reverse_proxy localhost:3001
'';
};
"git.ryot.foo" = {
useACMEHost = "ryot.foo";
extraConfig = ''
reverse_proxy localhost:3003
'';
};
"influx.ryot.foo" = {
useACMEHost = "ryot.foo";
extraConfig = ''
reverse_proxy localhost:8086
'';
};
"home.ryot.foo" = {
useACMEHost = "ryot.foo";
extraConfig = ''
reverse_proxy localhost:7475
'';
};
"komodo.ryot.foo" = {
useACMEHost = "ryot.foo";
extraConfig = ''
reverse_proxy localhost:9120
'';
};
"map.ryot.foo" = {
useACMEHost = "ryot.foo";
extraConfig = ''
reverse_proxy localhost:25566
'';
};
"outline.ryot.foo" = {
useACMEHost = "ryot.foo";
extraConfig = ''
reverse_proxy localhost:3480
'';
};
"plane.ryot.foo" = {
useACMEHost = "ryot.foo";
extraConfig = ''
reverse_proxy localhost:3000
'';
};
};
};
}

View file

@ -0,0 +1,8 @@
{
services.newt = {
enable = true;
id = "7o2m62kaxpoi5pb";
secret = "t97xvz0itdkga6jr8x88oddxijzs73yslpsunlvyqu9xiyys";
useHostNetwork = true;
};
}

View file

@ -5,7 +5,7 @@
*/ */
{ {
# Containers # Containers
virtualisation.oci-containers.containers."adguard-adguard" = { virtualisation.oci-containers.containers."adguard" = {
image = "adguard/adguardhome:latest"; image = "adguard/adguardhome:latest";
volumes = [ volumes = [
"/etc/adguard/confdir:/opt/adguardhome/conf:rw" "/etc/adguard/confdir:/opt/adguardhome/conf:rw"
@ -24,7 +24,8 @@
"--network=adguard_default" "--network=adguard_default"
]; ];
}; };
systemd.services."docker-adguard-adguard" = {
systemd.services."docker-adguard" = {
serviceConfig = { serviceConfig = {
Restart = lib.mkOverride 90 "always"; Restart = lib.mkOverride 90 "always";
RestartMaxDelaySec = lib.mkOverride 90 "1m"; RestartMaxDelaySec = lib.mkOverride 90 "1m";

View file

@ -1,72 +0,0 @@
{
services.caddy = {
enable = true;
virtualHosts = {
"adguard.ryot.foo" = {
useACMEHost = "ryot.foo";
extraConfig = ''
reverse_proxy localhost:3000
'';
};
"cloudflared.ryot.foo" = {
useACMEHost = "ryot.foo";
extraConfig = ''
reverse_proxy localhost:14333
'';
};
## openWRT ##
"wrt.ryot.foo" = {
useACMEHost = "ryot.foo";
extraConfig = ''
reverse_proxy http://104.40.3.1 {
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-Port {server_port}
}
'';
};
## PROXMOX NODES ##
"ochre.ryot.foo" = {
useACMEHost = "ryot.foo";
extraConfig = ''
reverse_proxy https://104.40.3.2:8006 {
transport http {
tls_insecure_skip_verify
# optional: tls_server_name 104.40.3.2
}
# ensure Proxmox sees the right Host
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-Port {server_port}
}
'';
};
"pve.ryot.foo" = {
useACMEHost = "ryot.foo";
extraConfig = ''
reverse_proxy https://104.40.3.3:8006 {
transport http {
tls_insecure_skip_verify
# optional: tls_server_name 104.40.3.3
}
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-Port {server_port}
}
'';
};
};
};
}

View file

@ -0,0 +1,8 @@
{
services.newt = {
enable = true;
id = "1jh3j2bhucdiq09";
secret = "f51n59bjwbg9c1wuyjg62my634mqqs1y199cwd1bxq3bes6p";
useHostNetwork = true;
};
}

View file

@ -0,0 +1,156 @@
# Auto-generated using compose2nix v0.3.1.
{
pkgs,
lib,
config,
...
}:
{
# Containers
virtualisation.oci-containers.containers."gerbil" = {
image = "fosrl/gerbil:1.0.0";
volumes = [
"/etc/pangolin/config:/var/config:rw"
];
ports = [
"51820:51820/udp"
"443:443/tcp"
"222:222/tcp"
"80:80/tcp"
];
cmd = [
"--reachableAt=http://gerbil:3003"
"--generateAndSaveKeyTo=/var/config/key"
"--remoteConfig=http://pangolin:3001/api/v1/gerbil/get-config"
"--reportBandwidthTo=http://pangolin:3001/api/v1/gerbil/receive-bandwidth"
];
dependsOn = [
"pangolin"
];
log-driver = "journald";
extraOptions = [
"--cap-add=NET_ADMIN"
# "--cap-add=SYS_MODULE"
"--network-alias=gerbil"
"--network=pangolin"
];
};
systemd.services."docker-gerbil" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
RestartMaxDelaySec = lib.mkOverride 90 "1m";
RestartSec = lib.mkOverride 90 "100ms";
RestartSteps = lib.mkOverride 90 9;
};
after = [
"docker-network-pangolin.service"
];
requires = [
"docker-network-pangolin.service"
];
partOf = [
"docker-compose-pangolin-root.target"
];
wantedBy = [
"docker-compose-pangolin-root.target"
];
};
virtualisation.oci-containers.containers."pangolin" = {
image = "fosrl/pangolin:1.5.1";
volumes = [
"/etc/pangolin/config:/app/config:rw"
];
log-driver = "journald";
extraOptions = [
"--health-cmd=[\"curl\", \"-f\", \"http://localhost:3001/api/v1/\"]"
"--health-interval=3s"
"--health-retries=15"
"--health-timeout=3s"
"--network-alias=pangolin"
"--network=pangolin"
];
};
systemd.services."docker-pangolin" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
RestartMaxDelaySec = lib.mkOverride 90 "1m";
RestartSec = lib.mkOverride 90 "100ms";
RestartSteps = lib.mkOverride 90 9;
};
after = [
"docker-network-pangolin.service"
"pangolin-config-sync.service"
];
requires = [
"docker-network-pangolin.service"
"pangolin-config-sync.service"
];
partOf = [
"docker-compose-pangolin-root.target"
];
wantedBy = [
"docker-compose-pangolin-root.target"
];
};
virtualisation.oci-containers.containers."traefik" = {
image = "traefik:v3.4.0";
environment = {
"CLOUDFLARE_DNS_API_TOKEN" = config.secretsSpec.api.cloudflare;
};
volumes = [
"/etc/pangolin/config/letsencrypt:/letsencrypt:rw"
"/etc/pangolin/config/traefik:/etc/traefik:ro"
];
cmd = [ "--configFile=/etc/traefik/traefik_config.yml" ];
dependsOn = [
"gerbil"
"pangolin"
];
log-driver = "journald";
extraOptions = [
"--network=container:gerbil"
];
};
systemd.services."docker-traefik" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
RestartMaxDelaySec = lib.mkOverride 90 "1m";
RestartSec = lib.mkOverride 90 "100ms";
RestartSteps = lib.mkOverride 90 9;
};
partOf = [
"docker-compose-pangolin-root.target"
];
wantedBy = [
"docker-compose-pangolin-root.target"
];
};
# Networks
systemd.services."docker-network-pangolin" = {
path = [ pkgs.docker ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStop = "docker network rm -f pangolin";
};
script = ''
docker network inspect pangolin || docker network create pangolin --driver=bridge
'';
partOf = [ "docker-compose-pangolin-root.target" ];
wantedBy = [ "docker-compose-pangolin-root.target" ];
};
# Root service
# When started, this will automatically create all resources and start
# the containers. When stopped, this will teardown all resources.
systemd.targets."docker-compose-pangolin-root" = {
unitConfig = {
Description = "Root target generated by compose2nix.";
};
wantedBy = [ "multi-user.target" ];
};
}

View file

@ -0,0 +1,230 @@
{
lib,
config,
pkgs,
...
}:
let
smtp = config.secretsSpec.users.admin.smtp;
pangolin = config.secretsSpec.docker.pangolin;
# Create the configuration files as derivations
pangolinConfigFile = pkgs.writeText "pangolin-config.yml" ''
app:
dashboard_url: "https://pangolin.ryot.foo"
log_level: "debug"
save_logs: true
domains:
domain1:
base_domain: "ryot.foo"
cert_resolver: "letsencrypt"
prefer_wildcard_cert: true
domain2:
base_domain: "toph.cc"
cert_resolver: "letsencrypt"
prefer_wildcard_cert: true
domain3:
base_domain: "goldenlemon.cc"
cert_resolver: "letsencrypt"
prefer_wildcard_cert: true
domain4:
base_domain: "kwahson.xyz"
cert_resolver: "letsencrypt"
prefer_wildcard_cert: true
server:
external_port: 3000
internal_port: 3001
next_port: 3002
internal_hostname: "pangolin"
session_cookie_name: "p_session_token"
resource_access_token_param: "p_token"
resource_access_token_headers:
id: "P-Access-Token-Id"
token: "P-Access-Token"
resource_session_request_param: "p_session_request"
secret: "${pangolin.SECRET}"
traefik:
cert_resolver: "letsencrypt"
http_entrypoint: "web"
https_entrypoint: "websecure"
gerbil:
start_port: 51820
base_endpoint: "pangolin.ryot.foo"
use_subdomain: false
block_size: 24
site_block_size: 30
subnet_group: 104.40.3.1/24
rate_limits:
global:
window_minutes: 1
max_requests: 100
email:
smtp_host: "${smtp.host}"
smtp_port: ${toString smtp.port}
smtp_user: "${smtp.user}"
smtp_pass: "${smtp.password}"
no_reply: "no-reply@ryot.foo"
users:
server_admin:
email: "${pangolin.USER}"
password: "${pangolin.PASSWORD}"
flags:
require_email_verification: true
disable_signup_without_invite: true
disable_user_create_org: true
allow_raw_resources: true
allow_base_domain_resources: true
'';
traefikConfigFile = pkgs.writeText "traefik-config.yml" ''
api:
insecure: true
dashboard: true
providers:
http:
endpoint: "http://pangolin:3001/api/v1/traefik-config"
pollInterval: "5s"
file:
filename: "/etc/traefik/dynamic_config.yml"
experimental:
plugins:
badger:
moduleName: "github.com/fosrl/badger"
version: "v1.2.0"
log:
level: "DEBUG"
format: "common"
certificatesResolvers:
letsencrypt:
acme:
dnsChallenge:
provider: cloudflare
delayBeforeCheck: 60
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"
email: chris@toph.cc
storage: "/letsencrypt/acme.json"
caServer: "https://acme-v02.api.letsencrypt.org/directory"
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
transport:
respondingTimeouts:
readTimeout: "30m"
http:
tls:
certResolver: "letsencrypt"
tcp-222:
address: ":222/tcp"
serversTransport:
insecureSkipVerify: true
'';
dynamicConfigFile = pkgs.writeText "dynamic-config.yml" ''
http:
middlewares:
redirect-to-https:
redirectScheme:
scheme: https
routers:
# HTTP to HTTPS redirect router
main-app-router-redirect:
rule: "Host(`pangolin.ryot.foo`)"
service: next-service
entryPoints:
- web
middlewares:
- redirect-to-https
# Next.js router
next-router:
rule: "Host(`pangolin.ryot.foo`) && !PathPrefix(`/api/v1`)"
service: next-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
domains:
- main: "ryot.foo"
sans:
- "*.ryot.foo"
# API router
api-router:
rule: "Host(`pangolin.ryot.foo`) && PathPrefix(`/api/v1`)"
service: api-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
# WebSocket router
ws-router:
rule: "Host(`pangolin.ryot.foo`)"
service: api-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
services:
next-service:
loadBalancer:
servers:
- url: "http://pangolin:3002"
api-service:
loadBalancer:
servers:
- url: "http://pangolin:3000"
'';
keyFile = pkgs.writeText "pangolin-key" pangolin.KEY;
in
{
imports = lib.custom.scanPaths ./.;
boot.kernelModules = [ "wireguard" ];
## Tmp files and Service to Avoid symlinks
systemd.tmpfiles.rules = [
"d /etc/pangolin/config 0755 root root -"
"d /etc/pangolin/config/traefik 0755 root root -"
"d /etc/pangolin/config/letsencrypt 0755 root root -"
];
systemd.services.pangolin-config-sync = {
description = "Sync Pangolin configuration files";
wantedBy = [ "multi-user.target" ];
before = [ "docker-compose-pangolin-root.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
cp ${keyFile} /etc/pangolin/config/key
chmod 0600 /etc/pangolin/config/key
cp ${pangolinConfigFile} /etc/pangolin/config/config.yml
cp ${traefikConfigFile} /etc/pangolin/config/traefik/traefik_config.yml
cp ${dynamicConfigFile} /etc/pangolin/config/traefik/dynamic_config.yml
'';
};
}

View file

@ -1,20 +0,0 @@
{
services.caddy = {
enable = true;
virtualHosts = {
"upsnap.ryot.foo" = {
useACMEHost = "ryot.foo";
extraConfig = ''
reverse_proxy localhost:8090
'';
};
"sock.ryot.foo" = {
useACMEHost = "ryot.foo";
extraConfig = ''
reverse_proxy localhost:9120
'';
};
};
};
}

View file

@ -0,0 +1,7 @@
{
services.newt = {
enable = true;
id = "3p15lzqz0ep9f46";
secret = "8uz056bzh22vuemtsxda31ibiu7jkqmyn1b7bifbqk38nlm4";
};
}

View file

@ -31,7 +31,6 @@ in
"hosts/global/core" "hosts/global/core"
## Optional Configs ## ## Optional Configs ##
"hosts/global/common/acme"
"hosts/global/common/docker.nix" "hosts/global/common/docker.nix"
]) ])
]; ];

129
modules/nixos/newt.nix Normal file
View file

@ -0,0 +1,129 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.newt;
in
{
options.services.newt = {
enable = mkEnableOption "Newt container service";
id = mkOption {
type = types.str;
description = "Newt ID for authentication";
};
image = mkOption {
type = types.str;
default = "fosrl/newt";
description = "Docker image to use for Newt";
};
networkName = mkOption {
type = types.str;
default = "newt";
description = "Docker network name to use";
};
networkAlias = mkOption {
type = types.str;
default = "newt";
description = "Network alias for the container";
};
pangolinEndpoint = mkOption {
type = types.str;
default = "https://pangolin.ryot.foo";
description = "Pangolin endpoint URL";
};
secret = mkOption {
type = types.str;
description = "Newt secret for authentication";
};
useHostNetwork = mkOption {
type = types.bool;
default = false;
description = "Whether to use host networking instead of Docker networks";
};
};
config = mkIf cfg.enable {
# Container
virtualisation.oci-containers.containers."newt" = {
image = cfg.image;
environment = {
"DOCKER_SOCKET" = "/var/run/docker.sock";
"NEWT_ID" = cfg.id;
"NEWT_SECRET" = cfg.secret;
"PANGOLIN_ENDPOINT" = cfg.pangolinEndpoint;
};
volumes = [
"/var/run/docker.sock:/var/run/docker.sock:rw"
];
log-driver = "journald";
extraOptions =
if cfg.useHostNetwork then
[
"--network=host"
]
else
[
"--network-alias=${cfg.networkAlias}"
"--network=${cfg.networkName}"
];
};
# Container service with proper dependencies
systemd.services."docker-newt" = {
serviceConfig = {
Restart = lib.mkOverride 90 "always";
RestartMaxDelaySec = lib.mkOverride 90 "1m";
RestartSec = lib.mkOverride 90 "100ms";
RestartSteps = lib.mkOverride 90 9;
};
after = mkIf (!cfg.useHostNetwork) [
"docker-network-${cfg.networkName}.service"
];
requires = mkIf (!cfg.useHostNetwork) [
"docker-network-${cfg.networkName}.service"
];
partOf = [
"docker-compose-newt-root.target"
];
wantedBy = [
"docker-compose-newt-root.target"
];
};
# Docker network service (only when not using host network)
systemd.services."docker-network-${cfg.networkName}" = mkIf (!cfg.useHostNetwork) {
path = [ pkgs.docker ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStop = "docker network rm -f ${cfg.networkName}";
};
script = ''
docker network inspect ${cfg.networkName} || docker network create ${cfg.networkName}
'';
partOf = [ "docker-compose-newt-root.target" ];
wantedBy = [ "docker-compose-newt-root.target" ];
};
# Root target
systemd.targets."docker-compose-newt-root" = {
unitConfig = {
Description = "Root target generated by compose2nix.";
};
wantedBy = [ "multi-user.target" ];
};
};
}

Binary file not shown.