Commit b5119b2
Changed files (6)
modules
core
services
mihomo
modules/core/openssh.nix
@@ -56,6 +56,12 @@ in {
];
};
+ tailscaleGrant = {host, ...}:
+ lib.optional (host.settings.core.openssh.server.enable) {
+ src = ["*"];
+ ip = lib.unique ["tcp:22" "tcp:${toString host.settings.core.openssh.server.port}"];
+ };
+
nixos = {host, ...}: {
services.openssh = {
enable = host.settings.core.openssh.server.enable;
modules/services/headscale/_lib.nix
@@ -52,6 +52,60 @@
reverseProxy
);
+ # Inject the implicit dst (the emitting host's tailscale addresses) into
+ # provenance-wrapped grants and dedupe identical entries so that multiple
+ # hosts sharing an explicit dst (e.g. a tag) collapse into one rule.
+ mkPolicyGrants = grants:
+ lib.unique (
+ map (grant: let
+ value = grant.value;
+ host = grant.source.host;
+ ip = value.ip or [];
+ app = value.app or [];
+ src = value.src or [];
+ in
+ assert lib.assertMsg (src != [])
+ "Host '${host.name}' emits a tailscale grant with no sources (src)";
+ assert lib.assertMsg (ip != [] || app != [])
+ "Host '${host.name}' emits a tailscale grant with no capabilities (ip or app)"; let
+ dst =
+ if value.dst or null != null
+ then [value.dst]
+ else let
+ v4 = host.address.ipv4.tailscale;
+ v6 = host.address.ipv6.tailscale;
+ in
+ assert lib.assertMsg (v4 != null || v6 != null)
+ "Host '${host.name}' emits a tailscale grant without a dst, but has no tailscale IPv4 or IPv6 address (see `address` in modules/hosts/schema.nix).";
+ lib.optionals (v4 != null) [v4] ++ lib.optionals (v6 != null) [v6];
+ in
+ {
+ inherit src dst;
+ }
+ // lib.optionalAttrs (ip != []) {inherit ip;}
+ // lib.optionalAttrs (app != []) {inherit app;})
+ grants
+ );
+
+ # Tailnet reverse-proxied domains are served by the host's caddy on the
+ # tailnet, so each host with such entries gets a grant for its caddy ports.
+ mkCaddyGrants = reverseProxy:
+ lib.unique (
+ map (r: let
+ v4 = r.source.host.address.ipv4.tailscale;
+ v6 = r.source.host.address.ipv6.tailscale;
+ in
+ assert lib.assertMsg (v4 != null || v6 != null)
+ "Host '${r.source.host.name}' serves tailnet domains but has no tailscale IPv4 or IPv6 address (see `address` in modules/hosts/schema.nix)."; {
+ src = ["*"];
+ ip = ["tcp:80" "tcp:443"];
+ dst = lib.optionals (v4 != null) [v4] ++ lib.optionals (v6 != null) [v6];
+ })
+ (lib.filter
+ (r: builtins.any (conf: conf.tailscale or false) (builtins.attrValues r.value))
+ reverseProxy)
+ );
+
mkDnsRecords = {
reverseProxy,
tailnetDomain,
@@ -98,8 +152,10 @@
tailnetEntries;
in {
inherit
+ mkCaddyGrants
mkDirectEntries
mkDnsRecords
+ mkPolicyGrants
;
configFilePath = "/var/lib/headscale/config.yaml";
modules/services/headscale/default.nix
@@ -7,6 +7,7 @@
includes = [
den.aspects.services.headscale.derp
den.aspects.services.headscale.dns
+ den.aspects.services.headscale.policy
];
settings.host = {
modules/services/headscale/policy.nix
@@ -0,0 +1,23 @@
+{
+ den.aspects.services.headscale.policy = {
+ nixos = {
+ tailscaleGrant,
+ reverseProxy,
+ pkgs,
+ lib,
+ ...
+ }: let
+ inherit
+ (import ./_lib.nix {inherit lib;})
+ mkCaddyGrants
+ mkPolicyGrants
+ ;
+ in {
+ services.headscale.settings.policy.path =
+ pkgs.writeText "headscale-policy.json"
+ (builtins.toJSON {
+ grants = mkPolicyGrants tailscaleGrant ++ mkCaddyGrants reverseProxy;
+ });
+ };
+ };
+}
modules/services/headscale/quirks.nix
@@ -0,0 +1,28 @@
+{den, ...}: {
+ den.quirks.tailscaleGrant.description = ''
+ Tailnet access grants emitted by aspects (host)
+
+ The destination is implicit (the emitting host's tailscale IPv4 and IPv6
+ addresses) unless `dst` is set explicitly. Grants are collected fleet-wide
+ and rendered into the headscale policy file.
+
+ Example:
+ {
+ src = ["*"]; # Sources allowed to connect
+ ip = ["tcp:22"]; # Network capabilities
+ app = []; # Application capabilities (optional)
+ dst = null; # Optional explicit destination ("tag:xxx")
+ }
+ '';
+
+ den.policies.collect-tailscale-grants = _: let
+ inherit (den.lib.policy) pipe;
+ in [
+ (pipe.from "tailscaleGrant" [
+ (pipe.collectAll ({host, ...}: true))
+ pipe.withProvenance
+ ])
+ ];
+
+ den.schema.host.includes = [den.policies.collect-tailscale-grants];
+}
modules/services/mihomo/default.nix
@@ -45,6 +45,7 @@
proxy-groups
rules
sniffer
+ grants
];
cache = {
@@ -105,4 +106,11 @@
};
};
};
+
+ den.aspects.services.mihomo.grants = {host, ...}: {
+ tailscaleGrant = lib.optional (host.settings.services.mihomo.tailscaleWebControl) {
+ src = ["autogroup:member"];
+ ip = ["tcp:${toString host.settings.services.mihomo.ports.controller}"];
+ };
+ };
}