service/webserver: migrate nginx to caddy

This commit is contained in:
Felix Buehler 2025-01-26 00:34:16 +01:00
parent 13470f55e4
commit 589714db4b
34 changed files with 347 additions and 688 deletions

View file

@ -136,21 +136,8 @@ in
blackbox = { blackbox = {
enable = true; enable = true;
}; };
# Webserver webserver = {
nginx = {
enable = true; enable = true;
sso = {
authKeyFile = secrets."sso/auth-key".path;
users = {
felix = {
passwordHashFile = secrets."sso/felix/password-hash".path;
totpSecretFile = secrets."sso/felix/totp-secret".path;
};
};
groups = {
root = [ "felix" ];
};
};
}; };
acme = { acme = {
enable = true; enable = true;

View file

@ -86,21 +86,8 @@ in
homer = { homer = {
enable = true; enable = true;
}; };
# Webserver webserver = {
nginx = {
enable = true; enable = true;
sso = {
authKeyFile = secrets."sso/auth-key".path;
users = {
felix = {
passwordHashFile = secrets."sso/felix/password-hash".path;
totpSecretFile = secrets."sso/felix/totp-secret".path;
};
};
groups = {
root = [ "felix" ];
};
};
}; };
acme = { acme = {
enable = true; enable = true;

View file

@ -25,8 +25,8 @@ in
acceptTerms = true; acceptTerms = true;
# Use DNS wildcard certificate # Use DNS wildcard certificate
certs = { certs = {
"${config.networking.domain}" = { "${domain}" = {
extraDomainNames = [ "*.${config.networking.domain}" ]; extraDomainNames = [ "*.${domain}" ];
dnsProvider = "inwx"; dnsProvider = "inwx";
inherit (cfg) credentialsFile; inherit (cfg) credentialsFile;
}; };

View file

@ -132,7 +132,7 @@ in
}; };
}; };
my.services.nginx.virtualHosts = [ my.services.webserver.virtualHosts = [
{ {
subdomain = "alerts"; subdomain = "alerts";
inherit (cfg) port; inherit (cfg) port;

View file

@ -28,7 +28,7 @@ in
inherit (cfg) downloadDir; inherit (cfg) downloadDir;
}; };
my.services.nginx.virtualHosts = [ my.services.webserver.virtualHosts = [
{ {
subdomain = "download"; subdomain = "download";
root = "${pkgs.ariang}/share/ariang"; root = "${pkgs.ariang}/share/ariang";

View file

@ -43,7 +43,7 @@ in
]; ];
}; };
my.services.nginx.virtualHosts = [ my.services.webserver.virtualHosts = [
{ {
subdomain = "subtitles"; subdomain = "subtitles";
inherit port; inherit port;

View file

@ -27,7 +27,6 @@
./mumble-server ./mumble-server
./navidrome ./navidrome
./nextcloud ./nextcloud
./nginx
./node-exporter ./node-exporter
./octoprint ./octoprint
./paperless ./paperless
@ -43,5 +42,6 @@
./ssh-server ./ssh-server
./tandoor-recipes ./tandoor-recipes
./vpn ./vpn
./webserver
]; ];
} }

View file

@ -1,5 +1,10 @@
# finance overview # finance overview
{ config, lib, ... }: {
config,
lib,
pkgs,
...
}:
let let
cfg = config.my.services.finance; cfg = config.my.services.finance;
inherit (config.networking) domain; inherit (config.networking) domain;
@ -20,18 +25,26 @@ in
services.firefly-iii = { services.firefly-iii = {
enable = true; enable = true;
virtualHost = "finance"; virtualHost = "finance";
enableNginx = true; user = "caddy";
group = "caddy";
settings = { settings = {
APP_KEY_FILE = cfg.appKeyFile; APP_KEY_FILE = cfg.appKeyFile;
SITE_OWNER = "server@buehler.rocks"; SITE_OWNER = "server@buehler.rocks";
}; };
}; };
services.nginx.virtualHosts."finance" = { my.services.webserver.virtualHosts = [
serverName = "finance.${domain}"; {
forceSSL = true; subdomain = "finance";
useACMEHost = domain; extraConfig = ''
}; file_server
root * "${config.services.firefly-iii.package}/public"
php_fastcgi unix/${config.services.phpfpm.pools."firefly-iii".socket} {
env modHeadersAvailable true
}
'';
}
];
webapps.apps.finance = { webapps.apps.finance = {
dashboard = { dashboard = {

View file

@ -45,17 +45,27 @@ in
enable = true; enable = true;
baseUrl = "https://news.${domain}"; baseUrl = "https://news.${domain}";
inherit (cfg) language passwordFile defaultUser; inherit (cfg) language passwordFile defaultUser;
virtualHost = null;
}; };
# Set up a Nginx virtual host. services.phpfpm.pools.freshrss.settings = {
services.nginx = { "listen.owner" = lib.mkForce config.services.caddy.user;
virtualHosts."freshrss" = { "listen.group" = lib.mkForce config.services.caddy.group;
serverName = "news.${domain}";
forceSSL = true;
useACMEHost = domain;
};
}; };
my.services.webserver.virtualHosts = [
{
subdomain = "news";
extraConfig = ''
root * ${config.services.freshrss.package}/p
php_fastcgi unix/${config.services.phpfpm.pools.freshrss.socket} {
env FRESHRSS_DATA_PATH ${config.services.freshrss.dataDir}
}
file_server
'';
}
];
webapps.apps.freshrss = { webapps.apps.freshrss = {
dashboard = { dashboard = {
name = "News"; name = "News";

View file

@ -66,7 +66,7 @@ in
# Proxy to Gitea # Proxy to Gitea
my.services = { my.services = {
nginx.virtualHosts = [ webserver.virtualHosts = [
{ {
subdomain = "code"; subdomain = "code";
inherit (cfg) port; inherit (cfg) port;

View file

@ -79,7 +79,7 @@ in
]; ];
}; };
my.services.nginx.virtualHosts = [ my.services.webserver.virtualHosts = [
{ {
subdomain = "visualization"; subdomain = "visualization";
inherit (cfg) port; inherit (cfg) port;

View file

@ -76,7 +76,7 @@ in
]; ];
}; };
my.services.nginx.virtualHosts = [ my.services.webserver.virtualHosts = [
{ {
subdomain = "notes"; subdomain = "notes";
inherit (cfg) port; inherit (cfg) port;

View file

@ -145,7 +145,7 @@ in
}; };
}; };
my.services.nginx.virtualHosts = [ my.services.webserver.virtualHosts = [
{ {
subdomain = "automation"; subdomain = "automation";
inherit (cfg) port; inherit (cfg) port;

View file

@ -16,7 +16,7 @@ in
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
my.services.nginx.virtualHosts = [ my.services.webserver.virtualHosts = [
{ {
subdomain = "blog"; subdomain = "blog";
root = inputs.stunkymonkey.packages.${config.nixpkgs.system}.default; root = inputs.stunkymonkey.packages.${config.nixpkgs.system}.default;

View file

@ -26,24 +26,18 @@ in
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
services.nginx.virtualHosts = {
# This is not a subdomain, cannot use my nginx wrapper module
${domain} = {
forceSSL = true;
useACMEHost = domain;
# TODO: 25.05 use stable # TODO: 25.05 use stable
root = pkgs.unstable.homer; services.caddy.virtualHosts.${domain} = {
locations."=/assets/config.yml" = { extraConfig = ''
alias = pkgs.writeText "homerConfig.yml" (builtins.toJSON homeConfig); import common
}; root * ${pkgs.unstable.homer}
}; file_server
# redirect any other attempt to the main site handle_path /assets/config.yml {
"${domain}-redirect" = { root * ${pkgs.writeText "homerConfig.yml" (builtins.toJSON homeConfig)}
forceSSL = true; file_server
}
'';
useACMEHost = domain; useACMEHost = domain;
default = true;
globalRedirect = "${domain}";
};
}; };
webapps = { webapps = {

View file

@ -47,7 +47,7 @@ in
}; };
# sadly the metrics do not contain application specific metrics, only c# -> no dashboard # sadly the metrics do not contain application specific metrics, only c# -> no dashboard
my.services.nginx.virtualHosts = [ my.services.webserver.virtualHosts = [
{ {
subdomain = "media"; subdomain = "media";
inherit port; inherit port;

View file

@ -14,7 +14,7 @@ in
enable = true; enable = true;
}; };
my.services.nginx.virtualHosts = [ my.services.webserver.virtualHosts = [
{ {
subdomain = "view"; subdomain = "view";
inherit (config.services.jellyseerr) port; inherit (config.services.jellyseerr) port;

View file

@ -7,11 +7,11 @@
}: }:
let let
cfg = config.my.services.mumble-server; cfg = config.my.services.mumble-server;
domain = "voice.${config.networking.domain}"; inherit (config.networking) domain;
in in
{ {
options.my.services.mumble-server = { options.my.services.mumble-server = {
enable = lib.mkEnableOption "RSS-Bridge service"; enable = lib.mkEnableOption "mumble server service";
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
@ -19,30 +19,30 @@ in
enable = true; enable = true;
openFirewall = true; openFirewall = true;
welcometext = "Welcome to the Mumble-Server!"; welcometext = "Welcome to the Mumble-Server!";
sslCert = "/var/lib/acme/${domain}/fullchain.pem"; sslCert = "${config.security.acme.certs.${domain}.directory}/fullchain.pem";
sslKey = "/var/lib/acme/${domain}/key.pem"; sslKey = "${config.security.acme.certs.${domain}.directory}/key.pem";
}; };
services.nginx.virtualHosts.${domain}.enableACME = true; # create a separate certificate for the mumble server
security.acme.certs."${domain}" = { security.acme = {
group = "voice-buehler-rocks"; certs.${domain} = {
postRun = '' reloadServices = [ "murmur" ];
if ${pkgs.systemd}/bin/systemctl is-active murmur.service; then group = "caddyandmurmur";
${pkgs.systemd}/bin/systemctl kill -s SIGUSR1 murmur.service
fi
'';
}; };
};
users.groups."voice-buehler-rocks".members = [ users.groups.caddyandmurmur.members = [
"caddy"
"murmur" "murmur"
"nginx"
]; ];
my.services.prometheus.rules = { my.services = {
acme.enable = true;
prometheus.rules = {
mumble_not_running = { mumble_not_running = {
condition = ''systemd_unit_state{name="murmur.service", state!="active"} > 0''; condition = ''systemd_unit_state{name="murmur.service", state!="active"} > 0'';
description = "{{$labels.host}} should have a running {{$labels.name}}"; description = "{{$labels.host}} should have a running {{$labels.name}}";
}; };
}; };
}; };
};
} }

View file

@ -89,7 +89,7 @@ in
}; };
}; };
my.services.nginx.virtualHosts = [ my.services.webserver.virtualHosts = [
{ {
subdomain = "music"; subdomain = "music";
inherit (cfg) port; inherit (cfg) port;
@ -101,7 +101,8 @@ in
name = "Music"; name = "Music";
category = "media"; category = "media";
icon = "music"; icon = "music";
url = "https://music.${domain}/app/#/login"; url = "https://music.${domain}";
method = "get";
}; };
}; };
}; };

View file

@ -97,17 +97,6 @@ in
# ]; # ];
#}; #};
# The service above configures the domain, no need for my wrapper
nginx.virtualHosts."cloud.${domain}" = {
forceSSL = true;
useACMEHost = domain;
# so homer can get the online status
extraConfig = lib.optionalString config.my.services.homer.enable ''
add_header Access-Control-Allow-Origin https://${domain};
'';
};
prometheus.exporters.nextcloud = { prometheus.exporters.nextcloud = {
enable = true; enable = true;
url = "https://cloud.${domain}"; url = "https://cloud.${domain}";
@ -144,6 +133,55 @@ in
# requires = [ "postgresql.service" ]; # requires = [ "postgresql.service" ];
# after = [ "postgresql.service" ]; # after = [ "postgresql.service" ];
#}; #};
services.phpfpm.pools.nextcloud.settings = {
"listen.owner" = config.services.caddy.user;
"listen.group" = config.services.caddy.group;
};
users.groups.nextcloud.members = [
"nextcloud"
config.services.caddy.user
];
my.services.webserver.virtualHosts = [
{
subdomain = "cloud";
extraConfig = ''
redir /.well-known/carddav /remote.php/dav/ 301
redir /.well-known/caldav /remote.php/dav/ 301
@forbidden {
path /.htaccess
path /data/*
path /config/*
path /db_structure
path /.xml
path /README
path /3rdparty/*
path /lib/*
path /templates/*
path /occ
path /console.php
}
respond @forbidden 403
header {
X-Frame-Options "sameorigin"
X-Permitted-Cross-Domain-Policies "none"
}
# TODO: `config.services.nextcloud.package` does not contain additional apps. in nixpkgs there is "nextcloud-with-apps".
# for now we use the path passed to nginx. Can be improved in 25.05 via: https://github.com/NixOS/nixpkgs/pull/376818
root * ${config.services.nginx.virtualHosts."cloud.${domain}".root}
file_server
php_fastcgi unix/${config.services.phpfpm.pools."nextcloud".socket} {
root ${config.services.nginx.virtualHosts."cloud.${domain}".root}
env front_controller_active true
env modHeadersAvailable true
}
'';
}
];
my.services.backup = { my.services.backup = {
exclude = [ exclude = [

View file

@ -1,452 +0,0 @@
# A simple abstraction layer for almost all of my services' needs
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.services.nginx;
virtualHostOption =
with lib;
types.submodule {
options = {
subdomain = mkOption {
type = types.str;
example = "dev";
description = ''
Which subdomain, under config.networking.domain, to use
for this virtual host.
'';
};
port = mkOption {
type = with types; nullOr port;
default = null;
example = 8080;
description = ''
Which port to proxy to, through localhost, for this virtual host.
This option is incompatible with `root`.
'';
};
root = mkOption {
type = with types; nullOr path;
default = null;
example = "/var/www/blog";
description = ''
The root folder for this virtual host. This option is incompatible
with `port`.
'';
};
sso = {
enable = mkEnableOption "SSO authentication";
};
extraConfig = mkOption {
type = types.attrs; # FIXME: forward type of virtualHosts
example = literalExpression ''
{
locations."/socket" = {
proxyPass = "http://localhost:8096/";
proxyWebsockets = true;
};
}
'';
default = { };
description = ''
Any extra configuration that should be applied to this virtual host.
'';
};
};
};
in
{
imports = [ ./sso ];
options.my.services.nginx = with lib; {
enable = mkEnableOption "Nginx";
acme = {
credentialsFile = mkOption {
type = types.str;
example = "/var/lib/acme/creds.env";
description = ''
INWX API key file as an 'EnvironmentFile' (see `systemd.exec(5)`)
'';
};
};
virtualHosts = mkOption {
type = types.listOf virtualHostOption;
default = [ ];
example = literalExpression ''
[
{
subdomain = "gitea";
port = 8080;
}
{
subdomain = "dev";
root = "/var/www/dev";
}
{
subdomain = "jellyfin";
port = 8096;
extraConfig = {
locations."/socket" = {
proxyPass = "http://localhost:8096/";
proxyWebsockets = true;
};
};
}
]
'';
description = ''
List of virtual hosts to set-up using default settings.
'';
};
sso = {
authKeyFile = mkOption {
type = types.str;
example = "/var/lib/nginx-sso/auth-key.txt";
description = ''
Path to the auth key.
'';
};
subdomain = mkOption {
type = types.str;
default = "login";
example = "auth";
description = "Which subdomain, to use for SSO.";
};
port = mkOption {
type = types.port;
default = 8082;
example = 8080;
description = "Port to use for internal webui.";
};
users = mkOption {
type = types.attrsOf (
types.submodule {
options = {
passwordHashFile = mkOption {
type = types.str;
example = "/var/lib/nginx-sso/alice/password-hash.txt";
description = "Path to file containing the user's password hash.";
};
totpSecretFile = mkOption {
type = types.str;
example = "/var/lib/nginx-sso/alice/totp-secret.txt";
description = "Path to file containing the user's TOTP secret.";
};
};
}
);
example = literalExpression ''
{
alice = {
passwordHashFile = "/var/lib/nginx-sso/alice/password-hash.txt";
totpSecretFile = "/var/lib/nginx-sso/alice/totp-secret.txt";
};
}
'';
description = "Definition of users";
};
groups = mkOption {
type = with types; attrsOf (listOf str);
example = literalExpression ''
{
root = [ "alice" ];
users = [ "alice" "bob" ];
}
'';
description = "Groups of users";
};
};
};
config = lib.mkIf cfg.enable {
assertions = lib.flip builtins.map cfg.virtualHosts (
{ subdomain, ... }@args:
let
conflicts = [
"port"
"root"
];
optionsNotNull = builtins.map (v: args.${v} != null) conflicts;
optionsSet = lib.filter lib.id optionsNotNull;
in
{
assertion = builtins.length optionsSet == 1;
message = ''
Subdomain '${subdomain}' must have exactly one of ${
lib.concatStringsSep ", " (builtins.map (v: "'${v}'") conflicts)
} configured.
'';
}
)
# ++ (
# let
# ports = lib.my.mapFilter
# (v: v != null)
# ({ port, ... }: port)
# cfg.virtualHosts;
# lib.unique ports;
# lib.compareLists ports
# portCounts = lib.my.countValues ports;
# nonUniquesCounts = lib.filterAttrs (_: v: v != 1) portCounts;
# nonUniques = builtins.attrNames nonUniquesCounts;
# mkAssertion = port: {
# assertion = false;
# message = "Port ${port} cannot appear in multiple virtual hosts.";
# };
# in
# map mkAssertion nonUniques
# ) ++ (
# let
# subs = map ({ subdomain, ... }: subdomain) cfg.virtualHosts;
# subsCounts = lib.my.countValues subs;
# nonUniquesCounts = lib.filterAttrs (_: v: v != 1) subsCounts;
# nonUniques = builtins.attrNames nonUniquesCounts;
# mkAssertion = v: {
# assertion = false;
# message = ''
# Subdomain '${v}' cannot appear in multiple virtual hosts.
# '';
# };
# in
# map mkAssertion nonUniques
# )
;
services = {
nginx = {
enable = true;
statusPage = true; # For monitoring scraping.
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedTlsSettings = true;
recommendedProxySettings = true;
recommendedBrotliSettings = true;
recommendedZstdSettings = true;
# Only allow PFS-enabled ciphers with AES256
sslCiphers = "AES256+EECDH:AES256+EDH:!aNULL";
commonHttpConfig = ''
# Add HSTS header with preloading to HTTPS requests.
# Adding this header to HTTP requests is discouraged
map $scheme $hsts_header {
https "max-age=31536000; includeSubdomains; preload";
}
add_header Strict-Transport-Security $hsts_header;
# CORS header
# some applications set it to wildcard, therefore this overrides it
proxy_hide_header Access-Control-Allow-Origin;
add_header Access-Control-Allow-Origin https://${config.networking.domain};
# Minimize information leaked to other domains
add_header 'Referrer-Policy' 'strict-origin-when-cross-origin';
# Disable embedding as a frame
add_header X-Frame-Options DENY;
# Prevent injection of code in other mime types (XSS Attacks)
add_header X-Content-Type-Options nosniff;
# Enable XSS protection of the browser.
# May be unnecessary when CSP is configured properly (see above)
add_header X-XSS-Protection "1; mode=block";
# This might create errors
proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
# Enable CSP for your services.
#add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always;
'';
virtualHosts =
let
genAttrs' = values: f: lib.listToAttrs (map f values);
inherit (config.networking) domain;
mkVHost =
{ subdomain, ... }@args:
lib.nameValuePair "${subdomain}.${domain}" (
lib.foldl lib.recursiveUpdate { } [
# Base configuration
{
forceSSL = true;
useACMEHost = domain;
}
# Proxy to port
(lib.optionalAttrs (args.port != null) {
locations."/".proxyPass = "http://localhost:${toString args.port}";
})
# Serve filesystem content
(lib.optionalAttrs (args.root != null) { inherit (args) root; })
# VHost specific configuration
args.extraConfig
# SSO configuration
(lib.optionalAttrs args.sso.enable {
extraConfig =
(args.extraConfig.extraConfig or "")
+ ''
error_page 401 = @error401;
'';
locations = {
"@error401".return = ''
302 https://${cfg.sso.subdomain}.${config.networking.domain}/login?go=$scheme://$http_host$request_uri
'';
"/" = {
extraConfig =
(args.extraConfig.locations."/".extraConfig or "")
+ ''
# Use SSO
auth_request /sso-auth;
# Set username through header
auth_request_set $username $upstream_http_x_username;
proxy_set_header X-User $username;
# Renew SSO cookie on request
auth_request_set $cookie $upstream_http_set_cookie;
add_header Set-Cookie $cookie;
'';
};
"/sso-auth" = {
proxyPass = "http://localhost:${toString cfg.sso.port}/auth";
extraConfig = ''
# Do not allow requests from outside
internal;
# Do not forward the request body
proxy_pass_request_body off;
proxy_set_header Content-Length "";
# Set X-Application according to subdomain for matching
proxy_set_header X-Application "${subdomain}";
# Set origin URI for matching
proxy_set_header X-Origin-URI $request_uri;
'';
};
};
})
]
);
in
genAttrs' cfg.virtualHosts mkVHost;
sso = {
enable = true;
configuration = {
listen = {
addr = "localhost";
inherit (cfg.sso) port;
};
audit_log = {
target = [ "fd://stdout" ];
events = [
"access_denied"
"login_success"
"login_failure"
"logout"
"validate"
];
headers = [
"x-origin-uri"
"x-application"
];
};
cookie = {
domain = ".${config.networking.domain}";
secure = true;
authentication_key = {
_secret = cfg.sso.authKeyFile;
};
};
login = {
title = "Bühlers's SSO";
default_method = "simple";
hide_mfa_field = false;
names = {
simple = "Username / Password";
};
};
providers = {
simple =
let
applyUsers = lib.flip lib.mapAttrs cfg.sso.users;
in
{
users = applyUsers (_: v: { _secret = v.passwordHashFile; });
mfa = applyUsers (
_: v: [
{
provider = "totp";
attributes = {
secret = {
_secret = v.totpSecretFile;
};
};
}
]
);
inherit (cfg.sso) groups;
};
};
acl = {
rule_sets = [
{
rules = [
{
field = "x-application";
present = true;
}
];
allow = [ "@root" ];
}
];
};
};
};
};
# services.prometheus = lib.mkIf cfg.monitoring.enable {
prometheus = {
exporters.nginx.enable = true;
scrapeConfigs = [
{
job_name = "nginx";
static_configs = [
{
targets = [ "localhost:${toString config.services.prometheus.exporters.nginx.port}" ];
labels = {
instance = config.networking.hostName;
};
}
];
}
];
};
grafana.provision = {
dashboards.settings.providers = [
{
name = "Nginx";
options.path = pkgs.grafana-dashboards.nginx;
disableDeletion = true;
}
];
};
};
my.services.nginx.virtualHosts = [
{
subdomain = "login";
inherit (cfg.sso) port;
}
];
my.services.backup = {
exclude = [
# fails often because the file changed
"/var/log/nginx/access.log"
];
};
networking.firewall.allowedTCPPorts = [
80
443
];
# Nginx needs to be able to read the certificates
users.users.nginx.extraGroups = [ "acme" ];
};
}

View file

@ -1,94 +0,0 @@
# I must override the module to allow having runtime secrets
{
config,
lib,
pkgs,
utils,
...
}:
let
cfg = config.services.nginx.sso;
pkg = lib.getBin cfg.package;
confPath = "/var/lib/nginx-sso/config.json";
in
{
disabledModules = [ "services/security/nginx-sso.nix" ];
options.services.nginx.sso = with lib; {
enable = mkEnableOption "nginx-sso service";
package = mkOption {
type = types.package;
default = pkgs.nginx-sso;
defaultText = "pkgs.nginx-sso";
description = ''
The nginx-sso package that should be used.
'';
};
configuration = mkOption {
type = types.attrsOf types.unspecified;
default = { };
example = literalExpression ''
{
listen = { addr = "localhost"; port = 8080; };
providers.token.tokens = {
myuser = "MyToken";
};
acl = {
rule_sets = [
{
rules = [ { field = "x-application"; equals = "MyApp"; } ];
allow = [ "myuser" ];
}
];
};
}
'';
description = ''
nginx-sso configuration
(<link xlink:href="https://github.com/Luzifer/nginx-sso/wiki/Main-Configuration">documentation</link>)
as a Nix attribute set.
'';
};
};
config = lib.mkIf cfg.enable {
systemd.services.nginx-sso = {
description = "Nginx SSO Backend";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
StateDirectory = "nginx-sso";
WorkingDirectory = "/var/lib/nginx-sso";
# The files to be merged might not have the correct permissions
ExecStartPre = ''+${pkgs.writeScript "merge-nginx-sso-config" ''
#!${pkgs.bash}/bin/bash
rm -f '${confPath}'
${utils.genJqSecretsReplacementSnippet cfg.configuration confPath}
# Fix permissions
chown nginx-sso:nginx-sso ${confPath}
chmod 0600 ${confPath}
''}'';
ExecStart = lib.mkForce ''
${pkg}/bin/nginx-sso \
--config ${confPath} \
--frontend-dir ${pkg}/share/frontend
'';
Restart = "always";
User = "nginx-sso";
Group = "nginx-sso";
};
};
users.users.nginx-sso = {
isSystemUser = true;
group = "nginx-sso";
};
users.groups.nginx-sso = { };
};
}

View file

@ -45,7 +45,7 @@ in
# monitoring is not really useful, because it only contains the http-worker infos -> skipped for now # monitoring is not really useful, because it only contains the http-worker infos -> skipped for now
my.services.nginx.virtualHosts = [ my.services.webserver.virtualHosts = [
{ {
subdomain = "docs"; subdomain = "docs";
inherit (cfg) port; inherit (cfg) port;

View file

@ -21,7 +21,7 @@ in
inherit (cfg) port; inherit (cfg) port;
}; };
my.services.nginx.virtualHosts = [ my.services.webserver.virtualHosts = [
{ {
subdomain = "passworts"; subdomain = "passworts";
inherit (cfg) port; inherit (cfg) port;

View file

@ -85,24 +85,10 @@ in
]; ];
}; };
my.services.nginx.virtualHosts = [ my.services.webserver.virtualHosts = [
{ {
subdomain = "photos"; subdomain = "photos";
inherit (cfg) port; inherit (cfg) port;
extraConfig = {
locations."/" = {
proxyWebsockets = true;
extraConfig = ''
# Allow large file uploads
client_max_body_size 1G;
# Configure timeout
proxy_read_timeout 600s;
proxy_send_timeout 600s;
send_timeout 600s;
'';
};
};
} }
]; ];

View file

@ -188,7 +188,7 @@ in
}; };
}; };
nginx.virtualHosts = [ webserver.virtualHosts = [
{ {
subdomain = "monitor"; subdomain = "monitor";
inherit (cfg) port; inherit (cfg) port;

View file

@ -83,7 +83,7 @@ in
}; };
}; };
my.services.nginx.virtualHosts = [ my.services.webserver.virtualHosts = [
{ {
subdomain = "log"; subdomain = "log";
inherit (cfg) port; inherit (cfg) port;

View file

@ -43,7 +43,7 @@ in
]; ];
}; };
my.services.nginx.virtualHosts = [ my.services.webserver.virtualHosts = [
{ {
subdomain = "indexer"; subdomain = "indexer";
inherit port; inherit port;

View file

@ -43,7 +43,7 @@ in
]; ];
}; };
my.services.nginx.virtualHosts = [ my.services.webserver.virtualHosts = [
{ {
subdomain = "movies"; subdomain = "movies";
inherit port; inherit port;

View file

@ -1,5 +1,10 @@
# Get RSS feeds from websites that don't natively have one # Get RSS feeds from websites that don't natively have one
{ config, lib, ... }: {
config,
lib,
pkgs,
...
}:
let let
cfg = config.my.services.rss-bridge; cfg = config.my.services.rss-bridge;
domain = "rss-bridge.${config.networking.domain}"; domain = "rss-bridge.${config.networking.domain}";
@ -13,13 +18,23 @@ in
services.rss-bridge = { services.rss-bridge = {
enable = true; enable = true;
config.system.enabled_bridges = [ "*" ]; # Whitelist all config.system.enabled_bridges = [ "*" ]; # Whitelist all
virtualHost = domain; virtualHost = null;
user = "caddy";
group = "caddy";
}; };
services.nginx.virtualHosts.${domain} = { my.services.webserver.virtualHosts = [
forceSSL = true; {
enableACME = true; subdomain = "rss-bridge";
}; extraConfig = ''
root * ${pkgs.rss-bridge}
php_fastcgi unix/${config.services.phpfpm.pools."rss-bridge".socket} {
env RSSBRIDGE_fileCache_path ${config.services.rss-bridge.dataDir}/cache/
}
file_server
'';
}
];
webapps.apps.rss-bridge = { webapps.apps.rss-bridge = {
dashboard = { dashboard = {

View file

@ -49,7 +49,7 @@ in
]; ];
}; };
my.services.nginx.virtualHosts = [ my.services.webserver.virtualHosts = [
{ {
subdomain = "series"; subdomain = "series";
inherit port; inherit port;

View file

@ -22,7 +22,7 @@ in
}; };
# Proxy to Tandoor-Recipes # Proxy to Tandoor-Recipes
my.services.nginx.virtualHosts = [ my.services.webserver.virtualHosts = [
{ {
subdomain = "recipes"; subdomain = "recipes";
inherit (cfg) port; inherit (cfg) port;

View file

@ -53,17 +53,10 @@ in
# Proxy to Headscale # Proxy to Headscale
my.services = { my.services = {
nginx.virtualHosts = [ webserver.virtualHosts = [
{ {
subdomain = "vpn"; subdomain = "vpn";
inherit (cfg) port; inherit (cfg) port;
extraConfig = {
locations = {
"/" = {
proxyWebsockets = true;
};
};
};
} }
]; ];

View file

@ -0,0 +1,181 @@
# public webserver with reverseproxy
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.services.webserver;
inherit (config.networking) domain;
virtualHostOption = lib.types.submodule {
options = {
subdomain = lib.mkOption {
type = lib.types.str;
example = "dev";
description = ''
Which subdomain, under config.networking.domain, to use
for this virtual host.
'';
};
port = lib.mkOption {
type = with lib.types; nullOr port;
default = null;
example = 8080;
description = ''
Which port to proxy to, through localhost, for this virtual host.
This option is incompatible with `root`.
'';
};
root = lib.mkOption {
type = with lib.types; nullOr path;
default = null;
example = "/var/www/blog";
description = ''
The root folder for this virtual host. This option is incompatible
with `port`.
'';
};
extraConfig = lib.mkOption {
type = with lib.types; nullOr lines;
example = lib.literalExpression ''
{
locations."/socket" = {
proxyPass = "http://localhost:8096/";
proxyWebsockets = true;
};
}
'';
default = null;
description = ''
Any extra configuration that should be applied to this virtual host.
'';
};
};
};
in
{
options.my.services.webserver = {
enable = lib.mkEnableOption "webserver";
virtualHosts = lib.mkOption {
type = lib.types.listOf virtualHostOption;
default = [ ];
example = lib.literalExpression ''
[
{
subdomain = "gitea";
port = 8080;
}
{
subdomain = "dev";
root = "/var/www/dev";
}
{
subdomain = "jellyfin";
port = 8096;
extraConfig = {
locations."/socket" = {
proxyPass = "http://localhost:8096/";
proxyWebsockets = true;
};
};
}
]
'';
description = ''
List of virtual hosts to set-up using default settings.
'';
};
};
config = lib.mkIf cfg.enable {
services = {
nginx.enable = false;
caddy = {
enable = true;
email = "server@buehler.rocks";
globalConfig = ''
servers{
metrics
}
'';
extraConfig = ''
(compress) {
encode gzip zstd
}
(headers) {
header {
# enable CORS
Access-Control-Allow-Origin "https://${config.networking.domain}"
# disable FLoC tracking
Permissions-Policy interest-cohort=()
# enable HSTS
Strict-Transport-Security max-age=31536000;
# disable clients from sniffing the media type
X-Content-Type-Options "nosniff"
# clickjacking protection
X-Frame-Options "DENY"
# enable XSS protection
X-XSS-Protection "1; mode=block"
# referrer policy
Referrer-Policy "strict-origin-when-cross-origin"
}
}
(common) {
import headers
import compress
}
'';
virtualHosts =
let
mkVHost =
{ subdomain, ... }@args:
lib.nameValuePair "${subdomain}.${domain}" (
lib.foldl lib.recursiveUpdate { } [
{
useACMEHost = domain;
extraConfig = ''
import common
${lib.optionalString (args.root != null) ''
root * ${args.root}
file_server
''}
${lib.optionalString (args.port != null) ''
reverse_proxy localhost:${toString args.port} {
# remove CORS headers from proxied server, because duplicate headers are not allowed
# remove after new release: https://github.com/navidrome/navidrome/commit/657fe11f5327ff7a3cb6aa9308b0bb7c71eea5c6
header_down -Access-Control-Allow-Origin
}
''}
${lib.optionalString (args.extraConfig != null) args.extraConfig}
'';
}
]
);
in
lib.listToAttrs (map mkVHost cfg.virtualHosts);
};
prometheus.scrapeConfigs = [
{
job_name = "caddy";
static_configs = [
{
targets = [ "localhost:2019" ];
labels.instance = config.networking.hostName;
}
];
}
];
};
networking.firewall.allowedTCPPorts = [
80
443
];
};
}