yumeami/modules/nixos/services/networking/httpd/default.nix
2026-05-31 12:34:09 -05:00

283 lines
7.4 KiB
Nix

{
config,
inputs,
lib,
namespace,
...
}: let
inherit (lib) mkIf mkEnableOption mapAttrsToList listToAttrs replaceStrings mkMerge concatStringsSep filterAttrs;
inherit (lib.${namespace}) getAttrByNamespace mkOptionsWithNamespace readJsonOrEmpty getIn resolveHostIP flattenHostServices;
inherit (config.networking) hostName;
base = "${namespace}.services.networking.httpd";
cfg = getAttrByNamespace config base;
networkingCfg = getAttrByNamespace config "${namespace}.services.networking";
miasmaCfg = getAttrByNamespace config "${namespace}.services.networking.miasma";
networkServices = networkingCfg.network-services;
networkServicesFlat = flattenHostServices networkServices;
crypt = "${inputs.self}/secrets/crypt";
mkLocalhostConfig = name: service: let
p = toString service.port;
in ''
# --- ${name} (localhost access) ---
RewriteCond %{HTTP_HOST} ^${name}\.localhost$ [NC]
RewriteRule ^/(.*) http://localhost:${p}/$1 [P,L]
ProxyPassReverse / http://localhost:${p}/
'';
mkVirtualHost = {
domain,
useSSL,
injectHoneypot ? false,
}: host: name: service: let
inherit (config.sops) secrets;
hostIP = resolveHostIP networkingCfg.devices host;
p = toString service.port;
certs = replaceStrings ["*" "."] ["wildcard" "_"] domain;
sslConfig =
if useSSL
then {
forceSSL = true;
sslServerCert = secrets."ssl/${certs}/cert".path;
sslServerKey = secrets."ssl/${certs}/key".path;
}
else {};
miasma = networkServicesFlat.miasma;
honeypotConfig =
if injectHoneypot
then ''
AddOutputFilterByType SUBSTITUTE text/html
SubstituteMaxLineLength 30m
Substitute 's|</body>|<a href="${miasmaCfg.linkPrefix}/" style="display:none" aria-hidden="true" tabindex="-1">Amazing high quality data here!</a></body>|i'
ProxyPass ${miasmaCfg.linkPrefix} http://${resolveHostIP networkingCfg.devices miasma.host}:${toString miasma.port}/
ProxyPassReverse ${miasmaCfg.linkPrefix} http://${resolveHostIP networkingCfg.devices miasma.host}:${toString miasma.port}/
''
else "";
robotsConfig =
if injectHoneypot
then ''
ProxyPass /robots.txt !
Alias /robots.txt ${inputs.dotfiles}/httpd/robots/${name}.txt
''
else "";
in {
name = replaceStrings ["*"] [name] domain;
value =
{
acmeRoot = null;
documentRoot = "/var/empty";
extraConfig = ''
UseCanonicalName Off
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 5
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Port "443"
RequestHeader set X-Forwarded-For %{REMOTE_ADDR}s
RewriteEngine On
ProxyPreserveHost On
${honeypotConfig}
${robotsConfig}
# --- ${name} (subdomain access) ---
ProxyPass / http://${hostIP}:${p}/
ProxyPassReverse / http://${hostIP}:${p}/
'';
}
// sslConfig;
};
in {
options = mkOptionsWithNamespace base {
enable = mkEnableOption "Apache HTTPD";
};
config = mkIf cfg.enable {
services.httpd = {
enable = true;
extraModules = [
"filter"
"proxy"
"proxy_http"
"rewrite"
"substitute"
];
extraConfig = ''
ErrorDocument 400 /400.html
ErrorDocument 401 /401.html
ErrorDocument 403 /403.html
ErrorDocument 404 /404.html
ErrorDocument 500 /500.html
'';
virtualHosts = mkMerge [
{
"_default_" = {
acmeRoot = null;
documentRoot = "/var/empty";
servedDirs = [
{
dir = "/var/www/error";
urlPath = "/";
}
];
extraConfig = ''
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/(400|401|403|404|500)\.html$
RewriteRule ^ - [L,R=404]
'';
};
}
{
"_default_" = let
inherit (config.sops) secrets;
in {
acmeRoot = null;
documentRoot = "/var/empty";
servedDirs = [
{
dir = "/var/www/error";
urlPath = "/";
}
];
extraConfig = ''
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/(400|401|403|404|500)\.html$
RewriteRule ^ - [L,R=404]
'';
forceSSL = true;
sslServerCert = secrets."ssl/wildcard_cpatino_com/cert".path;
sslServerKey = secrets."ssl/wildcard_cpatino_com/key".path;
};
}
{
"localhost" = let
localhostProxyConfig =
networkServices.${hostName}
|> mapAttrsToList mkLocalhostConfig
|> concatStringsSep "\n";
in {
acmeRoot = null;
serverAliases = ["*.localhost"];
documentRoot = "/var/empty";
servedDirs = [
{
dir = "/var/www/error";
urlPath = "/";
}
];
extraConfig = ''
UseCanonicalName Off
RewriteEngine On
${localhostProxyConfig}
RewriteCond %{REQUEST_URI} !^/(400|401|403|404|500)\.html$
RewriteRule ^ - [L,R=404]
'';
};
}
(networkServices.${hostName}
|> filterAttrs (_: svc: svc.internal)
|> mapAttrsToList (name: svc:
(mkVirtualHost {
domain = "*.yumeami.sh";
useSSL = true;
})
hostName
name
svc)
|> listToAttrs)
(mkIf (builtins.elem hostName networkingCfg.gateways) (
networkServicesFlat
|> filterAttrs (_: svc: svc.public)
|> mapAttrsToList (name: svc:
(mkVirtualHost {
domain = "*.cpatino.com";
useSSL = true;
injectHoneypot = true;
})
svc.host
name
svc)
|> listToAttrs
))
];
};
security.acme = {
acceptTerms = true;
defaults.email = "c4patino@gmail.com";
defaults = {
dnsProvider = "cloudflare";
dnsProviderConfig = let
secrets = readJsonOrEmpty "${crypt}/secrets.json";
in {
CF_Token = getIn secrets "cloudflare";
};
};
};
networking.firewall.allowedTCPPorts = [80 443];
systemd.tmpfiles.rules = [
"L+ /var/www 555 root root - ${inputs.dotfiles + "/httpd"}"
];
sops.secrets = let
inherit (config.networking) hostName;
inherit (config.users.users) wwwrun;
global =
[
"ssl/wildcard_cpatino_com/cert"
"ssl/wildcard_cpatino_com/key"
]
|> map (name: {
inherit name;
value = {
owner = wwwrun.name;
group = wwwrun.group;
};
});
hostSpecific =
[
"ssl/wildcard_yumeami_sh/cert"
"ssl/wildcard_yumeami_sh/key"
]
|> map (name: {
inherit name;
value = {
sopsFile = "${inputs.self}/secrets/sops/${hostName}.yaml";
owner = wwwrun.name;
group = wwwrun.group;
};
});
in
listToAttrs (global ++ hostSpecific);
};
}