at 25.11-pre 13 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 cfg = config.services.cloudflared; 9 10 certificateFile = lib.mkOption { 11 type = with lib.types; nullOr path; 12 description = '' 13 Account certificate file, necessary to create, delete and manage tunnels. It can be obtained by running `cloudflared login`. 14 15 Note that this is **necessary** for a fully declarative set up, as routes can not otherwise be created outside of the Cloudflare interface. 16 17 See [Cert.pem](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-useful-terms/#certpem) for information about the file, and [Tunnel permissions](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/local-management/tunnel-permissions/) for a comparison between the account certificate and the tunnel credentials file. 18 ''; 19 default = null; 20 }; 21 22 originRequest = { 23 connectTimeout = lib.mkOption { 24 type = with lib.types; nullOr str; 25 default = null; 26 example = "30s"; 27 description = '' 28 Timeout for establishing a new TCP connection to your origin server. This excludes the time taken to establish TLS, which is controlled by [tlsTimeout](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/local-management/ingress/#tlstimeout). 29 ''; 30 }; 31 32 tlsTimeout = lib.mkOption { 33 type = with lib.types; nullOr str; 34 default = null; 35 example = "10s"; 36 description = '' 37 Timeout for completing a TLS handshake to your origin server, if you have chosen to connect Tunnel to an HTTPS server. 38 ''; 39 }; 40 41 tcpKeepAlive = lib.mkOption { 42 type = with lib.types; nullOr str; 43 default = null; 44 example = "30s"; 45 description = '' 46 The timeout after which a TCP keepalive packet is sent on a connection between Tunnel and the origin server. 47 ''; 48 }; 49 50 noHappyEyeballs = lib.mkOption { 51 type = with lib.types; nullOr bool; 52 default = null; 53 example = false; 54 description = '' 55 Disable the happy eyeballs algorithm for IPv4/IPv6 fallback if your local network has misconfigured one of the protocols. 56 ''; 57 }; 58 59 keepAliveConnections = lib.mkOption { 60 type = with lib.types; nullOr int; 61 default = null; 62 example = 100; 63 description = '' 64 Maximum number of idle keepalive connections between Tunnel and your origin. This does not restrict the total number of concurrent connections. 65 ''; 66 }; 67 68 keepAliveTimeout = lib.mkOption { 69 type = with lib.types; nullOr str; 70 default = null; 71 example = "1m30s"; 72 description = '' 73 Timeout after which an idle keepalive connection can be discarded. 74 ''; 75 }; 76 77 httpHostHeader = lib.mkOption { 78 type = with lib.types; nullOr str; 79 default = null; 80 example = ""; 81 description = '' 82 Sets the HTTP `Host` header on requests sent to the local service. 83 ''; 84 }; 85 86 originServerName = lib.mkOption { 87 type = with lib.types; nullOr str; 88 default = null; 89 example = ""; 90 description = '' 91 Hostname that `cloudflared` should expect from your origin server certificate. 92 ''; 93 }; 94 95 caPool = lib.mkOption { 96 type = with lib.types; nullOr (either str path); 97 default = null; 98 example = ""; 99 description = '' 100 Path to the certificate authority (CA) for the certificate of your origin. This option should be used only if your certificate is not signed by Cloudflare. 101 ''; 102 }; 103 104 noTLSVerify = lib.mkOption { 105 type = with lib.types; nullOr bool; 106 default = null; 107 example = false; 108 description = '' 109 Disables TLS verification of the certificate presented by your origin. Will allow any certificate from the origin to be accepted. 110 ''; 111 }; 112 113 disableChunkedEncoding = lib.mkOption { 114 type = with lib.types; nullOr bool; 115 default = null; 116 example = false; 117 description = '' 118 Disables chunked transfer encoding. Useful if you are running a WSGI server. 119 ''; 120 }; 121 122 proxyAddress = lib.mkOption { 123 type = with lib.types; nullOr str; 124 default = null; 125 example = "127.0.0.1"; 126 description = '' 127 `cloudflared` starts a proxy server to translate HTTP traffic into TCP when proxying, for example, SSH or RDP. This configures the listen address for that proxy. 128 ''; 129 }; 130 131 proxyPort = lib.mkOption { 132 type = with lib.types; nullOr int; 133 default = null; 134 example = 0; 135 description = '' 136 `cloudflared` starts a proxy server to translate HTTP traffic into TCP when proxying, for example, SSH or RDP. This configures the listen port for that proxy. If set to zero, an unused port will randomly be chosen. 137 ''; 138 }; 139 140 proxyType = lib.mkOption { 141 type = 142 with lib.types; 143 nullOr (enum [ 144 "" 145 "socks" 146 ]); 147 default = null; 148 example = ""; 149 description = '' 150 `cloudflared` starts a proxy server to translate HTTP traffic into TCP when proxying, for example, SSH or RDP. This configures what type of proxy will be started. Valid options are: 151 152 - `""` for the regular proxy 153 - `"socks"` for a SOCKS5 proxy. Refer to the [tutorial on connecting through Cloudflare Access using kubectl](https://developers.cloudflare.com/cloudflare-one/tutorials/kubectl/) for more information. 154 ''; 155 }; 156 }; 157in 158{ 159 imports = [ 160 (lib.mkRemovedOptionModule 161 [ 162 "services" 163 "cloudflared" 164 "user" 165 ] 166 '' 167 Cloudflared now uses a dynamic user, and this option no longer has any effect. 168 169 If the user is still necessary, please define it manually using users.users.cloudflared. 170 '' 171 ) 172 173 (lib.mkRemovedOptionModule 174 [ 175 "services" 176 "cloudflared" 177 "group" 178 ] 179 '' 180 Cloudflared now uses a dynamic user, and this option no longer has any effect. 181 182 If the group is still necessary, please define it manually using users.groups.cloudflared. 183 '' 184 ) 185 ]; 186 187 options.services.cloudflared = { 188 inherit certificateFile; 189 190 enable = lib.mkEnableOption "Cloudflare Tunnel client daemon (formerly Argo Tunnel)"; 191 192 package = lib.mkPackageOption pkgs "cloudflared" { }; 193 194 tunnels = lib.mkOption { 195 description = '' 196 Cloudflare tunnels. 197 ''; 198 type = lib.types.attrsOf ( 199 lib.types.submodule ( 200 { name, ... }: 201 { 202 options = { 203 inherit certificateFile originRequest; 204 205 credentialsFile = lib.mkOption { 206 type = lib.types.path; 207 description = '' 208 Credential file. 209 210 See [Credentials file](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-useful-terms/#credentials-file). 211 ''; 212 }; 213 214 warp-routing = { 215 enabled = lib.mkOption { 216 type = with lib.types; nullOr bool; 217 default = null; 218 description = '' 219 Enable warp routing. 220 221 See [Connect from WARP to a private network on Cloudflare using Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/tutorials/warp-to-tunnel/). 222 ''; 223 }; 224 }; 225 226 default = lib.mkOption { 227 type = lib.types.str; 228 description = '' 229 Catch-all service if no ingress matches. 230 231 See `service`. 232 ''; 233 example = "http_status:404"; 234 }; 235 236 ingress = lib.mkOption { 237 type = 238 with lib.types; 239 attrsOf ( 240 either str ( 241 submodule ( 242 { hostname, ... }: 243 { 244 options = { 245 inherit originRequest; 246 247 service = lib.mkOption { 248 type = with lib.types; nullOr str; 249 default = null; 250 description = '' 251 Service to pass the traffic. 252 253 See [Supported protocols](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/local-management/ingress/#supported-protocols). 254 ''; 255 example = "http://localhost:80, tcp://localhost:8000, unix:/home/production/echo.sock, hello_world or http_status:404"; 256 }; 257 258 path = lib.mkOption { 259 type = with lib.types; nullOr str; 260 default = null; 261 description = '' 262 Path filter. 263 264 If not specified, all paths will be matched. 265 ''; 266 example = "/*.(jpg|png|css|js)"; 267 }; 268 269 }; 270 } 271 ) 272 ) 273 ); 274 default = { }; 275 description = '' 276 Ingress rules. 277 278 See [Ingress rules](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/local-management/ingress/). 279 ''; 280 example = { 281 "*.domain.com" = "http://localhost:80"; 282 "*.anotherone.com" = "http://localhost:80"; 283 }; 284 }; 285 }; 286 } 287 ) 288 ); 289 290 default = { }; 291 example = { 292 "00000000-0000-0000-0000-000000000000" = { 293 credentialsFile = "/tmp/test"; 294 ingress = { 295 "*.domain1.com" = { 296 service = "http://localhost:80"; 297 }; 298 }; 299 default = "http_status:404"; 300 }; 301 }; 302 }; 303 }; 304 305 config = lib.mkIf cfg.enable { 306 systemd.targets = lib.mapAttrs' ( 307 name: tunnel: 308 lib.nameValuePair "cloudflared-tunnel-${name}" { 309 description = "Cloudflare tunnel '${name}' target"; 310 requires = [ "cloudflared-tunnel-${name}.service" ]; 311 after = [ "cloudflared-tunnel-${name}.service" ]; 312 unitConfig.StopWhenUnneeded = true; 313 } 314 ) config.services.cloudflared.tunnels; 315 316 systemd.services = lib.mapAttrs' ( 317 name: tunnel: 318 let 319 filterConfig = lib.attrsets.filterAttrsRecursive ( 320 _: v: 321 !builtins.elem v [ 322 null 323 [ ] 324 { } 325 ] 326 ); 327 328 filterIngressSet = lib.filterAttrs (_: v: builtins.typeOf v == "set"); 329 filterIngressStr = lib.filterAttrs (_: v: builtins.typeOf v == "string"); 330 331 ingressesSet = filterIngressSet tunnel.ingress; 332 ingressesStr = filterIngressStr tunnel.ingress; 333 334 fullConfig = filterConfig { 335 tunnel = name; 336 credentials-file = "/run/credentials/cloudflared-tunnel-${name}.service/credentials.json"; 337 warp-routing = filterConfig tunnel.warp-routing; 338 originRequest = filterConfig tunnel.originRequest; 339 ingress = 340 (map ( 341 key: 342 { 343 hostname = key; 344 } 345 // lib.getAttr key (filterConfig (filterConfig ingressesSet)) 346 ) (lib.attrNames ingressesSet)) 347 ++ (map (key: { 348 hostname = key; 349 service = lib.getAttr key ingressesStr; 350 }) (lib.attrNames ingressesStr)) 351 ++ [ { service = tunnel.default; } ]; 352 }; 353 354 mkConfigFile = pkgs.writeText "cloudflared.yml" (builtins.toJSON fullConfig); 355 certFile = if (tunnel.certificateFile != null) then tunnel.certificateFile else cfg.certificateFile; 356 in 357 lib.nameValuePair "cloudflared-tunnel-${name}" { 358 after = [ 359 "network.target" 360 "network-online.target" 361 ]; 362 wants = [ 363 "network.target" 364 "network-online.target" 365 ]; 366 wantedBy = [ "multi-user.target" ]; 367 serviceConfig = { 368 RuntimeDirectory = "cloudflared-tunnel-${name}"; 369 RuntimeDirectoryMode = "0400"; 370 LoadCredential = [ 371 "credentials.json:${tunnel.credentialsFile}" 372 ] ++ (lib.optional (certFile != null) "cert.pem:${certFile}"); 373 374 ExecStart = "${cfg.package}/bin/cloudflared tunnel --config=${mkConfigFile} --no-autoupdate run"; 375 Restart = "on-failure"; 376 DynamicUser = true; 377 }; 378 379 environment.TUNNEL_ORIGIN_CERT = lib.mkIf (certFile != null) ''%d/cert.pem''; 380 } 381 ) config.services.cloudflared.tunnels; 382 }; 383 384 meta.maintainers = with lib.maintainers; [ 385 bbigras 386 anpin 387 ]; 388}