diff --git a/docs/examples/vaultwarden/README.md b/docs/examples/vaultwarden/README.md new file mode 100644 index 0000000..dbe2d4c --- /dev/null +++ b/docs/examples/vaultwarden/README.md @@ -0,0 +1,30 @@ +# Vaultwarden setup + +This folder contain an example configuration for setting up +Vaultwarden on Linode. But before deploying to linode, you can +actually test the deployment locally with VirtualBox. + +First, [setup NixOS on a Linode instance](/docs/tutorials/linode.md). + +When that's done, explore the files in this folder. + +To try it out locally, follow [deploy to staging](/docs/tutorials/deploystaging.md). + +```bash +nixops set-args --arg domain '"dev.mydomain.com"' --network dev +nixops info --network dev +``` + +The TL; DR version is, assuming you're where this file is located: + +```bash +export NIXOPS_DEPLOYMENT=vaultwarden-staging +export DISNIXOS_USE_NIXOPS=1 + +nixops create ./network-virtualbox.nix -d vaultwarden-staging + +nixops deploy --network dev +nixops reboot + +disnixos-env -s services.nix -n network-virtualbox.nix -d distribution.nix +``` diff --git a/docs/examples/vaultwarden/dev/nixops.nix b/docs/examples/vaultwarden/dev/nixops.nix new file mode 100644 index 0000000..6f1516d --- /dev/null +++ b/docs/examples/vaultwarden/dev/nixops.nix @@ -0,0 +1,35 @@ +{ + domain ? "dev.mydomain.com", +}: + +{ + network = { + storage.legacy = {}; + }; + + machine1 = { system, pkgs, lib, ... }: + with lib; + let + utils = pkgs.lib.callPackageWith pkgs ./../../../../utils.nix { }; + + base = ((import ./../network.nix).machine1 { + inherit system pkgs lib; + inherit domain utils; + secret = x: x; + }); + + vbox = (import ./../network.nix).virtualbox; + + mkPortMapping = {name, host, guest, protocol ? "tcp"}: + ["--natpf1" "${name},${protocol},,${toString host},,${toString guest}"]; + in + recursiveUpdate base { + deployment.targetEnv = "virtualbox"; + deployment.virtualbox = { + memorySize = 1024; + vcpu = 2; + headless = true; + vmFlags = concatMap mkPortMapping vbox.portMappings; + }; + }; +} diff --git a/docs/examples/vaultwarden/distribution.nix b/docs/examples/vaultwarden/distribution.nix new file mode 100644 index 0000000..ebfb6e1 --- /dev/null +++ b/docs/examples/vaultwarden/distribution.nix @@ -0,0 +1,23 @@ +{ infrastructure +, pkgs ? import {} +}: + +with infrastructure; +let + customPkgs = (pkgs.callPackage (./../../..) {}).customPkgs { + inherit pkgs; + }; + + keycloak = customPkgs.keycloak {}; + vaultwarden = customPkgs.vaultwarden {}; +in +{ + HaproxyService = [ machine1 ]; + + KeycloakService = [ machine1 ]; + KeycloakCliService = [ machine1 ]; + + KeycloakHaproxyService = [ machine1 ]; +} +// keycloak.distribute [ machine1 ] +// vaultwarden.distribute [ machine1 ] diff --git a/docs/examples/vaultwarden/machines/machine1-configuration.nix b/docs/examples/vaultwarden/machines/machine1-configuration.nix new file mode 100644 index 0000000..16a39bd --- /dev/null +++ b/docs/examples/vaultwarden/machines/machine1-configuration.nix @@ -0,0 +1,160 @@ +{ hostname +, userName +, userPackages +, systemPackages +, address +, gateway +, sshPublicKey +, allowedTCPPorts +}: + +{ + imports = + [ # Include the results of the hardware scan. + ./machine1-hardware-configuration.nix + ]; + + # Use the GRUB 2 boot loader. + boot.loader.grub.enable = true; + boot.loader.grub.version = 2; + # boot.loader.grub.efiSupport = true; + # boot.loader.grub.efiInstallAsRemovable = true; + # boot.loader.efi.efiSysMountPoint = "/boot/efi"; + # Define on which hard drive you want to install Grub. + # boot.loader.grub.device = "/dev/sda"; # or "nodev" for efi only + + networking.hostName = hostname; # Define your hostname. + # Pick only one of the below networking options. + # networking.wireless.enable = true; # Enables wireless support via wpa_supplicant. + networking.networkmanager.enable = true; # Easiest to use and most distros use this by default. + networking.usePredictableInterfaceNames = false; + networking.enableIPv6 = false; + + # Set your time zone. + # time.timeZone = "Europe/Amsterdam"; + + # Configure network proxy if necessary + # networking.proxy.default = "http://user:password@proxy:port/"; + # networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain"; + + # Select internationalisation properties. + # i18n.defaultLocale = "en_US.UTF-8"; + # console = { + # font = "Lat2-Terminus16"; + # keyMap = "us"; + # useXkbConfig = true; # use xkbOptions in tty. + # }; + + # Enable the X11 windowing system. + # services.xserver.enable = true; + + # Configure keymap in X11 + # services.xserver.layout = "us"; + # services.xserver.xkbOptions = { + # "eurosign:e"; + # "caps:escape" # map caps to escape. + # }; + + # Enable CUPS to print documents. + # services.printing.enable = true; + + # Enable sound. + # sound.enable = true; + # hardware.pulseaudio.enable = true; + + # Enable touchpad support (enabled default in most desktopManager). + # services.xserver.libinput.enable = true; + + # Define a user account. Don't forget to set a password with ‘passwd’. + users.users.${userName} = { + isNormalUser = true; + extraGroups = [ "wheel" "networkmanager" ]; # Enable ‘sudo’ for the user. + packages = userPackages; + openssh.authorizedKeys.keys = [ sshPublicKey ]; + }; + + # List packages installed in system profile. To search, run: + # $ nix search wget + environment.systemPackages = systemPackages; + + # Some programs need SUID wrappers, can be configured further or are + # started in user sessions. + # programs.mtr.enable = true; + # programs.gnupg.agent = { + # enable = true; + # enableSSHSupport = true; + # }; + + # List services that you want to enable: + + # Enable the OpenSSH daemon. + services.openssh = { + enable = true; + permitRootLogin = "yes"; + passwordAuthentication = false; + }; + + nix.trustedUsers = [ + "deployer" + ]; + + users.groups.deployer = {}; + users.users.deployer = { + isSystemUser = true; + group = "deployer"; + extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user. + openssh.authorizedKeys.keys = [ sshPublicKey ]; + }; + users.users."root" = { + openssh.authorizedKeys.keys = [ sshPublicKey ]; + }; + + security.sudo.wheelNeedsPassword = false; + + services.longview = { + enable = true; + apiKeyFile = "/var/lib/longview/apiKeyFile"; + + apacheStatusUrl = ""; + nginxStatusUrl = ""; + + mysqlUser = ""; + mysqlPassword = ""; + }; + + # Open ports in the firewall. + networking.firewall.allowedTCPPorts = allowedTCPPorts; + # networking.firewall.allowedUDPPorts = [ ... ]; + # Or disable the firewall altogether. + # networking.firewall.enable = false; + + networking.domain = "members.linode.com"; + networking.search = [ "members.linode.com" ]; + networking.resolvconf.extraOptions = [ "rotate" ]; + networking.nameservers = [ + "173.230.145.5" + "173.230.147.5" + "173.230.155.5" + "173.255.212.5" + "173.255.219.5" + "173.255.241.5" + "173.255.243.5" + "173.255.244.5" + "74.207.241.5" + "74.207.242.5" + ]; + + # Copy the NixOS configuration file and link it from the resulting system + # (/run/current-system/configuration.nix). This is useful in case you + # accidentally delete configuration.nix. + # system.copySystemConfiguration = true; + + # This value determines the NixOS release from which the default + # settings for stateful data, like file locations and database versions + # on your system were taken. It‘s perfectly fine and recommended to leave + # this value at the release version of the first install of this system. + # Before changing this value read the documentation for this option + # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html). + system.stateVersion = "22.05"; # Did you read the comment? + +} diff --git a/docs/examples/vaultwarden/machines/machine1-hardware-configuration.nix b/docs/examples/vaultwarden/machines/machine1-hardware-configuration.nix new file mode 100644 index 0000000..77a9ab6 --- /dev/null +++ b/docs/examples/vaultwarden/machines/machine1-hardware-configuration.nix @@ -0,0 +1,57 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = + [ (modulesPath + "/profiles/qemu-guest.nix") + ]; + + boot.initrd.availableKernelModules = [ "virtio_pci" "virtio_scsi" "ahci" "sd_mod" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = + { device = "/dev/disk/by-label/nixos"; + fsType = "ext4"; + }; + + swapDevices = + [ { device = "/dev/disk/by-label/swap"; } + ]; + + boot.kernelParams = [ "console=ttyS0,19200n8" ]; + boot.loader.grub.extraConfig = '' + serial --speed=19200 --unit=0 --word=8 --parity=no --stop=1; + terminal_input serial; + terminal_output serial + ''; + boot.loader.grub.forceInstall = true; + boot.loader.grub.device = "nodev"; + boot.loader.timeout = 10; + + # Enables DHCP on each ethernet and wireless interface. In case of scripted networking + # (the default) this is the recommended approach. When using systemd-networkd it's + # still possible to use this option, but it's recommended to use it in conjunction + # with explicit per-interface declarations with `networking.interfaces..useDHCP`. + networking.useDHCP = lib.mkDefault false; + # networking.interfaces.eth0.useDHCP = true; + networking.interfaces.eth0 = { + ipv4 = { + addresses = [ + { + address = "45.79.76.142"; + prefixLength = 24; + } + ]; + }; + }; + networking.defaultGateway = { + address = "45.79.76.1"; + interface = "eth0"; + }; + + hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/docs/examples/vaultwarden/network.nix b/docs/examples/vaultwarden/network.nix new file mode 100644 index 0000000..9e59014 --- /dev/null +++ b/docs/examples/vaultwarden/network.nix @@ -0,0 +1,220 @@ +rec { + machine1 = { system + , pkgs + , lib + , utils + , domain + , secret + , ... }: + let + customPkgs = (pkgs.callPackage (./../../..) {}).customPkgs { + inherit system pkgs utils secret; + }; + + vaultwarden = customPkgs.vaultwarden {}; + keycloak = customPkgs.keycloak {}; + + httpUser = "http"; + httpGroup = "http"; + httpRoot = "/usr/share/webapps"; + + phpfpmUser = "phpfpm"; + phpfpmGroup = "phpfpm"; + phpfpmRoot = "/run/php-fpm"; + + keycloakUser = "keycloak"; + keycloakGroup = "keycloak"; + + caddyHttpPort = 10001; + caddyHttpsPort = 10002; + + keycloaksecretsdir = "/run/keys/keycloakcliconfig"; + keycloakusers = [ "me" "friend" ]; + in + rec { + users.groups = { + http = { + name = httpGroup; + }; + phpfpm = { + name = phpfpmGroup; + }; + keycloak = { + name = keycloakGroup; + }; + keycloakcli = { + name = "keycloakcli"; + }; + "${vaultwarden.group}" = { + name = "${vaultwarden.group}"; + }; + }; + users.users = { + http = { + name = httpUser; + group = httpGroup; + home = httpRoot; + isSystemUser = true; + }; + phpfpm = { + name = phpfpmUser; + group = phpfpmGroup; + home = phpfpmRoot; + isSystemUser = true; + }; + keycloak = { + name = keycloakUser; + group = keycloakGroup; + # home ? + isSystemUser = true; + }; + keycloakcli = { + name = "keycloakcli"; + group = "keycloakcli"; + extraGroups = [ "keys" ]; + isSystemUser = true; + }; + "${vaultwarden.user}" = { + name = vaultwarden.user; + group = vaultwarden.group; + extraGroups = [ "keys" ]; + isSystemUser = true; + }; + }; + + # deployment.keys = { + # linode.text = '' + # LINODE_HTTP_TIMEOUT=10 + # LINODE_POLLING_INTERVAL=10 + # LINODE_PROPAGATION_TIMEOUT=240 + # LINODE_TOKEN=383525f4d58919d43506e6ab43a549a6eda6491eccb8e384d43013f0bcf45d47 + # ''; + + # keycloakdbpassword.text = '' + # KC_DB_PASSWORD="${secret "${domain}/keycloakdbpassword"}" + # ''; + + # keycloakinitialadmin.text = '' + # KEYCLOAK_ADMIN_PASSWORD="${secret "${domain}/${keycloak.subdomain}/admin"}" + # ''; + + # # This convention is for keycloak-cli-config + # "keycloak.password" = { + # destDir = keycloaksecretsdir; + # user = "keycloakcli"; + # text = secret "${domain}/${keycloak.subdomain}/admin"; + # }; + # "keycloakusers" = + # let + # e = str: lib.strings.escape [''\''] (lib.strings.escape [''"''] str); + # in + # { + # user = "keycloakcli"; + # text = lib.concatMapStringsSep "\n" + # (name: "KEYCLOAK_USERS_${lib.strings.toUpper name}_PASSWORD=${e (secret "${domain}/${keycloak.subdomain}/${name}")}") + # keycloakusers; + # }; + # } + # // vaultwarden.deployKeys domain; + + security.acme = { + acceptTerms = true; + certs = { + "${domain}" = { + extraDomainNames = ["*.${domain}"]; + }; + }; + defaults = { + group = httpGroup; + email = "ibizapeanut@gmail.com"; + dnsProvider = "linode"; + dnsResolver = "8.8.8.8"; + credentialsFile = "/run/keys/linode"; + enableDebugLogs = true; + }; + }; + + services = { + openssh = { + enable = true; + }; + + disnix = { + enable = true; + # useWebServiceInterface = true; + }; + + dnsmasq = { + enable = true; + servers = [ "192.168.50.15" "192.168.50.1" ]; + extraConfig = + let + subdomains = [ + "machine1" + keycloak.subdomain + vaultwarden.subdomain + ]; + + inherit domain; + in (lib.concatMapStrings + (subdomain: "address=/${subdomain}.${domain}/127.0.0.1\naddress=/${subdomain}/127.0.0.1\n") + subdomains) + ; + }; + + # tomcat.enable = false; + + postgresql = { + enable = true; + package = pkgs.postgresql_14; + + port = 5432; + enableTCPIP = true; + authentication = pkgs.lib.mkOverride 10 '' + local all all trust + host all all 127.0.0.1/32 trust + host all all ::1/128 trust + ''; + }; + }; + + dysnomia = { + enable = true; + enableLegacyModules = false; + extraContainerProperties = { + system = { + inherit domain; + }; + postgresql-database = { + service_name = "postgresql.service"; + port = builtins.toString services.postgresql.port; + }; + keycloaksecrets = { + rootdir = keycloaksecretsdir; + }; + }; + }; + + networking.firewall.allowedTCPPorts = [ services.postgresql.port ] ++ virtualbox.guestPorts; + }; + + virtualbox = rec { + portMappings = [ + { name = "ssh"; + host = 22; + guest = 22; + } + { name = "dns"; + host = 53; + guest = 53; + } + { name = "https"; + host = 443; + guest = 443; + } + ]; + + hostPorts = map (x: x.host) portMappings; + guestPorts = map (x: x.guest) portMappings; + }; +} diff --git a/docs/examples/vaultwarden/prod/nixops.nix b/docs/examples/vaultwarden/prod/nixops.nix new file mode 100644 index 0000000..bc98a03 --- /dev/null +++ b/docs/examples/vaultwarden/prod/nixops.nix @@ -0,0 +1,40 @@ +let + hostname = "machine1"; + domain = "mydomain.com"; +in +{ + machine1 = { system, pkgs, lib, ... }: + let + utils = pkgs.lib.callPackageWith pkgs ./utils.nix { }; + + base = ((import ./network.nix).machine1 { + inherit system pkgs lib; + inherit domain utils; + }); + + vbox = (import ./network.nix).virtualbox; + in + lib.recursiveUpdate base rec { + deployment.targetHost = hostname; + imports = [ + (import ./machines/machine1-configuration.nix { + inherit hostname; + + userName = "me"; + userPackages = with pkgs; []; + systemPackages = with pkgs; [ + curl + inetutils + mtr + sysstat + tmux + vim + ]; + address = "45.79.76.142"; + gateway = "45.79.76.1"; + sshPublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB/UeAaMECJNLMZ23vLb3A3XT7OJDcpj2OWgXzt8+GLU me@laptop"; + allowedTCPPorts = vbox.guestPorts; + }) + ]; + }; +} diff --git a/docs/examples/vaultwarden/result b/docs/examples/vaultwarden/result new file mode 120000 index 0000000..37db51b --- /dev/null +++ b/docs/examples/vaultwarden/result @@ -0,0 +1 @@ +/nix/store/wpq9c65h5ahq6aspdvprc547rmym7qgc-virtualbox-nixops-22.11.vmdk \ No newline at end of file diff --git a/docs/examples/vaultwarden/services.nix b/docs/examples/vaultwarden/services.nix new file mode 100644 index 0000000..a8ab6a2 --- /dev/null +++ b/docs/examples/vaultwarden/services.nix @@ -0,0 +1,198 @@ +{ system, pkgs, distribution, invDistribution }: + +let + utils = pkgs.lib.callPackageWith pkgs ./utils.nix { }; + + customPkgs = (pkgs.callPackage (./../../..) {}).customPkgs { + inherit system pkgs utils; + }; + + getTarget = name: builtins.elemAt (builtins.getAttr name distribution) 0; + + getDomain = name: (getTarget name).containers.system.domain; + + realm = "myrealm"; + + smtp = utils.recursiveMerge [ + { + from = "vaultwarden@${realm}.com"; + fromName = "vaultwarden"; + port = 587; + authMechanism = "Login"; + } + vaultwarden.smtp + ]; + + keycloak = customPkgs.keycloak {}; + + KeycloakService = customPkgs.mkKeycloakService { + name = "KeycloakService"; + subdomain = keycloak.subdomain; + + # TODO: Get these from infrastructure.nix + user = "keycloak"; + group = "keycloak"; + + postgresServiceName = (getTarget "KeycloakPostgresDB").containers.postgresql-database.service_name; + initialAdminUsername = "admin"; + + keys = { + dbPassword = "keycloakdbpassword"; + initialAdminPassword = "keycloakinitialadmin"; + }; + + # logLevel = "DEBUG,org.hibernate:info,org.keycloak.authentication:debug,org.keycloak:info,org.postgresql:info,freemarker:info"; + logLevel = "INFO"; + hostname = "${keycloak.subdomain}.${getDomain "KeycloakService"}"; + listenPort = 8080; + + dbType = "postgres"; + dbDatabase = keycloak.database.name; + dbUsername = keycloak.database.username; + dbHost = {KeycloakPostgresDB}: KeycloakPostgresDB.target.properties.hostname; + dbPort = (getTarget "KeycloakPostgresDB").containers.postgresql-database.port; + + KeycloakPostgresDB = keycloak.db; + }; + + KeycloakCliService = customPkgs.mkKeycloakCliService rec { + name = "KeycloakCliService"; + + keycloakServiceName = "keycloak.service"; + keycloakSecretsDir = (getTarget name).containers.keycloaksecrets.rootdir; + keycloakUrl = "https://${keycloak.subdomain}.${(getDomain "KeycloakService")}"; + keycloakUser = KeycloakService.initialAdminUsername; + keys = { + userpasswords = "keycloakusers"; + }; + + dependsOn = { + inherit KeycloakService HaproxyService; + }; + + config = (utils.recursiveMerge [ + rec { + inherit realm; + domain = getDomain name; + roles = { + user = []; + admin = ["user"]; + }; + users = { + me = { + email = "me@${domain}"; + firstName = "Me"; + lastName = "Me"; + roles = ["admin"]; + initialPassword = true; + }; + friend = { + email = "friend@${domain}"; + firstName = "Friend"; + lastName = "Friend"; + roles = ["user"]; + initialPassword = true; + }; + }; + } + vaultwarden.keycloakCliConfig + ]); + }; + + KeycloakHaproxyService = customPkgs.mkKeycloakHaproxyService { + name = "KeycloakHaproxyService"; + + domain = "https://${keycloak.subdomain}.${getDomain "KeycloakService"}"; + realms = [realm]; + + inherit KeycloakService; + }; + + vaultwarden = customPkgs.vaultwarden { + subdomain = "vaultwarden"; + ingress = 18005; + sso.realm = realm; + sso.userRole = "user"; + sso.adminRole = "admin"; + + inherit smtp; + + inherit distribution HaproxyService KeycloakService KeycloakCliService; + }; + + HaproxyService = customPkgs.mkHaproxyService { + name = "HaproxyService"; + + user = "http"; + group = "http"; + + dependsOn = { + inherit KeycloakHaproxyService; + }; + + config = {...}: + let + domain = getDomain "HaproxyService"; + in { + certPath = "/var/lib/acme/${domain}/full.pem"; + stats = { + port = 8404; + uri = "/stats"; + refresh = "10s"; + prometheusUri = "/metrics"; + }; + defaults = { + default-server = "init-addr last,none"; + }; + resolvers = { + default = { + nameservers = { + ns1 = "127.0.0.1:53"; + }; + }; + }; + sites = { + vaultwarden = vaultwarden.haproxy distribution.VaultwardenService; + keycloak = { + frontend = { + capture = [ + "request header origin len 128" + ]; + acl = { + acl_keycloak = "hdr_beg(host) ${keycloak.subdomain}."; + acl_keycloak_authorized_origin = "capture.req.hdr(0) -m end .${domain}"; + }; + use_backend = "if acl_keycloak"; + http-response = { + add-header = map (x: x + " if acl_keycloak_authorized_origin") [ + "Access-Control-Allow-Origin %[capture.req.hdr(0)]" + "Access-Control-Allow-Methods GET,\\ HEAD,\\ OPTIONS,\\ POST,\\ PUT" + "Access-Control-Allow-Credentials true" + "Access-Control-Allow-Headers Origin,\\ Accept,\\ X-Requested-With,\\ Content-Type,\\ Access-Control-Request-Method,\\ Access-Control-Request-Headers,\\ Authorization" + ]; + }; + }; + backend = { + servers = [ + { + name = "keycloak1"; + address = "127.0.0.1:8080"; # TODO: should use the hostname + resolvers = "default"; + } + ]; + cookie = "JSESSIONID prefix"; + }; + }; + }; + }; + }; +in + +with pkgs.lib.attrsets; +rec { + inherit KeycloakPostgresDB KeycloakService KeycloakCliService KeycloakHaproxyService; + + inherit HaproxyService; +} +// keycloak.services +// vaultwarden.services