diff --git a/source/_posts/caddy-nixos-part-1.md b/source/_posts/caddy-nixos-part-1.md index a33e47e..29a99ae 100644 --- a/source/_posts/caddy-nixos-part-1.md +++ b/source/_posts/caddy-nixos-part-1.md @@ -133,7 +133,7 @@ shred -uz configuration.7z configuration.nix Following is my "configuration.nix". I'll show you how to secure NixOS using hashed password, firewall, DNS-over-TLS and USBGuard in my next post. After that, I'll show you how to setup Caddy and Tor (they are disabled for now). -``` +``` nix /etc/nixos/configuration.nix { config, pkgs, ... }: { diff --git a/source/_posts/caddy-nixos-part-2.md b/source/_posts/caddy-nixos-part-2.md index 8fdfd7b..b893019 100644 --- a/source/_posts/caddy-nixos-part-2.md +++ b/source/_posts/caddy-nixos-part-2.md @@ -2,7 +2,7 @@ title: "Setup Caddy as a reverse proxy on NixOS (Part 2: Hardening)" excerpt: "Part 2: Securing NixOS" date: 2020-03-04 -updated: 2020-04-22 +updated: 2020-11-09 tags: - server - linux @@ -10,6 +10,8 @@ tags: - nixos --- +> 9 Nov 2020: Updated to NixOS 20.09 syntax. + In this post, I show you how I securely configure the NixOS, the server OS behind this website. This post is Part 2 of a series of articles that show you how I set up Caddy and Tor hidden service on NixOS: @@ -38,13 +40,13 @@ In NixOS, instead of using `useradd` and `passwd` to manage users, you could als First, I disabled `useradd` and `passwd`. -``` js +``` nix users.mutableUsers = false; ``` ## Disable root -``` js +``` nix users.root.hashedPassword = "*"; ``` @@ -52,7 +54,7 @@ users.root.hashedPassword = "*"; User's password can be configured by `users..password`, obviously this means the password is stored in plain text. Even if you lock down `configuration.nix` with `chmod 600` (which I did), "it is (still) world-readable in the Nix store". The safer way is to store in a hashed form, -``` js +``` nix users..hashedPassword = "xxxx"; ``` @@ -62,7 +64,7 @@ Note that the hash is still world-readable. A more secure option is to use `user You might be wondering why not just `passwordFile` during installation. The issue is that, in the live CD environment, the "/etc/" folder refers to the live CD's not the actual one which is located in "/mnt/etc/". I mean, you _could_ try "/mnt/etc/nixos/nixos.password", but you gotta remember to update the option after reboot otherwise you would get locked out. "./nixos.password" value doesn't work because `passwordFile` option doesn't support relative path, it must be a full path. Hence, I have use `hashedPassword` during the initial setup and then switch to `passwordFile`. Remember to remove the `hashedPassword` option once you have set up `passwordFile`. -``` js +``` nix passwordFile = "/etc/nixos/nixos.password"; isNormalUser = true; extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user. @@ -84,7 +86,7 @@ For separation of privilege, each service is launched with different user under Combining with the previous user configs, I ended up with: -``` js +``` nix users = { mutableUsers = false; @@ -144,7 +146,7 @@ $ google-authenticator Once the secret is generated, TOTP can be enabled using the following config. I configured it to require OTP when login and sudo, in addition to password. -``` js +``` nix ## Requires OTP to login & sudo security.pam = { services.login.googleAuthenticator.enable = true; @@ -158,7 +160,7 @@ Since DNS is not encrypted in transit, it risks being tampered. To resolve that, I use Cloudflare DNS simply because I'm already using its CDN, using other alternatives wouldn't have the privacy benefit since Cloudflare already knows that a visitor is browsing this website though its CDN. Refer to stubby.yml for a full list of supported servers. -``` js +``` nix ## DNS-over-TLS services.stubby = { enable = true; @@ -181,7 +183,7 @@ I use Cloudflare DNS simply because I'm already using its CDN, using other alter Then I point systemd's resolved to stubby. I do configure it to fallback to unencrypted DNS if stubby is not responsive (which does happen). Whether you need an unsecured fallback depends on your cost-benefit. For me, the cost of the site being inaccessible (due to unresponsive stubby) outweighs the benefit of having enforced encryption (my setup is opportunistic). -``` +``` nix networking.nameservers = [ "::1" "127.0.0.1" ]; services.resolved = { enable = true; @@ -201,7 +203,7 @@ By default, Linux program cannot bind to port <=1024 for security reason. If a p In my case, I configure iptables to port forward 443 to 4430, so any traffic that hits 443 will be redirected to 4430. Both ports need to be opened, but I do configure my dedicated firewall (separate from the web server) to allow port 443 only. -``` js +``` nix ## Port forwarding networking.firewall = { enable = true; @@ -225,7 +227,7 @@ In the config, you can also specify the time that the server will reboot. I reco (For more advanced usage of `dates`, see [`systemd.time`](https://jlk.fjfi.cvut.cz/arch/manpages/man/systemd.time.7#CALENDAR_EVENTS)) -``` js +``` nix system.autoUpgrade = { enable = true; allowReboot = true; @@ -239,16 +241,14 @@ In the config, you can also specify the time that the server will reboot. I reco I use USBGuard utility to allow or deny USB devices. In a virtual server environment, I only need to use the virtualised USB keyboard. Configuration is easy and straightforward. First, I generate a policy (with root privilege) to allow all currently connected devices: ``` -# usbguard generate-policy > /var/lib/usbguard/rules.conf +$ sudo usbguard generate-policy > /var/lib/usbguard/rules.conf ``` Then, I just simply enable the service: -``` js - services.usbguard = { - enable = true; - ruleFile = "/var/lib/usbguard/rules.conf"; - }; +``` nix + # Load "/var/lib/usbguard/rules.conf" by default + services.usbguard.enable = true; ``` Once enabled, any device not whitelisted in the policy will not be accessible. @@ -306,12 +306,12 @@ Kernel compiled with additional security-oriented patch set. [More details](http _NixOS [defaults](https://nixos.wiki/wiki/Linux_kernel) to the latest LTS kernel_ -``` +``` nix # Latest LTS kernel boot.kernelPackages = pkgs.linuxPackages_hardened; ``` -``` +``` nix # Latest kernel boot.kernelPackages = pkgs.linuxPackages_latest_hardened; ``` @@ -320,11 +320,13 @@ _NixOS [defaults](https://nixos.wiki/wiki/Linux_kernel) to the latest LTS kernel Since my web server has limited disk space, it needs to run [garbage collector](https://nixos.org/nixos/manual/index.html#sec-nix-gc) from time to time. -``` +Since [unattended upgrade](#Unattended-upgrade) is executed on 00:00, I delay garbage collection to 01:00 to avoid time conflict. The order doesn't matter, but there should be at least 15 minutes buffer. + +``` nix ## Garbage collector nix.gc = { automatic = true; - # Every Monday 00:00 - dates = "weekly UTC"; + # Every Monday 01:00 (UTC) + dates = "Monday 01:00 UTC"; }; ``` diff --git a/source/_posts/caddy-nixos-part-3.md b/source/_posts/caddy-nixos-part-3.md index 4fcea44..e31c2d9 100644 --- a/source/_posts/caddy-nixos-part-3.md +++ b/source/_posts/caddy-nixos-part-3.md @@ -2,7 +2,7 @@ title: "Setup Caddy as a reverse proxy on NixOS (Part 3: Caddy)" excerpt: "Part 3: Configure Caddy" date: 2020-03-14 -updated: 2020-09-09 +updated: 2020-11-09 tags: - server - linux @@ -10,6 +10,8 @@ tags: - nixos --- +> 9 Nov 2020: Updated to Caddy 2.1 syntax. Refer to {% post_link caddy-upgrade-v2-proxy 'this article' %} for upgrade guide. + In this segment, I show you how I set up this website (mdleom.com) to reverse proxy to curben.netlify.app using Caddy on NixOS (see above diagram). If you're not using NixOS, simply skip to the [Caddyfile](#Caddyfile) section. This post is Part 2 of a series of articles that show you how I set up Caddy and Tor hidden service on NixOS: @@ -26,11 +28,10 @@ This post is Part 2 of a series of articles that show you how I set up Caddy and In NixOS, Caddy can be easily configured through "configuration.nix", without even touching a Caddyfile, if you have a rather simple setup. For example, to serve static files from "/var/www/" folder, -``` plain configuration.nix +``` nix configuration.nix services.caddy = { enable = true; email = example@example.com; - agree = true; config = '' example.com { @@ -58,7 +59,7 @@ caddy.nix grants `CAP_NET_BIND_SERVICE` capability which is not needed in my use I created another nix file which is similar to "caddy.nix", but without `CAP_NET_BIND_SERVICE` capability. I also removed Let's Encrypt-related options since I'm using Cloudflare origin certificate. I renamed the `options.services.caddy` to `options.services.caddyProxy` to avoid clash with "caddy.nix". Save the file to "/etc/caddy/caddyProxy.nix" with root as owner. We'll revisit this file in "[configuration.nix](#configuration.nix)" section later in this guide. -``` plain /etc/caddy/caddyProxy.nix +``` nix /etc/caddy/caddyProxy.nix { config, lib, pkgs, ... }: with lib; @@ -75,6 +76,16 @@ in { description = "Path to Caddyfile"; }; + adapter = mkOption { + default = "caddyfile"; + example = "nginx"; + type = types.str; + description = '' + Name of the config adapter to use. + See https://caddyserver.com/docs/config-adapters for the full list. + ''; + }; + dataDir = mkOption { default = "/var/lib/caddyProxy"; type = types.path; @@ -97,32 +108,32 @@ in { systemd.services.caddyProxy = { description = "Caddy web server"; after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; # systemd-networkd-wait-online.service wantedBy = [ "multi-user.target" ]; - environment = mkIf (versionAtLeast config.system.stateVersion "17.09") - { CADDYPATH = cfg.dataDir; }; - startLimitIntervalSec = 86400; # 21.03+ # https://github.com/NixOS/nixpkgs/pull/97512 - # startLimitBurst = 5; + # startLimitIntervalSec = 14400; + # startLimitBurst = 10; serviceConfig = { - ExecStart = '' - ${cfg.package}/bin/caddy -root=/var/tmp -conf=${cfg.config} - ''; - ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + ExecStart = "${cfg.package}/bin/caddy run --config ${cfg.config} --adapter ${cfg.adapter}"; + ExecReload = "${cfg.package}/bin/caddy reload --config ${cfg.config} --adapter ${cfg.adapter}"; Type = "simple"; User = "caddyProxy"; Group = "caddyProxy"; - Restart = "on-failure"; - # <= 20.09 - StartLimitBurst = 5; + Restart = "on-abnormal"; + StartLimitIntervalSec = 14400; + StartLimitBurst = 10; NoNewPrivileges = true; - LimitNPROC = 64; + LimitNPROC = 512; LimitNOFILE = 1048576; PrivateTmp = true; PrivateDevices = true; ProtectHome = true; ProtectSystem = "full"; ReadWriteDirectories = cfg.dataDir; + KillMode = "mixed"; + KillSignal = "SIGQUIT"; + TimeoutStopSec = "5s"; }; }; @@ -130,7 +141,7 @@ in { home = cfg.dataDir; createHome = true; }; - + users.groups.caddyProxy = { members = [ "caddyProxy" ]; }; @@ -188,7 +199,11 @@ Subsequent configurations (directives) shall be inside the curly braces. Let's s ``` mdleom.com:4430 www.mdleom.com:4430 { tls /var/lib/caddyProxy/mdleom.com.pem /var/lib/caddyProxy/mdleom.com.key { - clients /var/lib/caddyProxy/origin-pull-ca.pem + protocols tls1.3 + client_auth { + mode require_and_verify + trusted_ca_cert_file /var/lib/caddyProxy/origin-pull-ca.pem + } } } ``` @@ -198,10 +213,8 @@ mdleom.com:4430 www.mdleom.com:4430 { Connection to www.mdleom.com is redirected to mdleom.com with HTTP 301 status. ``` - redir 301 { - if {label1} is www - / https://mdleom.com{uri} - } + @www host www.mdleom.com + redir @www https://mdleom.com{uri} permanent ``` `{label1}` placeholder refers to the first part of the request hostname, e.g. if hostname is `foo.bar.com`, `{label1}` is foo, `{label2}` is bar and so on. @@ -211,10 +224,8 @@ Connection to www.mdleom.com is redirected to mdleom.com with HTTP 301 status. If you prefer to redirect apex to www, ``` - redir 301 { - if {label1} is mdleom - / https://www.mdleom.com{uri} - } + @www host mdleom.com + redir @www https://www.mdleom.com{uri} permanent ``` ### Reverse proxy @@ -229,103 +240,104 @@ Aside from reverse proxy to curben.netlify.app, I also configured my Netlify web In Caddyfile, the config can be expressed as: ``` plain - proxy /img https://cdn.statically.io/img/gitlab.com/curben/blog/raw/site { - without /img + handle_path /img/* { + rewrite * /img/gitlab.com/curben/blog/raw/site{path} + reverse_proxy https://cdn.statically.io } - rewrite /screenshot { - r (.*) - to /screenshot{1}?mobile=true + handle_path /screenshot/* { + rewrite * /screenshot/curben.netlify.app{path}?mobile=true + + reverse_proxy https://cdn.statically.io } - proxy /screenshot https://cdn.statically.io/screenshot/curben.netlify.app { - without /screenshot - } - - proxy / https://curben.netlify.app + reverse_proxy https://curben.netlify.app ``` -`without` directive is necessary to remove `libs/` from the path, so that "mdleom.com/libs/foo/bar.js" is linked to "https://cdn.statically.io/libs/foo/bar.js", not "https://cdn.statically.io/libs/libs/foo/bar.js". - -For `/screenshot`, since the `proxy` doesn't support variable like the Netlify `:splat`, to prepend "?mobile=true" to the link in the background (without using 301 redirection), I use `rewrite` directive which has a regex match function. I use the regex to capture the path after `screenshot` and call it using `{1}`. +`rewrite` directive is necessary to remove `img/` and `screenshot/*` from the path, so that "mdleom.com/img/foo.jpg" is linked to "https://cdn.statically.io/img/foo.jpg", not "https://cdn.statically.io/img/img/foo.jpg". ### Host header To make sure Caddy sends the correct `Host:` header to the upstream/backend locations, I use `header_upstream` option, -``` plain - proxy /img https://cdn.statically.io/img/gitlab.com/curben/blog/raw/site { - without /img - header_upstream Host cdn.statically.io +{% codeblock mark:5,13,18 %} + handle_path /img/* { + rewrite * /img/gitlab.com/curben/blog/raw/site{path} + + reverse_proxy https://cdn.statically.io { + header_up Host cdn.statically.io + } } - rewrite /screenshot { - r (.*) - to /screenshot{1}?mobile=true + handle_path /screenshot/* { + rewrite * /screenshot/curben.netlify.app{path}?mobile=true + + reverse_proxy https://cdn.statically.io { + header_up Host cdn.statically.io + } } - proxy /screenshot https://cdn.statically.io/screenshot/curben.netlify.app { - without /screenshot - header_upstream Host cdn.statically.io + reverse_proxy https://curben.netlify.app { + header_up Host curben.netlify.app } - - proxy / https://curben.netlify.app { - header_upstream Host cdn.statically.io - } -``` - -There are a few repetitions for rewriting the header for Statically. I can group that option as a global variable and call it using `import`. - -``` -(staticallyCfg) { - header_upstream Host cdn.statically.io -} - -mdleom.com { - proxy /img ... { - import staticallyCfg - } - - proxy /screenshot ... { - import staticallyCfg - } -} -``` +{% endcodeblock %} ### Add or remove headers -To prevent any unnecessary request headers from being sent to the upstreams, I use `header_upstream`. I use it to remove cookie, referer and [other headers](https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers-) added by Cloudflare. Since there are many headers to remove, I group them as a global variable. I apply it to all `proxy` directive. +To prevent any unnecessary request headers from being sent to the upstreams, I use `header_up`. I use it to remove cookie, referer and [other headers](https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers-) added by Cloudflare. Since there are many headers to remove, I group them as a global variable. I apply it to all `reverse_proxy` directives. -``` +{% codeblock mark:25,34,40 %} (removeHeaders) { - header_upstream -cookie - header_upstream -referer - # Remove Cloudflare headers - # https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers- - header_upstream -cf-ipcountry - header_upstream -cf-connecting-ip - header_upstream -x-forwarded-for - header_upstream -x-forwarded-proto - header_upstream -cf-ray - header_upstream -cf-visitor - header_upstream -true-client-ip - header_upstream -cdn-loop - header_upstream -cf-request-id - header_upstream -cf-cache-status + header_up -cdn-loop + header_up -cf-cache-status + header_up -cf-connecting-ip + header_up -cf-ipcountry + header_up -cf-ray + header_up -cf-request-id + header_up -cf-visitor + header_up -cookie + header_up -referer + header_up -sec-ch-ua + header_up -sec-ch-ua-mobile + header_up -true-client-ip + header_up -via + header_up -x-forwarded-for + header_up -x-forwarded-proto + header_up User-Agent "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" } mdleom.com { - proxy /img ... { + handle_path /img/* { + rewrite * /img/gitlab.com/curben/blog/raw/site{path} + + reverse_proxy https://cdn.statically.io { + import removeHeaders + header_up Host cdn.statically.io + } + } + + handle_path /screenshot/* { + rewrite * /screenshot/curben.netlify.app{path}?mobile=true + + reverse_proxy https://cdn.statically.io { + import removeHeaders + header_up Host cdn.statically.io + } + } + + reverse_proxy https://curben.netlify.app { import removeHeaders + header_up Host curben.netlify.app } } -``` +{% endcodeblock %} -The upstream locations insert some information into the response headers that are irrelevant to the site visitors. I use `header` directive to filter them out. It applies to all `proxy` directive. +The upstream locations insert some information into the response headers that are irrelevant to the site visitors. I use `header` directive to filter them out. It also applies to all `reverse_proxy` directives. ``` - header / { - -server + header { + -access-control-allow-origin + -access-control-expose-headers -alt-svc -cdn-cache -cdn-cachedat @@ -334,121 +346,209 @@ The upstream locations insert some information into the response headers that ar -cdn-requestcountrycode -cdn-requestid -cdn-uid + -cf-bgj -cf-cache-status + -cf-polished -cf-ray -cf-request-id + -content-disposition -etag + -expect-ct + -server -set-cookie + -timing-allow-origin + -via -x-bytes-saved -x-cache + -x-cache-hits -x-nf-request-id -x-served-by - Cache-Control "max-age=604800, public" + -x-timer + Clear-Site-Data `"cookies", "storage"` + Content-Language "en-GB" + Content-Security-Policy "default-src 'self'; child-src 'none'; connect-src 'none'; font-src 'none'; frame-src 'none'; img-src 'self'; manifest-src 'none'; media-src 'none'; object-src 'none'; prefetch-src 'none'; script-src 'self'; style-src 'self'; worker-src 'none'; base-uri 'none'; form-action https://duckduckgo.com https://3g2upl4pq6kufc4m.onion; frame-ancestors 'none'; block-all-mixed-content" + Expires "0" + Feature-Policy "accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; camera 'none'; display-capture 'none'; document-domain 'none'; encrypted-media 'none'; fullscreen 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'none'; speaker 'none'; sync-xhr 'none'; usb 'none'; vibrate 'none'; vr 'none'; wake-lock 'none'; webauthn 'none'; xr-spatial-tracking 'none'" Referrer-Policy "no-referrer" + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + X-XSS-Protection "1; mode=block" + defer } ``` I also add the `Cache-Control` and `Referrer-Policy` to the response header. Use minus (-) sign before each option to remove particular header. Without minus sign, the specified header is either added or replacing an existing one. -### header and header_downstream +### Cache-Control `/libs` folder contains third-party libraries. Since the library is usually requested by a specific version, we can safely assume that the response would remain the same. This means I can set long expiration and `immutable` on the response. [`immutable`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Revalidation_and_reloading) is to tell the browser that revalidation is not needed. ``` - header / { - Cache-Control "max-age=604800, public" + header { + Cache-Control "max-age=86400, public" } - header /libs { + header /libs/* { Cache-Control "public, max-age=31536000, immutable" } ``` ### Complete Caddyfile -``` plain Caddyfile -(removeHeaders) { - header_upstream -cookie - header_upstream -referer - # Remove Cloudflare headers - # https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers- - header_upstream -cf-ipcountry - header_upstream -cf-connecting-ip - header_upstream -x-forwarded-for - header_upstream -x-forwarded-proto - header_upstream -cf-ray - header_upstream -cf-visitor - header_upstream -true-client-ip - header_upstream -cdn-loop - header_upstream -cf-request-id - header_upstream -cf-cache-status +Since I also set up reverse proxy for {% post_link tor-hidden-onion-nixos 'Tor Onion' %} and {% post_link i2p-eepsite-nixos 'I2P Eepsite' %}, I refactor most of the configuration into "common.conf" and import it into "caddyProxy.conf". + +``` plain common.conf +## Optional: disable admin endpoint and http->https redirect +#{ +# admin off +# auto_https disable_redirects +#} + +(setHeaders) { + -access-control-allow-origin + -access-control-expose-headers + -alt-svc + -cdn-cache + -cdn-cachedat + -cdn-edgestorageid + -cdn-pullzone + -cdn-requestcountrycode + -cdn-requestid + -cdn-uid + -cf-bgj + -cf-cache-status + -cf-polished + -cf-ray + -cf-request-id + -content-disposition + -etag + -expect-ct + -server + -set-cookie + -timing-allow-origin + -via + -x-bytes-saved + -x-cache + -x-cache-hits + -x-nf-request-id + -x-served-by + -x-timer + Cache-Control "max-age=86400, public" + Clear-Site-Data `"cookies", "storage"` + Content-Language "en-GB" + Content-Security-Policy "default-src 'self'; child-src 'none'; connect-src 'none'; font-src 'none'; frame-src 'none'; img-src 'self'; manifest-src 'none'; media-src 'none'; object-src 'none'; prefetch-src 'none'; script-src 'self'; style-src 'self'; worker-src 'none'; base-uri 'none'; form-action https://duckduckgo.com https://3g2upl4pq6kufc4m.onion; frame-ancestors 'none'; block-all-mixed-content" + Expires "0" + Feature-Policy "accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; camera 'none'; display-capture 'none'; document-domain 'none'; encrypted-media 'none'; fullscreen 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'none'; speaker 'none'; sync-xhr 'none'; usb 'none'; vibrate 'none'; vr 'none'; wake-lock 'none'; webauthn 'none'; xr-spatial-tracking 'none'" + Referrer-Policy "no-referrer" + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + X-XSS-Protection "1; mode=block" } -(staticallyCfg) { - header_downstream Strict-Transport-Security "max-age=31536000" - header_upstream Host cdn.statically.io +(removeHeaders) { + header_up -cdn-loop + header_up -cf-cache-status + header_up -cf-connecting-ip + header_up -cf-ipcountry + header_up -cf-ray + header_up -cf-request-id + header_up -cf-visitor + header_up -cookie + header_up -referer + header_up -sec-ch-ua + header_up -sec-ch-ua-mobile + header_up -true-client-ip + header_up -via + header_up -x-forwarded-for + header_up -x-forwarded-proto + header_up User-Agent "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" } +(oneWeekCache) { + Cache-Control "max-age=604800, public" +} + +(pathProxy) { + header /js/* { + import oneWeekCache + defer + } + + header /css/* { + import oneWeekCache + defer + } + + header /svg/* { + import oneWeekCache + defer + } + + header /libs/* { + Cache-Control "max-age=31536000, public, immutable" + defer + } + + handle_path /img/* { + rewrite * /img/gitlab.com/curben/blog/raw/site{path} + + reverse_proxy https://cdn.statically.io { + import removeHeaders + header_up Host cdn.statically.io + } + } + + header /img/* { + import oneWeekCache + defer + } + + handle_path /screenshot/* { + rewrite * /screenshot/curben.netlify.app{path}?mobile=true + + reverse_proxy https://cdn.statically.io { + import removeHeaders + header_up Host cdn.statically.io + } + } + + header /screenshot/* { + import oneWeekCache + defer + } + + reverse_proxy https://curben.netlify.app { + import removeHeaders + header_up Host curben.netlify.app + } +} +``` + +``` plain caddyProxy.conf +import common.conf + ## mdleom.com mdleom.com:4430 www.mdleom.com:4430 { tls /var/lib/caddyProxy/mdleom.com.pem /var/lib/caddyProxy/mdleom.com.key { - clients /var/lib/caddyProxy/origin-pull-ca.pem + protocols tls1.3 + client_auth { + mode require_and_verify + trusted_ca_cert_file /var/lib/caddyProxy/origin-pull-ca.pem + } } # www -> apex - redir 301 { - if {label1} is www - / https://mdleom.com{uri} + @www host www.mdleom.com + redir @www https://mdleom.com{uri} permanent + + header { + import setHeaders + Onion-Location "http://xw226dvxac7jzcpsf4xb64r4epr6o5hgn46dxlqk7gnjptakik6xnzqd.onion" + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" + defer } - header / { - -server - -alt-svc - -cdn-cache - -cdn-cachedat - -cdn-edgestorageid - -cdn-pullzone - -cdn-requestcountrycode - -cdn-requestid - -cdn-uid - -cf-cache-status - -cf-ray - -cf-request-id - -etag - -set-cookie - -x-bytes-saved - -x-cache - -x-nf-request-id - -x-served-by - Cache-Control "max-age=604800, public" - Referrer-Policy "no-referrer" - } - - header /libs { - Cache-Control "public, max-age=31536000, immutable" - } - - proxy /img https://cdn.statically.io/img/gitlab.com/curben/blog/raw/site { - without /img - import removeHeaders - import staticallyCfg - } - - rewrite /screenshot { - r (.*) - to /screenshot{1}?mobile=true - } - - proxy /screenshot https://cdn.statically.io/screenshot/curben.netlify.app { - without /screenshot - import removeHeaders - import staticallyCfg - } - - proxy / https://curben.netlify.app { - import removeHeaders - header_upstream Host curben.netlify.app - } + import pathProxy } ``` @@ -456,7 +556,7 @@ mdleom.com:4430 www.mdleom.com:4430 { One last thing to do is to import "[caddyProxy.nix](#caddyProxy.nix)" and enable `services.caddyProxy`. -``` js /etc/nixos/configuration.nix +``` nix /etc/nixos/configuration.nix require = [ /etc/caddy/caddyProxy.nix ]; services.caddyProxy = { enable = true; diff --git a/source/_posts/i2p-eepsite-nixos.md b/source/_posts/i2p-eepsite-nixos.md index fbb36ac..dfee7ce 100644 --- a/source/_posts/i2p-eepsite-nixos.md +++ b/source/_posts/i2p-eepsite-nixos.md @@ -2,7 +2,7 @@ title: "How to make your website available over I2P Eepsite on NixOS" excerpt: "A guide on I2P Eepsite on NixOS" date: 2020-03-21 -updated: 2020-09-09 +updated: 2020-11-09 tags: - server - linux @@ -12,6 +12,8 @@ tags: - censorship --- +> 9 Nov 2020: Updated to Caddy 2.1 syntax. Refer to {% post_link caddy-upgrade-v2-proxy 'this article' %} for upgrade guide. + In this segment, I show you how I set up I2P Eepsite service that reverse proxy to curben.netlify.app. This website can be accessed using this [B32 address](http://ggucqf2jmtfxcw7us5sts3x7u2qljseocfzlhzebfpihkyvhcqfa.b32.i2p) or [mdleom.i2p](http://mdleom.i2p/) This post is Part 5 of a series of articles that show you how I set up Caddy, Tor hidden service and I2P Eepsite on NixOS: @@ -123,6 +125,16 @@ in { description = "Path to Caddyfile"; }; + adapter = mkOption { + default = "caddyfile"; + example = "nginx"; + type = types.str; + description = '' + Name of the config adapter to use. + See https://caddyserver.com/docs/config-adapters for the full list. + ''; + }; + dataDir = mkOption { default = "/var/lib/caddyI2p"; type = types.path; @@ -145,40 +157,40 @@ in { systemd.services.caddyI2p = { description = "Caddy web server"; after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; # systemd-networkd-wait-online.service wantedBy = [ "multi-user.target" ]; - environment = mkIf (versionAtLeast config.system.stateVersion "17.09") - { CADDYPATH = cfg.dataDir; }; - startLimitIntervalSec = 86400; # 21.03+ # https://github.com/NixOS/nixpkgs/pull/97512 - # startLimitBurst = 5; + # startLimitIntervalSec = 14400; + # startLimitBurst = 10; serviceConfig = { - ExecStart = '' - ${cfg.package}/bin/caddy -root=/var/tmp -conf=${cfg.config} - ''; - ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + ExecStart = "${cfg.package}/bin/caddy run --config ${cfg.config} --adapter ${cfg.adapter}"; + ExecReload = "${cfg.package}/bin/caddy reload --config ${cfg.config} --adapter ${cfg.adapter}"; Type = "simple"; - User = "caddyProxy"; - Group = "caddyProxy"; - Restart = "on-failure"; - # <= 20.09 - StartLimitBurst = 5; + User = "caddyI2p"; + Group = "caddyI2p"; + Restart = "on-abnormal"; + StartLimitIntervalSec = 14400; + StartLimitBurst = 10; NoNewPrivileges = true; - LimitNPROC = 64; + LimitNPROC = 512; LimitNOFILE = 1048576; PrivateTmp = true; PrivateDevices = true; ProtectHome = true; ProtectSystem = "full"; ReadWriteDirectories = cfg.dataDir; + KillMode = "mixed"; + KillSignal = "SIGQUIT"; + TimeoutStopSec = "5s"; }; }; - + users.users.caddyI2p = { home = cfg.dataDir; createHome = true; }; - + users.groups.caddyI2p = { members = [ "caddyI2p" ]; }; @@ -188,11 +200,13 @@ in { ### File ownership and permissions -After you save the file to **/etc/caddy/caddyI2p.nix**, remember to restrict it to root. +After you save the file to **/etc/caddy/caddyI2p.nix**, remember to restrict it to `caddyI2p` user. ``` -# chown root:root /etc/caddy/caddyI2p.nix -# chown 600 /etc/caddy/caddyI2p.nix +$ chown caddyI2p:caddyI2p /etc/caddy/caddyI2p.nix +$ chown 600 /etc/caddy/caddyI2p.nix +# "common.conf" must be readable by other users +$ chmod o+r /etc/caddy/common.conf ``` ## caddyFile @@ -200,97 +214,36 @@ After you save the file to **/etc/caddy/caddyI2p.nix**, remember to restrict it Create a new caddyFile in `/etc/caddy/caddyI2p.conf` and starts with the following config: ``` -ggucqf2jmtfxcw7us5sts3x7u2qljseocfzlhzebfpihkyvhcqfa.b32.i2p:8081 mdleom.i2p:8081 { +http://ggucqf2jmtfxcw7us5sts3x7u2qljseocfzlhzebfpihkyvhcqfa.b32.i2p:8081 http://mdleom.i2p:8081 { bind ::1 - tls off - - header / { + header { -strict-transport-security + defer } } ``` -Update the B32 address as per the value derived from the [previous section](#B32-address). `mdleom.i2p` is my I2P domain that I registered with a jump service like [stats.i2p](http://stats.i2p/) and it acts as a shortcut to my B32 address. `tls` (HTTPS) is disabled here because it's not necessary as Tor hidden service already encrypts the traffic. Let's Encrypt doesn't support validating a .i2p address. Since HTTPS is not enabled, `strict-transport-security` (HSTS) no longer applies and the header needs to be removed to prevent the browser from attempting to connect to `https://`. It binds to loopback so it only listens to localhost. +Update the B32 address as per the value derived from the [previous section](#B32-address). `mdleom.i2p` is my I2P domain that I registered with a jump service like [stats.i2p](http://stats.i2p/) and it acts as a shortcut to my B32 address. HTTPS is disabled by specifying `http://` prefix, HTTPS is not necessary as Eepsite already encrypts the traffic. Let's Encrypt doesn't support validating a .i2p address. Since HTTPS is not enabled, `strict-transport-security` (HSTS) no longer applies and the header needs to be removed to prevent the browser from attempting to connect to `https://`. It binds to IPv6 loopback so it only listens to localhost, specify `bind 127.0.0.1 ::1` if you need IPv4. -The rest are similar to "[caddyTor.conf](/blog/2020/03/16/tor-hidden-onion-nixos/#caddyTor.conf)" and "[caddyProxy.conf](/blog/2020/03/14/caddy-nix-part-3/#caddyFile)". +The rest are similar to "[caddyTor.conf](/blog/2020/03/16/tor-hidden-onion-nixos/#caddyTor.conf)" and "[caddyProxy.conf](/blog/2020/03/14/caddy-nix-part-3/#Complete-Caddyfile)". Content of "common.conf" is available at [this section](/blog/2020/03/14/caddy-nix-part-3/#Complete-Caddyfile). ``` plain /etc/caddy/caddyI2p.conf -(removeHeaders) { - header_upstream -cookie - header_upstream -referer - header_upstream -cf-ipcountry - header_upstream -cf-connecting-ip - header_upstream -x-forwarded-for - header_upstream -x-forwarded-proto - header_upstream -cf-ray - header_upstream -cf-visitor - header_upstream -true-client-ip - header_upstream -cdn-loop - header_upstream -cf-request-id - header_upstream -cf-cache-status -} - -(staticallyCfg) { - header_upstream Host cdn.statically.io -} +import common.conf # I2P Eepsite -ggucqf2jmtfxcw7us5sts3x7u2qljseocfzlhzebfpihkyvhcqfa.b32.i2p:8081 mdleom.i2p:8081 { +http://ggucqf2jmtfxcw7us5sts3x7u2qljseocfzlhzebfpihkyvhcqfa.b32.i2p:8081 http://mdleom.i2p:8081 { bind ::1 - tls off - - header / { - -server - -alt-svc - -cdn-cache - -cdn-cachedat - -cdn-edgestorageid - -cdn-pullzone - -cdn-requestcountrycode - -cdn-requestid - -cdn-uid - -cf-cache-status - -cf-ray - -cf-request-id - -etag - -set-cookie - -strict-transport-security - -x-bytes-saved - -x-cache - -x-nf-request-id - -x-served-by - Cache-Control "max-age=604800, public" - Referrer-Policy "no-referrer" + header { + import setHeaders + -strict-transport-origin + defer } - header /libs { - Cache-Control "public, max-age=31536000, immutable" - } - - proxy /img https://cdn.statically.io/img/gitlab.com/curben/blog/raw/site { - without /img - import removeHeaders - import staticallyCfg - } - - rewrite /screenshot { - r (.*) - to /screenshot{1}?mobile=true - } - - proxy /screenshot https://cdn.statically.io/screenshot/curben.netlify.app { - without /screenshot - import removeHeaders - import staticallyCfg - } - - proxy / https://curben.netlify.app { - import removeHeaders - header_upstream Host curben.netlify.app - } + import pathProxy } + ``` ### Alternate Caddyfile @@ -299,17 +252,16 @@ There is another approach which is suitable if you have a website that you don't ``` # Do not use this approach unless you are absolutely sure -ggucqf2jmtfxcw7us5sts3x7u2qljseocfzlhzebfpihkyvhcqfa.b32.i2p:8081 mdleom.i2p:8081 { +http://ggucqf2jmtfxcw7us5sts3x7u2qljseocfzlhzebfpihkyvhcqfa.b32.i2p:8081 http://mdleom.i2p:8081 { bind ::1 - tls off - - header / { + header { -strict-transport-security + defer } - proxy / https://mdleom.com { - header_upstream Host mdleom.com + reverse_proxy https://mdleom.com { + header_up Host mdleom.com } } ``` @@ -318,7 +270,7 @@ ggucqf2jmtfxcw7us5sts3x7u2qljseocfzlhzebfpihkyvhcqfa.b32.i2p:8081 mdleom.i2p:808 Start the Caddy service. -``` js /etc/nixos/configuration.nix +``` nix /etc/nixos/configuration.nix require = [ /etc/caddy/caddyProxy.nix /etc/caddy/caddyTor.nix /etc/caddy/caddyI2p.nix ]; services.caddyI2p = { enable = true; diff --git a/source/_posts/tor-hidden-onion-nixos.md b/source/_posts/tor-hidden-onion-nixos.md index e873fbf..fdb030c 100644 --- a/source/_posts/tor-hidden-onion-nixos.md +++ b/source/_posts/tor-hidden-onion-nixos.md @@ -2,7 +2,7 @@ title: "How to make your website available over Tor hidden service on NixOS" excerpt: "A guide on Tor hidden service on NixOS" date: 2020-03-16 -updated: 2020-09-09 +updated: 2020-11-09 tags: - server - linux @@ -12,6 +12,8 @@ tags: - censorship --- +> 9 Nov 2020: Updated to Caddy 2.1 syntax. Refer to {% post_link caddy-upgrade-v2-proxy 'this article' %} for upgrade guide. + In this segment, I show you how I set up Tor hidden (.onion) service that reverse proxy to curben.netlify.app. This website can be accessed through the following [.onion address](http://xw226dvxac7jzcpsf4xb64r4epr6o5hgn46dxlqk7gnjptakik6xnzqd.onion). This post is Part 4 of a series of articles that show you how I set up Caddy, Tor hidden service and I2P Eepsite on NixOS: @@ -32,7 +34,7 @@ Note that this only applies to the traffic between visitor and the (Caddy) web s The first step is to bring up a Tor hidden service to get an onion address. Add the following options to **configuration.nix**: -``` plain /etc/nixos/configuration.nix +``` nix /etc/nixos/configuration.nix ## Tor onion services.tor = { enable = true; @@ -87,19 +89,29 @@ I set up another Caddy-powered reverse proxy which is separate from the {% post_ with lib; let - cfg = config.services.caddyTor; + cfg = config.services.caddyProxy; in { - options.services.caddyTor = { + options.services.caddyProxy = { enable = mkEnableOption "Caddy web server"; config = mkOption { - default = "/etc/caddy/caddyTor.conf"; + default = "/etc/caddy/caddyProxy.conf"; type = types.str; description = "Path to Caddyfile"; }; + adapter = mkOption { + default = "caddyfile"; + example = "nginx"; + type = types.str; + description = '' + Name of the config adapter to use. + See https://caddyserver.com/docs/config-adapters for the full list. + ''; + }; + dataDir = mkOption { - default = "/var/lib/caddyTor"; + default = "/var/lib/caddyProxy"; type = types.path; description = '' The data directory, for storing certificates. Before 17.09, this @@ -117,45 +129,45 @@ in { }; config = mkIf cfg.enable { - systemd.services.caddyTor = { + systemd.services.caddyProxy = { description = "Caddy web server"; after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; # systemd-networkd-wait-online.service wantedBy = [ "multi-user.target" ]; - environment = mkIf (versionAtLeast config.system.stateVersion "17.09") - { CADDYPATH = cfg.dataDir; }; - startLimitIntervalSec = 86400; # 21.03+ # https://github.com/NixOS/nixpkgs/pull/97512 - # startLimitBurst = 5; + # startLimitIntervalSec = 14400; + # startLimitBurst = 10; serviceConfig = { - ExecStart = '' - ${cfg.package}/bin/caddy -root=/var/tmp -conf=${cfg.config} - ''; - ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + ExecStart = "${cfg.package}/bin/caddy run --config ${cfg.config} --adapter ${cfg.adapter}"; + ExecReload = "${cfg.package}/bin/caddy reload --config ${cfg.config} --adapter ${cfg.adapter}"; Type = "simple"; User = "caddyProxy"; Group = "caddyProxy"; - Restart = "on-failure"; - # <= 20.09 - StartLimitBurst = 5; + Restart = "on-abnormal"; + StartLimitIntervalSec = 14400; + StartLimitBurst = 10; NoNewPrivileges = true; - LimitNPROC = 64; + LimitNPROC = 512; LimitNOFILE = 1048576; PrivateTmp = true; PrivateDevices = true; ProtectHome = true; ProtectSystem = "full"; ReadWriteDirectories = cfg.dataDir; + KillMode = "mixed"; + KillSignal = "SIGQUIT"; + TimeoutStopSec = "5s"; }; }; - - users.users.caddyTor = { + + users.users.caddyProxy = { home = cfg.dataDir; createHome = true; }; - users.groups.caddyTor = { - members = [ "caddyTor" ]; + users.groups.caddyProxy = { + members = [ "caddyProxy" ]; }; }; } @@ -175,96 +187,40 @@ After you save the file to **/etc/caddy/CaddyTor.nix**, remember to restrict it Create a new caddyFile in `/etc/caddy/caddyTor.conf` and starts with the following config: ``` -xw226dvxac7jzcpsf4xb64r4epr6o5hgn46dxlqk7gnjptakik6xnzqd.onion:8080 { +import common.conf + +# Tor onion +http://xw226dvxac7jzcpsf4xb64r4epr6o5hgn46dxlqk7gnjptakik6xnzqd.onion:8080 { bind ::1 - tls off - - header / { - -strict-transport-security + header { + import setHeaders + -strict-transport-origin + defer } + + import pathProxy } ``` -Update the onion address to the value shown in "[/var/lib/tor/onion/myOnion/hostname](#configuration.nix)". `tls` (HTTPS) is disabled here because it's not necessary as Tor hidden service already encrypts the traffic. Let's Encrypt doesn't support validating a .onion address. The only way is to purchase the cert from [Digicert](https://www.digicert.com/blog/ordering-a-onion-certificate-from-digicert/). Since HTTPS is not enabled, `strict-transport-security` (HSTS) no longer applies and the header needs to be removed to prevent the browser from attempting to connect to `https://`. It binds to loopback so it only listens to localhost. +Update the onion address to the value shown in "[/var/lib/tor/onion/myOnion/hostname](#configuration.nix)". HTTPS is disabled by specifying `http://` prefix, HTTPS is not necessary as Tor hidden service already encrypts the traffic. Let's Encrypt doesn't support validating a .onion address. The only way is to purchase the cert from [Digicert](https://www.digicert.com/blog/ordering-a-onion-certificate-from-digicert/). Since HTTPS is not enabled, `strict-transport-security` (HSTS) no longer applies and the header needs to be removed to prevent the browser from attempting to connect to `https://`. It binds to IPv6 loopback so it only listens to localhost, specify `bind 127.0.0.1 ::1` if you need IPv4. -The rest are similar to "[caddyProxy.conf](/blog/2020/03/14/caddy-nix-part-3/#caddyFile)". +The rest are similar to "[caddyProxy.conf](blog/2020/03/14/caddy-nix-part-3/#Complete-Caddyfile)". Content of "common.conf" is available at [this section](/blog/2020/03/14/caddy-nix-part-3/#Complete-Caddyfile). ``` plain /etc/caddy/caddyTor.conf -(removeHeaders) { - header_upstream -cookie - header_upstream -referer - header_upstream -cf-ipcountry - header_upstream -cf-connecting-ip - header_upstream -x-forwarded-for - header_upstream -x-forwarded-proto - header_upstream -cf-ray - header_upstream -cf-visitor - header_upstream -true-client-ip - header_upstream -cdn-loop - header_upstream -cf-request-id - header_upstream -cf-cache-status -} - -(staticallyCfg) { - header_upstream Host cdn.statically.io -} +import common.conf # Tor onion -xw226dvxac7jzcpsf4xb64r4epr6o5hgn46dxlqk7gnjptakik6xnzqd.onion:8080 { +http://xw226dvxac7jzcpsf4xb64r4epr6o5hgn46dxlqk7gnjptakik6xnzqd.onion:8080 { bind ::1 - tls off - - header / { - -server - -alt-svc - -cdn-cache - -cdn-cachedat - -cdn-edgestorageid - -cdn-pullzone - -cdn-requestcountrycode - -cdn-requestid - -cdn-uid - -cf-cache-status - -cf-ray - -cf-request-id - -etag - -set-cookie - -strict-transport-security - -x-bytes-saved - -x-cache - -x-nf-request-id - -x-served-by - Cache-Control "max-age=604800, public" - Referrer-Policy "no-referrer" + header { + import setHeaders + -strict-transport-origin + defer } - header /libs { - Cache-Control "public, max-age=31536000, immutable" - } - - proxy /img https://cdn.statically.io/img/gitlab.com/curben/blog/raw/site { - without /img - import removeHeaders - import staticallyCfg - } - - rewrite /screenshot { - r (.*) - to /screenshot{1}?mobile=true - } - - proxy /screenshot https://cdn.statically.io/screenshot/curben.netlify.app { - without /screenshot - import removeHeaders - import staticallyCfg - } - - proxy / https://curben.netlify.app { - import removeHeaders - header_upstream Host curben.netlify.app - } + import pathProxy } ``` @@ -276,17 +232,16 @@ This is also suitable if you have a website that you can't root access. ``` # Do not use this approach unless you are absolutely sure -xw226dvxac7jzcpsf4xb64r4epr6o5hgn46dxlqk7gnjptakik6xnzqd.onion:8080 { +http://xw226dvxac7jzcpsf4xb64r4epr6o5hgn46dxlqk7gnjptakik6xnzqd.onion:8080 { bind ::1 - tls off - - header / { + header { -strict-transport-security + defer } - proxy / https://mdleom.com { - header_upstream Host mdleom.com + reverse_proxy https://mdleom.com { + header_up Host mdleom.com } } ```