From 045f15239ac1ed879afa5ac506591dca2f26e574 Mon Sep 17 00:00:00 2001 From: oddlama Date: Sat, 25 May 2024 15:49:41 +0200 Subject: [PATCH] feat: adguardhome use DoT; fix loki home proxy; allow arbitrary telegraf secrets --- hosts/sire/default.nix | 3 - hosts/sire/guests/loki.nix | 9 +- hosts/ward/guests/adguardhome.nix | 9 +- hosts/ward/guests/web-proxy.nix | 66 +++++++++ hosts/ward/net.nix | 22 ++- .../secrets/web-proxy/github-access-token.age | Bin 0 -> 429 bytes modules/oa2p.nix | 139 ------------------ modules/oauth2-proxy.nix | 1 - modules/telegraf.nix | 30 +++- ...4cc24a93a8f0382290-github-access-token.age | 8 + users/myuser/secrets/user.nix.age | Bin 4928 -> 5039 bytes 11 files changed, 114 insertions(+), 173 deletions(-) create mode 100644 hosts/ward/secrets/web-proxy/github-access-token.age delete mode 100644 modules/oa2p.nix create mode 100644 secrets/rekeyed/ward-web-proxy/248016a36e45ca4cc24a93a8f0382290-github-access-token.age diff --git a/hosts/sire/default.nix b/hosts/sire/default.nix index 3c8f5b7..192d909 100644 --- a/hosts/sire/default.nix +++ b/hosts/sire/default.nix @@ -42,9 +42,6 @@ }; }; - # TODO track my github stats - # services.telegraf.extraConfig.inputs.github = {}; - guests = let mkGuest = guestName: { enableStorageDataset ? false, diff --git a/hosts/sire/guests/loki.nix b/hosts/sire/guests/loki.nix index 6f8b517..600409d 100644 --- a/hosts/sire/guests/loki.nix +++ b/hosts/sire/guests/loki.nix @@ -1,12 +1,10 @@ { config, - lib, nodes, ... }: let sentinelCfg = nodes.sentinel.config; wardWebProxyCfg = nodes.ward-web-proxy.config; - wardCfg = nodes.ward.config; lokiDomain = "loki.${config.repo.secrets.global.domains.me}"; in { wireguard.proxy-sentinel = { @@ -44,14 +42,11 @@ in { proxyWebsockets = true; extraConfig = '' auth_basic "Authentication required"; - auth_basic_user_file ${wardWebProxyCfg.age.secrets.loki-basic-auth-hashes.path}; + auth_basic_user_file ${sentinelCfg.age.secrets.loki-basic-auth-hashes.path}; proxy_read_timeout 1800s; proxy_connect_timeout 1600s; - ${lib.concatMapStrings (ip: "allow ${ip};\n") wardCfg.wireguard.proxy-home.server.reservedAddresses} - deny all; - access_log off; ''; }; @@ -89,7 +84,7 @@ in { proxyWebsockets = true; extraConfig = '' auth_basic "Authentication required"; - auth_basic_user_file ${sentinelCfg.age.secrets.loki-basic-auth-hashes.path}; + auth_basic_user_file ${wardWebProxyCfg.age.secrets.loki-basic-auth-hashes.path}; proxy_read_timeout 1800s; proxy_connect_timeout 1600s; diff --git a/hosts/ward/guests/adguardhome.nix b/hosts/ward/guests/adguardhome.nix index 88483ea..fbe92e5 100644 --- a/hosts/ward/guests/adguardhome.nix +++ b/hosts/ward/guests/adguardhome.nix @@ -59,12 +59,11 @@ in { # allowed_clients = [ # ]; #trusted_proxies = []; - ratelimit = 60; + ratelimit = 300; upstream_dns = [ - "1.1.1.1" - # FIXME: enable ipv6 "2606:4700:4700::1111" - "8.8.8.8" - # FIXME: enable ipv6 "2001:4860:4860::8844" + "https://dns.cloudflare.com/dns-query" + "https://dns.google/dns-query" + "https://doh.mullvad.net/dns-query" ]; bootstrap_dns = [ "1.1.1.1" diff --git a/hosts/ward/guests/web-proxy.nix b/hosts/ward/guests/web-proxy.nix index 1f7c78a..cb9d5e6 100644 --- a/hosts/ward/guests/web-proxy.nix +++ b/hosts/ward/guests/web-proxy.nix @@ -33,6 +33,72 @@ in { inherit (acme) certs wildcardDomains; }; + age.secrets.github-access-token = { + rekeyFile = config.node.secretsDir + "/github-access-token.age"; + mode = "440"; + group = "telegraf"; + }; + + meta.telegraf.secrets."@GITHUB_ACCESS_TOKEN@" = config.age.secrets.github-access-token.path; + services.telegraf.extraConfig.inputs = { + ping = [ + { + method = "native"; + urls = [ + "192.168.178.1" + "192.168.1.1" + ]; + tags.type = "internal"; + fieldpass = [ + "percent_packet_loss" + "average_response_ms" + "standard_deviation_ms" + "reply_received" + "percent_reply_loss" + ]; + } + { + method = "native"; + urls = [ + "1.1.1.1" + "8.8.8.8" + config.repo.secrets.global.domains.me + config.repo.secrets.global.domains.personal + ]; + tags.type = "external"; + fieldpass = [ + "percent_packet_loss" + "average_response_ms" + "standard_deviation_ms" + "reply_received" + "percent_reply_loss" + ]; + } + ]; + + # FIXME: pls define this on the relevant hosts. Then we can ping it from multiple other hosts + #http_response = [ + # { + # urls = [ + # ]; + # response_string_match = "Index of /"; + # response_status_code = 200; + # } + #]; + + github = { + access_token = "@GITHUB_ACCESS_TOKEN@"; + repositories = [ + "oddlama/agenix-rekey" + "oddlama/autokernel" + "oddlama/gentoo-install" + "oddlama/nix-config" + "oddlama/nix-topology" + "oddlama/vane" + ]; + }; + }; + services.nginx = { upstreams.fritzbox = { servers."192.168.178.1" = {}; diff --git a/hosts/ward/net.nix b/hosts/ward/net.nix index cc8d190..fdf8705 100644 --- a/hosts/ward/net.nix +++ b/hosts/ward/net.nix @@ -75,25 +75,23 @@ in { IPForward = "yes"; IPv6PrivacyExtensions = "yes"; IPv6SendRA = true; + IPv6AcceptRA = false; + DHCPPrefixDelegation = true; MulticastDNS = true; }; # Announce a static prefix ipv6Prefixes = [ {ipv6PrefixConfig.Prefix = lanCidrv6;} ]; - # Delegate prefix from wan - #dhcpPrefixDelegationConfig = { - # UplinkInterface = "wan"; - # Announce = true; - # SubnetId = "auto"; - #}; + # Delegate prefix + dhcpPrefixDelegationConfig = { + SubnetId = "22"; + }; # Provide a DNS resolver - # TODO ipv6SendRAConfig = { - # TODO EmitDNS = true; - # TODO # TODO change to self later - # TODO #DNS = lib.net.cidr.host 1 net.lan.ipv6cidr; - # TODO DNS = ["2606:4700:4700::1111" "2001:4860:4860::8888"]; - # TODO }; + ipv6SendRAConfig = { + EmitDNS = true; + DNS = lib.net.cidr.host 3 lanCidrv6; + }; linkConfig.RequiredForOnline = "routable"; }; # Remaining macvtap interfaces should not be touched. diff --git a/hosts/ward/secrets/web-proxy/github-access-token.age b/hosts/ward/secrets/web-proxy/github-access-token.age new file mode 100644 index 0000000000000000000000000000000000000000..9a196bca90ab7934ff54065f8a14de1f83abe127 GIT binary patch literal 429 zcmWm7J&V&|003aYEu-Kn0a1jKYm-a!rk=A?-T_RX7*CTWvg(!98c zg3F1EliUw*aS%an7CAv2ba3nHBHYx;L6m#`z~dlD!FZNd%iK@mog_u|f+ncPq|G#& zbc-zP6UD(EUaG8fQo~AUtq;AR=x2hJxIIud#$%IjIC|=b#BPx1=1S3hHRq>&MTczx zRraD|n10phiEscycW4S&#pU&|^b2aqG&yp;G9W)Bg@R>6Z*m$)qJefbI#z}m>e{j^ zuO@LV2xT7fK@4W<++v6WFOsJt3-|IkSt&6rTb2)sgAhnnn~vyQFGoTFKu{k^*Vi6} zQs`umoIly$zsnFYss>rsO|TVWgR7bvkYO)jvcXIW5E|z|ENfFeJl&nI=~6QmzSQq5qKQQOHXpEW9`0S- z-rSF!kAGkO>0UkhaQyDoH~GiCn_&C4_;UJQI6FUn@$>T2*Ny!B^X6%I`Fty#UHrPT QJ@@tboqx}EZr#{||G!V0o&W#< literal 0 HcmV?d00001 diff --git a/modules/oa2p.nix b/modules/oa2p.nix deleted file mode 100644 index 5c8b87a..0000000 --- a/modules/oa2p.nix +++ /dev/null @@ -1,139 +0,0 @@ -{ - config, - lib, - ... -}: let - cfg = config.services.oauth2-proxy.nginx; -in { - disabledModules = ["services/security/oauth2-proxy-nginx.nix"]; - options.services.oauth2-proxy.nginx = { - proxy = lib.mkOption { - type = lib.types.str; - default = config.services.oauth2-proxy.httpAddress; - defaultText = lib.literalExpression "config.services.oauth2-proxy.httpAddress"; - description = '' - The address of the reverse proxy endpoint for oauth2-proxy - ''; - }; - - domain = lib.mkOption { - type = lib.types.str; - description = '' - The domain under which the oauth2-proxy will be accesible and the path of cookies are set to. - This setting must be set to ensure back-redirects are working properly - if oauth2-proxy is configured with {option}`services.oauth2-proxy.cookie.domain` - or multiple {option}`services.oauth2-proxy.nginx.virtualHosts` that are not on the same domain. - ''; - }; - - virtualHosts = lib.mkOption { - type = let - vhostSubmodule = lib.types.submodule { - options = { - allowed_groups = lib.mkOption { - type = lib.types.nullOr (lib.types.listOf lib.types.str); - description = "List of groups to allow access to this vhost, or null to allow all."; - default = null; - }; - allowed_emails = lib.mkOption { - type = lib.types.nullOr (lib.types.listOf lib.types.str); - description = "List of emails to allow access to this vhost, or null to allow all."; - default = null; - }; - allowed_email_domains = lib.mkOption { - type = lib.types.nullOr (lib.types.listOf lib.types.str); - description = "List of email domains to allow access to this vhost, or null to allow all."; - default = null; - }; - }; - }; - oldType = lib.types.listOf lib.types.str; - convertFunc = x: - lib.warn "services.oauth2-proxy.nginx.virtualHosts should be an attrset, found ${lib.generators.toPretty {} x}" - lib.genAttrs - x (_: {}); - newType = lib.types.attrsOf vhostSubmodule; - in - lib.types.coercedTo oldType convertFunc newType; - default = {}; - example = { - "protected.foo.com" = { - allowed_groups = ["admins"]; - allowed_emails = ["boss@foo.com"]; - }; - }; - description = '' - Nginx virtual hosts to put behind the oauth2 proxy. - You can exclude specific locations by setting `auth_request off;` in the locations extraConfig setting. - ''; - }; - }; - - config.services.oauth2-proxy = lib.mkIf (cfg.virtualHosts != {} && (lib.hasPrefix "127.0.0.1:" cfg.proxy)) { - enable = true; - }; - - config.services.nginx = lib.mkIf (cfg.virtualHosts != {} && config.services.oauth2-proxy.enable) (lib.mkMerge ([ - { - virtualHosts.${cfg.domain}.locations."/oauth2/" = { - proxyPass = cfg.proxy; - extraConfig = '' - proxy_set_header X-Scheme $scheme; - proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri; - ''; - }; - } - ] - ++ lib.optional (cfg.virtualHosts != {}) { - recommendedProxySettings = true; # needed because duplicate headers - } - ++ (lib.mapAttrsToList (vhost: conf: { - virtualHosts.${vhost} = { - locations = { - "/oauth2/auth" = let - maybeQueryArg = name: value: - if value == null - then null - else "${name}=${lib.concatStringsSep "," (builtins.map lib.escapeURL value)}"; - allArgs = lib.mapAttrsToList maybeQueryArg conf; - cleanArgs = builtins.filter (x: x != null) allArgs; - cleanArgsStr = lib.concatStringsSep "&" cleanArgs; - in { - # nginx doesn't support passing query string arguments to auth_request, - # so pass them here instead - proxyPass = "${cfg.proxy}/oauth2/auth?${cleanArgsStr}"; - extraConfig = '' - auth_request off; - proxy_set_header X-Scheme $scheme; - # nginx auth_request includes headers but not body - proxy_set_header Content-Length ""; - proxy_pass_request_body off; - ''; - }; - "@redirectToAuth2ProxyLogin" = { - return = "307 https://${cfg.domain}/oauth2/start?rd=$scheme://$host$request_uri"; - extraConfig = '' - auth_request off; - ''; - }; - }; - - extraConfig = '' - auth_request /oauth2/auth; - error_page 401 = @redirectToAuth2ProxyLogin; - - # pass information via X-User and X-Email headers to backend, - # requires running with --set-xauthrequest flag - auth_request_set $user $upstream_http_x_auth_request_user; - auth_request_set $email $upstream_http_x_auth_request_email; - proxy_set_header X-User $user; - proxy_set_header X-Email $email; - - # if you enabled --cookie-refresh, this is needed for it to work with auth_request - auth_request_set $auth_cookie $upstream_http_set_cookie; - add_header Set-Cookie $auth_cookie; - ''; - }; - }) - cfg.virtualHosts))); -} diff --git a/modules/oauth2-proxy.nix b/modules/oauth2-proxy.nix index 6c2bb55..55e431e 100644 --- a/modules/oauth2-proxy.nix +++ b/modules/oauth2-proxy.nix @@ -16,7 +16,6 @@ cfg = config.meta.oauth2-proxy; in { - imports = [./oa2p.nix]; options.meta.oauth2-proxy = { enable = mkEnableOption "oauth2 proxy"; diff --git a/modules/telegraf.nix b/modules/telegraf.nix index 8227a02..bfad346 100644 --- a/modules/telegraf.nix +++ b/modules/telegraf.nix @@ -27,6 +27,15 @@ in { description = "Scrape sensors with lm_sensors. You should disable this for virtualized hosts."; }; + secrets = mkOption { + type = types.attrsOf types.path; + default = {}; + example = { + "@INFLUX_TOKEN@" = "/run/agenix/influx-token"; + }; + description = "Additional secrets to replace in pre-start. The attr name will be searched and replaced in the config with the value read from the given file."; + }; + influxdb2 = { domain = mkOption { type = types.str; @@ -86,6 +95,8 @@ in { group = "telegraf"; }; + meta.telegraf.secrets."@INFLUX_TOKEN@" = config.age.secrets.telegraf-influxdb-token.path; + security.elewrap.telegraf-sensors = mkIf cfg.scrapeSensors { command = ["${pkgs.lm_sensors}/bin/sensors" "-A" "-u"]; targetUser = "root"; @@ -125,7 +136,7 @@ in { outputs = { influxdb_v2 = { urls = ["https://${cfg.influxdb2.domain}"]; - token = "$INFLUX_TOKEN"; + token = "@INFLUX_TOKEN@"; inherit (cfg.influxdb2) organization bucket; }; }; @@ -202,12 +213,19 @@ in { (pkgs.writeShellScriptBin "sensors" config.security.elewrap.telegraf-sensors.path)) ]; serviceConfig = { - Environment = "INFLUX_TOKEN=\$INFLUX_TOKEN"; # Required so the first envsubst in the original module doesn't change it ExecStartPre = mkAfter [ - (pkgs.writeShellScript "pre-start-token" '' - export INFLUX_TOKEN=$(< ${config.age.secrets.telegraf-influxdb-token.path}) - ${pkgs.envsubst}/bin/envsubst -i /var/run/telegraf/config.toml -o /var/run/telegraf/config.toml - '') + ( + pkgs.writeShellScript "pre-start-token" (lib.concatLines ( + lib.flip lib.mapAttrsToList config.meta.telegraf.secrets ( + key: secret: '' + ${lib.getExe pkgs.replace-secret} \ + ${lib.escapeShellArg key} \ + ${lib.escapeShellArg secret} \ + /var/run/telegraf/config.toml + '' + ) + )) + ) ]; # For wireguard statistics AmbientCapabilities = ["CAP_NET_ADMIN"]; diff --git a/secrets/rekeyed/ward-web-proxy/248016a36e45ca4cc24a93a8f0382290-github-access-token.age b/secrets/rekeyed/ward-web-proxy/248016a36e45ca4cc24a93a8f0382290-github-access-token.age new file mode 100644 index 0000000..80dfdde --- /dev/null +++ b/secrets/rekeyed/ward-web-proxy/248016a36e45ca4cc24a93a8f0382290-github-access-token.age @@ -0,0 +1,8 @@ +age-encryption.org/v1 +-> ssh-ed25519 NwOpTA Rq5PyLLJdiYKkL+9vW+BS7sbFHBFoLwdxxWgPTH+0wU +wr1wRFGKYVgziwtzOm95pO4ZPnzF+pTuXlhedNd7HoU +-> Or$Ir-grease =Z{2b= (FvA*6m iV4)_ +tePL0HELOtPx9UAsd0K7CvKv/56vQP++Y/PI5LMIl1IP7Opv2tiOqlB+R5bqWClO +lzV5jX+CVtQNABFgyDrQDfeqUgrw +--- YYlss0gmwNI2pNYbukrJl0hZpdCC8xKh+trLpAeTqFg +?1Ý7©ˆåˆ§¡êdÒÛ=2P»(û˜­Uuø/98ÊÖ.ËŒ¯;gyþúÄNö]ÄkdÞ|L/QJÞ…6ÀûMUÝ$Ørs9 \ No newline at end of file diff --git a/users/myuser/secrets/user.nix.age b/users/myuser/secrets/user.nix.age index 649c61b1b7183e16fa76b6319eb9960533b8d34a..c0100054c7496f1e7f0f38d4f62e4ca868483a90 100644 GIT binary patch literal 5039 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 Fwn4EDbE*IU literal 4928 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