diff --git a/hosts/nb-dominik/configuration.nix b/hosts/nb-dominik/configuration.nix index e69de29..be3d3e3 100644 --- a/hosts/nb-dominik/configuration.nix +++ b/hosts/nb-dominik/configuration.nix @@ -0,0 +1,209 @@ +# Edit this configuration file to define what should be installed on +# your system. Help is available in the configuration.nix(5) man page +# and in the NixOS manual (accessible by running ‘nixos-help’). + +{ config, pkgs, ... }: +{ + nixpkgs.config.allowUnfree = true; + + imports = + [ # Include the results of the hardware scan. + ./utils/modules/clevis.nix + + ./utils/modules/sops.nix + ./utils/modules/nur.nix + ./utils/modules/sway/sway.nix + # ./modules/gnome.nix + ./utils/modules/nvim/default.nix + ./utils/modules/tuxedo.nix + ./utils/modules/autoupgrade.nix + + # ./pkgs/howdy/howdy-module.nix + # ./pkgs/howdy/ir-toggle-module.nix + + # ./modules/howdy + + ./hardware-configuration.nix + ]; + + nixpkgs.overlays = [ (import ./overlays/packages.nix) ]; + + # security.sudo.wheelNeedsPassword = false; + services.clevis.uuid = "7435d48f-f942-485b-9817-328ad3fc0b93"; + + # nixos cross building qemu + boot.binfmt.emulatedSystems = [ "aarch64-linux" ]; + boot.supportedFilesystems = [ "ntfs" ]; + boot.plymouth.enable = true; + boot.plymouth.theme = "breeze"; + boot.kernelParams = ["quiet"]; + # boot.loader.systemd-boot.netbootxyz.enable = true; + # boot.plymouth.themePackages = [ pkgs.nixos-bgrt-plymouth ]; + # boot.plymouth.theme = "nixos-bgrt"; + # allow hibernation + security.protectKernelImage = false; + + sops.defaultSopsFile = ./secrets.yaml; + sops.age.keyFile = "/var/lib/sops-nix/key.txt"; + sops.age.generateKey = true; + + sops.secrets.epicenter_vpn_ca = {}; + sops.secrets.epicenter_vpn_cert = {}; + sops.secrets.epicenter_vpn_key = {}; + sops.secrets.wg_private_key = {}; + sops.secrets.wg_preshared_key = {}; + sops.secrets.wg-cloonar-key = {}; + + virtualisation.docker.enable = true; + virtualisation.virtualbox.host = { + enable = true; + enableExtensionPack = true; + }; + + networking.hostName = "cl-nb-01"; # Define your hostname. + networking.resolvconf.enable = true; + networking.networkmanager.enable = true; # Easiest to use and most distros use this by default. + networking.extraHosts = '' + 10.25.0.25 archive.zeichnemit.at epicenter.works en.epicenter.works + 10.25.0.100 download.intra.epicenter.works + 127.0.0.1 wohnservice.local mieterhilfe.local wohnpartner.local wohnberatung.local wienbautvor.local wienwohntbesser.local + 127.0.0.1 wohnservice-wien.local mieterhilfe.local wohnpartner-wien.local wohnberatung-wien.local wienbautvor.local wienwohntbesser.local + 127.0.0.1 diabetes.local + ''; + + # Set your time zone. + time.timeZone = "Europe/Vienna"; + console.keyMap = "de"; + + users.users.dominik = { + isNormalUser = true; + extraGroups = [ "wheel" "disk" "video" "audio" "mysql" "docker" "vboxusers" "networkmanager" "onepassword" "onepassword-cli" "dialout" ]; # Enable ‘sudo’ for the user. + }; + + environment.systemPackages = with pkgs; [ + bento + vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default. + wget + docker-compose + drone-cli + wireguard-tools + libftdi1 + ]; + + environment.variables = { + TERMINAL_COMMAND = "alacritty"; + }; + + services.blueman.enable = true; + + services.printing.enable = true; + services.printing.drivers = [ pkgs.brlaser ]; + + services.mysql = { + enable = true; + package = pkgs.mariadb; + ensureUsers = [ + { + name = "dominik"; + ensurePermissions = { + "*.*" = "ALL PRIVILEGES"; + }; + } + ]; + }; + + services.postgresql = { + enable = true; + ensureUsers = [ + { + name = "dominik"; + ensurePermissions = { + "DATABASE \"zammad\"" = "ALL PRIVILEGES"; + }; + } + ]; + ensureDatabases = [ "zammad" ]; + }; + + system.stateVersion = "22.11"; # Did you read the comment? + + security.polkit.enable = true; + systemd = { + user.services.polkit-gnome-authentication-agent-1 = { + description = "polkit-gnome-authentication-agent-1"; + wantedBy = [ "graphical-session.target" ]; + wants = [ "graphical-session.target" ]; + after = [ "graphical-session.target" ]; + serviceConfig = { + Type = "simple"; + ExecStart = "${pkgs.polkit_gnome}/libexec/polkit-gnome-authentication-agent-1"; + Restart = "on-failure"; + RestartSec = 1; + TimeoutStopSec = 10; + }; + }; + }; + + + networking.firewall = { + allowedUDPPorts = [ 51820 ]; # Clients and peers can use the same port, see listenport + # if packets are still dropped, they will show up in dmesg + logReversePathDrops = true; + # wireguard trips rpfilter up + extraCommands = '' + ip46tables -t mangle -I nixos-fw-rpfilter -p udp -m udp --sport 51820 -j RETURN + ip46tables -t mangle -I nixos-fw-rpfilter -p udp -m udp --dport 51820 -j RETURN + ''; + extraStopCommands = '' + ip46tables -t mangle -D nixos-fw-rpfilter -p udp -m udp --sport 51820 -j RETURN || true + ip46tables -t mangle -D nixos-fw-rpfilter -p udp -m udp --dport 51820 -j RETURN || true + ''; + }; + # networking.wireguard.interfaces = { + # wg0 = { + # # Determines the IP address and subnet of the client's end of the tunnel interface. + # ips = [ "10.42.98.201/32" ]; + # listenPort = 51820; # to match firewall allowedUDPPorts (without this wg uses random port numbers) + # + # # Path to the private key file. + # # + # # Note: The private key can also be included inline via the privateKey option, + # # but this makes the private key world-readable; thus, using privateKeyFile is + # # recommended. + # privateKeyFile = config.sops.secrets.wg-cloonar-key.path; + # + # peers = [ + # { + # publicKey = "TKQVDmBnf9av46kQxLQSBDhAeaK8r1zh8zpU64zuc1Q="; + # allowedIPs = [ "0.0.0.0/0" ]; + # endpoint = "vpn.cloonar.com:51820"; # ToDo: route to endpoint not automatically configured https://wiki.archlinux.org/index.php/WireGuard#Loop_routing https://discourse.nixos.org/t/solved-minimal-firewall-setup-for-wireguard-client/7577 + # persistentKeepalive = 25; + # } + # ]; + # }; + # }; + + # Facial recognition "Windows hello" + # services.ir-toggle.enable = true; + # services.howdy = { + # enable = true; + # device = "/dev/video2"; + # }; + nix = { + settings.auto-optimise-store = true; + # autoOptimiseStore = true; + gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 30d"; + }; + # Free up to 1GiB whenever there is less than 100MiB left. + extraOptions = '' + min-free = ${toString (100 * 1024 * 1024)} + max-free = ${toString (1024 * 1024 * 1024)} + ''; + }; + + +} + diff --git a/hosts/nb-dominik/hardware-configuration.nix b/hosts/nb-dominik/hardware-configuration.nix new file mode 100644 index 0000000..fda9b13 --- /dev/null +++ b/hosts/nb-dominik/hardware-configuration.nix @@ -0,0 +1,58 @@ +# 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 + "/installer/scan/not-detected.nix") + ]; + + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + boot.initrd.availableKernelModules = [ "xhci_pci" "thunderbolt" "nvme" "usb_storage" "sd_mod" "rtsx_pci_sdmmc" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-intel" ]; + boot.kernelParams = [ "resume=/swap/swapfile" "resume_offset=533760" ]; + boot.resumeDevice = "/dev/disk/by-uuid/92284909-c5dd-4e0f-ab22-64157c8175cb"; + boot.extraModulePackages = [ ]; + + fileSystems."/" = + { device = "/dev/disk/by-uuid/92284909-c5dd-4e0f-ab22-64157c8175cb"; + fsType = "btrfs"; + options = [ "subvol=root" ]; + }; + + # boot.initrd.luks.devices."nixos-enc".device = "/dev/disk/by-uuid/7435d48f-f942-485b-9817-328ad3fc0b93"; + + fileSystems."/home" = + { device = "/dev/disk/by-uuid/92284909-c5dd-4e0f-ab22-64157c8175cb"; + fsType = "btrfs"; + options = [ "subvol=home" ]; + }; + + fileSystems."/boot" = + { device = "/dev/disk/by-uuid/C281-E509"; + fsType = "vfat"; + }; + + fileSystems."/swap" = + { device = "/dev/disk/by-uuid/92284909-c5dd-4e0f-ab22-64157c8175cb"; + fsType = "btrfs"; + options = [ "subvol=swap" ]; + }; + + swapDevices = [{ + device = "/swap/swapfile"; + size = (1024 * 16); + }]; + + # 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 true; + # networking.interfaces.wlp52s0.useDHCP = lib.mkDefault true; + + powerManagement.cpuFreqGovernor = lib.mkDefault "powersave"; + hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/utils/modules/autoupgrade.nix b/utils/modules/autoupgrade.nix new file mode 100644 index 0000000..5bd412a --- /dev/null +++ b/utils/modules/autoupgrade.nix @@ -0,0 +1,7 @@ +{ config, ... }: + +{ + system.autoUpgrade.enable = true; + system.autoUpgrade.allowReboot = false; + system.autoUpgrade.channel = "https://channels.nixos.org/nixos-23.05"; +} diff --git a/utils/modules/bento/fleet.nix b/utils/modules/bento/fleet.nix new file mode 100644 index 0000000..df6107d --- /dev/null +++ b/utils/modules/bento/fleet.nix @@ -0,0 +1,36 @@ +{ + lib, + pkgs, + ... +}: let + create_users = host: { + users.users."${host.username}" = { + createHome = false; + home = "/home/chroot/" + host.username; + isNormalUser = false; + isSystemUser = true; + group = "sftp_users"; + openssh.authorizedKeys.keys = [host.key]; + shell = null; + }; + }; + + users = [ + { + username = "notebook"; + key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDN/2SAFm50kraB1fepAizox/QRXxB7WbqVbH+5OPalDT47VIJGNKOKhixQoqhABHxEoLxdf/C83wxlCVlPV9poLfDgVkA3Lyt5r3tSFQ6QjjOJAgchWamMsxxyGBedhKvhiEzcr/Lxytnoz3kjDG8fqQJwEpdqMmJoMUfyL2Rqp16u+FQ7d5aJtwO8EUqovhMaNO7rggjPpV/uMOg+tBxxmscliN7DLuP4EMTA/FwXVzcFNbOx3K9BdpMRAaSJt4SWcJO2cS2KHA5n/H+PQI7nz5KN3Yr/upJN5fROhi/SHvK39QOx12Pv7FCuWlc+oR68vLaoCKYhnkl3DnCfc7A7"; + } + ]; +in { + imports = builtins.map create_users users; + + users.groups = {sftp_users = {};}; + + services.openssh.extraConfig = '' + Match Group sftp_users + X11Forwarding no + AllowTcpForwarding no + ChrootDirectory %h + ForceCommand internal-sftp + ''; +} diff --git a/utils/modules/bitwarden.nix b/utils/modules/bitwarden.nix new file mode 100644 index 0000000..c1c9d96 --- /dev/null +++ b/utils/modules/bitwarden.nix @@ -0,0 +1,116 @@ +{ + pkgs, + config, + ... +}: let + ldapConfig = { + vaultwarden_url = "https://bitwarden.cloonar.com"; + vaultwarden_admin_token = "@ADMIN_TOKEN@"; + ldap_host = "localhost"; + ldap_bind_dn = "cn=vmail,dc=cloonar,dc=com"; + ldap_bind_password = "@LDAP_PASSWORD@"; + ldap_search_base_dn = "dc=cloonar,dc=com"; + ldap_search_filter = "(&(objectClass=inetOrgPerson))"; + ldap_sync_interval_seconds = 3600; + }; + + ldapConfigFile = + pkgs.runCommand "config.toml" + { + buildInputs = [pkgs.remarshal]; + preferLocalBuild = true; + } '' + remarshal -if json -of toml \ + < ${pkgs.writeText "config.json" (builtins.toJSON ldapConfig)} \ + > $out + ''; +in { + packageOverrides = pkgs: { + nur = import (builtins.fetchTarball "https://github.com/nix-community/NUR/archive/master.tar.gz") { + inherit pkgs; + }; + }; + + environment.systemPackages = with pkgs; [ + nur.repos.mic92.vaultwarden_ldap + ]; + + services.vaultwarden = { + enable = true; + dbBackend = "mysql"; + config = { + domain = "https://bitwarden.cloonar.com"; + signupsAllowed = false; + rocketPort = 3011; + databaseUrl = "mysql://bitwarden:<${config.sops.secrets.bitwarden-db-password.path}@localhost/bitwarden"; + enableDbWal = "false"; + websocketEnabled = true; + smtpHost = "smtp.cloonar.com"; + smtpFrom = "bitwarden@cloonar.com"; + smtpUsername = "bitwarden@cloonar.com"; + }; + }; + + systemd.services.vaultwarden.serviceConfig = { + EnvironmentFile = [config.sops.secrets.bitwarden-smtp-password.path]; + }; + + systemd.services.vaultwarden_ldap = { + wantedBy = ["multi-user.target"]; + + preStart = '' + sed \ + -e "s=@LDAP_PASSWORD@=$(<${config.sops.secrets.bitwarden-ldap-password.path})=" \ + -e "s=@ADMIN_TOKEN@=$(<${config.sops.secrets.bitwarden-admin-token.path})=" \ + ${ldapConfigFile} \ + > /run/vaultwarden_ldap/config.toml + ''; + + serviceConfig = { + Restart = "on-failure"; + RestartSec = "2s"; + ExecStart = "${config.nur.repos.mic92.vaultwarden_ldap}/bin/vaultwarden_ldap"; + Environment = "CONFIG_PATH=/run/vaultwarden_ldap/config.toml"; + + RuntimeDirectory = ["vaultwarden_ldap"]; + User = "vaultwarden_ldap"; + }; + }; + + services.nginx = { + virtualHosts."bitwarden.cloonar.com" = { + forceSSL = true; + enableACME = true; + acmeRoot = null; + extraConfig = '' + client_max_body_size 128M; + ''; + locations."/" = { + proxyPass = "http://localhost:3011"; + proxyWebsockets = true; + }; + locations."/notifications/hub" = { + proxyPass = "http://localhost:3012"; + proxyWebsockets = true; + }; + locations."/notifications/hub/negotiate" = { + proxyPass = "http://localhost:3011"; + proxyWebsockets = true; + }; + }; + }; + + sops.secrets = { + bitwarden-admin-token.owner = "vaultwarden_ldap"; + bitwarden-ldap-password.owner = "vaultwarden_ldap"; + bitwarden-db-password.owner = "vaultwarden"; + bitwarden-smtp-password.owner = "vaultwarden"; + }; + + users.users.vaultwarden_ldap = { + isSystemUser = true; + group = "vaultwarden_ldap"; + }; + + users.groups.vaultwarden_ldap = {}; +} diff --git a/utils/modules/bitwarden/default.nix b/utils/modules/bitwarden/default.nix new file mode 100644 index 0000000..782dc29 --- /dev/null +++ b/utils/modules/bitwarden/default.nix @@ -0,0 +1,126 @@ +{ + pkgs, + config, + ... +}: let + ldapConfig = { + vaultwarden_url = "https://bitwarden.cloonar.com"; + vaultwarden_admin_token = "@ADMIN_TOKEN@"; + ldap_host = "ldap.cloonar.com"; + ldap_ssl = true; + ldap_bind_dn = "cn=bitwarden,ou=system,ou=users,dc=cloonar,dc=com"; + ldap_bind_password = "@LDAP_PASSWORD@"; + ldap_search_base_dn = "ou=users,dc=cloonar,dc=com"; + ldap_search_filter = "(&(objectClass=cloonarUser))"; + ldap_sync_interval_seconds = 3600; + }; + + ldapConfigFile = + pkgs.runCommand "config.toml" + { + buildInputs = [pkgs.remarshal]; + preferLocalBuild = true; + } '' + remarshal -if json -of toml \ + < ${pkgs.writeText "config.json" (builtins.toJSON ldapConfig)} \ + > $out + ''; +in { + imports = [ + ../nur.nix + ]; + + environment.systemPackages = with pkgs; [ + nur.repos.mic92.vaultwarden_ldap + ]; + + services.vaultwarden = { + enable = true; + dbBackend = "mysql"; + config = { + domain = "https://bitwarden.cloonar.com"; + signupsAllowed = false; + rocketPort = 3011; + enableDbWal = "false"; + websocketEnabled = true; + smtpHost = "mail.cloonar.com"; + smtpFrom = "bitwarden@cloonar.com"; + smtpUsername = "bitwarden@cloonar.com"; + }; + }; + + systemd.services.vaultwarden.serviceConfig = { + EnvironmentFile = [config.sops.secrets.bitwarden-smtp-password.path]; + }; + + systemd.services.vaultwarden_ldap = { + wantedBy = ["multi-user.target"]; + + preStart = '' + sed \ + -e "s=@LDAP_PASSWORD@=$(<${config.sops.secrets.bitwarden-ldap-password.path})=" \ + -e "s=@ADMIN_TOKEN@=$(<${config.sops.secrets.bitwarden-admin-token.path})=" \ + ${ldapConfigFile} \ + > /run/vaultwarden_ldap/config.toml + ''; + + serviceConfig = { + Restart = "on-failure"; + RestartSec = "2s"; + ExecStart = "${pkgs.nur.repos.mic92.vaultwarden_ldap}/bin/vaultwarden_ldap"; + Environment = "CONFIG_PATH=/run/vaultwarden_ldap/config.toml"; + + RuntimeDirectory = ["vaultwarden_ldap"]; + User = "vaultwarden_ldap"; + }; + }; + + services.nginx.virtualHosts."bitwarden.cloonar.com" = { + forceSSL = true; + enableACME = true; + acmeRoot = null; + extraConfig = '' + client_max_body_size 128M; + ''; + locations."/" = { + proxyPass = "http://localhost:3011"; + proxyWebsockets = true; + }; + locations."/notifications/hub" = { + proxyPass = "http://localhost:3012"; + proxyWebsockets = true; + }; + locations."/notifications/hub/negotiate" = { + proxyPass = "http://localhost:3011"; + proxyWebsockets = true; + }; + }; + + sops.secrets = { + bitwarden-admin-token = { + owner = "vaultwarden_ldap"; + sopsFile = ./secrets.yaml; + }; + bitwarden-ldap-password = { + owner = "vaultwarden_ldap"; + sopsFile = ./secrets.yaml; + }; + bitwarden-db-password = { + owner = "vaultwarden"; + sopsFile = ./secrets.yaml; + }; + bitwarden-smtp-password = { + owner = "vaultwarden"; + sopsFile = ./secrets.yaml; + }; + }; + + users.users.vaultwarden_ldap = { + isSystemUser = true; + group = "vaultwarden_ldap"; + }; + + users.groups.vaultwarden_ldap = {}; + + services.mysqlBackup.databases = [ "bitwarden" ]; +} diff --git a/utils/modules/bitwarden/secrets.yaml b/utils/modules/bitwarden/secrets.yaml new file mode 100644 index 0000000..e2172d2 --- /dev/null +++ b/utils/modules/bitwarden/secrets.yaml @@ -0,0 +1,33 @@ +bitwarden-admin-token: ENC[AES256_GCM,data:nCj7kwQHTwezG3hh5J+c2MmUXwlGpdNjeh4A4SK/wgdBroAAghMSTuT6B7sjPgX5PmyBpzspdI3XqVUoBHzL6g==,iv:11C/ScaTqI1VlBSd71TA2cZNAu/wSbOs6rnDTlKlPsI=,tag:8eD0VkJn/KZ49yMe4D/MrA==,type:str] +bitwarden-db-password: ENC[AES256_GCM,data:4l3ntOHX4pdiUzfSqOwzObgMRp9eS5fjze6rJu1h3kKr/g/lsESLWiIHUoguixaNmoPU2zy42jEDvhXII6R+1g==,iv:mEMGGGyWerJaAvo7ymNfkR1YgTG1ieB3n40BB6L+UM4=,tag:iRd88BjFMMht9Ku9K34SXQ==,type:str] +bitwarden-ldap-password: ENC[AES256_GCM,data:g6tp0NzXk3ZJTGKHSzFxVZs4DhauzPS6SGW99WFX/CO0Wprgp9lh/evI6T56g2YhIv/3jqNSmi+p1FwdOzValw==,iv:mHMlhJx2aKLLkrPy+Z+/6plS/uMiK+xhYk/PF5m7+wQ=,tag:BgRNstiVnN95/pSX0DYfSw==,type:str] +bitwarden-smtp-password: ENC[AES256_GCM,data:4ruP8yMeTG5A19Oyvv2MBTj2LwecwwYc8BBU1xDT2i757orCNrQHJd0VLtzynluS9ge4vAU7G8islKwR/IIDGsEq74//CxJIyXyH9XLBfc5Jb2Rs1uz/Nz2uCWOCqm1AZ2/8uxXOPPNVhKcs3wxOLbLnA3Yzh+VFKsKIO753FkKllpFbeZanhfD2/N4fAGU4C5F+0HcrLBLBGC3X/CfQyPUSio1uwWPxRJR94DlRdPq+ir4YXHW48Mw/33lJZ+HqApk1Nf+gmTff7XTib1d44ac4JR8m20D8qOQ2Y9vfqJOxD7/PdgeqRLXN3K1PaSDE7JkWoiE0dM3vJ0q+Pqf47tm/xT4qaJvqI0jLXMwqmUg=,iv:TiZrLMPx9UbUf/4zKmRWTERM8phtyTX7Q3dCFqn+Ew4=,tag:55tuxMBWu6WpT4BllKV+pA==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMUmttaXRmcXh2UDRyL1pJ + MmxMSzBqUmxlY290K1djQ3ZSYWM1OXhFbEhvCjlmNnd6T2Z2bHNZK3oxRmllNUFI + RzNvc3hqMXIwd3dwa3crckxVNmNyQXcKLS0tIHFLSXpUOFpyaUVXa2hJU0pheU5h + NlZoeVNYdTQ1c0pUQ1o4NnJxaWFTTFEK0l7vHpXj00fUFno5gjS3apPRWercgng6 + SOygmyQiP/EWI/G0M6W8gjq76pQamYJWVVJVFwxqg4BxAhdMwxtVXQ== + -----END AGE ENCRYPTED FILE----- + - recipient: age1y6lvl5jkwc47p5ae9yz9j9kuwhy7rtttua5xhygrgmr7ehd49svsszyt42 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCNjE2c21zVktrTUJDVWJ2 + UWdqZHErWkpGVnY5Q0F3QUlBNkprRFJFbHpjCnJvQmZoZ0x2c0dxU0FwbExGaTIz + eGFoczZma2dBVEJjQ1ljb24zYWJEQWsKLS0tIDVmWWQyYmQ1RS9oV0p0ZmwyUzJI + NkdzbFczM0VteW5ESGFNcmNJdFRNODAK8HRoc2thwAH8pvnMwesRs8OesVGHW/Iu + viPtt+w6FNwYt4LXnFlBMdGXfrWE0fWxokM0sm5GglKuFGn1zf3+VA== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2023-01-28T21:53:06Z" + mac: ENC[AES256_GCM,data:jZq4UzkxyX/UhrmeKO7sFQpTlMB13lyi5/duXA0s2XX3W0U9g+TSZm21WiRGPjKmteJg0w2OhFsNk/y0uvD/oPE1ttLz/YRgiinuCoyufoX51AgQqS0KFxNBkTaDzoaKk3z1j8nEhAY2U0YS4fpOCNAkMsKdVZeTVOitcp/UeIE=,iv:5EzYCqUZri1VmD9wqQGxpypZe4F2h8W3D8a7mYbBBrg=,tag:iEFJBFmRJVw4YP5/V+21dQ==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.7.3 diff --git a/utils/modules/borgbackup.nix b/utils/modules/borgbackup.nix new file mode 100644 index 0000000..0d7999b --- /dev/null +++ b/utils/modules/borgbackup.nix @@ -0,0 +1,87 @@ +{ pkgs, config, lib, ... }: + +let + repo = config.borgbackup.repo; + #repo = config.borgrepo; + #repo = "u149513-sub3@u149513-sub3.your-backup.de:borg"; + borgMount = pkgs.writeShellScriptBin "borg-mount" '' + export BORG_PASSCOMMAND='cat ${config.sops.secrets.borg-passphrase.path}' + borg mount --rsh "ssh -p23 -i ${config.sops.secrets.borg-ssh-key.path}" ${repo}::$1 $2 + ''; + borgList = pkgs.writeShellScriptBin "borg-list" '' + export BORG_PASSCOMMAND='cat ${config.sops.secrets.borg-passphrase.path}' + borg --rsh "ssh -p23 -i ${config.sops.secrets.borg-ssh-key.path}" list ${repo} + ''; + + borgBackup = pkgs.writeShellScriptBin "borg-backup" '' + systemctl restart borgbackup-job-default.service + ''; + + borgRestore = pkgs.writeShellScriptBin "borg-restore" '' + cd / + export BORG_PASSCOMMAND='cat ${config.sops.secrets.borg-passphrase.path}' + borg --rsh "ssh -p23 -i ${config.sops.secrets.borg-ssh-key.path}" list ${repo} + borg extract --list --rsh "ssh -p23 -i ${config.sops.secrets.borg-ssh-key.path}" ${repo}::$1 + ''; +in { + options = with lib; with types; { + borgbackup = mkOption { + description = "Options for borg module"; + type = submodule { + options.repo = mkOption { + type = types.str; + description = "borg repo"; + }; + }; + }; + }; + + + + config = { + sops.secrets.borg-passphrase = {}; + sops.secrets.borg-ssh-key = {}; + + environment.systemPackages = [ + borgMount + borgList + borgBackup + borgRestore + ]; + + services.borgbackup.jobs.default = { + paths = [ + "/home" + "/var" + "/root" + ]; + exclude = [ + "/var/lib/containerd" + # already included in database backup + "/var/lib/mysql" + "/var/lib/postgresql" + "/var/lib/docker/" + "/var/log" + "/var/cache" + "/var/tmp" + "/var/log" + ]; + environment.BORG_RSH = "ssh -p23 -i ${config.sops.secrets.borg-ssh-key.path}"; + repo = repo; + encryption = { + mode = "repokey"; + passCommand = "cat ${config.sops.secrets.borg-passphrase.path}"; + }; + compression = "auto,zstd"; + startAt = "*-*-* 03:00:00"; + + prune.keep = { + within = "1d"; # Keep all archives from the last day + daily = 7; + weekly = 4; + monthly = 6; + }; + }; + }; + +} diff --git a/utils/modules/build-netboot/default.nix b/utils/modules/build-netboot/default.nix new file mode 100644 index 0000000..4601711 --- /dev/null +++ b/utils/modules/build-netboot/default.nix @@ -0,0 +1,38 @@ +let + # NixOS 22.11 as of 2023-01-12 + nixpkgs = builtins.getFlake "github:nixos/nixpkgs/54644f409ab471e87014bb305eac8c50190bcf48"; + + sys = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + ({ config, pkgs, lib, modulesPath, ... }: { + imports = [ + (modulesPath + "/installer/netboot/netboot-minimal.nix") + ]; + config = { + ## Some useful options for setting up a new system + # services.getty.autologinUser = lib.mkForce "root"; + # users.users.root.openssh.authorizedKeys.keys = [ ... ]; + # console.keyMap = "de"; + # hardware.video.hidpi.enable = true; + + system.stateVersion = config.system.nixos.release; + }; + }) + ]; + }; + + run-pixiecore = let + hostPkgs = if sys.pkgs.system == builtins.currentSystem + then sys.pkgs + else nixpkgs.legacyPackages.${builtins.currentSystem}; + build = sys.config.system.build; + in hostPkgs.writers.writeBash "run-pixiecore" '' + exec ${hostPkgs.pixiecore}/bin/pixiecore \ + boot ${build.kernel}/bzImage ${build.netbootRamdisk}/initrd \ + --cmdline "init=${build.toplevel}/init loglevel=4" \ + --debug --dhcp-no-bind \ + --port 64172 --status-port 64172 "$@" + ''; +in + run-pixiecore diff --git a/utils/modules/clevis.nix b/utils/modules/clevis.nix new file mode 100644 index 0000000..54ad02e --- /dev/null +++ b/utils/modules/clevis.nix @@ -0,0 +1,55 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + uuid = ""; + cfg = config.services.clevis; +in +{ + options.services.clevis = { + uuid = mkOption { + type = types.str; + default = ""; + description = lib.mdDoc '' + UUID of device to decrypt with clevis. + ''; + }; + }; + config = { + environment.systemPackages = with pkgs; [ + clevis + ]; + + boot.initrd.extraUtilsCommands = '' + # clevis dependencies + copy_bin_and_libs ${pkgs.curl}/bin/curl + copy_bin_and_libs ${pkgs.bash}/bin/bash + copy_bin_and_libs ${pkgs.jose}/bin/jose + + # clevis scripts and binaries + for i in ${pkgs.clevis}/bin/* ${pkgs.clevis}/bin/.clevis-wrapped; do + copy_bin_and_libs "$i" + done + ''; + + boot.initrd.luks.devices."nixos-enc" = { + device = "/dev/disk/by-uuid/${cfg.uuid}"; + preOpenCommands = with pkgs; '' + # what would be a sensible way of automating this? at the very least the versions should not be hard coded + ln -s ../.. /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-${bash.name} + ln -s ../.. /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-${clevis.name} + ln -s ../.. /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-${coreutils.name} + + # this runs in the background so that /crypt-ramfs/device gets set up, which implies crypt-askpass + # is ready to receive an input which it will write to /crypt-ramfs/passphrase. + # for some reason writing that file directly does not seem to work, which is why the pipe is used. + # the clevis_luks_unlock_device function is equivalent to the clevis-luks-pass command but avoid + # needing to pass the slot argument. + # using clevis-luks-unlock directly can successfully open the luks device but requires the name + # argument to be passed and will not be detected by the stage-1 luks root stuff. + bash -e -c 'while [ ! -f /crypt-ramfs/device ]; do sleep 1; done; . /bin/clevis-luks-common-functions; clevis_luks_unlock_device "$(cat /crypt-ramfs/device)" | cryptsetup-askpass' & + ''; + }; + }; +} diff --git a/utils/modules/deconz/default.nix b/utils/modules/deconz/default.nix new file mode 100644 index 0000000..c659563 --- /dev/null +++ b/utils/modules/deconz/default.nix @@ -0,0 +1,60 @@ +{ config, lib, pkgs, stdenv, ... }: +let + deconz-full = pkgs.callPackage ./pkg/default.nix { }; + deconz = deconz-full.deCONZ; +in +{ + environment.systemPackages = with pkgs; [ + deconz + ]; + + + users.users."deconz" = { + createHome = true; + isSystemUser = true; + group = "dialout"; + home = "/home/deconz"; + }; + + systemd.services.deconz = { + enable = true; + description = "deconz"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + stopIfChanged = false; + serviceConfig = { + ExecStart = "${deconz}/bin/deCONZ -platform minimal --http-port=8080 --ws-port=8081 --http-listen=127.0.0.1 --dev=/dev/ttyACM0"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + Restart = "always"; + RestartSec = "10s"; + # StartLimitInterval = "1min"; + # StateDirectory = "/var/lib/deconz"; + User = "deconz"; + # DeviceAllow = "char-ttyUSB rwm"; + # DeviceAllow = "char-usb_device rwm"; + # AmbientCapabilities="CAP_NET_BIND_SERVICE CAP_KILL CAP_SYS_BOOT CAP_SYS_TIME"; + }; + }; + + services.nginx.virtualHosts."deconz.cloonar.com" = { + forceSSL = true; + enableACME = true; + acmeRoot = null; + extraConfig = '' + proxy_buffering off; + ''; + locations."/".extraConfig = '' + set $p 8080; + if ($http_upgrade = "websocket") { + set $p 8081; + } + proxy_pass http://127.0.0.1:$p; + proxy_set_header Host $host; + proxy_redirect http:// https://; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + ''; + }; +} diff --git a/utils/modules/deconz/pkg/default.nix b/utils/modules/deconz/pkg/default.nix new file mode 100644 index 0000000..932c0ef --- /dev/null +++ b/utils/modules/deconz/pkg/default.nix @@ -0,0 +1,50 @@ +{ config, pkgs, stdenv, buildFHSUserEnv, fetchurl, dpkg, qt5, sqlite, hicolor-icon-theme, libcap, libpng, libxcrypt-legacy, ... }: +#ith import {}; +let +version = "2.21.02"; +name = "deconz-${version}"; +in +rec { + deCONZ-deb = stdenv.mkDerivation { + #builder = ./builder.sh; + inherit name; + dpkg = dpkg; + src = fetchurl { + url = "https://deconz.dresden-elektronik.de/ubuntu/stable/${name}-qt5.deb"; + sha256 = "2d5ab8af471ffa82fb0fd0c8a2f0bb09e7c0bd9a03ef887abe49c616c63042f0"; + }; + + dontConfigure = true; + dontBuild = true; + dontStrip = true; + + buildInputs = [ dpkg sqlite hicolor-icon-theme libcap libpng qt5.qtbase qt5.qtserialport qt5.qtwebsockets qt5.wrapQtAppsHook libxcrypt-legacy ]; # qt5.qtserialport qt5.qtwebsockets ]; + + unpackPhase = "dpkg-deb -x $src ."; + installPhase = '' + cp -r usr/* . + cp -r ${libxcrypt-legacy}/lib/* share/deCONZ/plugins/ + cp -r share/deCONZ/plugins/* lib/ + cp -r . $out + ''; + + }; + deCONZ = buildFHSUserEnv { + name = "deCONZ"; + targetPkgs = pkgs: [ + deCONZ-deb + ]; + multiPkgs = pkgs: [ + dpkg + qt5.qtbase + qt5.qtserialport + qt5.qtwebsockets + qt5.wrapQtAppsHook + sqlite + hicolor-icon-theme + libcap + libpng + ]; + runScript = "deCONZ"; + }; +} diff --git a/utils/modules/dovecot.nix b/utils/modules/dovecot.nix new file mode 100644 index 0000000..db362ad --- /dev/null +++ b/utils/modules/dovecot.nix @@ -0,0 +1,264 @@ +{ pkgs +, config +, ... +}: +let + domain = config.networking.domain; + # domain = "cloonar.com"; + + ldapConfig = pkgs.writeText "dovecot-ldap.conf" '' + hosts = ldap.cloonar.com + tls = yes + dn = "cn=vmail,ou=system,ou=users,dc=cloonar,dc=com" + dnpass = "@ldap-password@" + auth_bind = no + ldap_version = 3 + base = ou=users,dc=%Dd + user_filter = (&(objectClass=mailAccount)(mail=%u)) + user_attrs = \ + quota=quota_rule=*:bytes=%$, \ + =home=/var/vmail/%d/%n/, \ + =mail=maildir:/var/vmail/%d/%n/Maildir + pass_attrs = mail=user,userPassword=password + pass_filter = (&(objectClass=mailAccount)(mail=%u)) + iterate_attrs = =user=%{ldap:mail} + iterate_filter = (objectClass=mailAccount) + scope = subtree + default_pass_scheme = CRYPT + ''; + + doveSync = pkgs.writeShellScriptBin "dove-sync.sh" '' + #!/usr/bin/env bash + SERVER=''${1} + + if [ -z "$SERVER" ]; then + echo "use as dove-sync.sh host.example.com" + exit 1 + fi + + doveadm user *@cloonar.com | while read user; do + doveadm -v sync -u $user $SERVER + done + + doveadm user *@optiprot.eu | while read user; do + doveadm -v sync -u $user $SERVER + done + + doveadm user *@superbros.tv | while read user; do + doveadm -v sync -u $user $SERVER + done + + doveadm user *@ghetto.at | while read user; do + doveadm -v sync -u $user $SERVER + done + + doveadm user *@szaku-consulting.at | while read user; do + doveadm -v sync -u $user $SERVER + done + ''; + + quotaWarning = pkgs.writeShellScriptBin "quota-warning.sh" '' + #!/usr/bin/env bash + PERCENT=''${1} + USER=''${2} + + cat << EOF | /usr/lib/dovecot/deliver -d ''${USER} -o "plugin/quota=dict:User quota::noenforcing:proxy::quotadict" + From: no-reply@$(hostname -f) + Subject: Warning: Your mailbox is now ''${PERCENT}% full. + + Your mailbox is now ''${PERCENT}% full, please clean up some mails for further incoming mails. + EOF + + if [ ''${PERCENT} -ge 95 ]; then + DOMAIN="$(echo ''${USER} | awk -F'@' '{print $2}')" + cat << EOF | /usr/lib/dovecot/deliver -d postmaster@''${DOMAIN} -o "plugin/quota=dict:User quota::noenforcing:proxy::quotadict" + From: no-reply@$(hostname -f) + Subject: Mailbox Quota Warning: ''${PERCENT}% full, ''${USER} + + Mailbox (''${USER}) is now ''${PERCENT}% full, please clean up some mails for + further incoming mails. + EOF + fi + ''; +in +{ + environment.systemPackages = with pkgs; [ + doveSync + ]; + + services.dovecot2 = { + enable = true; + enableImap = true; + enableLmtp = true; + enablePAM = false; + mailLocation = "maildir:/var/vmail/%d/%n/Maildir"; + mailUser = "vmail"; + mailGroup = "vmail"; + extraConfig = '' + ssl = yes + ssl_cert = '. Currently only 'postfix' protocol is supported. + executable = quota-status -p postfix + client_limit = 1 + inet_listener { + address = 127.0.0.1 + port = 12340 + } + } + + protocol sieve { + managesieve_logout_format = bytes ( in=%i : out=%o ) + } + + plugin { + sieve_dir = /var/vmail/%d/%n/sieve/scripts/ + sieve = /var/vmail/%d/%n/sieve/active-script.sieve + sieve_extensions = +vacation-seconds +editheader + sieve_vacation_min_period = 1min + + fts = lucene + fts_lucene = whitespace_chars=@. + + quota_warning = storage=100%% quota-warning 100 %u + quota_warning2 = storage=95%% quota-warning 95 %u + quota_warning3 = storage=90%% quota-warning 90 %u + quota_warning4 = storage=85%% quota-warning 85 %u + + quota_grace = 10%% + + quota_status_success = DUNNO + quota_status_nouser = DUNNO + quota_status_overquota = "552 5.2.2 Mailbox is full" + } + + # If you have Dovecot v2.2.8+ you may get a significant performance improvement with fetch-headers: + imapc_features = $imapc_features fetch-headers + # Read multiple mails in parallel, improves performance + mail_prefetch_count = 20 + ''; + modules = [ + pkgs.dovecot_pigeonhole + ]; + protocols = [ + "sieve" + ]; + }; + + users.users.vmail = { + home = "/var/vmail"; + createHome = true; + isSystemUser = true; + uid = 1000; + shell = "/run/current-system/sw/bin/nologin"; + }; + + security.dhparams = { + enable = true; + params.dovecot2 = { }; + }; + + sops.secrets.dovecot-ldap-password = { + sopsFile = ./openldap/secrets.yaml; + }; + + systemd.services.dovecot2.preStart = '' + sed -e "s/@ldap-password@/$(cat ${config.sops.secrets.dovecot-ldap-password.path})/" ${ldapConfig} > /run/dovecot2/ldap.conf + ''; + + systemd.services.dovecot2 = { + wants = [ "acme-imap.${domain}.service" ]; + after = [ "acme-imap.${domain}.service" ]; + }; + + users.groups.acme.members = [ "openldap" ]; + + /* trigger the actual certificate generation for your hostname */ + security.acme.certs."imap.${domain}" = { + extraDomainNames = [ + "imap-test.${domain}" + "imap-02.${domain}" + ]; + postRun = "systemctl restart dovecot2.service"; + }; + + networking.firewall.allowedTCPPorts = [ + 143 # imap + 993 # imaps + 4190 # sieve + ]; +} diff --git a/utils/modules/drone-runner.nix b/utils/modules/drone-runner.nix new file mode 100644 index 0000000..04ba91a --- /dev/null +++ b/utils/modules/drone-runner.nix @@ -0,0 +1,34 @@ +{ pkgs, ... }: + +{ + virtualisation.docker.enable = true; + + systemd.services.drone-runner = { + description = "Drone Server (CI CD Service)"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pkgs.docker ]; + + serviceConfig = { + # Type = "simple"; + Name = "drone-runner"; + User = "drone-server"; + Group = "drone-server"; + Restart = "always"; + ExecStartPre= '' + -${pkgs.docker}/bin/docker stop %n \ + -${pkgs.docker}/bin/docker rm %n \ + ${pkgs.docker}/bin/docker pull drone/drone:1 + ''; + ExecStart= '' + ${pkgs.docker}/bin/docker run --rm --name %n \ + --volume=/var/run/docker.sock:/var/run/docker.sock \ + --env=DRONE_RPC_PROTO=https \ + --env=DRONE_RPC_HOST=drone.cloonar.com \ + --env=DRONE_RPC_SECRET=super-duper-secret \ + --env=DRONE_RUNNER_CAPACITY=2 \ + drone/drone-runner-docker:1 + ''; + }; + }; +} diff --git a/utils/modules/drone-server.nix b/utils/modules/drone-server.nix new file mode 100644 index 0000000..9be2448 --- /dev/null +++ b/utils/modules/drone-server.nix @@ -0,0 +1,57 @@ +{ config, pkgs, ... }: + +{ + virtualisation.docker.enable = true; + + users.users.drone-server = { + isSystemUser = true; + group = "drone-server"; + home = "/var/lib/drone-server"; + createHome = true; + }; + users.groups.drone-server = { }; + users.groups.docker.members = [ "drone-server" ]; + + systemd.services.drone-server = { + description = "Drone Server (CI CD Service)"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pkgs.docker ]; + + serviceConfig = { + # Type = "simple"; + Name = "drone-server"; + User = "drone-server"; + Group = "drone-server"; + Restart = "always"; + ExecStartPre= '' + -${pkgs.docker}/bin/docker stop %n \ + -${pkgs.docker}/bin/docker rm %n \ + ${pkgs.docker}/bin/docker pull drone/drone:1 + ''; + ExecStart= '' + ${pkgs.docker}/bin/docker run --rm --name %n \ + --env=DRONE_AGENTS_ENABLED=true \ + --env=DRONE_GOGS_SERVER=https://git.cloonar.com \ + --env=DRONE_GIT_ALWAYS_AUTH=true \ + --env=DRONE_RPC_SECRET=super-duper-secret \ + --env=DRONE_SERVER_HOST=drone.cloonar.com \ + --env=DRONE_SERVER_PROTO=https \ + --env=DRONE_USER_CREATE=username:dominik.polakovics,admin:true \ + -v /var/lib/drone-server:/data \ + --publish=8080:80 \ + drone/drone:2 + ''; + }; + }; + + services.nginx.enable = true; + services.nginx.virtualHosts."drone.cloonar.com" = { + forceSSL = true; + enableACME = true; + acmeRoot = null; + locations."/" = { + proxyPass = "http://localhost:8080"; + }; + }; +} diff --git a/utils/modules/drone/runner.nix b/utils/modules/drone/runner.nix new file mode 100644 index 0000000..ef2a257 --- /dev/null +++ b/utils/modules/drone/runner.nix @@ -0,0 +1,49 @@ +{ config, pkgs, ... }: + +{ + virtualisation.docker.enable = true; + + users.users.drone-runner = { + isSystemUser = true; + group = "drone-runner"; + home = "/var/lib/drone-runner"; + createHome = true; + }; + users.groups.drone-runner = { }; + users.groups.docker.members = [ "drone-runner" ]; + + systemd.services.drone-runner = { + description = "Drone Runner (CI CD Service)"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pkgs.docker ]; + + serviceConfig = { + # Type = "simple"; + Name = "drone-runner"; + User = "drone-runner"; + Group = "drone-runner"; + Restart = "always"; + ExecStartPre= '' + -${pkgs.docker}/bin/docker stop %n \ + -${pkgs.docker}/bin/docker rm %n \ + ${pkgs.docker}/bin/docker pull drone/drone:1 + ''; + ExecStart= '' + ${pkgs.docker}/bin/docker run --rm --name %n \ + --volume=/var/run/docker.sock:/var/run/docker.sock \ + --env-file=/run/secrets/drone-runner \ + --env=DRONE_RPC_PROTO=https \ + --env=DRONE_RPC_HOST=drone.cloonar.com \ + --env=DRONE_RUNNER_CAPACITY=2 \ + drone/drone-runner-docker:1 + ''; + }; + }; + + sops.secrets.drone-runner = { + sopsFile = ./secrets.yaml; + owner = config.systemd.services.drone-runner.serviceConfig.User; + key = "drone"; + }; +} diff --git a/utils/modules/drone/secrets.yaml b/utils/modules/drone/secrets.yaml new file mode 100644 index 0000000..cd972be --- /dev/null +++ b/utils/modules/drone/secrets.yaml @@ -0,0 +1,30 @@ +drone: ENC[AES256_GCM,data:Z1Rjso+5XYfvp2xJDXCQkI88GXl83v2oEkMLmOV/rb0DwRmhxCYzYX6fcdidk271Drf1YaPstVvm2LQB38jlBnJtg98aAGegj2fWfT44IbPIi8qDe93M2gFxFDgosoA2eOS2MjEwyBDp9GEUnKyi2gHR8khnTCvegVIntsusWOW/1tbzymKXavZAJUlX+82d/+6NWUEcnbislxhyph8P1Lgw546q,iv:SllCBHlq8ZCBqOHwMaCUcX6D/VDWsbN7uICZKb/R35w=,tag:mEb4E02VUaYGVjyI30FcXA==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0OW1JN0hjYjh4cDlmLyt6 + dHRlSjN6Y1JWUFdzNWlZZ3c0Z2F4bXBCa1NFCjM3b3pPZVhtbDdob3lsR2xlMmJI + bjRRMHFjQ2kwWWJKT1p5VW5NVGJuZ3MKLS0tICtRcTFoSmxyeUhaaVlxQUxRWkJl + SXR2M293UFBxNFovRnlTQ1o4SzloaEEK+onGdd/7aEF71ibLoLXE5/SbJQWsKigh + h8BhfT1z9P5UYNoGHVv8Ry6LndyrBLEv+PUBuT0XJpEVPjKLm99KbQ== + -----END AGE ENCRYPTED FILE----- + - recipient: age106n5n3rrrss45eqqzz8pq90la3kqdtnw63uw0sfa2mahk5xpe30sxs5x58 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyL3dDczRNMjNQUWVjelR5 + TG93QUFjVGtMNFplaTErOTJjT2dHbWtWUVNzCjNTV0tUY2hpcnp1SDZ4UTB2aFNI + M2JwSkdNS0RFQVlPRUNzRG41aW5aS3cKLS0tIEJtaTRXdTI3NGJxZENJTk9jT1hi + N3RLRjdkMmZkSmZWZGlYbXRRUTJOZFEK2bJo7iyE3A5ds7tW5bAHgyfGqgH4cRjY + hLzYp083QYbXKAqP1w8a3JFXofv1RWd7tUb61I6R4Rd6hXZUv1a5Qw== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2023-02-10T12:35:53Z" + mac: ENC[AES256_GCM,data:44J9abLbHkvjAtIUqXVZlcEAnizgg5yxKwyaZhnqIzzebWEpzqcKP6b72blaD7/jSdAiUo7bk/m4BxKVGHf9XKGxyLastbgYoFtz40rsKg9LOKpEfO2kl3JV5dj7C1f8IgsHWZ8L3Vb6KFKcrK2bzjZ5K5p22hCze4lQbK7CZTE=,iv:TE+6juCOTjTrx5nQhi8W5gaZkMFYrEDtoPrGdSTJSNE=,tag:AVsCIkzPjtfk3uSlsv6Dlg==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.7.3 diff --git a/utils/modules/drone/server.nix b/utils/modules/drone/server.nix new file mode 100644 index 0000000..ab06520 --- /dev/null +++ b/utils/modules/drone/server.nix @@ -0,0 +1,64 @@ +{ config, pkgs, ... }: + +{ + virtualisation.docker.enable = true; + + users.users.drone-server = { + isSystemUser = true; + group = "drone-server"; + home = "/var/lib/drone-server"; + createHome = true; + }; + users.groups.drone-server = { }; + users.groups.docker.members = [ "drone-server" ]; + + systemd.services.drone-server = { + description = "Drone Server (CI CD Service)"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pkgs.docker ]; + + serviceConfig = { + # Type = "simple"; + Name = "drone-server"; + User = "drone-server"; + Group = "drone-server"; + Restart = "always"; + ExecStartPre= '' + -${pkgs.docker}/bin/docker stop %n \ + -${pkgs.docker}/bin/docker rm %n \ + ${pkgs.docker}/bin/docker pull drone/drone:1 + ''; + ExecStart= '' + ${pkgs.docker}/bin/docker run --rm --name %n \ + --env-file=/run/secrets/drone-server \ + --env=DRONE_AGENTS_ENABLED=true \ + --env=DRONE_GITEA_SERVER=https://git.cloonar.com \ + --env=DRONE_GITEA_CLIENT_ID=6a7b8c57-bd71-49c8-b67d-c2de68fda649 \ + --env=DRONE_GIT_ALWAYS_AUTH=true \ + --env=DRONE_SERVER_HOST=drone.cloonar.com \ + --env=DRONE_SERVER_PROTO=https \ + --env=DRONE_USER_CREATE=username:dominik.polakovics,admin:true \ + -v /var/lib/drone:/data \ + --publish=8080:80 \ + drone/drone:2 + ''; + }; + }; + + services.nginx.enable = true; + services.nginx.virtualHosts."drone.cloonar.com" = { + forceSSL = true; + enableACME = true; + acmeRoot = null; + locations."/" = { + proxyPass = "http://localhost:8080"; + }; + }; + + sops.secrets.drone-server = { + sopsFile = ./secrets.yaml; + owner = config.systemd.services.drone-server.serviceConfig.User; + key = "drone"; + }; +} diff --git a/utils/modules/gitea.nix b/utils/modules/gitea.nix new file mode 100644 index 0000000..e53dc69 --- /dev/null +++ b/utils/modules/gitea.nix @@ -0,0 +1,25 @@ +{ config, ... }: +let + domain = "git.cloonar.com"; +in +{ + services.nginx.virtualHosts."${domain}" = { + enableACME = true; + forceSSL = true; + locations."/" = { + proxyPass = "http://localhost:3001/"; + }; + }; + + services.gitea = { + enable = true; + appName = "Cloonar Gitea server"; # Give the site a name + domain = domain; + rootUrl = "https://${domain}/"; + httpPort = 3001; + settings = { + service.DISABLE_REGISTRATION = true; + webhook.ALLOWED_HOST_LIST = "drone.cloonar.com"; + }; + }; +} diff --git a/utils/modules/gnome.nix b/utils/modules/gnome.nix new file mode 100644 index 0000000..2486339 --- /dev/null +++ b/utils/modules/gnome.nix @@ -0,0 +1,28 @@ +{ config, pkgs, ... }: + +{ + hardware.pulseaudio.enable = false; + + + services.xserver = { + enable = true; + libinput.enable = true; + displayManager.gdm.enable = true; + displayManager.defaultSession = "sway"; + desktopManager.gnome = { + enable = true; + extraGSettingsOverrides = '' + [org.gnome.desktop.interface] + gtk-theme='Dracula' + ''; + }; + }; + + environment.systemPackages = with pkgs; [ + dracula-theme + gnome.gnome-tweaks + gnome.dconf-editor + gnomeExtensions.vitals + gnomeExtensions.forge + ]; +} diff --git a/utils/modules/gogs.nix b/utils/modules/gogs.nix new file mode 100644 index 0000000..ab979bb --- /dev/null +++ b/utils/modules/gogs.nix @@ -0,0 +1,37 @@ +{ config, ... }: + +{ + services.gogs = { + enable = true; + domain = "git.cloonar.com"; + rootUrl = "http://git.cloonar.com/"; + httpAddress = "git.cloonar.com"; + httpPort = 3000; + extraConfig = '' + [server] + EXTERNAL_URL = http://git.cloonar.com/ + [auth] + DISABLE_REGISTRATION = true + [security] + # specific network address, separated by commas, no port needed + LOCAL_NETWORK_ALLOWLIST = drone.cloonar.com git-2.cloonar.com + ''; + }; + + services.nginx.enable = true; + services.nginx.virtualHosts."git.cloonar.com" = { + forceSSL = true; + enableACME = true; + acmeRoot = null; + locations."/" = { + proxyPass = "http://git.cloonar.com:3000"; + proxyWebsockets = true; + extraConfig = + "proxy_connect_timeout 300;" + + "proxy_send_timeout 300;" + + "proxy_read_timeout 300;" + + "send_timeout 300;" + ; + }; + }; +} diff --git a/utils/modules/gogs/default.nix b/utils/modules/gogs/default.nix new file mode 100644 index 0000000..19a8692 --- /dev/null +++ b/utils/modules/gogs/default.nix @@ -0,0 +1,15 @@ +{ + services.gogs = { + enable = true; + domain = "git.cloonar.com"; + rootUrl = "http://git.cloonar.com/"; + httpAddress = "git.cloonar.com"; + httpPort = 3000; + extraConfig = '' + [server] + EXTERNAL_URL = http://git.cloonar.com/ + [auth] + DISABLE_REGISTRATION = true + ''; + }; +} diff --git a/utils/modules/home-assistant/ac.nix b/utils/modules/home-assistant/ac.nix new file mode 100644 index 0000000..bfc909f --- /dev/null +++ b/utils/modules/home-assistant/ac.nix @@ -0,0 +1,103 @@ +{ + services.home-assistant.extraComponents = [ + "daikin" + ]; + + services.home-assistant.config = { + sensor = [ + { + name = "Living Room Window Handle"; + platform = "enocean"; + id = [ 129 0 227 53 ]; + device_class = "windowhandle"; + } + ]; + "automation ac_livingroom" = { + alias = "ac_livingroom"; + hide_entity = true; + trigger = { + platform = "state"; + entity_id = "sensor.windowhandle_living_room_window_handle"; + to = [ "open" "tilt" ]; + }; + action = { + service = "climate.set_hvac_mode"; + target = { + entity_id = "climate.livingroom_ac"; + }; + data = { + hvac_mode = "off"; + }; + }; + }; + "automation ac_eco" = { + alias = "ac_eco"; + hide_entity = true; + trigger = { + platform = "state"; + entity_id = [ + "climate.livingroom_ac" + "climate.bedroom_ac" + ]; + to = [ + "heat" + "cold" + ]; + }; + action = { + service = "climate.set_preset_mode"; + target = { + entity_id = "{{ trigger.entity_id }}"; + }; + data = { + preset_mode = "eco"; + }; + }; + }; + "automation bedroom_ac_on" = { + alias = "bedroom ac on"; + hide_entity = true; + trigger = { + platform = "time"; + at = "00:30:00"; + }; + action = { + choose = [ + { + conditions = [ "{{ states('sensor.bedroom_ac_inside_temperature') > 25 and states('sensor.bedroom_ac_outside_temperature') > 22 }}" ]; + sequence = [ + { + service = "climate.set_hvac_mode"; + target = { + entity_id = "climate.bedroom_ac"; + }; + data = { + hvac_mode = "cold"; + }; + } + ]; + } + ]; + }; + }; + "automation bedroom_ac_off" = { + alias = "bedroom ac on"; + hide_entity = true; + trigger = { + platform = "template"; + value_template = '' + {{ now().timestamp() | timestamp_custom('%H:%M') == (as_timestamp(strptime(states('sensor.bedtime_alarm'), "%H:%M")) - 1800) | timestamp_custom('%H:%M', false) }} + ''; + }; + action = { + service = "climate.set_hvac_mode"; + target = { + entity_id = "climate.bedroom_ac"; + }; + data = { + hvac_mode = "off"; + }; + }; + }; + }; +} diff --git a/utils/modules/home-assistant/aeg.nix b/utils/modules/home-assistant/aeg.nix new file mode 100644 index 0000000..f3982f3 --- /dev/null +++ b/utils/modules/home-assistant/aeg.nix @@ -0,0 +1,31 @@ +{ pkgs, ... }: +{ + services.home-assistant.package = pkgs.home-assistant.override { + extraPackages = ps: with ps; [ pyelectroluxconnect ]; + + packageOverrides = self: super: { + pyelectroluxconnect = super.buildPythonPackage rec { + pname = "pyelectroluxconnect"; + version = "0.3.12"; + + src = super.fetchPypi { + inherit pname version; + sha256 = "sha256-g9UxkWuTIqJe0/CDk3kwU3dSmc+GXlfDMxdzu6CqyY0="; + }; + + doCheck = false; + }; + }; + }; + # services.home-assistant.extraPackages = python3Packages: with python3Packages; [ + # (callPackage ../../pkgs/pyelectroluxconnect.nix) + # ]; + + services.home-assistant.config = { + electrolux_status = { + username = "dominik@superbros.tv"; + password = "U26tTTYtXdhErWpbRxRRVZy541vFvWyn"; + region = "emea"; + }; + }; +} diff --git a/utils/modules/home-assistant/battery.nix b/utils/modules/home-assistant/battery.nix new file mode 100644 index 0000000..35eff34 --- /dev/null +++ b/utils/modules/home-assistant/battery.nix @@ -0,0 +1,91 @@ +{ + services.home-assistant.config = { + sensor = [ + { + platform = "template"; + sensors = { + sensors_lowest_battery_level = { + friendly_name = "Lowest battery level (Sensors)"; + entity_id = "sun.sun"; + device_class = "battery"; + unit_of_measurement = "%"; + value_template = '' + {% set domains = ['sensor', 'battery'] %} + {% set ns = namespace(min_batt=100, entities=[]) %} + {%- set exclude_sensors = ['sensor.sensors_lowest_battery_level','sensor.dominiks_iphone_battery_level'] -%} + {% for domain in domains %} + {% set ns.entities = states[domain] %} + {% for sensor in exclude_sensors %} + {% set ns.entities = ns.entities | rejectattr('entity_id', 'equalto', sensor) %} + {% endfor %} + {% set batt_sensors = ns.entities | selectattr('attributes.device_class','equalto','battery') | map(attribute='state') | reject('equalto', 'unknown') | reject('equalto', 'None') | map('int') | reject('equalto', 0) | list %} + {% set batt_attrs = ns.entities | selectattr('attributes.battery_level','defined') | map(attribute='attributes.battery_level') | reject('equalto', 'unknown') | reject('equalto', 'None') | map('int') | reject('equalto', 0) | list %} + {% set batt_lvls = batt_sensors + batt_attrs %} + {% if batt_lvls|length > 0 %} + {% set _min = batt_lvls|min %} + {% if _min < ns.min_batt %} + {% set ns.min_batt = _min %} + {% endif %} + {% endif %} + {% endfor %} + {{ ns.min_batt }} + ''; + }; + }; + } + ]; + binary_sensor = [ + { + platform = "template"; + sensors = { + sensor_low_battery = { + value_template = "{{ states('sensor.sensors_lowest_battery_level')|int <= 30 }}"; + friendly_name = "A sensor has low battery"; + device_class = "problem"; + }; + }; + } + ]; + alert = { + sensor_low_battery = { + name = "Sensor has low battery!"; + message = '' + {%- set domains = ['sensor', 'battery'] -%} + {%- set threshold = 30 -%} + {%- set exclude_entities = ['sensor.sensors_lowest_battery_level','sensor.dominiks_iphone_battery_level'] -%} + Sensors are below 50% battery: + {%- for domain in domains -%} + {%- for item in states[domain] -%} + {%- if item.entity_id not in exclude_entities -%} + {%- if item.attributes.battery_level is defined -%} + {%- set level = item.attributes.battery_level|int -%} + {% if level > 0 and level < threshold %} + - {{ item.attributes.friendly_name }} ({{ item.attributes['battery_level']|int}}%) + {%- endif -%} + {%- endif -%} + {%- if item.attributes.device_class is defined and item.attributes.device_class == 'battery' -%} + {%- set level = item.state|int -%} + {% if level > 0 and level <= threshold %} + - {{ item.attributes.friendly_name }} ({{ item.state|int }}%) + {%- endif -%} + {%- endif %} + {%- endif -%} + {%- endfor -%} + {%- endfor -%} + ''; + entity_id = "binary_sensor.sensor_low_battery"; + state = "on"; + repeat = [ + 5 + 60 + 360 + ]; + skip_first = true; + can_acknowledge = true; + notifiers = [ + "NotificationGroup" + ]; + }; + }; + }; +} diff --git a/utils/modules/home-assistant/default.nix b/utils/modules/home-assistant/default.nix new file mode 100644 index 0000000..8b253ec --- /dev/null +++ b/utils/modules/home-assistant/default.nix @@ -0,0 +1,128 @@ +{ pkgs, ... }: { + imports = [ + ./ac.nix + # ./aeg.nix + ./battery.nix + ./ecovacs.nix + ./enocean.nix + ./ldap.nix + ./light.nix + ./locks.nix + ./multimedia.nix + ./notify.nix + ./pc.nix + ./presence.nix + ./pushover.nix + ./scene-switch.nix + ./sleep.nix + ./snapcast.nix + ]; + + services.home-assistant = { + enable = true; + }; + + services.home-assistant.extraComponents = [ + "mobile_app" + "shopping_list" + "backup" + "denonavr" + "androidtv" + "rainbird" + ]; + + services.home-assistant.config = + let + hiddenEntities = [ + "sensor.last_boot" + "sensor.date" + ]; + in + { + homeassistant = { + name = "Home"; + latitude = "!secret home_latitude"; + longitude = "!secret home_longitude"; + # elevation = "!secret home_elevation"; + unit_system = "metric"; + time_zone = "Europe/Vienna"; + country = "AT"; + }; + automation = "!include automations.yaml"; + frontend = { }; + http = { + use_x_forwarded_for = true; + trusted_proxies = [ + "127.0.0.1" + "::1" + ]; + }; + history.exclude = { + entities = hiddenEntities; + domains = [ + "automation" + "updater" + ]; + }; + "map" = { }; + enocean = { + device = "/dev/serial/by-id/usb-EnOcean_GmbH_EnOcean_USB_300_DC_FT5OI9YG-if00-port0"; + }; + # logbook.exclude.entities = "hiddenEntities"; + logger = { + default = "info"; + }; + + #icloud = { + # username = "!secret icloud_email"; + # password = "!secret icloud_password"; + # with_family = true; + #}; + network = { }; + zeroconf = { }; + system_health = { }; + default_config = { }; + system_log = { }; + sensor = [ + { + platform = "template"; + sensors.bedtime_alarm = { + friendly_name = "Bedtime Alarm"; + value_template = "09:00"; + }; + } + ]; + }; + + services.nginx.virtualHosts."home-assistant.cloonar.com" = { + forceSSL = true; + enableACME = true; + acmeRoot = null; + extraConfig = '' + proxy_buffering off; + ''; + locations."/".extraConfig = '' + proxy_pass http://127.0.0.1:8123; + proxy_set_header Host $host; + proxy_redirect http:// https://; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + ''; + }; + + sops.secrets."home-assistant-secrets.yaml" = { + sopsFile = ./secrets.yaml; + owner = "hass"; + path = "/var/lib/hass/secrets.yaml"; + restartUnits = [ "home-assistant.service" ]; + }; + + users.users.hass.extraGroups = [ "dialout" ]; + + networking.firewall = { + allowedUDPPorts = [ 5683 ]; + }; + +} diff --git a/utils/modules/home-assistant/ecovacs.nix b/utils/modules/home-assistant/ecovacs.nix new file mode 100644 index 0000000..890a5aa --- /dev/null +++ b/utils/modules/home-assistant/ecovacs.nix @@ -0,0 +1,5 @@ +{ + services.home-assistant.extraComponents = [ + "ecovacs" + ]; +} diff --git a/utils/modules/home-assistant/enocean.nix b/utils/modules/home-assistant/enocean.nix new file mode 100644 index 0000000..e637036 --- /dev/null +++ b/utils/modules/home-assistant/enocean.nix @@ -0,0 +1,12 @@ +{ + services.home-assistant.config = { + "binary_sensor pc_0" = [ + { + platform = "enocean"; + id = [ 254 235 105 198 ]; + name = "enocean_switch_pc"; + } + ]; + logger.logs."homeassistant.components.enocean" = "debug"; + }; +} diff --git a/utils/modules/home-assistant/ldap.nix b/utils/modules/home-assistant/ldap.nix new file mode 100644 index 0000000..99a65ce --- /dev/null +++ b/utils/modules/home-assistant/ldap.nix @@ -0,0 +1,62 @@ +{ pkgs +, config +, lib +, ... }: +let + ldap-auth-sh = pkgs.stdenv.mkDerivation { + name = "ldap-auth-sh"; + + src = pkgs.fetchFromGitHub { + owner = "efficiosoft"; + repo = "ldap-auth-sh"; + rev = "93b2c00413942908139e37c7432a12bcb705ac87"; + sha256 = "1pymp6ki353aqkigr89g7hg5x1mny68m31c3inxf1zr26n5s2kz8"; + }; + + nativeBuildInputs = [ pkgs.makeWrapper ]; + installPhase = '' + mkdir -p $out/etc + cat > $out/etc/home-assistant.cfg << 'EOF' + CLIENT="ldapsearch" + SERVER="ldaps://ldap.cloonar.com:636" + USERDN="cn=home-assistant,ou=system,ou=users,dc=cloonar,dc=com" + PW="$(<${config.sops.secrets.home-assistant-ldap.path})" + BASEDN="ou=users,dc=cloonar,dc=com" + SCOPE="one" + FILTER="(&(objectClass=cloonarUser)(memberOf=cn=HomeAssistant,ou=groups,dc=cloonar,dc=com)(mail=$(ldap_dn_escape "$username")))" + USERNAME_PATTERN='^[a-z|A-Z|0-9|_|-|.|@]+$' + on_auth_success() { + # print the meta entries for use in HA + if echo "$output" | grep -qE '^(dn|DN):: '; then + # ldapsearch base64 encodes non-ascii + output=$(echo "$output" | sed -n -e "s/^\(dn\|DN\)\s*::\s*\(.*\)$/\2/p" | base64 -d) + else + output=$(echo "$output" | sed -n -e "s/^\(dn\|DN\)\s*:\s*\(.*\)$/\2/p") + fi + name=$(echo "$output" | sed -nr 's/^cn=([^,]+).*/\1/Ip') + [ -z "$name" ] || echo "name=$name" + } + EOF + install -D -m755 ldap-auth.sh $out/bin/ldap-auth.sh + wrapProgram $out/bin/ldap-auth.sh \ + --prefix PATH : ${lib.makeBinPath [pkgs.openldap pkgs.coreutils pkgs.gnused pkgs.gnugrep]} \ + --add-flags "$out/etc/home-assistant.cfg" + ''; + }; +in +{ + services.home-assistant.config.homeassistant.auth_providers = [ + { + type = "command_line"; + command = "${ldap-auth-sh}/bin/ldap-auth.sh"; + meta = true; + } + ]; + + + sops.secrets.home-assistant-ldap = { + sopsFile = ./secrets.yaml; + owner = "hass"; + }; +} + diff --git a/utils/modules/home-assistant/light.nix b/utils/modules/home-assistant/light.nix new file mode 100644 index 0000000..871106c --- /dev/null +++ b/utils/modules/home-assistant/light.nix @@ -0,0 +1,335 @@ +{ + services.home-assistant.extraComponents = [ + "deconz" + "shelly" + "sun" + ]; + + services.home-assistant.config = { + homeassistant = { + customize_domain = { + light = { + assumed_state = false; + }; + }; + }; + "automation light_sunrise" = { + alias = "light_sunrise"; + hide_entity = true; + trigger = { + platform = "sun"; + event = "sunrise"; + }; + action = { + service = "light.turn_on"; + target = { + entity_id = "{{ states.light | selectattr(\"state\",\"eq\",\"on\") | map(attribute=\"entity_id\") | list }}"; + }; + data = { + brightness_pct = 254; + color_temp = 250; + }; + }; + }; + "automation light_sunset" = { + alias = "light_sunset"; + hide_entity = true; + trigger = { + platform = "sun"; + event = "sunset"; + }; + action = { + service = "light.turn_on"; + target = { + entity_id = "{{ states.light | selectattr(\"state\",\"eq\",\"on\") | map(attribute=\"entity_id\") | list }}"; + }; + data = { + brightness_pct = 30; + color_temp = 450; + }; + }; + }; + "automation light_on" = { + alias = "light_on"; + hide_entity = true; + trigger = { + platform = "state"; + entity_id = [ + "light.bed_room" + "light.kitchen" + "light.livingroom_lights" + "light.hallway_lights" + "light.bathroom_light" + "light.toilett_lights" + "light.storage_lights" + ]; + to = "on"; + }; + action = [ + { + choose = [ + { + conditions = [ "{{ state_attr('sun.sun', 'elevation') < 5 and trigger.entity_id == 'light.toilett_lights' }}" ]; + sequence = [ + { + service = "light.turn_on"; + target = { + entity_id = "{{ trigger.entity_id }}"; + }; + data = { + brightness_pct = 30; + color_temp = 450; + }; + } + ]; + } + { + conditions = [ "{{ state_attr('sun.sun', 'elevation') < 5 and trigger.entity_id == 'light.hallway_lights' }}" ]; + sequence = [ + { + service = "light.turn_on"; + target = { + entity_id = "{{ trigger.entity_id }}"; + }; + data = { + brightness_pct = 1; + color_temp = 450; + }; + } + ]; + } + { + conditions = [ "{{ state_attr('sun.sun', 'elevation') < 5 and trigger.entity_id == 'light.bathroom_light' }}" ]; + sequence = [ + { + service = "light.turn_on"; + target = { + entity_id = "{{ trigger.entity_id }}"; + }; + data = { + brightness_pct = 30; + color_temp = 450; + }; + } + ]; + } + { + conditions = [ "{{ state_attr('sun.sun', 'elevation') < 5 and trigger.entity_id == 'light.livingroom_lights' }}" ]; + sequence = [ + { + service = "light.turn_on"; + target = { + entity_id = "{{ trigger.entity_id }}"; + }; + data = { + brightness_pct = 5; + color_temp = 450; + }; + } + ]; + } + { + conditions = [ "{{ state_attr('sun.sun', 'elevation') < 5 and state_attr(trigger.entity_id, 'is_deconz_group') != None }}" ]; + sequence = [ + { + service = "light.turn_on"; + target = { + entity_id = "{{ trigger.entity_id }}"; + }; + data = { + brightness_pct = 30; + color_temp = 450; + }; + } + ]; + } + { + conditions = [ "{{ state_attr('sun.sun', 'elevation') > 4 }}" ]; + sequence = [ + { + service = "light.turn_on"; + target = { + entity_id = "{{ trigger.entity_id }}"; + }; + data = { + brightness_pct = 100; + color_temp = 250; + }; + } + ]; + } + ]; + } + ]; + }; + "automation bathroom light small" = { + alias = "bathroom light small"; + mode = "restart"; + hide_entity = true; + trigger = { + platform = "state"; + entity_id = [ + "light.bathroom_switch_channel_1" + ]; + from = "on"; + to = "off"; + }; + action = [ + { + service = "switch.turn_off"; + target = { + entity_id = "switch.bathroom_small"; + }; + } + ]; + }; + "automation bathroom light" = { + alias = "bathroom light"; + mode = "restart"; + hide_entity = true; + trigger = { + platform = "state"; + entity_id = [ + "light.bathroom_switch_channel_1" + ]; + from = "off"; + to = "on"; + }; + action = [ + { + delay = 3600; + } + { + service = "light.turn_off"; + target = { + entity_id = "light.bathroom_switch_channel_1"; + }; + } + ]; + }; + "automation bed_led" = { + alias = "bed_led"; + mode = "restart"; + hide_entity = true; + trigger = { + platform = "state"; + entity_id = [ + "light.bedroom_led" + ]; + from = "off"; + to = "on"; + }; + action = [ + { + delay = 10800; + } + { + service = "light.turn_off"; + target = { + entity_id = "{{ trigger.entity_id }}"; + }; + } + ]; + }; + "automation hallway_motion" = { + alias = "Hallway Motion"; + hide_entity = true; + trigger = { + platform = "state"; + entity_id = "binary_sensor.hallway_motion_motion"; + }; + action = { + service_template = "light.turn_{{ trigger.to_state.state }}"; + target = { + entity_id = "light.hallway_lights"; + }; + }; + }; + "automation bed_button_1" = { + alias = "bed_button_1"; + trigger = { + platform = "event"; + event_type = "shelly.click"; + event_data = { + device = "shellybutton1-E8DB84AA196D"; + }; + }; + action = [ + { + choose = [ + { + conditions = [ "{{ trigger.event.data.click_type == \"single\" }}" ]; + sequence = [ + { + service = "light.toggle"; + entity_id = "light.bed_reading_1"; + } + ]; + } + { + conditions = [ "{{ trigger.event.data.click_type == \"double\" }}" ]; + sequence = [ + { + service = "light.toggle"; + entity_id = "light.bedroom_lights"; + } + ]; + } + { + conditions = [ "{{ trigger.event.data.click_type == \"triple\" }}" ]; + sequence = [ + { + service = "light.toggle"; + entity_id = "light.bedroom_bed"; + } + ]; + } + ]; + } + ]; + }; + "automation bed_button_2" = { + alias = "bed_button_2"; + trigger = { + platform = "event"; + event_type = "shelly.click"; + event_data = { + device = "shellybutton1-E8DB84AA136D"; + }; + }; + action = [ + { + choose = [ + { + conditions = [ "{{ trigger.event.data.click_type == \"single\" }}" ]; + sequence = [ + { + service = "light.toggle"; + entity_id = "light.bed_reading_2"; + } + ]; + } + { + conditions = [ "{{ trigger.event.data.click_type == \"double\" }}" ]; + sequence = [ + { + service = "light.toggle"; + entity_id = "light.bedroom_lights"; + } + ]; + } + { + conditions = [ "{{ trigger.event.data.click_type == \"triple\" }}" ]; + sequence = [ + { + service = "light.toggle"; + entity_id = "light.bedroom_bed"; + } + ]; + } + ]; + } + ]; + }; + }; +} diff --git a/utils/modules/home-assistant/locks.nix b/utils/modules/home-assistant/locks.nix new file mode 100644 index 0000000..83b106f --- /dev/null +++ b/utils/modules/home-assistant/locks.nix @@ -0,0 +1,117 @@ +{ + services.home-assistant.extraComponents = [ + "nuki" + ]; + + services.home-assistant.config = { + "automation house_door" = { + alias = "house_door"; + mode = "restart"; + hide_entity = true; + trigger = { + platform = "state"; + entity_id = [ + "person.dominik" + ]; + from = "not_home"; + to = "home"; + }; + action = [ + { + service = "lock.unlock"; + target = { + entity_id = "lock.house_door"; + }; + } + { + delay = "00:05:00"; + } + { + service = "lock.lock"; + target = { + entity_id = "lock.house_door"; + }; + } + ]; + }; + "automation house_door_ring" = { + alias = "house_door_ring"; + trigger = { + platform = "event"; + event_type = "nuki_event"; + event_data = { + type = "ring"; + }; + }; + action = [ + { + choose = [ + { + conditions = [ "{{ state.house_door == \"unlocked\" }}" ]; + sequence = [ + { + service = "lock.lock"; + target = { + entity_id = "lock.house_door"; + }; + } + ]; + } + ]; + } + ]; + }; + binary_sensor = [ + { + platform = "template"; + sensors = { + lock_critical_battery = { + value_template = '' + {% set domains = ['lock'] %} + {% set ns = namespace(crit=battery_critical, entities=[]) %} + {% for domain in domains %} + {% set batt_critical = states[domain] | selectattr('attributes.battery_critical','defined') | map(attribute='attributes.battery_critical') | reject('equalto', 'unknown') | reject('equalto', 'None') | map('int') | reject('equalto', 0) | list %} + {% if batt_critical|length > 0 %} + {% set ns.battery_critical = true %} + {% endif %} + {% endfor %} + {{ ns.battery_critical }} + ''; + friendly_name = "A lock has critical battery"; + device_class = "problem"; + }; + }; + } + ]; + alert = { + battery_critical = { + name = "Lock has low battery!"; + message = '' + {%- set domains = ['lock'] -%} + Lock battery is critical: + {%- for domain in domains -%} + {%- for item in states[domain] -%} + {%- if item.attributes.battery_critical is defined -%} + {% if item.attributes.battery_critical %} + - {{ item.attributes.friendly_name }} + {%- endif -%} + {%- endif -%} + {%- endfor -%} + {%- endfor -%} + ''; + entity_id = "binary_sensor.lock_critical_battery"; + state = "on"; + repeat = [ + 5 + 60 + 360 + ]; + skip_first = true; + can_acknowledge = true; + notifiers = [ + "NotificationGroup" + ]; + }; + }; + }; +} diff --git a/utils/modules/home-assistant/multimedia.nix b/utils/modules/home-assistant/multimedia.nix new file mode 100644 index 0000000..b6a95e5 --- /dev/null +++ b/utils/modules/home-assistant/multimedia.nix @@ -0,0 +1,253 @@ +{ + services.home-assistant.config = { + binary_sensor = [ + { + name = "ps5_living"; + platform = "command_line"; + command = "python /var/lib/hass/ps5.py -q -b 10.42.96.176"; + device_class = "connectivity"; + scan_interval = 5; + } + { + platform = "template"; + sensors = { + multimedia_device_on = { + friendly_name = "Any multimedia device on"; + device_class = "connectivity"; + value_template = '' + {% if is_state('binary_sensor.ps5_living', 'on') or states('media_player.fire_tv_firetv_living_cloonar_com') != 'off' or states('device_tracker.xbox') == 'home' %} + on + {% else %} + off + {% endif %} + ''; + }; + }; + } + ]; + # "automation tv scene" = { + # alias = "auto tv scene"; + # hide_entity = true; + # trigger = { + # platform = "event"; + # event_type = "button_pressed"; + # event_data = { + # id = [ 254 235 105 198 ]; + # }; + # }; + # action = { + # service_template = "switch.turn_on"; + # data_template = { + # entity_id = "switch.computer"; + # }; + # }; + # }; + # "automation beamer switch" = { + # alias = "auto beamer scene"; + # hide_entity = true; + # trigger = { + # platform = "state"; + # entity_id = "sensor.computer_power"; + # }; + # condition = { + # condition = "and"; + # conditions = [ + # { + # condition = "numeric_state"; + # entity_id = "sensor.computer_power"; + # below = 15; + # } + # "{{ (as_timestamp(now()) - as_timestamp(states.switch.computer.last_changed)) > 300 }}" + # ]; + # }; + # action = { + # service = "switch.turn_off"; + # target = { + # entity_id = [ "switch.computer" ]; + # }; + # }; + # }; + "automation xbox on" = { + alias = "xbox on"; + hide_entity = true; + trigger = { + platform = "state"; + entity_id = "device-tracker.xbox"; + to = "home"; + }; + action = [ + { + service = "denonavr.get_command"; + target = { + entity_id = "media_player.marantz_sr6015"; + }; + data = { + command = "/goform/formiPhoneAppDirect.xml?SIGAME"; + }; + } + { + delay = 5; + } + { + service = "denonavr.get_command"; + target = { + entity_id = "media_player.marantz_sr6015"; + }; + data = { + command = "/goform/formiPhoneAppDirect.xml?PWSTANDBY"; + }; + } + ]; + }; + "automation firetv on" = { + alias = "firetv on"; + hide_entity = true; + trigger = { + platform = "state"; + entity_id = "media_player.fire_tv_firetv_living_cloonar_com"; + from = "off"; + }; + action = [ + { + service = "denonavr.get_command"; + target = { + entity_id = "media_player.marantz_sr6015"; + }; + data = { + command = "/goform/formiPhoneAppDirect.xml?SIMPLAY"; + }; + } + { + delay = 5; + } + { + service = "denonavr.get_command"; + target = { + entity_id = "media_player.marantz_sr6015"; + }; + data = { + command = "/goform/formiPhoneAppDirect.xml?PWSTANDBY"; + }; + } + ]; + }; + "automation ps5 on" = { + alias = "ps5 on"; + hide_entity = true; + trigger = { + platform = "state"; + entity_id = "binary_sensor.ps5_living"; + to = "on"; + }; + action = [ + { + service = "denonavr.get_command"; + target = { + entity_id = "media_player.marantz_sr6015"; + }; + data = { + command = "/goform/formiPhoneAppDirect.xml?SIBD"; + }; + } + { + delay = 5; + } + { + service = "denonavr.get_command"; + target = { + entity_id = "media_player.marantz_sr6015"; + }; + data = { + command = "/goform/formiPhoneAppDirect.xml?PWSTANDBY"; + }; + } + ]; + }; + "automation all multimedia off" = { + alias = "all multimedia off"; + trigger = { + platform = "state"; + entity_id = "binary_sensor.multimedia_device_on"; + to = "off"; + }; + action = [ + { + service = "androidtv.adb_command"; + target = { + device_id = "a5e50f268f3a2dbd0741fb8e9ff7f931"; + }; + data = { + command = "POWER"; + }; + } + { + service = "denonavr.get_command"; + target = { + entity_id = "media_player.marantz_sr6015"; + }; + data = { + command = "/goform/formiPhoneAppDirect.xml?PWSTANDBY"; + }; + } + ]; + }; + "automation all_multimedia_on" = { + alias = "all multimedia on"; + trigger = { + platform = "state"; + entity_id = "binary_sensor.multimedia_device_on"; + to = "on"; + }; + condition = { + condition = "or"; + conditions = [ + { + condition = "state"; + entity_id = "media_player.android_tv_metz_cloonar_com"; + state = "off"; + } + { + condition = "state"; + entity_id = "media_player.android_tv_metz_cloonar_com"; + state = "unavailable"; + } + ]; + }; + action = [ + { + service = "androidtv.adb_command"; + target = { + device_id = "a5e50f268f3a2dbd0741fb8e9ff7f931"; + }; + data = { + command = "POWER"; + }; + } + ]; + }; + # "automation multimedia input" = { + # hide_entity = true; + # trigger = { + # platform = "state"; + # entity_id = "sensor.computer_power"; + # }; + # condition = { + # condition = "and"; + # conditions = [ + # { + # condition = "numeric_state"; + # entity_id = "sensor.computer_power"; + # below = 15; + # } + # "{{ (as_timestamp(now()) - as_timestamp(states.switch.computer.last_changed)) > 300 }}" + # ]; + # }; + # action = { + # service = "switch.turn_off"; + # target = { + # entity_id = [ "switch.computer" ]; + # }; + # }; + # }; + }; +} diff --git a/utils/modules/home-assistant/music.nix b/utils/modules/home-assistant/music.nix new file mode 100644 index 0000000..38add93 --- /dev/null +++ b/utils/modules/home-assistant/music.nix @@ -0,0 +1,65 @@ +{ + services.home-assistant.extraComponents = [ + "nuki" + ]; + + services.home-assistant.config = { + "automation house_door" = { + alias = "house_door"; + mode = "restart"; + hide_entity = true; + trigger = { + platform = "state"; + entity_id = [ + "person.dominik" + ]; + from = "not_home"; + to = "home"; + }; + action = [ + { + service = "lock.unlock"; + target = { + entity_id = "lock.house_door"; + }; + } + { + delay = "00:05:00"; + } + { + service = "lock.lock"; + target = { + entity_id = "lock.house_door"; + }; + } + ]; + }; + "automation house_door_ring" = { + alias = "house_door_ring"; + trigger = { + platform = "event"; + event_type = "nuki_event"; + event_data = { + type = "ring"; + }; + }; + action = [ + { + choose = [ + { + conditions = [ "{{ state.house_door == \"unlocked\" }}" ]; + sequence = [ + { + service = "lock.lock"; + target = { + entity_id = "lock.house_door"; + }; + } + ]; + } + ]; + } + ]; + }; + }; +} diff --git a/utils/modules/home-assistant/notify.nix b/utils/modules/home-assistant/notify.nix new file mode 100644 index 0000000..e69891d --- /dev/null +++ b/utils/modules/home-assistant/notify.nix @@ -0,0 +1,15 @@ +{ + services.home-assistant.config = { + notify = [ + { + name = "NotificationGroup"; + platform = "group"; + services = [ + { + service = "pushover_dominik"; + } + ]; + } + ]; + }; +} diff --git a/utils/modules/home-assistant/pc.nix b/utils/modules/home-assistant/pc.nix new file mode 100644 index 0000000..2380fe8 --- /dev/null +++ b/utils/modules/home-assistant/pc.nix @@ -0,0 +1,46 @@ +{ + services.home-assistant.config = { + "automation pc_switch" = { + alias = "switch pc"; + hide_entity = true; + trigger = { + platform = "event"; + event_type = "button_pressed"; + event_data = { + id = [ 254 235 105 198 ]; + }; + }; + action = { + service_template = "switch.turn_on"; + data_template = { + entity_id = "switch.computer"; + }; + }; + }; + "automation pc power" = { + alias = "auto pc power off"; + hide_entity = true; + trigger = { + platform = "state"; + entity_id = "sensor.computer_power"; + }; + condition = { + condition = "and"; + conditions = [ + { + condition = "numeric_state"; + entity_id = "sensor.computer_power"; + below = 15; + } + "{{ (as_timestamp(now()) - as_timestamp(states.switch.computer.last_changed)) > 300 }}" + ]; + }; + action = { + service = "switch.turn_off"; + target = { + entity_id = [ "switch.computer" ]; + }; + }; + }; + }; +} diff --git a/utils/modules/home-assistant/presence.nix b/utils/modules/home-assistant/presence.nix new file mode 100644 index 0000000..ce93305 --- /dev/null +++ b/utils/modules/home-assistant/presence.nix @@ -0,0 +1,23 @@ +{ + services.home-assistant.extraComponents = [ + "mqtt_room" + "opnsense" + ]; + services.home-assistant.config = { + opnsense = { + url = "https://fw.cloonar.com/api"; + api_secret = "!secret opnsense_api_secret"; + api_key = "!secret opnsense_api_key"; + }; + sensor = [ + { + platform = "mqtt_room"; + name = "Dominiks iPhone BLE"; + device_id = "roomAssistant:d2a41d13-16bf-41fb-af4b-c520bdc7b68a"; + # device_id = "0a666fe0ccd0d587414fec9b9946168f"; + state_topic = "espresense/rooms"; + away_timeout = 30; + } + ]; + }; +} diff --git a/utils/modules/home-assistant/ps5.nix b/utils/modules/home-assistant/ps5.nix new file mode 100644 index 0000000..8570284 --- /dev/null +++ b/utils/modules/home-assistant/ps5.nix @@ -0,0 +1,37 @@ +{ config, ... }: +{ + config.sops.secrets.ps5-mqtt-env = { + sopsFile = ./secrets.yaml; + restartUnits = [ "podman-ps5Mqtt.service" ]; + }; + + config.virtualisation.oci-containers.containers = { + ps5Mqtt = { + image = "ghcr.io/funkeyflo/ps5-mqtt/amd64:latest "; + # ports = ["127.0.0.1:8645:8645"]; + volumes = [ + "/var/lib/ps5-mqtt:/config" + ]; + # entrypoint = "/config/run.sh"; + entrypoint = "/usr/bin/node"; + cmd = [ + "app/server/dist/index.js" + ]; + # entrypoint = "/bin/bash"; + # cmd = [ + # "-c \"echo $MQTT_HOST\"" + # ]; + environmentFiles = [ + config.sops.secrets.ps5-mqtt-env.path + ]; + extraOptions = [ + "--network=host" + ]; + }; + }; + + config.networking.firewall = { + enable = true; + allowedTCPPorts = [ 8645 ]; + }; +} diff --git a/utils/modules/home-assistant/pushover.nix b/utils/modules/home-assistant/pushover.nix new file mode 100644 index 0000000..d56b7b7 --- /dev/null +++ b/utils/modules/home-assistant/pushover.nix @@ -0,0 +1,16 @@ +{ + services.home-assistant.extraComponents = [ + "pushover" + ]; + + # services.home-assistant.config = { + # notify = [ + # { + # name = "pushover_dominik"; + # platform = "pushover"; + # api_key = "!secret pushover_dominik_api_key"; + # user_key = "!secret pushover_dominik_user_key"; + # } + # ]; + # }; +} diff --git a/utils/modules/home-assistant/scene-switch.nix b/utils/modules/home-assistant/scene-switch.nix new file mode 100644 index 0000000..70ba921 --- /dev/null +++ b/utils/modules/home-assistant/scene-switch.nix @@ -0,0 +1,21 @@ +{ + services.home-assistant.config = { + "automation scene_switch" = { + alias = "switch scene"; + hide_entity = true; + trigger = { + platform = "event"; + event_type = "button_pressed"; + event_data = { + id = [ 254 242 234 134 ]; + }; + }; + action = { + service_template = "switch.turn_on"; + data_template = { + entity_id = "switch.computer"; + }; + }; + }; + }; +} diff --git a/utils/modules/home-assistant/secrets.yaml b/utils/modules/home-assistant/secrets.yaml new file mode 100644 index 0000000..24c37ef --- /dev/null +++ b/utils/modules/home-assistant/secrets.yaml @@ -0,0 +1,32 @@ +home-assistant-ldap: ENC[AES256_GCM,data:De7kuRji+flc0juqE3z1MyNo938Y18jhYtxHkvBX1AsmcJO6a37qJ6o9eCVyhcN/uyaS8cySucqKErh3SlCOZw==,iv:DD4Bp7yU0TCQ/Zeildmrt1HSUbuJgQ0L2UUSmMi6Obo=,tag:t1Hz0GiXzodVK64e0Xuf6g==,type:str] +home-assistant-secrets.yaml: ENC[AES256_GCM,data:owCBlfPXA66zxGaMwo6mViSZS7f3WPXOTtWiWFR8R/9Cqv7YvIaY7cqYX1OYFfEJyMyq6YLhVV9Zi0/K40VtUmhgR324x/knQbKkZGxv6gwqdIZ/zaZgQIu29xtY4DQnK24/5942HtisSnQnJxgbpchnjJCAQqri2/68LjGh8GoJVhF34Zji0MHBLAxR1y5JApOb5GUpc0ftMu4j6cbU/qxiZbRkzbrjlgAjBFjARYmsaWiilXg5jT2pf0Fz6bOHslcO+b3qePgIb/cJPP9aVYy76QA0oZ03f3Qu0w+IsjBKuXGaWKVBWBDyK1E37Y/Xif8w3H3cPqkAI6qxCPXi7djYfEpOz2M1L+5GTJGJz8fmvYzVJqU+hpZeI/7qJdiQ4/98YfuDU6nTEwWumcsPKsafJzdSeVfFYcE2x1H+QJDfJ3sgWJkfLDOrRxaXIwoXeSBSYAXPxjdomBzcdgKvo8MBH47pDbR4hK+y8+0LnoPafIyh8+FeAFzbn6/9ScWzF/MOyxFnap3edd12k5yWN+Yc4xacye3RR1h/mp1+DQF9xZ1xMTE70nwNdtIsGU0cV8qUvPgknZB/0US2oZ19fdxVuAN2rneEkK/nIGoKVw==,iv:x/F8CnsxROweCosvX1yAMHzwtI34kGauPvWF8yu2Yf8=,tag:WbzjtVhEPgUhL8g07kHjdA==,type:str] +ps5-mqtt-env: ENC[AES256_GCM,data:g079HmYjMQ/Dr/vHSuxnLDwyOG3bSmzGtUfQLXJgKFUoC+5dAyUwYu8WRD1hskER2v8yIz2oHx8dQXLuWsmKYRErk6Sybpe0+FOcqOvAXgzv1ow95sjClkrS+rwjHcoHb1nts5lP5bGkY0e4Z6Dfn5AoeQ4pEA1TOzANvPDqDZUBh+L4hUDkDSWg7sAH3pHK0BqZHwiDNrvE9ac8MFHJmrPVEr4dqhRwAip+YMAdGCwp0ofdm2amUL7aHTaCQhjgsAW306C8ksMwuFE+dAvsqJGZ1N5T2nxP9LYVWcc8ZsKV8VklZ/QS6ScBGz4Oi4YpPDvkt+ErY929t0vISjKengnDHhu/+WYaBxeVbre5G29hK+jrnmUHFBa6pjwCSFVJq8mRIn6KorGwltxObyFBxddf+kiAMDbvnqW3E6sZtW+mF+48hTD8ygjSE8D8h7IoyYYTh0nPY4DzkfMR2OXqpz+bh+dpjnh3UQktJ1HXh902A2ljmxUEMGWtcOc2fDTxEMheTqdvg2z7Ek/FIwq/curVbdHIrzeIXDis7LIRk4G8b4mA6nHG4KrzCF00LAV7Ph9Nx1wAgrxvBTq4bqnO2VP44vtDsQ/O+Dr+wZSlP3EL4PsAJznfa7YcvStvV1Nn6FrVY04W,iv:1vttyQqYffChK12Wy5KTZEZ00pESsMefXHbulKsDSqU=,tag:pzgw0jJqICaro2U2FfQEiA==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBieVNmLy9zK1hXWHdGRHF3 + WXd5b0pjR0pBaFkvVSt5ZVNyaUlCL0R1c2lnCkRCY1RmZnY1UVFuWURUMElHcFU1 + WHgrZE5ZY3FQUVp2VkhIcHh3WDV4aUUKLS0tIGFRZFNsKzU4ZDhPQ0NLbmtPSEJB + YW8zNjhYWGNRcy9VbEZoaWplakVNWkUK48LBhFusDMZj2momMwRXdU7bLiGvzvqX + QwdxorLMP2/GW6x5xpFj1khLCwxYDOys4xGvmE89hYZa++OSYU+Ejw== + -----END AGE ENCRYPTED FILE----- + - recipient: age1ezq2j34qngky22enhnslx6hzh4ekwk8dtmn6c9us0uqxqpn7hgpsspjz58 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwY0pGaUU3alJrYmhQM2Y4 + TTMrbGsrRThlTUN1R3RCbFZFeVR1KzVDMWg4CnlQTzBzdTNPMTFvTUVYRXFEbUVP + YWFvRTJqZjcxZHhtRkJrajNuUWoyYzgKLS0tIEhVcFI4N0E4VEVOZDIxREJ4bkNi + UjRhRVpkTHF5a3p2bjhiVDZwMWRWMkEKpsHLWcPGQWpBo4Z8h7XFOP0bCct83BPj + d/QDjarzugd6jamWVXKidZwADxfP59Pvo9JmLlFL5isgZ1TL3ZHL+g== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2023-04-26T13:33:07Z" + mac: ENC[AES256_GCM,data:i/2+Qr0ihMaDPF22pyo7Qy3SYEr0Fr5dfRJSZ9xMu/5TFSm3GXNhd2KZVXjl6isbPbD22/siuI2MZeSB/iTFcRadtswmGnb6Lgsi8K/LULKvwXHf5t/Py/z59CpXBSgfUQQ6BuodNc25DRpCX8HEhFfd3Ajgyavc8vHVpnwG8eQ=,iv:Xu3WsyIqDbtReP9pBsiEf17pAbdVrY/y6wM2dleguFQ=,tag:gh6m7MUzMx2/lsZ5XExwyw==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.7.3 diff --git a/utils/modules/home-assistant/shelly.nix b/utils/modules/home-assistant/shelly.nix new file mode 100644 index 0000000..cf7efd0 --- /dev/null +++ b/utils/modules/home-assistant/shelly.nix @@ -0,0 +1,16 @@ +{ + services.home-assistant.config.shelly = { + FindAndroid = { + speech.text = "Send notification"; + action = { + service = "notify.pushover"; + data = { + message = "Phonefinderalert"; + target = "android"; + data.sound = "echo"; + data.priority = 1; + }; + }; + }; + }; +} diff --git a/utils/modules/home-assistant/sleep.nix b/utils/modules/home-assistant/sleep.nix new file mode 100644 index 0000000..44a963d --- /dev/null +++ b/utils/modules/home-assistant/sleep.nix @@ -0,0 +1,59 @@ +{ + services.home-assistant.config = { + "automation wakeup" = { + alias = "wakeup"; + hide_entity = true; + trigger = { + platform = "template"; + value_template = '' + {{ now().timestamp() | timestamp_custom('%H:%M') == (as_timestamp(strptime(states('sensor.bedtime_alarm'), "%H:%M")) - 1800) | timestamp_custom('%H:%M', false) }} + ''; + }; + action = { + service_template = "switch.turn_on"; + data_template = { + entity_id = "switch.coffee_switch"; + }; + }; + }; + "automation sleep" = { + alias = "sleep"; + hide_entity = true; + trigger = [ + { + platform = "event"; + event_type = "shelly.click"; + event_data = { + device = "shellybutton1-E8DB84AA196D"; + }; + } + { + platform = "event"; + event_type = "shelly.click"; + event_data = { + device = "shellybutton1-E8DB84AA136D"; + }; + } + ]; + action = [ + { + choose = [ + { + conditions = [ "{{ trigger.event.data.click_type == \"long\" }}" ]; + sequence = [ + { + service = "light.turn_off"; + entity_id = "all"; + } + { + service = "light.turn_on"; + entity_id = "light.bedroom_bed"; + } + ]; + } + ]; + } + ]; + }; + }; +} diff --git a/utils/modules/home-assistant/snapcast.nix b/utils/modules/home-assistant/snapcast.nix new file mode 100644 index 0000000..11afc45 --- /dev/null +++ b/utils/modules/home-assistant/snapcast.nix @@ -0,0 +1,66 @@ +{ + services.home-assistant = { + extraComponents = [ "snapcast" ]; + config = { + # "media_player" = { + # platform = "snapcast"; + # host = "snapcast.cloonar.com"; + # }; + "automation toilett_music" = { + alias = "toilett music"; + hide_entity = true; + trigger = { + platform = "state"; + entity_id = "light.toilett_switch"; + }; + action = { + choose = [ + { + conditions = [ "{{trigger.to_state.state == 'on'}}" ]; + sequence = [ + { + service = "media_player.volume_mute"; + target = { + entity_id = "media_player.snapcast_client_e4_5f_01_3c_fb_c3"; + }; + data = { + is_volume_muted = false; + }; + } + ]; + } + { + conditions = [ "{{trigger.to_state.state == 'off'}}" ]; + sequence = [ + { + service = "media_player.volume_mute"; + target = { + entity_id = "media_player.snapcast_client_e4_5f_01_3c_fb_c3"; + }; + data = { + is_volume_muted = true; + }; + } + ]; + } + ]; + }; + }; + "automation piano" = { + alias = "piano"; + hide_entity = true; + trigger = { + platform = "state"; + entity_id = "media_player.snapcast_client_e4_5f_01_96_c1_1e"; + attribute = "is_volume_muted"; + }; + action = { + service = "switch.turn_on"; + target = { + entity_id = "switch.piano_switch_power"; + }; + }; + }; + }; + }; +} diff --git a/utils/modules/howdy.nix b/utils/modules/howdy.nix new file mode 100644 index 0000000..37222a9 --- /dev/null +++ b/utils/modules/howdy.nix @@ -0,0 +1,90 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.howdy; + ircfg = config.services.ir-toggle; + + # `dark_threshold` is required for X1 Carbon 7th to work + configINI = pkgs.runCommand "config.ini" { } '' + cat ${cfg.package}/lib/security/howdy/config.ini > $out + substituteInPlace $out --replace 'device_path = none' 'device_path = ${cfg.device}' + substituteInPlace $out --replace 'dark_threshold = 50' 'dark_threshold = ${ + toString cfg.dark-threshold + }' + substituteInPlace $out --replace 'certainty = 3.5' 'certainty = ${ + toString cfg.certainty + }' + ''; + pam-rule = pkgs.lib.mkDefault (pkgs.lib.mkBefore + "auth sufficient ${pkgs.pam_python}/lib/security/pam_python.so ${config.services.howdy.package}/lib/security/howdy/pam.py"); +in { + options = { + services.ir-toggle = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable Chicony IR Emitter toggler. + ''; + }; + }; + services.howdy = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable howdy and PAM module for face recognition. + ''; + }; + + package = mkOption { + type = types.package; + default = pkgs.howdy; + defaultText = "pkgs.howdy"; + description = '' + Howdy package to use. + ''; + }; + + device = mkOption { + type = types.path; + default = "/dev/video0"; + description = '' + Device file connected to the IR sensor. + ''; + }; + + certainty = mkOption { + type = types.int; + default = 3.5; + description = '' + The certainty of the detected face belonging to the user of the account. On a scale from 1 to 10, values above 5 are not recommended. + ''; + }; + + dark-threshold = mkOption { + type = types.int; + default = 50; + description = '' + Because of flashing IR emitters, some frames can be completely unlit. Skip the frame if the lowest 1/8 of the histogram is above this percentage of the total. The lower this setting is, the more dark frames are ignored. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + environment.systemPackages = [ cfg.package pkgs.ir_toggle ]; + environment.etc."howdy/config.ini".source = configINI; + security.pam.services = { + sudo.text = pam-rule; # Sudo + login.text = pam-rule; # User login + polkit-1.text = pam-rule; # PolKit + i3lock.text = pam-rule; # i3lock + }; + powerManagement.resumeCommands = + "${pkgs.ir_toggle}/bin/chicony-ir-toggle on"; + services.udev.packages = [ pkgs.ir_toggle ]; + }; +} diff --git a/utils/modules/howdy/config.nix b/utils/modules/howdy/config.nix new file mode 100644 index 0000000..c585871 --- /dev/null +++ b/utils/modules/howdy/config.nix @@ -0,0 +1,45 @@ +{ + core = { + detection_notice = false; + timeout_notice = true; + no_confirmation = false; + suppress_unknown = false; + abort_if_ssh = true; + abort_if_lid_closed = true; + disabled = false; + use_cnn = false; + workaround = "off"; + }; + + video = { + certainty = 3.5; + timeout = 4; + device_path = "/dev/video2"; + warn_no_device = true; + max_height = 320; + frame_width = -1; + frame_height = -1; + dark_threshold = 60; + recording_plugin = "opencv"; + device_format = "v4l2"; + force_mjpeg = false; + exposure = -1; + rotate = 0; + }; + + snapshots = { + save_failed = false; + save_successful = false; + }; + + rubberstamps = { + enabled = false; + stamp_rules = "nod 5s failsafe min_distance=12"; + }; + + debug = { + end_report = false; + verbose_stamps = false; + gtk_stdout = false; + }; +} diff --git a/utils/modules/howdy/default.nix b/utils/modules/howdy/default.nix new file mode 100644 index 0000000..794a97d --- /dev/null +++ b/utils/modules/howdy/default.nix @@ -0,0 +1,123 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + pam-rule = pkgs.lib.mkDefault (pkgs.lib.mkBefore + '' + auth sufficient pam_unix.so try_first_pass nullok + auth sufficient ${config.services.howdy.package}/lib/security/pam_howdy.so + ''); + pam-sudo-rule = pkgs.lib.mkDefault (pkgs.lib.mkBefore + '' + auth sufficient ${config.services.howdy.package}/lib/security/pam_howdy.so + ''); + cfg = config.services.howdy; + irCfg = config.services.linux-enable-ir-emitter; + settingsType = pkgs.formats.ini { }; +in { + options = { + services.howdy = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable howdy and PAM module for face recognition. + ''; + }; + + package = mkOption { + type = types.package; + default = pkgs.howdy; + defaultText = "pkgs.howdy"; + description = '' + Howdy package to use. + ''; + }; + + settings = mkOption { + inherit (settingsType) type; + default = import ./config.nix; + description = mdDoc '' + Howdy configuration file. Refer to + + for options. + ''; + }; + }; + services.linux-enable-ir-emitter = { + enable = mkEnableOption (mdDoc "") // { + description = mdDoc '' + Whether to enable IR emitter hardware. Designed to be used with the + Howdy facial authentication. After enabling the service, configure + the emitter with `sudo linux-enable-ir-emitter configure`. + ''; + }; + + package = mkPackageOptionMD pkgs "linux-enable-ir-emitter" {} // { + description = mdDoc '' + Package to use for the Linux Enable IR Emitter service. + ''; + }; + + device = mkOption { + type = types.str; + default = "video2"; + description = mdDoc '' + IR camera device to depend on. For example, for `/dev/video2` + the value would be `video2`. Find this with the command + {command}`realpath /dev/v4l/by-path/`. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + # environment.systemPackages = [ cfg.package pkgs.ir_toggle ]; + # environment.etc."howdy/config.ini".source = configINI; + # security.pam.services = { + # sudo.text = pam-rule; # Sudo + # login.text = pam-rule; # User login + # polkit-1.text = pam-rule; # PolKit + # i3lock.text = pam-rule; # i3lock + # }; + # powerManagement.resumeCommands = + # "${pkgs.ir_toggle}/bin/chicony-ir-toggle on"; + # services.udev.packages = [ pkgs.ir_toggle ]; + + + + + + + environment.systemPackages = [ cfg.package irCfg.package ]; + + security.pam.services = { + sudo.text = pam-sudo-rule; # Sudo + login.text = pam-rule; # User login + polkit-1.text = pam-rule; # PolKit + swaylock.text = pam-rule; # i3lock + # gdm-password.text = pam-rule; # i3lock + }; + + systemd.services.linux-enable-ir-emitter = rec { + description = "Enable the infrared emitter"; + script = "${getExe irCfg.package} run"; + + wantedBy = [ + "multi-user.target" + "suspend.target" + "hybrid-sleep.target" + "hibernate.target" + "suspend-then-hibernate.target" + ]; + after = wantedBy ++ [ "dev-${irCfg.device}.device" ]; + }; + + systemd.tmpfiles.rules = [ + "d /var/lib/linux-enable-ir-emitter 0755 root root - -" + ]; + environment.etc."linux-enable-ir-emitter".source = "/var/lib/linux-enable-ir-emitter"; + environment.etc."howdy/config.ini".source = settingsType.generate "howdy-config.ini" cfg.settings; + }; +} diff --git a/utils/modules/i3.nix b/utils/modules/i3.nix new file mode 100644 index 0000000..8a558a2 --- /dev/null +++ b/utils/modules/i3.nix @@ -0,0 +1,22 @@ +{ config, pkgs, callPackage, ... }: + +{ + environment.pathsToLink = [ "/libexec" ]; # links /libexec from derivations to /run/current-system/sw + services.xserver = { + enable = true; + + desktopManager = { + xterm.enable = false; + }; + + windowManager.i3 = { + enable = true; + extraPackages = with pkgs; [ + dmenu #application launcher most people use + i3status # gives you the default i3 status bar + i3lock #default i3 screen locker + i3blocks #if you are planning on using i3blocks over i3status + ]; + }; + }; +} diff --git a/utils/modules/influxdb.nix b/utils/modules/influxdb.nix new file mode 100644 index 0000000..bf5d595 --- /dev/null +++ b/utils/modules/influxdb.nix @@ -0,0 +1,21 @@ +{ config, ... }: { + services.influxdb = { + enable = true; + extraConfig = { + http = { + auth-enabled = true; + log-enabled = false; + https-enabled = true; + https-certificate = "/var/lib/acme/influxdb.cloonar.com/fullchain.pem"; + https-private-key = "/var/lib/acme/influxdb.cloonar.com/key.pem"; + }; + }; + }; + + networking.firewall.allowedTCPPorts = [ 8086 ]; + + security.acme.certs."influxdb.cloonar.com" = { + postRun = "systemctl restart influxdb.service"; + group = "influxdb"; + }; +} diff --git a/utils/modules/lego/lego.nix b/utils/modules/lego/lego.nix new file mode 100644 index 0000000..e61fa1b --- /dev/null +++ b/utils/modules/lego/lego.nix @@ -0,0 +1,16 @@ +{ config, ... }: + +{ + sops.secrets.lego-credentials = { + sopsFile = ./secrets.yaml; + }; + + security.acme.acceptTerms = true; + security.acme.defaults.email = "admin+acme@cloonar.com"; + security.acme.defaults = { + dnsProvider = "hetzner"; + credentialsFile = config.sops.secrets.lego-credentials.path; + # We don't need to wait for propagation since this is a local DNS server + dnsPropagationCheck = true; + }; +} diff --git a/utils/modules/lego/secrets.yaml b/utils/modules/lego/secrets.yaml new file mode 100644 index 0000000..2f9fa60 --- /dev/null +++ b/utils/modules/lego/secrets.yaml @@ -0,0 +1,84 @@ +lego-credentials: ENC[AES256_GCM,data:cn7n1jOammEdvzYzBKJ086c1bHc77GN74uncg35ClaTBvb5w3F0lQazJqBJoIf365Q==,iv:FLrr7WwGgzjuENOEi/Sf8Ti6wcQLPnBkJ+/DxyCUM54=,tag:yQnDsDz+btx3MQu/4w2ixg==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBocUx0b1VtSlF4SVpvMXpr + NjZSSHdkaDVoeDRCTC9LRFI1bkJRQTMyUFdJCjJvN2NyY1JLMkVtUTF2eGN3Lzh5 + R3M4NUk2WUpFMTM4MHQxM2k0dkdxUWcKLS0tIFkrMUVSaHVCaEYydERacFBtQVVt + dXFENTFldVFWN3RQWTBKZHVtc0tza1kKeKGChclZahfDACUJxPsTn+4XomqifXP4 + VH+BxqmwkhgryRDoRrVy+vQnyK95WaDo3S/UIR2zgUR+cezt1DzR2A== + -----END AGE ENCRYPTED FILE----- + - recipient: age106n5n3rrrss45eqqzz8pq90la3kqdtnw63uw0sfa2mahk5xpe30sxs5x58 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnckpCQVZaOE9NT084d0Zk + TUtXN3EvcFZoOCs1aFloK2RSTVFyT2RWUzBVCjdCQzNGaWpqejhNdUtnZTl6RHpY + b2dvMjZIV2ZGYkwyNVpxaHRPUmt3bmsKLS0tIHJReVpvTzBqYS9PVThmRzZzZUtI + WjZmMXIxOWFScGlNSFdwbXdQcXB3d1UKHAkThsJ2unza8Yz/l0umryT8li74LKre + dQuP41RQOQBHisUUZhWeYkM+wJzayXr426IK19zAHPuNeutqcewYcA== + -----END AGE ENCRYPTED FILE----- + - recipient: age1y6lvl5jkwc47p5ae9yz9j9kuwhy7rtttua5xhygrgmr7ehd49svsszyt42 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaSGhiSkFKbHpGcjljZ3Uw + ZWZ5MS82Wk9YRnZlQVk2V2laMzRkK3dBdWg4CjJ5Wkd0bnNXbVpMYVUxSVR1Nnpn + dkFnbTV4eTYwWmdzWU9PZlozNytBWk0KLS0tIGVTL1RFbzBBM25nbFVtOEVQMmVm + bmQvemhIeU8wTGswTEN2ZjA2RjdaTW8KlorFf+agQuSwbN3Fkr5bUC2Ca6Sz8hHy + Faq+uNlMWHCrvE1DBP34D41LxCLDaDMYIJyUG7A4MZE2WUrJZ9c0vQ== + -----END AGE ENCRYPTED FILE----- + - recipient: age1ezq2j34qngky22enhnslx6hzh4ekwk8dtmn6c9us0uqxqpn7hgpsspjz58 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnT2VQd2VKeTFsQ3BPbXNq + cWRTREx0UE54RmNDSlBwK082azRZalQ0aUhVCk1HV0Y3RnFYbS83NXcvY3IwSGVG + ZElxcm1ETlFvVkhjR3RVNnNJQmR2dzAKLS0tIGpoYytWL25nQkFSMm5hQ29yYUd3 + UEp1cndyMG9Ba0RnT3NRdHAzRzBjdDAKIHXX0rnPkEz6Smw3sH8RgDdS92yOoFxz + 6uFUrqbxAW1+6EpgSPCi4GioAZyFayHdeuXQ5J9vApCDhHdsd6jMzw== + -----END AGE ENCRYPTED FILE----- + - recipient: age1jyeppc8yl2twnv8fwcewutd5gjewnxl59lmhev6ygds9qel8zf8syt7zz4 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBRVEyK3M2aXdwV3RTMGJy + NlBUaWFKemJDRHB3ck5xVG1BWW1CUjc1OEdZCnUzSktiUkRmcTNwOWZXTFhnUCtD + bHFCZ1ZhKytGc1hoOVQ0SFFyUkpmOHMKLS0tIGNWV3Vrd2J2TTYrUUhaSW0yak5W + UTRGd0FaZUk1RVFqS3NXWHZ6SFQ4MTAKsIWMYxczPfDg7G/H5Rcm7sD/2zPXWJfl + c2PiNSeZAfuCqAU/a9/2rz0kk3LdAW7d+foBOPeMkWnKs2pFJxNMXw== + -----END AGE ENCRYPTED FILE----- + - recipient: age1azmxsw5llmp2nnsv3yc2l8paelmq9rfepxd8jvmswgsmax0qyyxqdnsc7t + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTUTZKRGdzSHgybnB6ZExx + UVRCNTROS3QyOFBYSFc5blFEQythTndRSHhrCjJxcTNqejUxQWxRZzhhZVhNcjlR + MFY4LzdicGUwMm13R1k2ZUdDc0VrY2MKLS0tIFVyNGlJU3NyQnkzZEg4SEM1T1NZ + RHNUd053UUJyMnprbi9DR0JnSEQ5YjQKeXRdvnQRtkLs6yqVKlul4wp4PXQTpktZ + cUUWEaajUmXoEeHjFkfNqtsJkVG6ixnzs9tu/GeOCbTCZ9eFokUg2g== + -----END AGE ENCRYPTED FILE----- + - recipient: age1zkzpnfeakyvg3fqtyay32sushjx2hqe28y6hs6ss7plemzqjqa5s6s5yu3 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGNHJFa0ltM0JiQ1hOa0pQ + K3FndzhxaTBwZWgzNWg4RXBQdDV0WlZNZ1g4CkVhUlA2d0JjanorSlpyYVBUaEli + Mnl2VmJTNG9DcnZsSXZpUFZXTDZQRVUKLS0tIEtDZ2J3L0RtV1BybEJDZ0k2bGZV + YWY5QjlZZ1J2OEw2U0luZHNWQVFmRjQKZ9A54c5AXSm2aNasBinaWPDIo/xDXFqZ + 7+ZTJ82QiWBXpaLIpmPim3e9JHVzZ8NKdN0Y7imsYdR2gXRsxyv1SQ== + -----END AGE ENCRYPTED FILE----- + - recipient: age14uarclad0ty5supc8ep09793xrnwkv8a4h9j0fq8d8lc92n2dadqkf64vw + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1K1RSTVZOYmFxalFxc1g1 + OHpaUXNLOHF2WnQ1VUxLUENwclJoQXl2b20wCnJnOUs0cXlMTDRXdktJZ2x6bjNJ + UWJjYkZwR2ZKNnpsaVN0bzBWODZNL3MKLS0tIEUraytIc1d6dVVqa0VaSWJpcWRn + UWswVG5PaTdDZHlybGxpZ2tKb1liOWsKOuMm2+kofwGqC95KhfEecjwzjNCHPRRk + /61zp39+U6PeqP0gTbcy959aSDhfucrZKhBKP2VsTgP0BLDfZR2K4Q== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2022-11-09T07:12:13Z" + mac: ENC[AES256_GCM,data:gqsD5gTtE5ZqWzWKAAIscecvIsGSC9j4Cnbik6Yk7Jf7Z5/NIxbkInzDsLmlU3ObbLZAhGAlOAKIrUVy37rCcEZ+I04ICXK1dmUdsVud6E4SvTdDjh9qlXTbEkcDCY2YqXlTuQl6IZyveaPuF6fRe1FMh8JEpDv/foZTl8+AuQQ=,iv:+nV6YW9m1B0qo7xbB1lw9dgiQ877GQ6OxMqjk7lei10=,tag:NmeSwBWRKpqlwZxYYC7trg==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.7.3 diff --git a/utils/modules/mopidy.nix b/utils/modules/mopidy.nix new file mode 100644 index 0000000..0fdc254 --- /dev/null +++ b/utils/modules/mopidy.nix @@ -0,0 +1,59 @@ +{ pkgs, lib, ... }: +let +mopidy-autoplay = pkgs.python3Packages.buildPythonApplication rec { + pname = "Mopidy-Autoplay"; + version = "0.2.3"; + + src = pkgs.python3Packages.fetchPypi { + inherit pname version; + sha256 = "sha256-E2Q+Cn2LWSbfoT/gFzUfChwl67Mv17uKmX2woFz/3YM="; + }; + + propagatedBuildInputs = [ + pkgs.mopidy + ] ++ (with pkgs.python3Packages; [ + configobj + ]); + + # no tests implemented + doCheck = false; + + meta = with lib; { + homepage = "https://codeberg.org/sph/mopidy-autoplay"; + }; +}; +in +{ + services.mopidy = { + enable = true; + extensionPackages = [ pkgs.mopidy-iris pkgs.mopidy-tunein mopidy-autoplay ]; + configuration = '' + [audio] + output = audioresample ! audioconvert ! audio/x-raw,rate=48000,channels=2,format=S16LE ! wavenc ! filesink location=/run/snapserver/mopidy + + [file] + enabled = false + + [autoplay] + enabled = true + ''; + }; + + services.nginx.virtualHosts."mopidy.cloonar.com" = { + forceSSL = true; + enableACME = true; + acmeRoot = null; + extraConfig = '' + proxy_buffering off; + ''; + locations."/".extraConfig = '' + proxy_pass http://127.0.0.1:6680; + proxy_set_header Host $host; + proxy_redirect http:// https://; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + ''; + }; +} diff --git a/utils/modules/mosquitto.nix b/utils/modules/mosquitto.nix new file mode 100644 index 0000000..db5203b --- /dev/null +++ b/utils/modules/mosquitto.nix @@ -0,0 +1,28 @@ +{ config, pkgs, ... }: + +{ + services.mosquitto = { + enable = true; + + listeners = [ + { + users."espresense" = { + password = "insecure-password"; + acl = [ "readwrite #" ]; + }; + users."home-assistant" = { + hashedPassword = "$7$101$7uaagoQWQ3ICJ/wg$5cWZs4ae4DjToe44bOzpDopPv1kRaaVD+zF6BE64yDJH2/MBqXfD6f2/o9M/65ArhV92DAK+txXRYsEcZLl45A=="; + acl = [ "readwrite #" ]; + }; + users."ps5-mqtt" = { + password = "insecure-password"; + acl = [ "readwrite #" ]; + }; + } + ]; + }; + + networking.firewall = { + allowedTCPPorts = [ 1883 ]; + }; +} diff --git a/utils/modules/mysql.nix b/utils/modules/mysql.nix new file mode 100644 index 0000000..84578ea --- /dev/null +++ b/utils/modules/mysql.nix @@ -0,0 +1,78 @@ +{ pkgs, ... }: + +let + mysqlCreateDatabase = pkgs.writeShellScriptBin "mysql-create-database" '' + #!/usr/bin/env bash + if [ $# -lt 2 ] + then + echo "Usage: $0 " + exit 1 + fi + + if ! [ $EUID -eq 0 ] + then + echo "Must be root!" >&2 + exit 1 + fi + + DB="$1" + HOST="$2" + PASSWORD="$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64 | xargs)" + + cat <" + exit 1 + fi + + if ! [ $EUID -eq 0 ] + then + echo "Must be root!" >&2 + exit 1 + fi + + DB="$1" + PASSWORD="$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64 | xargs)" + + cat <" + exit 1 +fi + +if ! [ $EUID -eq 0 ] +then + echo "Must be root!" >&2 + exit 1 +fi + +DB="$1" +PASSWORD="$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64 | xargs)" + +cat <" + exit 1 +fi + +if ! [ $EUID -eq 0 ] +then + echo "Must be root!" >&2 + exit 1 +fi + +DB="$1" +PASSWORD="$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64 | xargs)" + +cat < 0 then + table.insert(result, symbols[name] .. " " .. count) + end + end + result = table.concat(result, " ") + return #result > 0 and result or "" +end + +local function custom_filter(buf, buf_nums) + local logs = vim.tbl_filter(function(b) + return is_ft(b, "log") + end, buf_nums) + if vim.tbl_isempty(logs) then + return true + end + local tab_num = vim.fn.tabpagenr() + local last_tab = vim.fn.tabpagenr "$" + local is_log = is_ft(buf, "log") + if last_tab == 1 then + return true + end + -- only show log buffers in secondary tabs + return (tab_num == last_tab and is_log) or (tab_num ~= last_tab and not is_log) +end + + +local config = { + active = true, + on_config_done = nil, + keymap = { + normal_mode = {}, + }, + highlights = { + background = { + italic = true, + }, + buffer_selected = { + bold = true, + }, + }, + options = { + mode = "buffers", -- set to "tabs" to only show tabpages instead + numbers = "none", -- can be "none" | "ordinal" | "buffer_id" | "both" | function + close_command = function(bufnr) -- can be a string | function, see "Mouse actions" + buf_kill("bd", bufnr, false) + end, + right_mouse_command = "vert sbuffer %d", -- can be a string | function, see "Mouse actions" + left_mouse_command = "buffer %d", -- can be a string | function, see "Mouse actions" + middle_mouse_command = nil, -- can be a string | function, see "Mouse actions" + indicator = { + icon = Icons.ui.BoldLineLeft, -- this should be omitted if indicator style is not 'icon' + style = "icon", -- can also be 'underline'|'none', + }, + buffer_close_icon = Icons.ui.Close, + modified_icon = Icons.ui.Circle, + close_icon = Icons.ui.BoldClose, + left_trunc_marker = Icons.ui.ArrowCircleLeft, + right_trunc_marker = Icons.ui.ArrowCircleRight, + --- name_formatter can be used to change the buffer's label in the bufferline. + --- Please note some names can/will break the + --- bufferline so use this at your discretion knowing that it has + --- some limitations that will *NOT* be fixed. + name_formatter = function(buf) -- buf contains a "name", "path" and "bufnr" + -- remove extension from markdown files for example + if buf.name:match "%.md" then + return vim.fn.fnamemodify(buf.name, ":t:r") + end + end, + max_name_length = 18, + max_prefix_length = 15, -- prefix used when a buffer is de-duplicated + truncate_names = true, -- whether or not tab names should be truncated + tab_size = 18, + diagnostics = "nvim_lsp", + diagnostics_update_in_insert = false, + diagnostics_indicator = diagnostics_indicator, + -- NOTE: this will be called a lot so don't do any heavy processing here + custom_filter = custom_filter, + offsets = { + { + filetype = "undotree", + text = "Undotree", + highlight = "PanelHeading", + padding = 1, + }, + { + filetype = "NvimTree", + text = "Explorer", + highlight = "PanelHeading", + padding = 1, + }, + { + filetype = "DiffviewFiles", + text = "Diff View", + highlight = "PanelHeading", + padding = 1, + }, + { + filetype = "flutterToolsOutline", + text = "Flutter Outline", + highlight = "PanelHeading", + }, + { + filetype = "packer", + text = "Packer", + highlight = "PanelHeading", + padding = 1, + }, + }, + color_icons = true, -- whether or not to add the filetype icon highlights + show_buffer_icons = true, -- disable filetype icons for buffers + show_buffer_close_icons = true, + show_close_icon = false, + show_tab_indicators = true, + persist_buffer_sort = true, -- whether or not custom sorted buffers should persist + -- can also be a table containing 2 custom separators + -- [focused and unfocused]. eg: { '|', '|' } + separator_style = "thin", + enforce_regular_tabs = false, + always_show_bufferline = false, + hover = { + enabled = false, -- requires nvim 0.8+ + delay = 200, + reveal = { "close" }, + }, + sort_by = "id", + }, +} + +--require('keymappings').load(config.keymap) + +require("bufferline").setup({ + options = config.options, + highlights = config.highlights, +}) + +--stylua: ignore + +-- Common kill function for bdelete and bwipeout +-- credits: based on bbye and nvim-bufdel +---@param kill_command? string defaults to "bd" +---@param bufnr? number defaults to the current buffer +---@param force? boolean defaults to false +function buf_kill(kill_command, bufnr, force) + kill_command = kill_command or "bd" + + local bo = vim.bo + local api = vim.api + local fmt = string.format + local fnamemodify = vim.fn.fnamemodify + + if bufnr == 0 or bufnr == nil then + bufnr = api.nvim_get_current_buf() + end + + local bufname = api.nvim_buf_get_name(bufnr) + + if not force then + local warning + if bo[bufnr].modified then + warning = fmt([[No write since last change for (%s)]], fnamemodify(bufname, ":t")) + elseif api.nvim_buf_get_option(bufnr, "buftype") == "terminal" then + warning = fmt([[Terminal %s will be killed]], bufname) + end + if warning then + vim.ui.input({ + prompt = string.format([[%s. Close it anyway? [y]es or [n]o (default: no): ]], warning), + }, function(choice) + if choice:match "ye?s?" then force = true end + end) + if not force then return end + end + end + + -- Get list of windows IDs with the buffer to close + local windows = vim.tbl_filter(function(win) + return api.nvim_win_get_buf(win) == bufnr + end, api.nvim_list_wins()) + + if force then + kill_command = kill_command .. "!" + end + + -- Get list of active buffers + local buffers = vim.tbl_filter(function(buf) + return api.nvim_buf_is_valid(buf) and bo[buf].buflisted + end, api.nvim_list_bufs()) + + -- If there is only one buffer (which has to be the current one), vim will + -- create a new buffer on :bd. + -- For more than one buffer, pick the previous buffer (wrapping around if necessary) + if #buffers > 1 and #windows > 0 then + for i, v in ipairs(buffers) do + if v == bufnr then + local prev_buf_idx = i == 1 and (#buffers - 1) or (i - 1) + local prev_buffer = buffers[prev_buf_idx] + for _, win in ipairs(windows) do + api.nvim_win_set_buf(win, prev_buffer) + end + end + end + end + + -- Check if buffer still exists, to ensure the target buffer wasn't killed + -- due to options like bufhidden=wipe. + if api.nvim_buf_is_valid(bufnr) and bo[bufnr].buflisted then + vim.cmd(string.format("%s %d", kill_command, bufnr)) + end +end + diff --git a/utils/modules/nvim/config/icons.lua b/utils/modules/nvim/config/icons.lua new file mode 100644 index 0000000..7f1e70a --- /dev/null +++ b/utils/modules/nvim/config/icons.lua @@ -0,0 +1,155 @@ +Icons = { + kind = { + Array = "", + Boolean = "蘒", + Class = "", + Color = "", + Constant = "", + Constructor = "", + Enum = "", + EnumMember = "", + Event = "", + Field = "", + File = "", + Folder = "", + Function = "", + Interface = "", + Key = "", + Keyword = "", + Method = "", + Module = "", + Namespace = "", + Null = "ﳠ", + Number = "", + Object = "", + Operator = "", + Package = "", + Property = "", + Reference = "", + Snippet = "", + String = "", + Struct = "", + Text = "", + TypeParameter = "", + Unit = "", + Value = "", + Variable = "", + }, + git = { + LineAdded = "", + LineModified = "", + LineRemoved = "", + FileDeleted = "", + FileIgnored = "◌", + FileRenamed = "➜", + FileStaged = "S", + FileUnmerged = "", + FileUnstaged = "", + FileUntracked = "U", + Diff = "", + Repo = "", + Octoface = "", + Branch = "", + }, + ui = { + ArrowCircleDown = "", + ArrowCircleLeft = "", + ArrowCircleRight = "", + ArrowCircleUp = "", + BoldArrowDown = "", + BoldArrowLeft = "", + BoldArrowRight = "", + BoldArrowUp = "", + BoldClose = "", + BoldDividerLeft = "", + BoldDividerRight = "", + BoldLineLeft = "▎", + BookMark = "", + BoxChecked = "", + Bug = "", + Stacks = " ", + Scopes = "", + Watches = "", + DebugConsole = " ", + Calendar = "", + Check = "", + ChevronRight = ">", + ChevronShortDown = "", + ChevronShortLeft = "", + ChevronShortRight = "", + ChevronShortUp = "", + Circle = "", + Close = "", + CloudDownload = "", + Code = "", + Comment = "", + Dashboard = "", + DividerLeft = "", + DividerRight = "", + DoubleChevronRight = "»", + Ellipsis = "…", + EmptyFolder = "", + EmptyFolderOpen = "", + File = "", + FileSymlink = "", + Files = "", + FindFile = "", + FindText = "", + Fire = "", + Folder = "", + FolderOpen = "", + FolderSymlink = "", + Forward = "", + Gear = "", + History = "", + Lightbulb = "", + LineLeft = "▏", + LineMiddle = "│", + List = "", + Lock = "", + NewFile = "", + Note = "", + Package = "", + Pencil = "", + Plus = "", + Project = "", + Search = "", + SignIn = "", + SignOut = "", + Tab = "", + Table = "", + Target = "", + Telescope = "", + Text = "", + Tree = "", + Triangle = "契", + TriangleShortArrowDown = "", + TriangleShortArrowLeft = "", + TriangleShortArrowRight = "", + TriangleShortArrowUp = "", + }, + diagnostics = { + BoldError = "", + Error = "", + BoldWarning = "", + Warning = "", + BoldInformation = "", + Information = "", + BoldQuestion = "", + Question = "", + BoldHint = "", + Hint = "", + Debug = "", + Trace = "✎", + }, + misc = { + Robot = "ﮧ", + Squirrel = "", + Tag = "", + Watch = "", + Smiley = "ﲃ", + Package = "", + CircuitBoard = "", + }, +} + diff --git a/utils/modules/nvim/config/init.lua b/utils/modules/nvim/config/init.lua new file mode 100644 index 0000000..bf2ede8 --- /dev/null +++ b/utils/modules/nvim/config/init.lua @@ -0,0 +1,58 @@ +-- vim.opt.expandtab = true +-- vim.opt.hidden = true +-- vim.opt.incsearch = true +-- vim.opt.mouse = "a" +-- vim.opt.number = true +-- vim.opt.shiftwidth = 2 +-- vim.opt.splitbelow = true +-- vim.opt.splitright = true +-- vim.opt.signcolumn = "yes:3" +-- vim.opt.tabstop = 2 +-- vim.opt.timeoutlen = 0 +-- vim.wo.wrap = false +-- vim.opt.exrc = true +-- vim.cmd("syntax on") + +vim.opt.backup = false -- creates a backup file +vim.opt.clipboard = "unnamedplus" -- allows neovim to access the system clipboard +vim.opt.cmdheight = 2 -- more space in the neovim command line for displaying messages +vim.opt.colorcolumn = "99999" -- fixes indentline for now +vim.opt.completeopt = { "menuone", "noselect" } +vim.opt.conceallevel = 0 -- so that `` is visible in markdown files +vim.opt.fileencoding = "utf-8" -- the encoding written to a file +vim.opt.foldmethod = "manual" -- folding set to "expr" for treesitter based folding +vim.opt.foldexpr = "" -- set to "nvim_treesitter#foldexpr()" for treesitter based folding +vim.opt.guifont = "monospace:h17" -- the font used in graphical neovim applications +vim.opt.hidden = true -- required to keep multiple buffers and open multiple buffers +vim.opt.hlsearch = true -- highlight all matches on previous search pattern +vim.opt.ignorecase = true -- ignore case in search patterns +vim.opt.mouse = "a" -- allow the mouse to be used in neovim +vim.opt.pumheight = 10 -- pop up menu height +vim.opt.showmode = false -- we don't need to see things like -- INSERT -- anymore +vim.opt.showtabline = 2 -- always show tabs +vim.opt.smartcase = true -- smart case +vim.opt.smartindent = true -- make indenting smarter again +vim.opt.splitbelow = true -- force all horizontal splits to go below current window +vim.opt.splitright = true -- force all vertical splits to go to the right of current window +vim.opt.swapfile = false -- creates a swapfile +vim.opt.termguicolors = true -- set term gui colors (most terminals support this) +vim.opt.timeoutlen = 100 -- time to wait for a mapped sequence to complete (in milliseconds) +vim.opt.title = true -- set the title of window to the value of the titlestring +vim.opt.titlestring = "%<%F%=%l/%L - nvim" -- what the title of the window will be set to +vim.opt.undodir = vim.fn.stdpath "cache" .. "/undo" +vim.opt.undofile = true -- enable persistent undo +vim.opt.updatetime = 300 -- faster completion +vim.opt.writebackup = false -- if a file is being edited by another program (or was written to file while editing with another program) it is not allowed to be edited +vim.opt.expandtab = true -- convert tabs to spaces +vim.opt.shiftwidth = 2 -- the number of spaces inserted for each indentation +vim.opt.tabstop = 2 -- insert 2 spaces for a tab +vim.opt.cursorline = true -- highlight the current line +vim.opt.number = true -- set numbered lines +vim.opt.relativenumber = false -- set relative numbered lines +vim.opt.numberwidth = 4 -- set number column width to 2 {default 4} +vim.opt.signcolumn = "yes" -- always show the sign column otherwise it would shift the text each time +vim.opt.wrap = false -- display lines as one long line +vim.opt.spell = false +vim.opt.spelllang = "en" +vim.opt.scrolloff = 8 -- is one of my fav +vim.opt.sidescrolloff = 8 diff --git a/utils/modules/nvim/config/keymappings.lua b/utils/modules/nvim/config/keymappings.lua new file mode 100644 index 0000000..c320f74 --- /dev/null +++ b/utils/modules/nvim/config/keymappings.lua @@ -0,0 +1,145 @@ +local generic_opts_any = { noremap = true, silent = true } + +local generic_opts = { + insert_mode = generic_opts_any, + normal_mode = generic_opts_any, + visual_mode = generic_opts_any, + visual_block_mode = generic_opts_any, + command_mode = generic_opts_any, + term_mode = { silent = true }, +} + +local mode_adapters = { + insert_mode = "i", + normal_mode = "n", + term_mode = "t", + visual_mode = "v", + visual_block_mode = "x", + command_mode = "c", +} + +---@class Keys +---@field insert_mode table +---@field normal_mode table +---@field terminal_mode table +---@field visual_mode table +---@field visual_block_mode table +---@field command_mode table + +local defaults = { + insert_mode = { + -- Move current line / block with Alt-j/k ala vscode. + [""] = ":m .+1==gi", + -- Move current line / block with Alt-j/k ala vscode. + [""] = ":m .-2==gi", + -- navigation + [""] = "k", + [""] = "j", + [""] = "h", + [""] = "l", + }, + + normal_mode = { + -- Better window movement + [""] = "h", + [""] = "j", + [""] = "k", + [""] = "l", + + -- Resize with arrows + [""] = ":resize -2", + [""] = ":resize +2", + [""] = ":vertical resize -2", + [""] = ":vertical resize +2", + + -- Move current line / block with Alt-j/k a la vscode. + [""] = ":m .+1==", + [""] = ":m .-2==", + + -- QuickFix + ["]q"] = ":cnext", + ["[q"] = ":cprev", + [""] = ":call QuickFixToggle()", + }, + + term_mode = { + -- Terminal window navigation + [""] = "h", + [""] = "j", + [""] = "k", + [""] = "l", + }, + + visual_mode = { + -- Better indenting + ["<"] = ""] = ">gv", + + -- ["p"] = '"0p', + -- ["P"] = '"0P', + }, + + visual_block_mode = { + -- Move current line / block with Alt-j/k ala vscode. + [""] = ":m '>+1gv-gv", + [""] = ":m '<-2gv-gv", + }, + + command_mode = { + -- navigate tab completion with and + -- runs conditionally + [""] = { 'pumvisible() ? "\\" : "\\"', { expr = true, noremap = true } }, + [""] = { 'pumvisible() ? "\\" : "\\"', { expr = true, noremap = true } }, + }, +} + +if vim.fn.has "mac" == 1 then + defaults.normal_mode[""] = defaults.normal_mode[""] + defaults.normal_mode[""] = defaults.normal_mode[""] + defaults.normal_mode[""] = defaults.normal_mode[""] + defaults.normal_mode[""] = defaults.normal_mode[""] + Log:debug "Activated mac keymappings" +end + +function set_keymaps(mode, key, val) + local opt = generic_opts[mode] or generic_opts_any + if type(val) == "table" then + opt = val[2] + val = val[1] + end + if val then + vim.keymap.set(mode, key, val, opt) + else + pcall(vim.api.nvim_del_keymap, mode, key) + end +end + +function load_mode(mode, keymaps) + mode = mode_adapters[mode] or mode + for k, v in pairs(keymaps) do + set_keymaps(mode, k, v) + end +end + +function load(keymaps) + keymaps = keymaps or {} + for mode, mapping in pairs(keymaps) do + load_mode(mode, mapping) + end +end + +function load_defaults() + load(get_defaults()) + keys = keys or {} + for idx, _ in pairs(defaults) do + if not keys[idx] then + keys[idx] = {} + end + end +end + +function get_defaults() + return defaults +end + +load_defaults() diff --git a/utils/modules/nvim/config/lspconfig.lua b/utils/modules/nvim/config/lspconfig.lua new file mode 100644 index 0000000..29d5cd2 --- /dev/null +++ b/utils/modules/nvim/config/lspconfig.lua @@ -0,0 +1,50 @@ +local status, lspc = pcall(require, 'lspconfig') +if (not status) then return end + +lspc.clangd.setup{} + +local buf_map = function(bufnr, mode, lhs, rhs, opts) + vim.api.nvim_buf_set_keymap(bufnr, mode, lhs, rhs, opts or { + silent = true, + }) +end + +local protocol = require('vim.lsp.protocol') + +local on_attach = function(client, buffnr) + if client.server.capabilities.documentFormattingProvider then + vimapi.nvim_create_autocmd("BufWritePre", { + group = vim.api.nvim_create_augroup("format", { clear = true }), + buffer = buffnr, + callback = function() vim.lsp.buf.formatting_seq_sync() end + }) + end +end + +local capabilities = vim.lsp.protocol.make_client_capabilities() +capabilities.textDocument.completion.completionItem.snippetSupport = true +capabilities = require('cmp_nvim_lsp').default_capabilities(capabilities) + +local servers = { 'tsserver', 'sumneko_lua', 'cssls', 'yamlls', 'intelephense' } +for _, lsp in pairs(servers) do + require('lspconfig')[lsp].setup { + -- on_attach = on_attach, + capabilities = capabilities, + } +end + +lspc.sumneko_lua.setup({ + settings = { + Lua = { + diagnostics = { + globals = { 'vim' } + }, + workspace = { + library = vim.api.nvim_get_runtime_file("", true), + checkThirdParty = false + }, + }, + }, +}) + +-- lspc.intelephense.setup() diff --git a/utils/modules/nvim/config/nvim-cmp.lua b/utils/modules/nvim/config/nvim-cmp.lua new file mode 100644 index 0000000..13bff54 --- /dev/null +++ b/utils/modules/nvim/config/nvim-cmp.lua @@ -0,0 +1,73 @@ +local has_words_before = function() + local line, col = unpack(vim.api.nvim_win_get_cursor(0)) + return col ~= 0 and vim.api.nvim_buf_get_lines(0, line - 1, line, true)[1]:sub(col, col):match("%s") == nil +end + +local feedkey = function(key, mode) + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(key, true, true, true), mode, true) +end + +local cmp = require("cmp") +local lspkind = require("lspkind") + +cmp.setup({ + sources = { + { name = "nvim_lsp" }, + { name = "cmp_tabnine" }, + { name = "treesitter" }, + { name = "buffer" }, + { name = "path" }, + { name = "vsnip" }, + -- { name = "copilot" }, + }, + + snippet = { + expand = function(args) + vim.fn["vsnip#anonymous"](args.body) + end, + }, + + formatting = { + format = lspkind.cmp_format({ + with_text = true, + menu = { + buffer = "[Buf]", + nvim_lsp = "[LSP]", + nvim_lua = "[Lua]", + latex_symbols = "[Latex]", + treesitter = "[TS]", + cmp_tabnine = "[TN]", + vsnip = "[Snip]", + }, + }), + }, + + mapping = { + [""] = cmp.mapping.confirm({ select = true }), + [""] = cmp.mapping(function(fallback) + if cmp.visible() then + cmp.select_next_item() + elseif vim.fn["vsnip#available"](1) == 1 then + feedkey("(vsnip-expand-or-jump)", "") + elseif has_words_before() then + cmp.complete() + else + fallback() + end + end, { + "i", + "s", + }), + + [""] = cmp.mapping(function() + if cmp.visible() then + cmp.select_prev_item() + elseif vim.fn["vsnip#jumpable"](-1) == 1 then + feedkey("(vsnip-jump-prev)", "") + end + end, { + "i", + "s", + }), + }, +}) diff --git a/utils/modules/nvim/config/project.lua b/utils/modules/nvim/config/project.lua new file mode 100644 index 0000000..f56e374 --- /dev/null +++ b/utils/modules/nvim/config/project.lua @@ -0,0 +1,41 @@ +config = { + ---@usage set to false to disable project.nvim. + --- This is on by default since it's currently the expected behavior. + active = true, + + on_config_done = nil, + + ---@usage set to true to disable setting the current-woriking directory + --- Manual mode doesn't automatically change your root directory, so you have + --- the option to manually do so using `:ProjectRoot` command. + manual_mode = false, + + ---@usage Methods of detecting the root directory + --- Allowed values: **"lsp"** uses the native neovim lsp + --- **"pattern"** uses vim-rooter like glob pattern matching. Here + --- order matters: if one is not detected, the other is used as fallback. You + --- can also delete or rearangne the detection methods. + -- detection_methods = { "lsp", "pattern" }, -- NOTE: lsp detection will get annoying with multiple langs in one project + detection_methods = { "pattern" }, + + ---@usage patterns used to detect root dir, when **"pattern"** is in detection_methods + patterns = { ".git", "_darcs", ".hg", ".bzr", ".svn", "Makefile", "package.json", "pom.xml" }, + + ---@ Show hidden files in telescope when searching for files in a project + show_hidden = false, + + ---@usage When set to false, you will get a message when project.nvim changes your directory. + -- When set to false, you will get a message when project.nvim changes your directory. + silent_chdir = true, + + ---@usage list of lsp client names to ignore when using **lsp** detection. eg: { "efm", ... } + ignore_lsp = {}, +} + +local status_ok, project = pcall(require, "project_nvim") +if not status_ok then + return +end + +project.setup(config) + diff --git a/utils/modules/nvim/config/telescope.lua b/utils/modules/nvim/config/telescope.lua new file mode 100644 index 0000000..8ca6d9e --- /dev/null +++ b/utils/modules/nvim/config/telescope.lua @@ -0,0 +1,148 @@ +local function get_pickers(actions) + return { + find_files = { + theme = "dropdown", + hidden = true, + previewer = false, + }, + live_grep = { + --@usage don't include the filename in the search results + only_sort_text = true, + theme = "dropdown", + }, + grep_string = { + only_sort_text = true, + theme = "dropdown", + }, + buffers = { + theme = "dropdown", + previewer = false, + initial_mode = "normal", + mappings = { + i = { + [""] = actions.delete_buffer, + }, + n = { + ["dd"] = actions.delete_buffer, + }, + }, + }, + planets = { + show_pluto = true, + show_moon = true, + }, + git_files = { + theme = "dropdown", + hidden = true, + previewer = false, + show_untracked = true, + }, + lsp_references = { + theme = "dropdown", + initial_mode = "normal", + }, + lsp_definitions = { + theme = "dropdown", + initial_mode = "normal", + }, + lsp_declarations = { + theme = "dropdown", + initial_mode = "normal", + }, + lsp_implementations = { + theme = "dropdown", + initial_mode = "normal", + }, + } +end + +local ok, actions = pcall(require, "telescope.actions") +if not ok then + return +end + +local config = { + prompt_prefix = " ", + selection_caret = " ", + entry_prefix = " ", + initial_mode = "insert", + selection_strategy = "reset", + sorting_strategy = "descending", + layout_strategy = "horizontal", + layout_config = { + width = 0.75, + preview_cutoff = 120, + horizontal = { + preview_width = function(_, cols, _) + if cols < 120 then + return math.floor(cols * 0.8) + end + return math.floor(cols * 0.8) + end, + mirror = false, + }, + vertical = { mirror = false }, + }, + vimgrep_arguments = { + "rg", + "--color=never", + "--no-heading", + "--with-filename", + "--line-number", + "--column", + "--smart-case", + "--hidden", + "--glob=!.git/", + }, + ---@usage Mappings are fully customizable. Many familiar mapping patterns are setup as defaults. + mappings = { + i = { + [""] = actions.move_selection_next, + [""] = actions.move_selection_previous, + [""] = actions.close, + [""] = actions.cycle_history_next, + [""] = actions.cycle_history_prev, + [""] = actions.smart_send_to_qflist + actions.open_qflist, + [""] = actions.select_default, + }, + n = { + [""] = actions.move_selection_next, + [""] = actions.move_selection_previous, + [""] = actions.smart_send_to_qflist + actions.open_qflist, + }, + }, + pickers = get_pickers(actions), + file_ignore_patterns = {}, + path_display = { "smart" }, + winblend = 0, + border = {}, + borderchars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, + color_devicons = true, + set_env = { ["COLORTERM"] = "truecolor" }, -- default = nil, + extensions = { + fzf = { + fuzzy = true, -- false will only do exact matching + override_generic_sorter = true, -- override the generic sorter + override_file_sorter = true, -- override the file sorter + case_mode = "smart_case", -- or "ignore_case" or "respect_case" + }, + }, +} + + +local previewers = require "telescope.previewers" +local sorters = require "telescope.sorters" + +config = vim.tbl_extend("keep", { + file_previewer = previewers.vim_buffer_cat.new, + grep_previewer = previewers.vim_buffer_vimgrep.new, + qflist_previewer = previewers.vim_buffer_qflist.new, + file_sorter = sorters.get_fuzzy_file, + generic_sorter = sorters.get_generic_fuzzy_sorter, +}, config) + +local telescope = require "telescope" +telescope.setup(config) + +require("telescope").load_extension "projects" +require("telescope").load_extension "fzf" diff --git a/utils/modules/nvim/config/terminal.lua b/utils/modules/nvim/config/terminal.lua new file mode 100644 index 0000000..0708a1d --- /dev/null +++ b/utils/modules/nvim/config/terminal.lua @@ -0,0 +1,133 @@ +local config = { + active = true, + on_config_done = nil, + -- size can be a number or function which is passed the current terminal + size = 20, + open_mapping = [[]], + hide_numbers = true, -- hide the number column in toggleterm buffers + shade_filetypes = {}, + shade_terminals = true, + shading_factor = 2, -- the degree by which to darken to terminal colour, default: 1 for dark backgrounds, 3 for light + start_in_insert = true, + insert_mappings = true, -- whether or not the open mapping applies in insert mode + persist_size = false, + -- direction = 'vertical' | 'horizontal' | 'window' | 'float', + direction = "float", + close_on_exit = true, -- close the terminal window when the process exits + shell = vim.o.shell, -- change the default shell + -- This field is only relevant if direction is set to 'float' + float_opts = { + -- The border key is *almost* the same as 'nvim_win_open' + -- see :h nvim_win_open for details on borders however + -- the 'curved' border is a custom border type + -- not natively supported but implemented in this plugin. + -- border = 'single' | 'double' | 'shadow' | 'curved' | ... other options supported by win open + border = "curved", + -- width = , + -- height = , + winblend = 0, + highlights = { + border = "Normal", + background = "Normal", + }, + }, + -- Add executables on the config.lua + -- { exec, keymap, name} + -- lvim.builtin.terminal.execs = {{}} to overwrite + -- lvim.builtin.terminal.execs[#lvim.builtin.terminal.execs+1] = {"gdb", "tg", "GNU Debugger"} + -- TODO: pls add mappings in which key and refactor this + execs = { + { vim.o.shell, "", "Horizontal Terminal", "horizontal", 0.3 }, + { vim.o.shell, "", "Vertical Terminal", "vertical", 0.4 }, + { vim.o.shell, "", "Float Terminal", "float", nil }, + }, +} + +--- Get current buffer size +---@return {width: number, height: number} +local function get_buf_size() + local cbuf = vim.api.nvim_get_current_buf() + local bufinfo = vim.tbl_filter(function(buf) + return buf.bufnr == cbuf + end, vim.fn.getwininfo(vim.api.nvim_get_current_win()))[1] + if bufinfo == nil then + return { width = -1, height = -1 } + end + return { width = bufinfo.width, height = bufinfo.height } +end + +--- Get the dynamic terminal size in cells +---@param direction number +---@param size integer +---@return integer +local function get_dynamic_terminal_size(direction, size) + size = size or config.size + if direction ~= "float" and tostring(size):find(".", 1, true) then + size = math.min(size, 1.0) + local buf_sizes = get_buf_size() + local buf_size = direction == "horizontal" and buf_sizes.height or buf_sizes.width + return buf_size * size + else + return size + end +end + +Add_exec = function(opts) + local binary = opts.cmd:match "(%S+)" + if vim.fn.executable(binary) ~= 1 then + Log:debug("Skipping configuring executable " .. binary .. ". Please make sure it is installed properly.") + return + end + + vim.keymap.set({ "n", "t" }, opts.keymap, function() + M._exec_toggle { cmd = opts.cmd, count = opts.count, direction = opts.direction, size = opts.size() } + end, { desc = opts.label, noremap = true, silent = true }) +end + +local terminal = require "toggleterm" +terminal.setup(config) + +for i, exec in pairs(config.execs) do + local direction = exec[4] or config.direction + + local opts = { + cmd = exec[1], + keymap = exec[2], + label = exec[3], + -- NOTE: unable to consistently bind id/count <= 9, see #2146 + count = i + 100, + direction = direction, + size = function() + return get_dynamic_terminal_size(direction, exec[5]) + end, + } + + Add_exec(opts) +end + +_exec_toggle = function(opts) + local Terminal = require("toggleterm.terminal").Terminal + local term = Terminal:new { cmd = opts.cmd, count = opts.count, direction = opts.direction } + term:toggle(opts.size, opts.direction) +end + +Lazygit_toggle = function() + local Terminal = require("toggleterm.terminal").Terminal + local lazygit = Terminal:new { + cmd = "lazygit", + hidden = true, + direction = "float", + float_opts = { + border = "none", + width = 100000, + height = 100000, + }, + on_open = function(_) + vim.cmd "startinsert!" + end, + on_close = function(_) end, + count = 99, + } + lazygit:toggle() +end + diff --git a/utils/modules/nvim/config/theming.lua b/utils/modules/nvim/config/theming.lua new file mode 100644 index 0000000..eb3f6ac --- /dev/null +++ b/utils/modules/nvim/config/theming.lua @@ -0,0 +1,18 @@ +-- set colorscheme +vim.cmd 'set termguicolors' + +require("catppuccin").setup() + +vim.cmd [[colorscheme dracula]] + +-- enable colorizer +require'colorizer'.setup() + +-- set sign +vim.cmd 'sign define DiagnosticSignError text= linehl= texthl=DiagnosticSignError numhl=' +vim.cmd 'sign define DiagnosticSignHint text= linehl= texthl=DiagnosticSignHint numhl=' +vim.cmd 'sign define DiagnosticSignInfo text= linehl= texthl=DiagnosticSignInfo numhl=' +vim.cmd 'sign define DiagnosticSignWarn text= linehl= texthl=DiagnosticSignWarn numhl=' + +-- set lightline theme to horizon +vim.g.lightline = { colorscheme = "dracula" } diff --git a/utils/modules/nvim/config/treesitter-textobjects.lua b/utils/modules/nvim/config/treesitter-textobjects.lua new file mode 100644 index 0000000..8f2c6d6 --- /dev/null +++ b/utils/modules/nvim/config/treesitter-textobjects.lua @@ -0,0 +1,14 @@ +require'nvim-treesitter.configs'.setup { + textobjects = { + select = { + enable = true, + keymaps = { + -- You can use the capture groups defined in textobjects.scm + ["af"] = "@function.outer", + ["if"] = "@function.inner", + ["ac"] = "@class.outer", + ["ic"] = "@class.inner", + }, + }, + }, +} diff --git a/utils/modules/nvim/config/treesitter.lua b/utils/modules/nvim/config/treesitter.lua new file mode 100644 index 0000000..1f33ef0 --- /dev/null +++ b/utils/modules/nvim/config/treesitter.lua @@ -0,0 +1,30 @@ +require("nvim-treesitter.configs").setup({ + highlight = { + enable = true, + disable = {}, + }, + rainbow = { + enable = true, + extended_mode = true, + }, + autotag = { + enable = true, + }, + context_commentstring = { + enable = true, + }, + incremental_selection = { + enable = true, + keymaps = { + init_selection = "gnn", + node_incremental = "grn", + scope_incremental = "grc", + node_decremental = "grm", + }, + }, +}) + +-- breaks highlight +-- vim.cmd([[set foldmethod=expr]]) +-- vim.cmd([[set foldlevel=10]]) +-- vim.cmd([[set foldexpr=nvim_treesitter#foldexpr()]]) \ No newline at end of file diff --git a/utils/modules/nvim/config/utils.lua b/utils/modules/nvim/config/utils.lua new file mode 100644 index 0000000..cf92cbd --- /dev/null +++ b/utils/modules/nvim/config/utils.lua @@ -0,0 +1,26 @@ +-- null-ls +local nb = require('null-ls').builtins + +require('null-ls').setup({ + sources = { + nb.formatting.alejandra, + nb.code_actions.statix, + nb.diagnostics.cppcheck, + nb.diagnostics.deadnix, + nb.diagnostics.statix, + nb.diagnostics.eslint, + nb.completion.spell, + }, +}) + +require("gitsigns").setup() + +-- autopairs +require('nvim-autopairs').setup{} + +-- copy to system clipboard +vim.api.nvim_set_keymap( 'v', 'y', '"+y', {noremap = true}) +vim.api.nvim_set_keymap( 'n', 'y', ':%+y', {noremap = true}) + +-- paste from system clipboard +vim.api.nvim_set_keymap( 'n', 'p', '"+p', {noremap = true}) diff --git a/utils/modules/nvim/config/which-key.lua b/utils/modules/nvim/config/which-key.lua new file mode 100644 index 0000000..a00b932 --- /dev/null +++ b/utils/modules/nvim/config/which-key.lua @@ -0,0 +1,147 @@ +vim.g.mapleader = " " + +local function smart_quit() + local bufnr = vim.api.nvim_get_current_buf() + local modified = vim.api.nvim_buf_get_option(bufnr, "modified") + if modified then + vim.ui.input({ + prompt = "You have unsaved changes. Quit anyway? (y/n) ", + }, function(input) + if input == "y" then + vim.cmd "q!" + end + end) + else + vim.cmd "q!" + end +end + +local function find_project_files() + local _, builtin = pcall(require, "telescope.builtin") + local ok = pcall(builtin.git_files) + + if not ok then + builtin.find_files() + end +end + +local wk = require("which-key") + +wk.setup({}) + +wk.register({ + [""] = { + + [";"] = { "Alpha", "Dashboard" }, + ["w"] = { "w!", "Save" }, + ["q"] = { "smart_quit()", "Quit" }, + ["/"] = { "(comment_toggle_linewise_current)", "Comment toggle current line" }, + ["c"] = { "BufferKill", "Close Buffer" }, + ["f"] = { find_project_files, "Find File" }, + ["h"] = { "nohlsearch", "No Highlight" }, + b = { + name = "Buffers", + j = { "BufferLinePick", "Jump" }, + f = { "Telescope buffers", "Find" }, + b = { "BufferLineCyclePrev", "Previous" }, + n = { "BufferLineCycleNext", "Next" }, + -- w = { "BufferWipeout", "Wipeout" }, -- TODO: implement this for bufferline + e = { + "BufferLinePickClose", + "Pick which buffer to close", + }, + h = { "BufferLineCloseLeft", "Close all to the left" }, + l = { + "BufferLineCloseRight", + "Close all to the right", + }, + D = { + "BufferLineSortByDirectory", + "Sort by directory", + }, + L = { + "BufferLineSortByExtension", + "Sort by language", + }, + }, + -- " Available Debug Adapters: + -- " https://microsoft.github.io/debug-adapter-protocol/implementors/adapters/ + -- " Adapter configuration and installation instructions: + -- " https://github.com/mfussenegger/nvim-dap/wiki/Debug-Adapter-installation + -- " Debug Adapter protocol: + -- " https://microsoft.github.io/debug-adapter-protocol/ + -- " Debugging + g = { + name = "Git", + g = { Lazygit_toggle, "Lazygit" }, + j = { "lua require 'gitsigns'.next_hunk({navigation_message = false})", "Next Hunk" }, + k = { "lua require 'gitsigns'.prev_hunk({navigation_message = false})", "Prev Hunk" }, + l = { "lua require 'gitsigns'.blame_line()", "Blame" }, + p = { "lua require 'gitsigns'.preview_hunk()", "Preview Hunk" }, + r = { "lua require 'gitsigns'.reset_hunk()", "Reset Hunk" }, + R = { "lua require 'gitsigns'.reset_buffer()", "Reset Buffer" }, + s = { "lua require 'gitsigns'.stage_hunk()", "Stage Hunk" }, + u = { + "lua require 'gitsigns'.undo_stage_hunk()", + "Undo Stage Hunk", + }, + o = { "Telescope git_status", "Open changed file" }, + b = { "Telescope git_branches", "Checkout branch" }, + c = { "Telescope git_commits", "Checkout commit" }, + C = { + "Telescope git_bcommits", + "Checkout commit(for current file)", + }, + d = { + "Gitsigns diffthis HEAD", + "Git Diff", + }, + }, + l = { + name = "LSP", + a = { "lua vim.lsp.buf.code_action()", "Code Action" }, + d = { "Telescope diagnostics bufnr=0 theme=get_ivy", "Buffer Diagnostics" }, + w = { "Telescope diagnostics", "Diagnostics" }, + -- f = { require("lvim.lsp.utils").format, "Format" }, + i = { "LspInfo", "Info" }, + I = { "Mason", "Mason Info" }, + j = { + vim.diagnostic.goto_next, + "Next Diagnostic", + }, + k = { + vim.diagnostic.goto_prev, + "Prev Diagnostic", + }, + l = { vim.lsp.codelens.run, "CodeLens Action" }, + q = { vim.diagnostic.setloclist, "Quickfix" }, + r = { vim.lsp.buf.rename, "Rename" }, + s = { "Telescope lsp_document_symbols", "Document Symbols" }, + S = { + "Telescope lsp_dynamic_workspace_symbols", + "Workspace Symbols", + }, + e = { "Telescope quickfix", "Telescope Quickfix" }, + }, + + + a = { "lua require('telescope.builtin').lsp_code_actions()", "Code Actions" }, + d = { "lua require('telescope.builtin').lsp_document_diagnostics()", "LSP Diagnostics" }, + k = { "lua vim.lsp.buf.signature_help()", "Signature Help" }, + P = { "lua require'telescope'.extensions.projects.projects{}", "Signature Help" }, + } +}) + +wk.register( + { + ["/"] = { "(comment_toggle_linewise_visual)", "Comment toggle linewise (visual)" }, + }, + { + mode = "v", -- VISUAL mode + prefix = "", + buffer = nil, -- Global mappings. Specify a buffer number for buffer local mappings + silent = true, -- use `silent` when creating keymaps + noremap = true, -- use `noremap` when creating keymaps + nowait = true, -- use `nowait` when creating keymaps + } +) diff --git a/utils/modules/nvim/default.nix b/utils/modules/nvim/default.nix new file mode 100644 index 0000000..64e212b --- /dev/null +++ b/utils/modules/nvim/default.nix @@ -0,0 +1,83 @@ +{ pkgs, ... }: +{ + environment.variables = { EDITOR = "vim"; }; + + environment.systemPackages = with pkgs; [ + nodePackages.typescript-language-server + sumneko-lua-language-server + nodePackages.intelephense + nodePackages.vscode-css-languageserver-bin + nodePackages.yaml-language-server + gopls + lazygit + (neovim.override { + vimAlias = true; + configure = { + packages.myPlugins = with pkgs.vimPlugins; { + start = [ + bufferline-nvim + catppuccin-nvim + cmp-buffer + cmp-nvim-lsp + cmp-path + cmp-spell + cmp-treesitter + cmp-vsnip + comment-nvim + dracula-vim + friendly-snippets + gitsigns-nvim + lightline-vim + lspkind-nvim + neogit + null-ls-nvim + nvim-autopairs + nvim-cmp + nvim-colorizer-lua + nvim-lspconfig + nvim-tree-lua + nvim-ts-rainbow + pkgs.vimPlugins.nvim-treesitter.withAllGrammars + # (nvim-treesitter.withPlugins (_: pkgs.tree-sitter.allGrammars)) + plenary-nvim + project-nvim + telescope-fzf-native-nvim + telescope-nvim + toggleterm-nvim + vim-floaterm + vim-sneak + vim-vsnip + which-key-nvim + ]; + opt = []; + }; + customRC = let + luaRequire = module: + builtins.readFile (builtins.toString + ./config + + "/${module}.lua"); + luaConfig = builtins.concatStringsSep "\n" (map luaRequire [ + "init" + "keymappings" + "icons" + "lspconfig" + "nvim-cmp" + "theming" + "project" + "telescope" + "terminal" + "treesitter" + "treesitter-textobjects" + "utils" + "bufferline" + "which-key" + ]); + in '' + lua << EOF + ${luaConfig} + EOF + ''; + }; + } + )]; +} diff --git a/utils/modules/openldap.nix b/utils/modules/openldap.nix new file mode 100644 index 0000000..5acd3c9 --- /dev/null +++ b/utils/modules/openldap.nix @@ -0,0 +1,325 @@ +{ + pkgs, + config, + ... +}: { + services.openldap = { + enable = true; + + settings.attrs.olcLogLevel = "0"; + + settings.attrs.olcTLSCACertificateFile = config.sops.secrets.openldap-ca.path; + settings.attrs.olcTLSCertificateFile = config.sops.secrets.openldap-cert.path; + settings.attrs.olcTLSCertificateKeyFile = config.sops.secrets.openldap-key.path; + + settings.children = { + "cn=schema".includes = [ + "${pkgs.openldap}/etc/schema/core.ldif" + "${pkgs.openldap}/etc/schema/cosine.ldif" + "${pkgs.openldap}/etc/schema/inetorgperson.ldif" + "${pkgs.openldap}/etc/schema/nis.ldif" + ]; + + "olcDatabase={1}mdb".attrs = { + objectClass = ["olcDatabaseConfig" "olcMdbConfig"]; + olcDatabase = "{1}mdb"; + olcDbDirectory = "/var/lib/openldap/data"; + olcRootPW.path = config.sops.secrets.openldap-rootpw.path; + olcRootDN = "cn=admin,dc=cloonar,dc=com"; + olcSuffix = "dc=cloonar,dc=com"; + olcAccess = [ + '' + {0}to attrs=userPassword + by self write by anonymous auth + by dn.base="cn=dovecot,dc=cloonar,dc=com" read + by dn.base="cn=gogs,ou=system,ou=users,dc=cloonar,dc=com" read + read by * none + '' + '' + {1}to attrs=loginShell + by self write + by * read + '' + '' + {2}to dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" + by dn.base="cn=dovecot,dc=mail,dc=cloonar,dc=com" read + by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read + by * none + '' + '' + {3}to dn.subtree="ou=jabber,ou=users,dc=cloonar,dc=com" + by dn.base="cn=prosody,ou=system,ou=users,dc=eve" write + by * read + '' + '' + {4}to * by * read + '' + ]; + }; + "olcOverlay=syncprov,olcDatabase={1}mdb".attrs = { + objectClass = ["olcOverlayConfig" "olcSyncProvConfig"]; + olcOverlay = "syncprov"; + olcSpSessionLog = "100"; + }; + "olcDatabase={2}monitor".attrs = { + olcDatabase = "{2}monitor"; + objectClass = ["olcDatabaseConfig" "olcMonitorConfig"]; + olcAccess = [ + '' + {0}to * + by dn.exact="cn=netdata,ou=system,ou=users,dc=cloonar,dc=com" read + by * none + '' + ]; + }; + + "cn={1}bitwarden,cn=schema" = { + attrs = { + cn = "{1}bitwarden"; + objectClass = "olcSchemaConfig"; + olcObjectClasses = [ + '' + (1.3.6.1.4.1.28298.1.2.4 NAME 'bitwarden' + SUP uidObject AUXILIARY + DESC 'Added to an account to allow bitwarden access' + MUST (mail $ userPassword)) + '' + ] + }; + }; + # "cn={1}squid,cn=schema".attrs = { + # cn = "{1}squid"; + # objectClass = "olcSchemaConfig"; + # olcObjectClasses = [ + # '' + # (1.3.6.1.4.1.16548.1.2.4 NAME 'proxyUser' + # SUP top AUXILIARY + # DESC 'Account to allow a user to use the Squid proxy' + # MUST ( mail $ userPassword )) + # '' + # ]; + # }; + # "cn={1}grafana,cn=schema".attrs = { + # cn = "{1}grafana"; + # objectClass = "olcSchemaConfig"; + # olcObjectClasses = [ + # '' + # (1.3.6.1.4.1.28293.1.2.5 NAME 'grafana' + # SUP uidObject AUXILIARY + # DESC 'Added to an account to allow grafana access' + # MUST (mail)) + # '' + # ]; + # }; + "cn={2}postfix,cn=schema".attrs = { + cn = "{2}postfix"; + objectClass = "olcSchemaConfig"; + olcAttributeTypes = [ + '' + (1.3.6.1.4.1.12461.1.1.1 NAME 'postfixTransport' + DESC 'A string directing postfix which transport to use' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{20} SINGLE-VALUE)'' + '' + (1.3.6.1.4.1.12461.1.1.5 NAME 'mailbox' + DESC 'The absolute path to the mailbox for a mail account in a non-default location' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE) + '' + '' + (1.3.6.1.4.1.12461.1.1.6 NAME 'quota' + DESC 'A string that represents the quota on a mailbox' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE) + '' + '' + (1.3.6.1.4.1.12461.1.1.8 NAME 'maildrop' + DESC 'RFC822 Mailbox - mail alias' + EQUALITY caseIgnoreIA5Match + SUBSTR caseIgnoreIA5SubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256}) + '' + ]; + olcObjectClasses = [ + '' + (1.3.6.1.4.1.12461.1.2.1 NAME 'mailAccount' + SUP top AUXILIARY + DESC 'Mail account objects' + MUST ( mail $ userPassword ) + MAY ( cn $ description $ quota)) + '' + '' + (1.3.6.1.4.1.12461.1.2.2 NAME 'mailAlias' + SUP top STRUCTURAL + DESC 'Mail aliasing/forwarding entry' + MUST ( mail $ maildrop ) + MAY ( cn $ description )) + '' + '' + (1.3.6.1.4.1.12461.1.2.3 NAME 'mailDomain' + SUP domain STRUCTURAL + DESC 'Virtual Domain entry to be used with postfix transport maps' + MUST ( dc ) + MAY ( postfixTransport $ description )) + '' + '' + (1.3.6.1.4.1.12461.1.2.4 NAME 'mailPostmaster' + SUP top AUXILIARY + DESC 'Added to a mailAlias to create a postmaster entry' + MUST roleOccupant) + '' + ]; + }; + "cn={1}openssh,cn=schema".attrs = { + cn = "{1}openssh"; + objectClass = "olcSchemaConfig"; + olcAttributeTypes = [ + '' + (1.3.6.1.4.1.24552.500.1.1.1.13 + NAME 'sshPublicKey' + DESC 'MANDATORY: OpenSSH Public key' + EQUALITY octetStringMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 ) + '' + ]; + olcObjectClasses = [ + '' + (1.3.6.1.4.1.24552.500.1.1.2.0 + NAME 'ldapPublicKey' + SUP top AUXILIARY + DESC 'MANDATORY: OpenSSH LPK objectclass' + MUST ( sshPublicKey $ uid )) + '' + ]; + }; + "cn={1}nginx,cn=schema".attrs = { + cn = "{1}nginx"; + objectClass = "olcSchemaConfig"; + olcObjectClasses = [ + '' + (1.3.6.1.4.1.28295.1.2.4 NAME 'nginx' + SUP top AUXILIARY + DESC 'Added to an account to allow nginx access' + MUST ( mail $ userPassword )) + '' + ]; + }; + + "cn={1}nextcloud,cn=schema".attrs = { + cn = "{1}nextcloud"; + objectClass = "olcSchemaConfig"; + olcAttributeTypes = [ + '' + (1.3.6.1.4.1.39430.1.1.1 + NAME 'ownCloudQuota' + DESC 'User Quota (e.g. 15 GB)' + SYNTAX '1.3.6.1.4.1.1466.115.121.1.15') + '' + ]; + olcObjectClasses = [ + '' + (1.3.6.1.4.1.39430.1.2.1 + NAME 'ownCloud' + DESC 'ownCloud LDAP Schema' + AUXILIARY + MUST ( mail $ userPassword ) + MAY ( ownCloudQuota )) + '' + ]; + }; + "cn={1}gogs,cn=schema".attrs = { + cn = "{1}gitlab"; + objectClass = "olcSchemaConfig"; + olcObjectClasses = [ + '' + ( 1.3.6.1.4.1.28293.1.2.4 NAME 'gitlab' + SUP uidObject AUXILIARY + DESC 'Added to an account to allow gitlab access' + MUST (mail)) + '' + ]; + }; + "cn={1}iobroker,cn=schema".attrs = { + cn = "{1}homeAssistant"; + objectClass = "olcSchemaConfig"; + olcObjectClasses = [ + '' + (1.3.6.1.4.1.28297.1.2.4 NAME 'homeAssistant' + SUP uidObject AUXILIARY + DESC 'Added to an account to allow home-assistant access' + MUST (mail) ) + '' + ]; + }; + # "cn={1}ttrss,cn=schema".attrs = { + # cn = "{1}ttrss"; + # objectClass = "olcSchemaConfig"; + # olcObjectClasses = [ + # '' + # ( 1.3.6.1.4.1.28294.1.2.4 NAME 'ttrss' + # SUP top AUXILIARY + # DESC 'Added to an account to allow tinytinyrss access' + # MUST ( mail $ userPassword )) + # '' + # ]; + # }; + # "cn={1}prometheus,cn=schema".attrs = { + # cn = "{1}prometheus"; + # objectClass = "olcSchemaConfig"; + # olcObjectClasses = [ + # '' + # ( 1.3.6.1.4.1.28296.1.2.4 + # NAME 'prometheus' + # SUP uidObject AUXILIARY + # DESC 'Added to an account to allow prometheus access' + # MUST (mail)) + # '' + # ]; + # }; + # "cn={1}loki,cn=schema".attrs = { + # cn = "{1}loki"; + # objectClass = "olcSchemaConfig"; + # olcObjectClasses = [ + # '' + # ( 1.3.6.1.4.1.28299.1.2.4 + # NAME 'loki' + # SUP uidObject AUXILIARY + # DESC 'Added to an account to allow loki access' + # MUST (mail)) + # '' + # ]; + # }; + # "cn={1}flood,cn=schema".attrs = { + # cn = "{1}flood"; + # objectClass = "olcSchemaConfig"; + # olcObjectClasses = [ + # '' + # (1.3.6.1.4.1.28300.1.2.4 NAME 'flood' + # SUP uidObject AUXILIARY + # DESC 'Added to an account to allow flood access' + # MUST (mail)) + # '' + # ]; + # }; + }; + }; + + sops.secrets.openldap-rootpw = { + owner = "openldap"; + sopsFile = ./secrets.yaml; + }; + sops.secrets.openldap-ca = { + owner = "openldap"; + sopsFile = ./secrets.yaml; + }; + sops.secrets.openldap-cert = { + owner = "openldap"; + sopsFile = ./secrets.yaml; + }; + sops.secrets.openldap-key = { + owner = "openldap"; + sopsFile = ./secrets.yaml; + }; + + networking.firewall.allowedTCPPorts = [636]; +} diff --git a/utils/modules/openldap/default.nix b/utils/modules/openldap/default.nix new file mode 100644 index 0000000..afa4bd8 --- /dev/null +++ b/utils/modules/openldap/default.nix @@ -0,0 +1,480 @@ +{ + pkgs, + config, + ... +}: +let + domain = config.networking.domain; + # domain = "cloonar.com"; +in { + services.openldap = { + enable = true; + + urlList = [ "ldap:///" "ldaps:///" ]; + + settings.attrs = { + olcLogLevel = "-1"; + + olcTLSCACertificateFile = "/var/lib/acme/ldap.${domain}/full.pem"; + olcTLSCertificateFile = "/var/lib/acme/ldap.${domain}/cert.pem"; + olcTLSCertificateKeyFile = "/var/lib/acme/ldap.${domain}/key.pem"; + olcTLSCipherSuite = "HIGH:MEDIUM:+3DES:+RC4:+aNULL"; + olcTLSCRLCheck = "none"; + olcTLSVerifyClient = "never"; + olcTLSProtocolMin = "3.1"; + olcSecurity = "tls=1"; + }; + + settings.children = { + "cn=schema".includes = [ + "${pkgs.openldap}/etc/schema/core.ldif" + "${pkgs.openldap}/etc/schema/cosine.ldif" + "${pkgs.openldap}/etc/schema/inetorgperson.ldif" + "${pkgs.openldap}/etc/schema/nis.ldif" + ]; + + "olcDatabase={1}mdb".attrs = { + objectClass = ["olcDatabaseConfig" "olcMdbConfig"]; + + olcDatabase = "{1}mdb"; + olcDbDirectory = "/var/lib/openldap/data"; + + olcSuffix = "dc=cloonar,dc=com"; + + olcRootDN = "cn=admin,dc=cloonar,dc=com"; + olcRootPW.path = config.sops.secrets.openldap-rootpw.path; + + + olcAccess = [ + '' + {0}to attrs=userPassword + by self write + by anonymous auth + by dn="cn=owncloud,ou=system,ou=users,dc=cloonar,dc=com" write + by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read + by group.exact="cn=Administrators,ou=groups,dc=cloonar,dc=com" write + by * none + '' + '' + {1}to attrs=loginShell + by self write + by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read + by group.exact="cn=Administrators,ou=groups,dc=cloonar,dc=com" write + by * none + '' + '' + {2}to dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" + by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read + by group.exact="cn=Administrators,ou=groups,dc=cloonar,dc=com" write + by * none + '' + '' + {3}to * + by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read + by dn="cn=admin,dc=cloonar,dc=com" write + by group.exact="cn=Administrators,ou=groups,dc=cloonar,dc=com" write + by * none + '' + ]; + }; + "olcOverlay=memberof,olcDatabase={1}mdb".attrs = { + objectClass = [ "olcOverlayConfig" "olcMemberOf" ]; + olcOverlay = "memberof"; + olcMemberOfRefint = "TRUE"; + }; + "olcOverlay=ppolicy,olcDatabase={1}mdb".attrs = { + objectClass = [ "olcOverlayConfig" "olcPPolicyConfig" ]; + olcOverlay = "ppolicy"; + olcPPolicyHashCleartext = "TRUE"; + }; + # "olcOverlay=syncprov,olcDatabase={1}mdb".attrs = { + # objectClass = ["olcOverlayConfig" "olcSyncProvConfig"]; + # olcOverlay = "syncprov"; + # olcSpSessionLog = "100"; + # }; + "olcDatabase={2}monitor".attrs = { + olcDatabase = "{2}monitor"; + objectClass = ["olcDatabaseConfig" "olcMonitorConfig"]; + olcAccess = [ + '' + {0}to * + by dn.exact="cn=netdata,ou=system,ou=users,dc=cloonar,dc=com" read + by * none + '' + ]; + }; + + "olcDatabase={3}mdb".attrs = { + objectClass = ["olcDatabaseConfig" "olcMdbConfig"]; + + olcDatabase = "{3}mdb"; + olcDbDirectory = "/var/lib/openldap/data"; + + olcSuffix = "dc=ghetto,dc=at"; + + olcAccess = [ + '' + {0}to attrs=userPassword + by self write + by anonymous auth + by dn="cn=owncloud,ou=system,ou=users,dc=cloonar,dc=com" write + by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read + by group.exact="cn=Administrators,ou=groups,dc=cloonar,dc=com" write + by * none + '' + '' + {1}to * + by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read + by group.exact="cn=Administrators,ou=groups,dc=cloonar,dc=com" write + by * read + '' + ]; + }; + "olcOverlay=memberof,olcDatabase={3}mdb".attrs = { + objectClass = [ "olcOverlayConfig" "olcMemberOf" ]; + olcOverlay = "memberof"; + olcMemberOfRefint = "TRUE"; + }; + "olcOverlay=ppolicy,olcDatabase={3}mdb".attrs = { + objectClass = [ "olcOverlayConfig" "olcPPolicyConfig" ]; + olcOverlay = "ppolicy"; + olcPPolicyHashCleartext = "TRUE"; + }; + + "olcDatabase={4}mdb".attrs = { + objectClass = ["olcDatabaseConfig" "olcMdbConfig"]; + + olcDatabase = "{4}mdb"; + olcDbDirectory = "/var/lib/openldap/data"; + + olcSuffix = "dc=superbros,dc=tv"; + + olcAccess = [ + '' + {0}to attrs=userPassword + by self write + by anonymous auth + by dn="cn=owncloud,ou=system,ou=users,dc=cloonar,dc=com" write + by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read + by group.exact="cn=Administrators,ou=groups,dc=cloonar,dc=com" write + by * none + '' + '' + {1}to * + by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read + by group.exact="cn=Administrators,ou=groups,dc=cloonar,dc=com" write + by * read + '' + ]; + }; + "olcOverlay=memberof,olcDatabase={4}mdb".attrs = { + objectClass = [ "olcOverlayConfig" "olcMemberOf" ]; + olcOverlay = "memberof"; + olcMemberOfRefint = "TRUE"; + }; + "olcOverlay=ppolicy,olcDatabase={4}mdb".attrs = { + objectClass = [ "olcOverlayConfig" "olcPPolicyConfig" ]; + olcOverlay = "ppolicy"; + olcPPolicyHashCleartext = "TRUE"; + }; + + "olcDatabase={5}mdb".attrs = { + objectClass = ["olcDatabaseConfig" "olcMdbConfig"]; + + olcDatabase = "{5}mdb"; + olcDbDirectory = "/var/lib/openldap/data"; + + olcSuffix = "dc=optiprot,dc=eu"; + + olcAccess = [ + '' + {0}to attrs=userPassword + by self write + by anonymous auth + by dn="cn=owncloud,ou=system,ou=users,dc=cloonar,dc=com" write + by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read + by group.exact="cn=Administrators,ou=groups,dc=cloonar,dc=com" write + by * none + '' + '' + {1}to * + by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read + by group.exact="cn=Administrators,ou=groups,dc=cloonar,dc=com" write + by * read + '' + ]; + }; + "olcOverlay=memberof,olcDatabase={5}mdb".attrs = { + objectClass = [ "olcOverlayConfig" "olcMemberOf" ]; + olcOverlay = "memberof"; + olcMemberOfRefint = "TRUE"; + }; + "olcOverlay=ppolicy,olcDatabase={5}mdb".attrs = { + objectClass = [ "olcOverlayConfig" "olcPPolicyConfig" ]; + olcOverlay = "ppolicy"; + olcPPolicyHashCleartext = "TRUE"; + }; + + "olcDatabase={6}mdb".attrs = { + objectClass = ["olcDatabaseConfig" "olcMdbConfig"]; + + olcDatabase = "{6}mdb"; + olcDbDirectory = "/var/lib/openldap/data"; + + olcSuffix = "dc=szaku-consulting,dc=at"; + + olcAccess = [ + '' + {0}to attrs=userPassword + by self write + by anonymous auth + by dn="cn=owncloud,ou=system,ou=users,dc=cloonar,dc=com" write + by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read + by group.exact="cn=Administrators,ou=groups,dc=cloonar,dc=com" write + by * none + '' + '' + {1}to * + by dn.subtree="ou=system,ou=users,dc=cloonar,dc=com" read + by group.exact="cn=Administrators,ou=groups,dc=cloonar,dc=com" write + by * read + '' + ]; + }; + "olcOverlay=memberof,olcDatabase={6}mdb".attrs = { + objectClass = [ "olcOverlayConfig" "olcMemberOf" ]; + olcOverlay = "memberof"; + olcMemberOfRefint = "TRUE"; + }; + "olcOverlay=ppolicy,olcDatabase={6}mdb".attrs = { + objectClass = [ "olcOverlayConfig" "olcPPolicyConfig" ]; + olcOverlay = "ppolicy"; + olcPPolicyHashCleartext = "TRUE"; + }; + + # "cn=module{0},cn=config" = { + # attrs = { + # objectClass = "olcModuleList"; + # cn = "module{0}"; + # olcModuleLoad = "ppolicy.la"; + # }; + # }; + + "cn={3}cloonar,cn=schema" = { + attrs = { + cn = "{1}cloonar"; + objectClass = "olcSchemaConfig"; + olcObjectClasses = [ + '' + (1.3.6.1.4.1.28298.1.2.4 NAME 'cloonarUser' + SUP (mailAccount) AUXILIARY + DESC 'Cloonar Account' + MAY (sshPublicKey $ ownCloudQuota $ quota)) + '' + ]; + }; + }; + "cn={2}postfix,cn=schema".attrs = { + cn = "{2}postfix"; + objectClass = "olcSchemaConfig"; + olcAttributeTypes = [ + '' + (1.3.6.1.4.1.12461.1.1.1 NAME 'postfixTransport' + DESC 'A string directing postfix which transport to use' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{20} SINGLE-VALUE)'' + '' + (1.3.6.1.4.1.12461.1.1.5 NAME 'mailbox' + DESC 'The absolute path to the mailbox for a mail account in a non-default location' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE) + '' + '' + (1.3.6.1.4.1.12461.1.1.6 NAME 'quota' + DESC 'A string that represents the quota on a mailbox' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE) + '' + '' + (1.3.6.1.4.1.12461.1.1.8 NAME 'maildrop' + DESC 'RFC822 Mailbox - mail alias' + EQUALITY caseIgnoreIA5Match + SUBSTR caseIgnoreIA5SubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256}) + '' + ]; + olcObjectClasses = [ + '' + (1.3.6.1.4.1.12461.1.2.1 NAME 'mailAccount' + SUP top AUXILIARY + DESC 'Mail account objects' + MUST ( mail $ userPassword ) + MAY ( cn $ description $ quota)) + '' + '' + (1.3.6.1.4.1.12461.1.2.2 NAME 'mailAlias' + SUP top STRUCTURAL + DESC 'Mail aliasing/forwarding entry' + MUST ( mail $ maildrop ) + MAY ( cn $ description )) + '' + '' + (1.3.6.1.4.1.12461.1.2.3 NAME 'mailDomain' + SUP domain STRUCTURAL + DESC 'Virtual Domain entry to be used with postfix transport maps' + MUST ( dc ) + MAY ( postfixTransport $ description )) + '' + '' + (1.3.6.1.4.1.12461.1.2.4 NAME 'mailPostmaster' + SUP top AUXILIARY + DESC 'Added to a mailAlias to create a postmaster entry' + MUST roleOccupant) + '' + ]; + }; + "cn={1}openssh,cn=schema".attrs = { + cn = "{1}openssh"; + objectClass = "olcSchemaConfig"; + olcAttributeTypes = [ + '' + (1.3.6.1.4.1.24552.500.1.1.1.13 + NAME 'sshPublicKey' + DESC 'MANDATORY: OpenSSH Public key' + EQUALITY octetStringMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 ) + '' + ]; + olcObjectClasses = [ + '' + (1.3.6.1.4.1.24552.500.1.1.2.0 + NAME 'ldapPublicKey' + SUP top AUXILIARY + DESC 'MANDATORY: OpenSSH LPK objectclass' + MUST ( sshPublicKey $ uid )) + '' + ]; + }; + "cn={1}nextcloud,cn=schema".attrs = { + cn = "{1}nextcloud"; + objectClass = "olcSchemaConfig"; + olcAttributeTypes = [ + '' + (1.3.6.1.4.1.39430.1.1.1 + NAME 'ownCloudQuota' + DESC 'User Quota (e.g. 15 GB)' + SYNTAX '1.3.6.1.4.1.1466.115.121.1.15') + '' + ]; + olcObjectClasses = [ + '' + (1.3.6.1.4.1.39430.1.2.1 + NAME 'ownCloud' + DESC 'ownCloud LDAP Schema' + AUXILIARY + MUST ( mail $ userPassword ) + MAY ( ownCloudQuota )) + '' + ]; + }; + "cn={1}gogs,cn=schema".attrs = { + cn = "{1}gogs"; + objectClass = "olcSchemaConfig"; + olcObjectClasses = [ + '' + ( 1.3.6.1.4.1.28293.1.2.4 NAME 'gitlab' + SUP uidObject AUXILIARY + DESC 'Added to an account to allow gitlab access' + MUST (mail)) + '' + ]; + }; + "cn={1}homeAssistant,cn=schema".attrs = { + cn = "{1}homeAssistant"; + objectClass = "olcSchemaConfig"; + olcObjectClasses = [ + '' + (1.3.6.1.4.1.28297.1.2.4 NAME 'homeAssistant' + SUP uidObject AUXILIARY + DESC 'Added to an account to allow home-assistant access' + MUST (mail) ) + '' + ]; + }; + # "cn={1}ttrss,cn=schema".attrs = { + # cn = "{1}ttrss"; + # objectClass = "olcSchemaConfig"; + # olcObjectClasses = [ + # '' + # ( 1.3.6.1.4.1.28294.1.2.4 NAME 'ttrss' + # SUP top AUXILIARY + # DESC 'Added to an account to allow tinytinyrss access' + # MUST ( mail $ userPassword )) + # '' + # ]; + # }; + # "cn={1}prometheus,cn=schema".attrs = { + # cn = "{1}prometheus"; + # objectClass = "olcSchemaConfig"; + # olcObjectClasses = [ + # '' + # ( 1.3.6.1.4.1.28296.1.2.4 + # NAME 'prometheus' + # SUP uidObject AUXILIARY + # DESC 'Added to an account to allow prometheus access' + # MUST (mail)) + # '' + # ]; + # }; + # "cn={1}loki,cn=schema".attrs = { + # cn = "{1}loki"; + # objectClass = "olcSchemaConfig"; + # olcObjectClasses = [ + # '' + # ( 1.3.6.1.4.1.28299.1.2.4 + # NAME 'loki' + # SUP uidObject AUXILIARY + # DESC 'Added to an account to allow loki access' + # MUST (mail)) + # '' + # ]; + # }; + # "cn={1}flood,cn=schema".attrs = { + # cn = "{1}flood"; + # objectClass = "olcSchemaConfig"; + # olcObjectClasses = [ + # '' + # (1.3.6.1.4.1.28300.1.2.4 NAME 'flood' + # SUP uidObject AUXILIARY + # DESC 'Added to an account to allow flood access' + # MUST (mail)) + # '' + # ]; + # }; + }; + }; + + /* ensure openldap is launched after certificates are created */ + systemd.services.openldap = { + wants = [ "acme-${domain}.service" ]; + after = [ "acme-${domain}.service" ]; + }; + + users.groups.acme.members = [ "openldap" ]; + + /* trigger the actual certificate generation for your hostname */ + security.acme.certs."ldap.${domain}" = { + extraDomainNames = [ + "ldap-test.${domain}" + "ldap-02.${domain}" + ]; + postRun = "systemctl restart openldap.service"; + }; + + sops.secrets.openldap-rootpw = { + owner = "openldap"; + sopsFile = ./secrets.yaml; + }; + + networking.firewall.allowedTCPPorts = [ 389 636 ]; +} diff --git a/utils/modules/openldap/secrets.yaml b/utils/modules/openldap/secrets.yaml new file mode 100644 index 0000000..c6432b7 --- /dev/null +++ b/utils/modules/openldap/secrets.yaml @@ -0,0 +1,40 @@ +openldap-rootpw: ENC[AES256_GCM,data:cwxXkLd5jyrGI4IoewQL4HBH/Xi6SfVkizitKa0Qyr30SeH02VDLyGXzbpuRfZuPRePbLFEOMKUcCwkZV/2RTA==,iv:SqxCK5rnsgU6i68ZoBZtbRxLgUe59wMg7EYO5jlAwFw=,tag:SbORanDDqLPF+dNleqzYNg==,type:str] +dovecot-ldap-password: ENC[AES256_GCM,data:czkIYqmWXs0U6LFbetJg47VQHw+E5kcHpdwRGmZAKi6Q3hP/IOW8N1dhIkrQRLzKih9vWXKhyo6BDA2e6w6pHQ==,iv:Ka18K0/pGJubfiQ1GKq3uxwZ/CgujO1DulwBomXBbco=,tag:QACpoLJ+QIMM3mCI/vTjtA==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpcnBXWk1JaE9McXlzMUk3 + bXZHRXRQbDdsK25MNTY1MHA0UWhCcWJRdlVVCmlzcDE0L1ZOQzB6MVBPYUdncUZr + M0FGSkdxaFpiY2NUTlRBSUZZdUJmRzgKLS0tIGs3UlNwUDJYVTFHTXcvZkJCS0w5 + cGJic1JZTHE2NnkxN2JuYXY0TmZUWjAKN6orRU5LnJbl84HtKy0MBNA/PiuEmuhO + JL/tpFX+LiOScFHrvb40Ka6YvnyER+rufZXi1xknBzW1uyDt+lSyQw== + -----END AGE ENCRYPTED FILE----- + - recipient: age1jyeppc8yl2twnv8fwcewutd5gjewnxl59lmhev6ygds9qel8zf8syt7zz4 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1RWNUenVwS2xISmRQK3RN + ZEc5TGF2MGNocnZWNkhHQ1lGOU1adlFCZlJ3CmdOWC8vQVd4aEdLVTJtNTZCM1R5 + VndOM3RJRy9laW1pa1k0TUt5UTEyVmsKLS0tIFB5aDNZQXlTRlYvUkJaOXI3NVky + VHFINVFjVVVsTXViTDV0QmFBWTRsbVkKJCjMI1GImwSKpgTDVwF5xAdnbUqBkxUO + vYFySQg5p12lZ7RtMbxdql24a52J9Jm/2dMMKKph339vw/rcW7YRXQ== + -----END AGE ENCRYPTED FILE----- + - recipient: age1azmxsw5llmp2nnsv3yc2l8paelmq9rfepxd8jvmswgsmax0qyyxqdnsc7t + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDdXJGOFgzZFhFWXEvZkNB + OWI3MDQxVGZ5dGpXM245ZUlHZHJhRnR0UWl3CnNKeGhLNVdYVWdoWWFBaC90ZUhj + Mjc4MDQxa0ZaMnVaSndWRDFrTjVpZmMKLS0tIG9rZGJJb0J6SE1lSjdWSHc0V2FH + dGJqSzB5NE5ESzE1L0ZxTDBORnpvRUUKtKejHfzBGnrOJzPStRUcjD/cRq3BqsdP + PtSh9ujx/aazn1O86wMYuIgb1WfWL3ZyTtoPCukGKth9KT1JweU1eA== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2023-01-22T20:44:32Z" + mac: ENC[AES256_GCM,data:nKR47o4Evt4TPyndEwZlnP/ctGaaz6wwn0k+JnDCL3FW1TO64spNL7xDcoxWwPuRLrgjgtazsm4Tevplzc3J/N4dhnPAdiPtZOQd3tKibIJKDkxG+6upGvzMMrXXInzoGVqwFMrZmdIqlpLAgqX/1VwY4Tnrf0IfiwJ8wWmSZe8=,iv:FUL/gcDZBZrclYupzstSFG86NOnEOvvgr8ou7wVQ3AY=,tag:KPXm0HHwc8v64dnqGqlFUQ==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.7.3 diff --git a/utils/modules/parsecgaming.nix b/utils/modules/parsecgaming.nix new file mode 100644 index 0000000..e7c6878 --- /dev/null +++ b/utils/modules/parsecgaming.nix @@ -0,0 +1,2 @@ +#(import {}).callPackage (builtins.fetchurl "https://raw.githubusercontent.com/delroth/infra.delroth.net/master/pkgs/parsec.nix") {} +#(import {}).callPackage (builtins.fetchurl "https://raw.githubusercontent.com/delroth/infra.delroth.net/38a040e4bbfef7ee13c4b0a75dc79c77ddfdc759/pkgs/parsec.nix") {} diff --git a/utils/modules/plausible/default.nix b/utils/modules/plausible/default.nix new file mode 100644 index 0000000..610f9a1 --- /dev/null +++ b/utils/modules/plausible/default.nix @@ -0,0 +1,42 @@ +{ config, pkgs, ... }: + +{ + sops.secrets.plausible-admin-password = { + sopsFile = ./secrets.yaml; + }; + sops.secrets.plausible-secret = { + sopsFile = ./secrets.yaml; + }; + + services.plausible = { + enable = true; + releaseCookiePath = "/run/secrets/plausible-release-cookie"; + server = { + secretKeybaseFile = config.sops.secrets.plausible-secret.path; + baseUrl = "plausible.cloonar.com"; + }; + database.clickhouse = { + setup = false; + }; + database.postgres = { + dbname = "plausible"; + }; + adminUser = { + activate = true; + email = "plausible@cloonar.com"; + passwordFile = config.sops.secrets.plausible-admin-password.path; + }; + }; + + services.nginx.enable = true; + services.nginx.virtualHosts."plausible.cloonar.com" = { + forceSSL = true; + enableACME = true; + acmeRoot = null; + locations."/" = { + proxyPass = "http://localhost:8000"; + }; + }; + + services.postgresqlBackup.databases = [ "plausible" ]; +} diff --git a/utils/modules/plausible/secrets.yaml b/utils/modules/plausible/secrets.yaml new file mode 100644 index 0000000..82428f6 --- /dev/null +++ b/utils/modules/plausible/secrets.yaml @@ -0,0 +1,31 @@ +plausible-admin-password: ENC[AES256_GCM,data:S46PqHBpGRnAq5lv9Pb0NZalg+9hVj8330prH45DlhGG679W4gXzQOwetgQlsr3HAL8I3Uu6Dk4/FWHIKQKGFw==,iv:x9AXhJEoe0Ilx/WEc8f3g2hS0ZFo/DvKcwQBk62KUSk=,tag:Pn9lddsKeEKIv2IDQjFHjA==,type:str] +plausible-secret: ENC[AES256_GCM,data:HS5rDCbGzjhgv5lxNS9IgjrQ+EzBQgfVQxjHK3Kh++9Q1o7TO5IsFBaBw9BRXKMvzILpmCV737ltBcTV6zSZaZBiJmCf+hA7501Zn1RxWMjujlMWf+DTM/Ut7VK/4oc63K/4TLvvGhManl0ia0f8Rwm720JBdbnIrb7e6YSfe74=,iv:atUmTo4+uHFMIiWoxDe79Pj4dE00+0o30DsqUPoL9NY=,tag:jFd+xZvwyRB//Nx1e8BgKw==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMUGdqU1hWR1dMVXhnYUNY + ZFYvb3pCOG1DMWs5bGtscmNTdHdQY0ltRGhrCmhJZkdWMmNUMk5WaFI0REZNc0Jp + SkFHUzB0RjA5cWh2bDd6NDAxeUFqd1kKLS0tIEFmcFNOUTBPcXpJUDRLRGRVWWVC + b3FkcVRhLzNZa0poUmdPM3NoRnRnUnMK/HRww0ewz+LIBeWlbgHlgh/+mAJuhmwI + xEID8pv0gOh+HGEKdRa+BajLmDhTfvnoJadhVswSTMWc/iewgAPs3w== + -----END AGE ENCRYPTED FILE----- + - recipient: age1y6lvl5jkwc47p5ae9yz9j9kuwhy7rtttua5xhygrgmr7ehd49svsszyt42 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKcXdNeTFzeWJkSUx4TllE + azZtQXhtSmZHWk1ld05hcTNXTWhaQlY4eVFFClZrOTN5QTRHKzR1VFhENFEyQ3Qv + Yno4aUw3Y2c5Q1RUSTF0L1hBdTY5bE0KLS0tIHd0T2Zpeko5QVFnVHZ4alh5cUJz + ejh0bExnMEF1MUhLZ0N5SzFpZnM0cFUKgzlIYqwgi9nsik59S+Bxj/Mj5f09JS5I + nVG+2A05+8XxA+eGqcRTYFyo3w71R8hdfmu3vfXl+Ywb+P51K+SDkA== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2023-04-17T20:01:37Z" + mac: ENC[AES256_GCM,data:cm8XdC5CUaQ5KUrkKXSaEfJ/VbxpQQAJg5LZ5O5+3kkWL1qSAK3z4PSnGs58GFlEfw2iFcyOWQr/wH7GPZsSo8/J4fN0zTqYkn+Mn2GZaYMcqr1K1U8tIgq6qLGf2UqG5/AvED4nGY48/aGvGyYyKIak8sgPm8uGz+DwX+ApiMk=,iv:jNe/PmLq5fHuyxQQj4H4hF8V7rCl/Xj3wNMP2bEsEQY=,tag:BNOi0YVH83S4pV9e38WcHQ==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.7.3 diff --git a/utils/modules/postfix.nix b/utils/modules/postfix.nix new file mode 100644 index 0000000..9226b99 --- /dev/null +++ b/utils/modules/postfix.nix @@ -0,0 +1,246 @@ +{ pkgs +, lib +, config +, ... +}: +let + domain = config.networking.domain; + ldapServer = "ldap.cloonar.com"; + # domain = "cloonar.com"; + + domains = pkgs.writeText "domains.cf" '' + server_host = ldap://${ldapServer} + search_base = ou=domains,dc=cloonar,dc=com + version = 3 + bind = yes + start_tls = yes + bind_dn = cn=vmail,ou=system,ou=users,dc=cloonar,dc=com + bind_pw = @ldap-password@ + scope = one + query_filter = (&(dc=%s)(objectClass=mailDomain)) + result_attribute = postfixTransport + debuglevel = 0 + ''; + + mailboxes = pkgs.writeText "mailboxes.cf" '' + server_host = ldap://${ldapServer} + search_base = ou=users,dc=%2,dc=%1 + version = 3 + bind = yes + start_tls = yes + bind_dn = cn=vmail,ou=system,ou=users,dc=cloonar,dc=com + bind_pw = @ldap-password@ + scope = sub + query_filter = (&(uid=%u)(objectClass=mailAccount)) + result_attribute = mail + debuglevel = 0 + ''; + + senderLoginMaps = pkgs.writeText "sender_login_maps.cf" '' + server_host = ldap://${ldapServer} + search_base = dc=%2,dc=%1 + version = 3 + bind = yes + start_tls = yes + bind_dn = cn=vmail,ou=system,ou=users,dc=cloonar,dc=com + bind_pw = @ldap-password@ + scope = sub + query_filter = (|(&(objectClass=mailAccount)(uid=%u))(&(objectClass=mailAlias)(mail=%s))) + result_attribute = maildrop, mail + debuglevel = 0 + ''; + + accountsmap = pkgs.writeText "accountsmap.cf" '' + server_host = ldap://${ldapServer} + search_base = ou=users,dc=%2,dc=%1 + version = 3 + bind = yes + start_tls = yes + bind_dn = cn=vmail,ou=system,ou=users,dc=cloonar,dc=com + bind_pw = @ldap-password@ + scope = sub + query_filter = (&(objectClass=mailAccount)(uid=%u)) + result_attribute = mail + debuglevel = 0 + ''; + + aliases = pkgs.writeText "aliases.cf" '' + server_host = ldap://${ldapServer} + search_base = ou=aliases,dc=%2,dc=%1 + version = 3 + bind = yes + start_tls = yes + bind_dn = cn=vmail,ou=system,ou=users,dc=cloonar,dc=com + bind_pw = @ldap-password@ + scope = one + query_filter = (&(objectClass=mailAlias)(mail=%s)) + result_attribute = maildrop + debuglevel = 0 + ''; + + helo_access = pkgs.writeText "helo_access" '' + /^([0-9\.]+)$/ REJECT ACCESS DENIED. Your email was rejected because the sending mail server sent non RFC compliant HELO identity (''${1}) + cloonar.com REJECT ACCESS DENIED. Your email was rejected because the sending mail server sent non RFC compliant HELO identity (''${1}) + ghetto.at REJECT ACCESS DENIED. Your email was rejected because the sending mail server sent non RFC compliant HELO identity (''${1}) + ''; +in +{ + services.postfix = { + enable = true; + enableSubmission = true; + hostname = "mail.${domain}"; + domain = "cloonar.com"; + + masterConfig."465" = { + type = "inet"; + private = false; + command = "smtpd"; + args = [ + "-o smtpd_client_restrictions=permit_sasl_authenticated,reject" + "-o syslog_name=postfix/smtps" + "-o smtpd_tls_wrappermode=yes" + "-o smtpd_sasl_auth_enable=yes" + "-o smtpd_tls_security_level=none" + "-o smtpd_reject_unlisted_recipient=no" + "-o smtpd_recipient_restrictions=" + "-o smtpd_relay_restrictions=permit_sasl_authenticated,reject" + "-o milter_macro_daemon_name=ORIGINATING" + ]; + }; + + mapFiles."helo_access" = helo_access; + + config = { + # debug_peer_list = "10.42.96.190"; + # smtp_bind_address = config.networking.eve.ipv4.address; + # smtp_bind_address6 = "2a01:4f9:2b:1605::1"; + mailbox_transport = "lmtp:unix:private/dovecot-lmtp"; + virtual_mailbox_domains = "ldap:/run/postfix/domains.cf"; + virtual_mailbox_maps = "ldap:/run/postfix/mailboxes.cf"; + virtual_alias_maps = "ldap:/run/postfix/accountsmap.cf,ldap:/run/postfix/aliases.cf"; + virtual_transport = "lmtp:unix:private/dovecot-lmtp"; + smtpd_sender_login_maps = "ldap:/run/postfix/sender_login_maps.cf"; + + # Do not display the name of the recipient table in the "User unknown" responses. + # The extra detail makes trouble shooting easier but also reveals information + # that is nobody elses business. + show_user_unknown_table_name = "no"; + compatibility_level = "2"; + + # bigger attachement size + mailbox_size_limit = "202400000"; + message_size_limit = "51200000"; + smtpd_helo_required = "yes"; + smtpd_delay_reject = "yes"; + strict_rfc821_envelopes = "yes"; + + # send Limit + smtpd_error_sleep_time = "1s"; + smtpd_soft_error_limit = "10"; + smtpd_hard_error_limit = "20"; + + smtpd_use_tls = "yes"; + smtp_tls_note_starttls_offer = "yes"; + smtpd_tls_security_level = "may"; + smtpd_tls_auth_only = "yes"; + + smtp_dns_support_level = "dnssec"; + smtp_tls_security_level = "dane"; + + smtpd_tls_cert_file = "/var/lib/acme/mail.cloonar.com/full.pem"; + smtpd_tls_key_file = "/var/lib/acme/mail.cloonar.com/key.pem"; + smtpd_tls_CAfile = "/var/lib/acme/mail.cloonar.com/fullchain.pem"; + + smtpd_tls_dh512_param_file = config.security.dhparams.params.postfix512.path; + smtpd_tls_dh1024_param_file = config.security.dhparams.params.postfix2048.path; + + smtpd_tls_session_cache_database = ''btree:''${data_directory}/smtpd_scache''; + smtpd_tls_mandatory_protocols = "!SSLv2,!SSLv3,!TLSv1,!TLSv1.1"; + smtpd_tls_protocols = "!SSLv2,!SSLv3,!TLSv1,!TLSv1.1"; + smtpd_tls_mandatory_ciphers = "medium"; + tls_medium_cipherlist = "AES128+EECDH:AES128+EDH"; + + # authentication + smtpd_sasl_auth_enable = "yes"; + smtpd_sasl_local_domain = "$mydomain"; + smtpd_sasl_security_options = "noanonymous"; + smtpd_sasl_tls_security_options = "$smtpd_sasl_security_options"; + smtpd_sasl_type = "dovecot"; + smtpd_sasl_path = "/var/lib/postfix/queue/private/auth"; + smtpd_relay_restrictions = " + permit_mynetworks, + permit_sasl_authenticated, + defer_unauth_destination"; + smtpd_client_restrictions = " + permit_mynetworks, + permit_sasl_authenticated, + reject_invalid_hostname, + reject_unknown_client, + permit"; + smtpd_helo_restrictions = " + permit_mynetworks, + permit_sasl_authenticated, + reject_unauth_pipelining, + reject_non_fqdn_hostname, + reject_invalid_hostname, + warn_if_reject reject_unknown_hostname, + permit"; + smtpd_recipient_restrictions = " + permit_mynetworks, + permit_sasl_authenticated, + reject_non_fqdn_sender, + reject_non_fqdn_recipient, + reject_non_fqdn_hostname, + reject_invalid_hostname, + reject_unknown_sender_domain, + reject_unknown_recipient_domain, + reject_unknown_client_hostname, + reject_unauth_pipelining, + reject_unknown_client, + permit"; + smtpd_sender_restrictions = " + reject_non_fqdn_sender, + reject_unlisted_sender, + reject_authenticated_sender_login_mismatch, + permit_mynetworks, + permit_sasl_authenticated, + reject_unknown_sender_domain, + reject_unknown_client_hostname, + reject_unknown_address"; + + smtpd_etrn_restrictions = "permit_mynetworks, reject"; + smtpd_data_restrictions = "reject_unauth_pipelining, reject_multi_recipient_bounce, permit"; + }; + }; + + systemd.tmpfiles.rules = [ "d /run/postfix 0750 postfix postfix -" ]; + + systemd.services.postfix.preStart = '' + sed -e "s/@ldap-password@/$(cat ${config.sops.secrets.dovecot-ldap-password.path})/" ${domains} > /run/postfix/domains.cf + sed -e "s/@ldap-password@/$(cat ${config.sops.secrets.dovecot-ldap-password.path})/" ${mailboxes} > /run/postfix/mailboxes.cf + sed -e "s/@ldap-password@/$(cat ${config.sops.secrets.dovecot-ldap-password.path})/" ${accountsmap} > /run/postfix/accountsmap.cf + sed -e "s/@ldap-password@/$(cat ${config.sops.secrets.dovecot-ldap-password.path})/" ${aliases} > /run/postfix/aliases.cf + sed -e "s/@ldap-password@/$(cat ${config.sops.secrets.dovecot-ldap-password.path})/" ${senderLoginMaps} > /run/postfix/sender_login_maps.cf + ''; + + security.dhparams = { + enable = true; + params.postfix512.bits = 512; + params.postfix2048.bits = 1024; + }; + + security.acme.certs."mail.${domain}" = { + extraDomainNames = [ + "mail-test.${domain}" + "mail-02.${domain}" + ]; + postRun = "systemctl restart postfix.service"; + group = "postfix"; + }; + + networking.firewall.allowedTCPPorts = [ + 25 # smtp + 465 # smtps + 587 # submission + ]; +} diff --git a/utils/modules/room-assistant.nix b/utils/modules/room-assistant.nix new file mode 100644 index 0000000..b7a85ed --- /dev/null +++ b/utils/modules/room-assistant.nix @@ -0,0 +1,112 @@ +{ config, lib, pkgs, ... }: +with lib; +let + cfg = config.services.room-assistant; +in +{ + options = { + services.room-assistant = { + enable = mkEnableOption (lib.mdDoc "room-assistant"); + + name = mkOption { + type = with types; uniq string; + description = " + ... + "; + default = "room"; + }; + + mqttHost = mkOption { + type = with types; uniq string; + description = " + ... + "; + default = ""; + }; + + mqttUser = mkOption { + type = with types; uniq string; + description = " + ... + "; + default = "espresense"; + }; + + mqttPassword = mkOption { + type = with types; uniq string; + description = " + ... + "; + default = "insecure-password"; + }; + }; + }; + config = mkIf cfg.enable { + hardware = { + bluetooth.enable = true; + deviceTree.filter = "bcm2711-rpi-*.dtb"; + }; + + systemd.services = { + btattach = { + before = [ "bluetooth.service" ]; + after = [ "dev-ttyAMA0.device" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = "${pkgs.bluez}/bin/btattach -B /dev/ttyAMA0 -P bcm -S 3000000"; + }; + }; + }; + + virtualisation.docker.enable = true; + + environment.etc."room-assistant.yml" = { + text = '' + global: + instanceName: ${cfg.name} + integrations: + - homeAssistant + - bluetoothClassic + homeAssistant: + mqttUrl: 'mqtt://${cfg.mqttHost}' + mqttOptions: + username: ${cfg.mqttUser} + password: ${cfg.mqttPassword} + bluetoothClassic: + addresses: + - A8:5B:B7:98:84:F0 + - 00:24:E4:E6:FE:AD + + ''; + + # The UNIX file mode bits + mode = "0440"; + }; + + systemd.services."room-assistant" = { + description = "room-assistant"; + wantedBy = [ "multi-user.target" ]; + after = [ "docker.service" "docker.socket" ]; + requires = [ "docker.service" "docker.socket" ]; + script = '' + exec ${pkgs.docker}/bin/docker run \ + --rm \ + --name=room-assistant \ + --network=host \ + -v /var/run/dbus:/var/run/dbus \ + -v /etc/room-assistant.yml:/room-assistant/config/local.yml \ + --cap-add=NET_ADMIN \ + mkerix/room-assistant:2.20.0 + ''; + preStop = "${pkgs.docker}/bin/docker stop room-assistant"; + reload = "${pkgs.docker}/bin/docker restart room-assistant"; + serviceConfig = { + ExecStartPre = "-${pkgs.docker}/bin/docker rm -f room-assistant"; + ExecStopPost = "-${pkgs.docker}/bin/docker rm -f room-assistant"; + TimeoutStartSec = 0; + TimeoutStopSec = 120; + Restart = "always"; + }; + }; + }; +} diff --git a/utils/modules/roundcube.nix b/utils/modules/roundcube.nix new file mode 100644 index 0000000..2f037bc --- /dev/null +++ b/utils/modules/roundcube.nix @@ -0,0 +1,22 @@ +{ pkgs, ... }: +let + domain = "mail-test.cloonar.com"; +in +{ + services.roundcube = { + enable = true; + hostName = "${domain}"; + extraConfig = '' + $config['imap_host'] = 'tls://imap-test.cloonar.com'; + $config['smtp_server'] = "tls://mail-test.cloonar.com"; + $config['smtp_user'] = "%u"; + $config['smtp_pass'] = "%p"; + ''; + }; + + services.nginx.virtualHosts."${domain}" = { + forceSSL = true; + enableACME = true; + acmeRoot = null; + }; +} diff --git a/utils/modules/rspamd.nix b/utils/modules/rspamd.nix new file mode 100644 index 0000000..abab1e2 --- /dev/null +++ b/utils/modules/rspamd.nix @@ -0,0 +1,131 @@ +{ pkgs +, config +, ... +}: +let + domain = config.networking.domain; + + localConfig = pkgs.writeText "local.conf" '' + logging { + level = "notice"; + } + classifier "bayes" { + autolearn = true; + } + dkim_signing { + path = "/var/lib/rspamd/dkim/$domain.$selector.key"; + selector = "default"; + allow_username_mismatch = true; + } + arc { + path = "/var/lib/rspamd/dkim/$domain.$selector.key"; + selector = "default"; + allow_username_mismatch = true; + } + milter_headers { + use = ["authentication-results", "x-spam-status"]; + authenticated_headers = ["authentication-results"]; + } + replies { + action = "no action"; + } + url_reputation { + enabled = true; + } + phishing { + openphish_enabled = true; + # too much memory + #phishtank_enabled = true; + } + neural { + enabled = true; + } + neural_group { + symbols = { + "NEURAL_SPAM" { + weight = 3.0; # sample weight + description = "Neural network spam"; + } + "NEURAL_HAM" { + weight = -3.0; # sample weight + description = "Neural network ham"; + } + } + } + ''; + + sieve-spam-filter = pkgs.callPackage ../pkgs/sieve-spam-filter { }; +in +{ + services.rspamd = { + enable = true; + extraConfig = '' + .include(priority=1,duplicate=merge) "${localConfig}" + ''; + + postfix.enable = true; + workers.controller = { + extraConfig = '' + count = 1; + static_dir = "''${WWWDIR}"; + password = "$2$7rb4gnnw8qbcy3x3m7au8c4mezecfjim$da4ahtt3gnjtbj7ni6bt1q8jwgqtzxp5ck6941m6prjxsz3udfgb"; + enable_password = "$2$xo1qdd1zgozwto8yazr1o35zbarbzcgp$u8mx6hcsb1qdscejb4zadcb3iucmm4mw6btgmim9h6e5d8cpy5ib"; + ''; + }; + }; + + services.dovecot2 = { + mailboxes.Spam = { + auto = "subscribe"; + specialUse = "Junk"; + }; + extraConfig = '' + protocol imap { + mail_plugins = $mail_plugins imap_sieve + } + + plugin { + sieve_plugins = sieve_imapsieve sieve_extprograms + + # From elsewhere to Spam folder + imapsieve_mailbox1_name = Spam + imapsieve_mailbox1_causes = COPY + imapsieve_mailbox1_before = file:/var/lib/dovecot/sieve/report-spam.sieve + + # From Spam folder to elsewhere + imapsieve_mailbox2_name = * + imapsieve_mailbox2_from = Spam + imapsieve_mailbox2_causes = COPY + imapsieve_mailbox2_before = file:/var/lib/dovecot/sieve/report-ham.sieve + + # Move Spam emails to Spam folder + sieve_before = /var/lib/dovecot/sieve/move-to-spam.sieve + + sieve_pipe_bin_dir = ${sieve-spam-filter}/bin + sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment + } + ''; + }; + + services.nginx.enable = true; + services.nginx.virtualHosts."rspamd.${domain}" = { + forceSSL = true; + enableACME = true; + acmeRoot = null; + locations."/".extraConfig = '' + proxy_pass http://localhost:11334; + ''; + }; + + # systemd.services.rspamd.serviceConfig.SupplementaryGroups = [ "redis-rspamd" ]; + + systemd.services.dovecot2.preStart = '' + mkdir -p /var/lib/dovecot/sieve/ + for i in ${sieve-spam-filter}/share/sieve-rspamd-filter/*.sieve; do + dest="/var/lib/dovecot/sieve/$(basename $i)" + cp "$i" "$dest" + ${pkgs.dovecot_pigeonhole}/bin/sievec "$dest" + done + chown -R "${config.services.dovecot2.mailUser}:${config.services.dovecot2.mailGroup}" /var/lib/dovecot/sieve + ''; +} diff --git a/utils/modules/self-service-password.nix b/utils/modules/self-service-password.nix new file mode 100644 index 0000000..4c463a6 --- /dev/null +++ b/utils/modules/self-service-password.nix @@ -0,0 +1,118 @@ +{ pkgs, lib, config, ... }: +let + domain = "self-service.cloonar.com"; + php = pkgs.php82; + version = "1.5.2"; + dataDir = "/var/www/${domain}"; +in { + + environment.systemPackages = with pkgs; [ + smarty3 + ]; + + systemd.services."phpfpm-${domain}".serviceConfig.ProtectHome = lib.mkForce false; + + systemd.services.selfservicepassword_setup = let + overrideConfig = pkgs.writeText "nextcloud-config.php" '' + ${dataDir}/package.tar.gz + /run/current-system/sw/bin/tar xf ${dataDir}/package.tar.gz -C ${dataDir} + mv ${dataDir}/self-service-password-${version}/* ${dataDir}/public/ + rm -rf ${dataDir}/self-service-password-${version} + cp ${overrideConfig} ${dataDir}/public/conf/config.inc.local.php + ''; + path = [ pkgs.gzip pkgs.curl ]; + serviceConfig.Type = "oneshot"; + serviceConfig.User = domain; + }; + + services.phpfpm.pools."${domain}" = { + user = domain; + settings = { + "listen.owner" = config.services.nginx.user; + "pm" = "dynamic"; + "pm.max_children" = 32; + "pm.max_requests" = 500; + "pm.start_servers" = 2; + "pm.min_spare_servers" = 2; + "pm.max_spare_servers" = 5; + "php_flag[display_errors]" = "on"; + "php_admin_value[error_log]" = "/var/log/${domain}.error.log"; + "php_admin_flag[log_errors]" = "on"; + "php_value[include_path]" = ".:/usr/share/php:${pkgs.smarty3}"; + "catch_workers_output" = "yes"; + "access.log" = "/var/log/$pool.access.log"; + }; + phpPackage = php; + phpEnv."PATH" = lib.makeBinPath [ php ]; + }; + + services.nginx.virtualHosts."${domain}" = { + forceSSL = true; + enableACME = true; + acmeRoot = null; + root = "${dataDir}/public/htdocs"; + + locations."/favicon.ico".extraConfig = '' + log_not_found off; + access_log off; + ''; + + # extraConfig = '' + # if (!-e $request_filename) { + # rewrite ^/(.+)\.(\d+)\.(php|js|css|png|jpg|gif|gzip)$ /$1.$3 last; + # } + # ''; + + locations."/".extraConfig = '' + index index.php index.html; + try_files $uri $uri/ /index.php$is_args$args; + ''; + + locations."~ [^/]\.php(/|$)".extraConfig = '' + fastcgi_split_path_info ^(.+?\.php)(/.*)$; + if (!-f $document_root$fastcgi_script_name) { + return 404; + } + include ${pkgs.nginx}/conf/fastcgi_params; + include ${pkgs.nginx}/conf/fastcgi.conf; + fastcgi_buffer_size 32k; + fastcgi_buffers 8 16k; + fastcgi_connect_timeout 240s; + fastcgi_read_timeout 240s; + fastcgi_send_timeout 240s; + fastcgi_pass unix:${config.services.phpfpm.pools."${domain}".socket}; + fastcgi_index index.php; + ''; + + # locations."~ /\.".extraConfig = '' + # log_not_found off; + # deny all; + # ''; + # + # locations."~ /scripts".extraConfig = '' + # log_not_found off; + # deny all; + # ''; + }; + users.users."${domain}" = { + #isSystemUser = true; + isNormalUser = true; + createHome = true; + home = dataDir; + homeMode= "770"; + group = "nginx"; + }; + users.groups.${domain} = {}; +} diff --git a/utils/modules/services/web/stack.nix b/utils/modules/services/web/stack.nix new file mode 100644 index 0000000..7a0194c --- /dev/null +++ b/utils/modules/services/web/stack.nix @@ -0,0 +1,319 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.webstack; + + instanceOpts = { name, ... }: + { + options = { + user = mkOption { + type = types.nullOr types.str; + default = null; + description = lib.mdDoc '' + User of the typo3 instance. Defaults to attribute name in instances. + ''; + example = "example.org"; + }; + + domain = mkOption { + type = types.nullOr types.str; + default = null; + description = lib.mdDoc '' + Domain of the typo3 instance. Defaults to attribute name in instances. + ''; + example = "example.org"; + }; + + domainAliases = mkOption { + type = types.listOf types.str; + default = []; + example = [ "www.example.org" "example.org" ]; + description = lib.mdDoc '' + Additional domains served by this typo3 instance. + ''; + }; + + phpPackage = mkOption { + type = types.package; + example = literalExpression "pkgs.php"; + description = lib.mdDoc '' + Which PHP package to use in this typo3 instance. + ''; + }; + + enableMysql = mkEnableOption (lib.mdDoc "MySQL Database"); + enableDefaultLocations = mkEnableOption (lib.mdDoc "Create default nginx location directives") // { default = true; }; + + authorizedKeys = mkOption { + type = types.listOf types.str; + default = null; + description = lib.mdDoc '' + Authorized keys for the typo3 instance ssh user. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = '' + if (!-e $request_filename) { + rewrite ^/(.+)\.(\d+)\.(php|js|css|png|jpg|gif|gzip)$ /$1.$3 last; + } + ''; + description = lib.mdDoc '' + These lines go to the end of the vhost verbatim. + ''; + }; + + locations = mkOption { + type = types.attrsOf (types.submodule (import { + inherit lib config; + })); + default = {}; + example = literalExpression '' + { + "/" = { + proxyPass = "http://localhost:3000"; + }; + }; + ''; + description = lib.mdDoc "Declarative location config"; + }; + + }; + }; +in + +{ + options.services.webstack = { + dataDir = mkOption { + type = types.path; + default = "/var/www"; + description = lib.mdDoc '' + The data directory for MySQL. + + ::: {.note} + If left as the default value of `/var/www` this directory will automatically be created before the web + server starts, otherwise you are responsible for ensuring the directory exists with appropriate ownership and permissions. + ::: + ''; + }; + + instances = mkOption { + type = types.attrsOf (types.submodule instanceOpts); + default = {}; + description = lib.mdDoc "Create vhosts for typo3"; + example = literalExpression '' + { + "typo3.example.com" = { + domain = "example.com"; + domainAliases = [ "www.example.com" ]; + phpPackage = pkgs.php81; + authorizedKeys = [ + "ssh-rsa AZA==" + ]; + }; + }; + ''; + }; + }; + + config = { + systemd.services = mapAttrs' (instance: instanceOpts: + let + domain = if instanceOpts.domain != null then instanceOpts.domain else instance; + in + nameValuePair "phpfpm-${domain}" { + serviceConfig = { + ProtectHome = lib.mkForce "tmpfs"; + BindPaths = "BindPaths=/var/www/${domain}:/var/www/${domain}"; + }; + } + ) cfg.instances; + + services.phpfpm.pools = mapAttrs' (instance: instanceOpts: + let + domain = if instanceOpts.domain != null then instanceOpts.domain else instance; + user = if instanceOpts.user != null + then instanceOps.user + else builtins.replaceStrings ["." "-"] ["_" "_"] domain; + in + nameValuePair domain { + user = user; + settings = { + "listen.owner" = config.services.nginx.user; + "pm" = "dynamic"; + "pm.max_children" = 32; + "pm.max_requests" = 500; + "pm.start_servers" = 2; + "pm.min_spare_servers" = 2; + "pm.max_spare_servers" = 5; + "php_admin_value[error_log]" = "syslog"; + "php_admin_value[max_execution_time]" = 240; + "php_admin_value[max_input_vars]" = 1500; + "access.log" = "/var/log/$pool.access.log"; + }; + phpPackage = instanceOpts.phpPackage; + phpEnv."PATH" = pkgs.lib.makeBinPath [ instanceOpts.phpPackage ]; + } + ) cfg.instances; + + }; + + + config.services.nginx.virtualHosts = mapAttrs' (instance: instanceOpts: + let + domain = if instanceOpts.domain != null then instanceOpts.domain else instance; + user = if instanceOpts.user != null + then instanceOps.user + else builtins.replaceStrings ["." "-"] ["_" "_"] domain; + in + nameValuePair domain { + forceSSL = true; + enableACME = true; + acmeRoot = null; + root = cfg.dataDir + "/" + domain + "/public"; + + locations = lib.mkMerge [ + instanceOpts.locations + (mkIf instanceOpts.enableDefaultLocations { + "/favicon.ico".extraConfig = '' + log_not_found off; + access_log off; + ''; + + # Cache.appcache, your document html and data + "~* \\.(?:manifest|appcache|html?|xml|json)$".extraConfig = '' + expires -1; + # access_log logs/static.log; # I don't usually include a static log + ''; + + "~* \\.(jpe?g|png)$".extraConfig = '' + set $red Z; + + if ($http_accept ~* "webp") { + set $red A; + } + + if (-f $document_root/webp/$request_uri.webp) { + set $red "''${red}B"; + } + + if ($red = "AB") { + add_header Vary Accept; + rewrite ^ /webp/$request_uri.webp; + } + ''; + + # Cache Media: images, icons, video, audio, HTC + "~* \\.(?:jpg|jpeg|gif|png|webp|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc|woff2)$".extraConfig = '' + expires 1y; + access_log off; + add_header Cache-Control "public"; + ''; + + # Feed + "~* \\.(?:rss|atom)$".extraConfig = '' + expires 1h; + add_header Cache-Control "public"; + ''; + + # Cache CSS, Javascript, Images, Icons, Video, Audio, HTC, Fonts + "~* \\.(?:css|js|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc|woff2)$".extraConfig = '' + expires 1y; + access_log off; + add_header Cache-Control "public"; + ''; + + "/".extraConfig = '' + index index.php index.html; + try_files $uri $uri/ /index.php$is_args$args; + ''; + }) + { + "~ [^/]\\.php(/|$)".extraConfig = '' + fastcgi_split_path_info ^(.+?\.php)(/.*)$; + if (!-f $document_root$fastcgi_script_name) { + return 404; + } + include ${pkgs.nginx}/conf/fastcgi_params; + include ${pkgs.nginx}/conf/fastcgi.conf; + fastcgi_buffer_size 32k; + fastcgi_buffers 8 16k; + fastcgi_connect_timeout 240s; + fastcgi_read_timeout 240s; + fastcgi_send_timeout 240s; + fastcgi_pass unix:${config.services.phpfpm.pools."${domain}".socket}; + fastcgi_index index.php; + ''; + } + ]; + + extraConfig = instanceOpts.extraConfig; + + + # locations = mapAttrs' (location: locationOpts: + # nameValuePair location locationOpts) instanceOpts.locations; + + } + ) cfg.instances; + + config.users.users = mapAttrs' (instance: instanceOpts: + let + domain = if instanceOpts.domain != null then instanceOpts.domain else instance; + user = if instanceOpts.user != null + then instanceOps.user + else builtins.replaceStrings ["." "-"] ["_" "_"] domain; + in + nameValuePair user { + isNormalUser = true; + createHome = true; + home = "/var/www/" + domain; + homeMode= "770"; + group = config.services.nginx.group; + openssh.authorizedKeys.keys = instanceOpts.authorizedKeys; + } + ) cfg.instances; +config.users.groups = mapAttrs' (instance: instanceOpts: + let + domain = if instanceOpts.domain != null then instanceOpts.domain else instance; + user = if instanceOpts.user != null + then instanceOps.user + else builtins.replaceStrings ["." "-"] ["_" "_"] domain; + in nameValuePair user {}) cfg.instances; + + config.services.mysql.ensureUsers = mapAttrsToList (instance: instanceOpts: + let + domain = if instanceOpts.domain != null then instanceOpts.domain else instance; + user = if instanceOpts.user != null + then instanceOps.user + else builtins.replaceStrings ["." "-"] ["_" "_"] domain; + in + mkIf instanceOpts.enableMysql { + name = user; + ensurePermissions = { + "${user}.*" = "ALL PRIVILEGES"; + }; + }) cfg.instances; + + config.services.mysql.ensureDatabases = mapAttrsToList (instance: instanceOpts: + let + domain = if instanceOpts.domain != null then instanceOpts.domain else instance; + user = if instanceOpts.user != null + then instanceOps.user + else builtins.replaceStrings ["." "-"] ["_" "_"] domain; + in + mkIf instanceOpts.enableMysql user + ) cfg.instances; + config.services.mysqlBackup.databases = mapAttrsToList (instance: instanceOpts: + let + domain = if instanceOpts.domain != null then instanceOpts.domain else instance; + user = if instanceOpts.user != null + then instanceOps.user + else builtins.replaceStrings ["." "-"] ["_" "_"] domain; + in + mkIf instanceOpts.enableMysql user + ) cfg.instances; +} + diff --git a/utils/modules/services/web/typo3.nix b/utils/modules/services/web/typo3.nix new file mode 100644 index 0000000..7bb3836 --- /dev/null +++ b/utils/modules/services/web/typo3.nix @@ -0,0 +1,367 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.typo3; + + instanceOpts = { name, ... }: + { + options = { + user = mkOption { + type = types.nullOr types.str; + default = null; + description = lib.mdDoc '' + User of the typo3 instance. Defaults to attribute name in instances. + ''; + example = "example.org"; + }; + + domain = mkOption { + type = types.nullOr types.str; + default = null; + description = lib.mdDoc '' + Domain of the typo3 instance. Defaults to attribute name in instances. + ''; + example = "example.org"; + }; + + domainAliases = mkOption { + type = types.listOf types.str; + default = []; + example = [ "www.example.org" "example.org" ]; + description = lib.mdDoc '' + Additional domains served by this typo3 instance. + ''; + }; + + phpPackage = mkOption { + type = types.package; + example = literalExpression "pkgs.php"; + description = lib.mdDoc '' + Which PHP package to use in this typo3 instance. + ''; + }; + + authorizedKeys = mkOption { + type = types.listOf types.str; + default = null; + description = lib.mdDoc '' + Authorized keys for the typo3 instance ssh user. + ''; + }; + }; + }; +in + +{ + options.services.typo3 = { + dataDir = mkOption { + type = types.path; + default = "/var/www"; + description = lib.mdDoc '' + The data directory for MySQL. + + ::: {.note} + If left as the default value of `/var/www` this directory will automatically be created before the web + server starts, otherwise you are responsible for ensuring the directory exists with appropriate ownership and permissions. + ::: + ''; + }; + + instances = mkOption { + type = types.attrsOf (types.submodule instanceOpts); + default = {}; + description = lib.mdDoc "Create vhosts for typo3"; + example = literalExpression '' + { + "typo3.example.com" = { + domain = "example.com"; + domainAliases = [ "www.example.com" ]; + phpPackage = pkgs.php82; + authorizedKeys = [ + "ssh-rsa AZA==" + ]; + }; + }; + ''; + }; + }; + + config = { + # systemd.services = mapAttrs' (instance: instanceOpts: + # let + # domain = if instanceOpts.domain != null then instanceOpts.domain else instance; + # in + # nameValuePair "phpfpm-${domain}" { + # serviceConfig = { + # ProtectHome = lib.mkForce "tmpfs"; + # BindPaths = "BindPaths=/var/www/${domain}:/var/www/${domain}"; + # }; + # } + # ) cfg.instances; + + systemd.timers = mapAttrs' (instance: instanceOpts: + let + domain = if instanceOpts.domain != null then instanceOpts.domain else instance; + user = if instanceOpts.user != null + then instanceOps.user + else builtins.replaceStrings ["." "-"] ["_" "_"] domain; + in + nameValuePair ("typo3-cron-" + domain) { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "05:00"; + Unit = "typo3-cron-" + domain + ".service"; + }; + } + ) cfg.instances; + systemd.services = mapAttrs' (instance: instanceOpts: + let + domain = if instanceOpts.domain != null then instanceOpts.domain else instance; + user = if instanceOpts.user != null + then instanceOps.user + else builtins.replaceStrings ["." "-"] ["_" "_"] domain; + in + nameValuePair ("typo3-cron-" + domain) { + script = '' + set -eu + ${instanceOpts.phpPackage}/bin/php /var/www/${domain}/.Build/bin/typo3 scheduler:run + ${instanceOpts.phpPackage}/bin/php /var/www/${domain}/.Build/bin/typo3 ke_search:indexing + ''; + serviceConfig = { + Type = "oneshot"; + User = user; + }; + } + ) cfg.instances; + + services.phpfpm.pools = mapAttrs' (instance: instanceOpts: + let + domain = if instanceOpts.domain != null then instanceOpts.domain else instance; + user = if instanceOpts.user != null + then instanceOps.user + else builtins.replaceStrings ["." "-"] ["_" "_"] domain; + in + nameValuePair domain { + user = user; + settings = { + "listen.owner" = config.services.nginx.user; + "pm" = "dynamic"; + "pm.max_children" = 32; + "pm.max_requests" = 500; + "pm.start_servers" = 2; + "pm.min_spare_servers" = 2; + "pm.max_spare_servers" = 5; + "php_admin_value[error_log]" = "syslog"; + "php_admin_value[max_execution_time]" = 240; + "php_admin_value[max_input_vars]" = 1500; + "php_admin_value[upload_max_filesize]" = "256M"; + "php_admin_value[post_max_size]" = "256M"; + "access.log" = "/var/log/$pool.access.log"; + }; + phpOptions = '' + opcache.enable=1 + opcache.memory_consumption=128 + opcache.validate_timestamps=0 + opcache.revalidate_path=0 + ''; + phpPackage = instanceOpts.phpPackage; + phpEnv."PATH" = pkgs.lib.makeBinPath [ instanceOpts.phpPackage ]; + } + ) cfg.instances; + + }; + + + config.services.nginx.virtualHosts = mapAttrs' (instance: instanceOpts: + let + domain = if instanceOpts.domain != null then instanceOpts.domain else instance; + user = if instanceOpts.user != null + then instanceOps.user + else builtins.replaceStrings ["." "-"] ["_" "_"] domain; + in + nameValuePair domain { + forceSSL = true; + enableACME = true; + acmeRoot = null; + root = cfg.dataDir + "/" + domain + "/public"; + serverAliases = instanceOpts.domainAliases; + + locations."/favicon.ico".extraConfig = '' + log_not_found off; + access_log off; + ''; + + extraConfig = '' + if (!-e $request_filename) { + rewrite ^/(.+)\.(\d+)\.(php|js|css|png|jpg|gif|gzip)$ /$1.$3 last; + } + ''; + + # TYPO3 - Block access to composer files + locations."~* composer\\.(?:json|lock)".extraConfig = '' + deny all; + ''; + + + # TYPO3 - Block access to flexform files + locations."~* flexform[^.]*\\.xml".extraConfig = '' + deny all; + ''; + + # TYPO3 - Block access to language files + locations."~* locallang[^.]*\\.(?:xml|xlf)$".extraConfig = '' + deny all; + ''; + + # TYPO3 - Block access to static typoscript files + locations."~* ext_conf_template\\.txt|ext_typoscript_constants\\.txt|ext_typoscript_setup\\.txt".extraConfig = '' + deny all; + ''; + + # TYPO3 - Block access to miscellaneous protected files + locations."~* /.*\\.(?:bak|co?nf|cfg|ya?ml|ts|typoscript|tsconfig|dist|fla|in[ci]|log|sh|sql|sqlite)$".extraConfig = '' + deny all; + ''; + # locations."~* /.*\.(?:bak|cfg|co?nf|ya?ml|ts)$".extraConfig = '' + # deny all; + # ''; + + # TYPO3 - Block access to recycler and temporary directories + locations."~ _(?:recycler|temp)_/".extraConfig = '' + deny all; + ''; + + # TYPO3 - Block access to configuration files stored in fileadmin + locations."~ fileadmin/(?:templates)/.*\\.(?:txt|ts|typoscript)$".extraConfig = '' + deny all; + ''; + + + # TYPO3 - Block access to libraries, source and temporary compiled data + locations."~ ^(?:vendor|typo3_src|typo3temp/var)".extraConfig = '' + deny all; + ''; + + + # TYPO3 - Block access to protected extension directories + locations."~ (?:typo3conf/ext|typo3/sysext|typo3/ext)/[^/]+/(?:Configuration|Resources/Private|Tests?|Documentation|docs?)/".extraConfig = '' + deny all; + ''; + + # Cache.appcache, your document html and data + locations."~* \\.(?:manifest|appcache|html?|xml|json)$".extraConfig = '' + expires -1; + # access_log logs/static.log; # I don't usually include a static log + ''; + + # Cache Media: images, icons, video, audio, HTC + locations."~* \\.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc|woff2)$".extraConfig = '' + expires 1y; + access_log off; + add_header Cache-Control "public"; + ''; + + # Feed + locations."~* \\.(?:rss|atom)$".extraConfig = '' + expires 1h; + add_header Cache-Control "public"; + ''; + + # Cache CSS, Javascript, Images, Icons, Video, Audio, HTC, Fonts + locations."~* \\.(?:css|js|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc|woff2)$".extraConfig = '' + expires 1y; + access_log off; + add_header Cache-Control "public"; + ''; + + locations."/".extraConfig = '' + index index.php index.html; + try_files $uri $uri/ /index.php$is_args$args; + ''; + + # TYPO3 Backend URLs + locations."/typo3$".extraConfig = '' + rewrite ^ /typo3/; + ''; + + locations."/typo3/".extraConfig = '' + try_files $uri /typo3/index.php$is_args$args; + ''; + + locations."~ [^/]\\.php(/|$)".extraConfig = '' + fastcgi_split_path_info ^(.+?\.php)(/.*)$; + if (!-f $document_root$fastcgi_script_name) { + return 404; + } + include ${pkgs.nginx}/conf/fastcgi_params; + include ${pkgs.nginx}/conf/fastcgi.conf; + fastcgi_buffer_size 32k; + fastcgi_buffers 8 16k; + fastcgi_connect_timeout 240s; + fastcgi_read_timeout 240s; + fastcgi_send_timeout 240s; + fastcgi_pass unix:${config.services.phpfpm.pools."${domain}".socket}; + fastcgi_index index.php; + ''; + } + ) cfg.instances; + + config.users.users = mapAttrs' (instance: instanceOpts: + let + domain = if instanceOpts.domain != null then instanceOpts.domain else instance; + user = if instanceOpts.user != null + then instanceOps.user + else builtins.replaceStrings ["." "-"] ["_" "_"] domain; + in + nameValuePair user { + isNormalUser = true; + createHome = true; + home = "/var/www/" + domain; + homeMode= "770"; + group = config.services.nginx.group; + openssh.authorizedKeys.keys = instanceOpts.authorizedKeys; + } + ) cfg.instances; + config.users.groups = mapAttrs' (instance: instanceOpts: + let + domain = if instanceOpts.domain != null then instanceOpts.domain else instance; + user = if instanceOpts.user != null + then instanceOps.user + else builtins.replaceStrings ["." "-"] ["_" "_"] domain; + in nameValuePair user {}) cfg.instances; + + config.services.mysql.ensureUsers = mapAttrsToList (instance: instanceOpts: + let + domain = if instanceOpts.domain != null then instanceOpts.domain else instance; + user = if instanceOpts.user != null + then instanceOps.user + else builtins.replaceStrings ["." "-"] ["_" "_"] domain; + in + { + name = user; + ensurePermissions = { + "${user}.*" = "ALL PRIVILEGES"; + }; + }) cfg.instances; + + config.services.mysql.ensureDatabases = mapAttrsToList (instance: instanceOpts: + let + domain = if instanceOpts.domain != null then instanceOpts.domain else instance; + user = if instanceOpts.user != null + then instanceOps.user + else builtins.replaceStrings ["." "-"] ["_" "_"] domain; + in + user + ) cfg.instances; + config.services.mysqlBackup.databases = mapAttrsToList (instance: instanceOpts: + let + domain = if instanceOpts.domain != null then instanceOpts.domain else instance; + user = if instanceOpts.user != null + then instanceOps.user + else builtins.replaceStrings ["." "-"] ["_" "_"] domain; + in + user + ) cfg.instances; +} diff --git a/utils/modules/snapclient.nix b/utils/modules/snapclient.nix new file mode 100644 index 0000000..71b8101 --- /dev/null +++ b/utils/modules/snapclient.nix @@ -0,0 +1,38 @@ +{ config, pkgs, ... }: +{ + + # security.rtkit.enable = true; + # services.pipewire = { + # enable = true; + # alsa.enable = true; + # alsa.support32Bit = true; + # pulse.enable = true; + # + # }; + sound.enable = true; + hardware.pulseaudio.enable = true; + hardware.pulseaudio.support32Bit = true; + services.getty.autologinUser = "snapclient"; + + users.groups.snapclient = {}; + users.users.snapclient = { + isNormalUser = true; + group = "snapclient"; + extraGroups = [ "audio" "pipewire" ]; + }; + + systemd.user.services.snapclient = { + wantedBy = [ + "default.target" + ]; + after = [ + "network.target" + ]; + serviceConfig = { + # User = "snapclient"; + # Group = "snapclient"; + ExecStart = "${pkgs.snapcast}/bin/snapclient -h mopidy.cloonar.com -p 1704 --player pulse"; + Restart = "on-failure"; + }; + }; +} diff --git a/utils/modules/snapserver.nix b/utils/modules/snapserver.nix new file mode 100644 index 0000000..2f89daf --- /dev/null +++ b/utils/modules/snapserver.nix @@ -0,0 +1,80 @@ +{ pkgs, config, python3Packages, ... }: +let +shairport-sync = pkgs.shairport-sync.overrideAttrs (_: { + configureFlags = [ + "--with-alsa" "--with-pipe" "--with-pa" "--with-stdout" + "--with-avahi" "--with-ssl=openssl" "--with-soxr" + "--without-configfiles" + "--sysconfdir=/etc" + "--with-metadata" + ]; +}); +in +{ + services.snapserver = { + enable = true; + codec = "flac"; + http.docRoot = "${pkgs.snapcast}/share/snapserver/snapweb"; + streams.mopidy = { + type = "pipe"; + location = "/run/snapserver/mopidy"; + }; + streams.airplay = { + type = "airplay"; + location = "${shairport-sync}/bin/shairport-sync"; + query = { + devicename = "Multi Room"; + port = "5000"; + params = "--mdns=avahi"; + }; + }; + streams.mixed = { + type = "meta"; + location = "/airplay/mopidy"; + }; + }; + + services.avahi.enable = true; + services.avahi.publish.enable = true; + services.avahi.publish.userServices = true; + + # services.shairport-sync = { + # enable = true; + # arguments = "-v -o=pipe -- pipe:name=/run/snapserver/airplay"; + # }; + + services.nginx.virtualHosts."snapcast.cloonar.com" = { + forceSSL = true; + enableACME = true; + acmeRoot = null; + extraConfig = '' + proxy_buffering off; + ''; + locations."/".extraConfig = '' + proxy_pass http://127.0.0.1:1780; + proxy_set_header Host $host; + proxy_redirect http:// https://; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + ''; + }; + + + networking.firewall.allowedTCPPorts = [ + 80 # http + 443 # https + 1704 # snapcast + 1705 # snapcast + 5000 # airplay + 5353 # airplay + ]; + networking.firewall.allowedUDPPorts = [ + 5000 # airplay + 5353 # airplay + ]; + networking.firewall.allowedUDPPortRanges = [ + { from = 6001; to = 6011; } # airplay + ]; +} diff --git a/utils/modules/sops.nix b/utils/modules/sops.nix new file mode 100644 index 0000000..65c2dc2 --- /dev/null +++ b/utils/modules/sops.nix @@ -0,0 +1,6 @@ +{ + imports = [ + "${builtins.fetchTarball "https://github.com/Mic92/sops-nix/archive/master.tar.gz"}/modules/sops" + ]; + +} diff --git a/utils/modules/sway/directory-studio-nix b/utils/modules/sway/directory-studio-nix new file mode 100644 index 0000000..9633b42 --- /dev/null +++ b/utils/modules/sway/directory-studio-nix @@ -0,0 +1,7 @@ + +{ pkgs, ... }: +{ + environment.systemPackages = [ + pkgs.apache-directory-studio + ]; +} diff --git a/utils/modules/sway/parsec.nix b/utils/modules/sway/parsec.nix new file mode 100644 index 0000000..83d603d --- /dev/null +++ b/utils/modules/sway/parsec.nix @@ -0,0 +1,17 @@ +{ pkgs, ... }: + +let + unstable = import (fetchTarball https://nixos.org/channels/nixos-unstable/nixexprs.tar.xz) { + config = { allowUnfree = true; }; + }; + parsecDesktopItem = pkgs.makeDesktopItem { + name = "parsec"; + desktopName = "Parsec Gaming"; + exec = "parsecd"; + }; +in { + environment.systemPackages = with pkgs; [ + unstable.parsec-bin + parsecDesktopItem + ]; +} diff --git a/utils/modules/sway/signal-work.nix b/utils/modules/sway/signal-work.nix new file mode 100644 index 0000000..1f8e4c2 --- /dev/null +++ b/utils/modules/sway/signal-work.nix @@ -0,0 +1,17 @@ + +{ pkgs, ... }: + +let + signalWorkDesktopItem = pkgs.makeDesktopItem { + name = "signal-work"; + desktopName = "Signal with work profile"; + icon = "signal-desktop"; + exec = "signal-desktop --user-data-dir=/home/dominik/.config/Signal-work -- %u"; + }; +in { + environment.systemPackages = [ + pkgs.signal-desktop + pkgs.signal-cli + signalWorkDesktopItem + ]; +} diff --git a/utils/modules/sway/social.nix b/utils/modules/sway/social.nix new file mode 100644 index 0000000..4d5eacc --- /dev/null +++ b/utils/modules/sway/social.nix @@ -0,0 +1,12 @@ + +{ pkgs, ... }: + +let + socialDesktopItem = pkgs.makeDesktopItem { + name = "social"; + desktopName = "Firefox browser with social profile"; + exec = "firefox -P social"; + }; +in { + environment.systemPackages = [ socialDesktopItem ]; +} diff --git a/utils/modules/sway/sway.conf b/utils/modules/sway/sway.conf new file mode 100644 index 0000000..2822e65 --- /dev/null +++ b/utils/modules/sway/sway.conf @@ -0,0 +1,350 @@ +# Oxide theme +# +# Author: Diki Ananta Repository: https://github.com/dikiaap/dotfiles +# License: MIT +# i3 config file (v4) + +# font for window titles and bar +font pango:Source Sans Pro 10 + +# use win key +set $mod Mod4 + +# use these keys for focus, movement, and resize directions +set $left h +set $down j +set $up k +set $right l + +# define names for workspaces +set $ws1 "1: " +set $ws2 "2: " +set $ws3 "3: " +set $ws4 "4: " +set $ws5 "5: " +set $ws6 "6: " +set $ws7 "7: " +set $ws8 "8: " +set $ws9 "9: " +set $ws10 "10: " + +# assign workspaces to primary monitor +workspace $ws1 output primary +workspace $ws2 output primary +workspace $ws3 output primary +workspace $ws4 output primary +workspace $ws5 output primary +workspace $ws6 output primary +workspace $ws7 output primary +workspace $ws8 output primary +workspace $ws9 output primary + +# use $mod+Mouse to drag floating windows to their wanted position +floating_modifier $mod + +# control focused window when follows the mouse movements +focus_follows_mouse no + +# window border settings +default_border none +default_floating_border none + +# hiding borders adjacent to the screen edges +hide_edge_borders none + +# set popups during fullscreen mode +popup_during_fullscreen smart + +# start a terminal +#bindsym $mod+Return workspace $ws1; exec --no-startup-id alacritty +bindsym $mod+Return workspace $ws1; exec alacritty -t alacritty-sway + +# start a program launcher +# bindsym $mod+d exec --no-startup-id wofi -s /etc/wofi/style.css --show drun --lines 5 --columns 1 -t alacritty +bindsym $mod+d exec alacritty --class=launcher -e env TERMINAL_COMMAND="alacritty -e" sway-launcher-desktop +#bindsym $mod+d exec --no-startup-id bemenu +#bindsym $mod+d exec --no-startup-id i3-dmenu-desktop --dmenu="dmenu -i -fn 'Source Sans Pro-10' -nb '#212121' -sb '#2b83a6' -sf '#ffffff' +#bindsym $mod+d exec --no-startup-id bemenu --dmenu="dmenu -i -fn 'Source Sans Pro-10' -nb '#212121' -sb '#2b83a6' -sf '#ffffff' + +# start an explorer +bindsym $mod+e exec --no-startup-id pcmanfm + +# switching window with win+tab +bindsym $mod+Tab exec --no-startup-id wofi -show window + +# kill focused window +bindsym $mod+Shift+q kill + +# change keyboard layout +bindsym $mod+i swaymsg input "*" us +bindsym $mod+Shift+i swaymsg input "*" de + +# change focus +bindsym $mod+$left focus left +bindsym $mod+$down focus down +bindsym $mod+$up focus up +bindsym $mod+$right focus right + +# alternatively, you can use the cursor keys: +bindsym $mod+Left focus left +bindsym $mod+Down focus down +bindsym $mod+Up focus up +bindsym $mod+Right focus right + +# move focused window +bindsym $mod+Shift+$left move left +bindsym $mod+Shift+$down move down +bindsym $mod+Shift+$up move up +bindsym $mod+Shift+$right move right + +# alternatively, you can use the cursor keys: +bindsym $mod+Shift+Left move left +bindsym $mod+Shift+Down move down +bindsym $mod+Shift+Up move up +bindsym $mod+Shift+Right move right + +# split in horizontal orientation +bindsym $mod+c split h + +# split in vertical orientation +bindsym $mod+v split v + +# enter fullscreen mode for the focused container +bindsym $mod+f fullscreen toggle + +# change container layout +bindsym $mod+s layout stacking +bindsym $mod+w layout tabbed +bindsym $mod+p layout toggle split + +# toggle tiling / floating +# bindsym $mod+Shift+space floating toggle + +# change focus between tiling / floating windows +# bindsym $mod+space focus mode_toggle + +# focus the parent container +bindsym $mod+a focus parent + +# focus the child container +bindsym $mod+Shift+a focus child + +# move the currently focused window to the scratchpad +bindsym $mod+Shift+minus move scratchpad + +# show the next scratchpad window or hide the focused scratchpad window +bindsym $mod+minus scratchpad show + +# move focused floating window to the current position of the cursor +bindsym $mod+Shift+m move position mouse + +# set a window to stick to the glass +bindsym $mod+Shift+s sticky toggle + +# sticky preview for media +bindsym $mod+Shift+p fullscreen disable; floating enable; resize set 350 px 197 px; sticky enable; move window to position 1006 px 537 px + +# shortcut to change window border +bindsym $mod+t border normal 0 +bindsym $mod+y border pixel 1 +bindsym $mod+u border none + +# switch to workspace + +bindcode $mod+10 workspace $ws1 +bindcode $mod+11 workspace $ws2 +bindcode $mod+12 workspace $ws3 +bindcode $mod+13 workspace $ws4 +bindcode $mod+14 workspace $ws5 +bindcode $mod+15 workspace $ws6 +bindcode $mod+16 workspace $ws7 +bindcode $mod+17 workspace $ws8 +bindcode $mod+18 workspace $ws9 +bindcode $mod+19 workspace $ws10 + +# move focused container to workspace +bindcode $mod+Shift+10 move container to workspace $ws1 +bindcode $mod+Shift+11 move container to workspace $ws2 +bindcode $mod+Shift+12 move container to workspace $ws3 +bindcode $mod+Shift+13 move container to workspace $ws4 +bindcode $mod+Shift+14 move container to workspace $ws5 +bindcode $mod+Shift+15 move container to workspace $ws6 +bindcode $mod+Shift+16 move container to workspace $ws7 +bindcode $mod+Shift+17 move container to workspace $ws8 +bindcode $mod+Shift+18 move container to workspace $ws9 +bindcode $mod+Shift+19 move container to workspace $ws10 + +# shortcut applications +bindsym $mod+F1 exec gkamus +bindsym $mod+F2 workspace $ws2; exec subl +bindsym $mod+F3 workspace $ws3; exec chromium-browser +bindsym $mod+F4 workspace $ws4; exec pcmanfm +bindsym $mod+F5 workspace $ws5; exec evince +bindsym $mod+F6 workspace $ws6; exec audacious +bindsym $mod+F7 workspace $ws7; exec gcolor2 +bindsym $mod+F8 workspace $ws8; exec telegram +bindsym $mod+F9 workspace $ws9; exec go-for-it +bindsym Print exec --no-startup-id gnome-screenshot +bindcode $mod+9 exec swaylock --image ~/.wallpaper.jpg +bindsym Ctrl+Shift+Space exec 1password --quick-access +bindsym $mod+Space exec rofi-rbw --menu-keybindings ctrl+p:copy:password + +# volume +bindsym XF86AudioLowerVolume exec amixer -q sset Master 5%- unmute +bindsym XF86AudioRaiseVolume exec amixer -q sset Master 5%+ unmute +bindsym XF86AudioMute exec amixer -q sset Master toggle + +# touchpad +# bindsym XF86NotificationCenter exec swaymsg input type:touchpad events toggle enabled disabled + +# set brightness logarithmically by factor 1.4 +# .72 is just slightly bigger than 1 / 1.4 +bindsym --locked XF86MonBrightnessUp exec light -S "$(light -G | awk '{ print int(($1 + .72) * 1.4) }')" +bindsym --locked XF86MonBrightnessDown exec light -S "$(light -G | awk '{ print int($1 / 1.4) }')" + +# reload the configuration file +bindsym $mod+Shift+c reload + +# restart i3 inplace +bindsym $mod+Shift+r restart + +# manage i3 session +bindsym $mod+Shift+e exec swaynag --background f1fa8c --border ffb86c --border-bottom-size 0 --button-background ffb86c --button-text 282a36 -t warning -f "pango:Hack 9" -m "Do you really want to exit?" -B "  Exit " "swaymsg exit" -B "  Lock " "pkill swaynag && swaylock --image ~/.wallpaper.jpg" -B "  Reboot " "pkill swaynag && reboot" -B "  Shutdown " "pkill swaynag && shutdown -h now" -B " Suspend " "pkill swaynag && systemctl suspend" + +# resize window +bindsym $mod+r mode "  " +mode "  " { + # pressing left and up will shrink the window's width and height + # pressing right and down will grow the window's width and height + bindsym $left resize shrink width 10 px or 10 ppt + bindsym $down resize grow height 10 px or 10 ppt + bindsym $up resize shrink height 10 px or 10 ppt + bindsym $right resize grow width 10 px or 10 ppt + + # same bindings, but for the arrow keys + bindsym Left resize shrink width 10 px or 10 ppt + bindsym Down resize grow height 10 px or 10 ppt + bindsym Up resize shrink height 10 px or 10 ppt + bindsym Right resize grow width 10 px or 10 ppt + + # back to normal: Enter or win+r + bindsym Return mode "default" + bindsym $mod+r mode "default" +} + +# set specific windows to floating mode +for_window [window_role="app"] floating enable +#for_window [window_role="pop-up"] floating enable +for_window [window_role="task_dialog"] floating enable +for_window [title="Preferences$"] floating enable +for_window [class="Gkamus"] floating enable +for_window [class="Go-for-it"] floating enable +for_window [class="Lightdm-gtk-greeter-settings"] floating enable +for_window [class="Lxappearance"] floating enable +for_window [class="Menu"] floating enable +for_window [class="Nm-connection-editor"] floating enable +for_window [class="Software-properties-gtk"] floating enable +for_window [app_id="launcher"] floating enable + +# set specific windows to tabbed mode +#for_window [class="Rambox"]tabbed +#for_window [class="Signal"]tabbed + +# assign program to workspace +assign [title="alacritty-sway"] → $ws1 +assign [app_id="Alacritty" title="^(?!alacritty-sway)$"] → $ws2 +assign [app_id="chromium"] → $ws3 +assign [app_id="firefox"] → $ws3 +assign [app_id="pcmanfm"] → $ws4 +assign [app_id="libreoffice-calc"] → $ws5 +assign [app_id="libreoffice-writer"] → $ws5 +assign [class="vlc"] → $ws6 +assign [class="Gimp"] → $ws7 +assign [class="Signal"] → $ws8 +assign [app_id="social"] → $ws8 +assign [app_id="thunderbird"] → $ws8 +assign [class="Lightdm-gtk-greeter-settings"] → $ws10 +assign [class="Software-properties-gtk"] → $ws10 + +# class border backgr. text indicator child_border +client.focused #2b83a6 #2b83a6 #ffffff #dddddd #2b83a6 +client.focused_inactive #212121 #212121 #86888c #292d2e #5a5a5a +client.unfocused #212121 #212121 #86888c #292d2e #5a5a5a +client.urgent #d64e4e #d64e4e #ffffff #d64e4e #d64e4e +client.placeholder #212121 #0c0c0c #ffffff #212121 #262626 +client.background #212121 + +bar { + swaybar_command waybar +} + +# gaps +smart_gaps on +gaps inner 12 +gaps outer 0 + +# startup applications +exec /run/wrappers/bin/gnome-keyring-daemon --start --daemonize +exec dbus-sway-environment +exec configure-gtk +exec nm-applet --indicator +exec alacritty -t alacritty-sway +exec signal-desktop +exec firefox --name=social -P social +exec thunderbird +exec firefox +exec nextcloud +exec owncloud +exec swayidle \ + before-sleep 'loginctl lock-session $XDG_SESSION_ID' \ + lock 'swaylock --image ~/.wallpaper.jpg' \ + timeout 180 'swaylock --screenshots --effect-blur 7x5' \ + timeout 1800 'systemctl suspend' +exec dunst +#exec --no-startup-id swaybg -c "#000000" -m fill -i ~/.config/wallpaper/wot.jpg +# exec --no-startup-id gnome-keyring-daemon --start --components=pkcs11,secrets,ssh +exec 'sleep 2; swaymsg workspace $ws8; swaymsg layout tabbed' + +# wallpaper +output eDP-1 bg ~/.wallpaper.jpg fill +output DP-4 bg ~/.wallpaper.jpg fill +output DP-5 bg ~/.wallpaper.jpg fill + +input * xkb_layout "de" +input * xkb_variant "colemak,,typewriter" +input * xkb_options "grp:win_space_toggle" +input "MANUFACTURER1 Keyboard" xkb_model "pc101" + +# notebook +set $laptop eDP-1 +# bindswitch --reload --locked lid:on output $laptop disable +# bindswitch --reload lid:off output $laptop enable + +# A lock command used in several places +set $lock_script swaylock + +# A sleep command used in several places. +# We leave a bit of time for locking to happen before putting the system to sleep +set $sleep $lock_script && sleep 3 && systemctl suspend + +# Triggers a short notification +set $notify dunstify --timeout 1500 + +# Set your laptop screen name +set $laptop_screen 'eDP-1' + +# Clamshell mode or lock & sleep +# This is a if/else statement: [ outputs_count == 1 ] && true || false +#bindswitch --reload --locked lid:on exec '[ $(swaymsg -t get_outputs | grep name | wc -l) == 1 ] && ($sleep) || ($notify "Clamshell mode" "Laptop screen off" && swaymsg output $laptop_screen disable)' + +#bindswitch --reload --locked lid:off output $laptop_screen enable + +# disable xwayland +#xwayland disable + +# Touchpad +input type:touchpad { +tap enabled +natural_scroll enabled +} + diff --git a/utils/modules/sway/sway.nix b/utils/modules/sway/sway.nix new file mode 100644 index 0000000..e087448 --- /dev/null +++ b/utils/modules/sway/sway.nix @@ -0,0 +1,137 @@ +{ config, pkgs, lib, ... }: + +let + dbus-sway-environment = pkgs.writeTextFile { + name = "dbus-sway-environment"; + destination = "/bin/dbus-sway-environment"; + executable = true; + + text = '' + dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP=sway + systemctl --user stop pipewire pipewire-media-session xdg-desktop-portal xdg-desktop-portal-wlr + systemctl --user start pipewire pipewire-media-session xdg-desktop-portal xdg-desktop-portal-wlr + ''; + }; + +in { + imports = [ + ./social.nix + ./signal-work.nix + ./thunderbird.nix + ./parsec.nix + ]; + + hardware.pulseaudio.enable = false; + + services.xserver = { + enable = true; + excludePackages = [ pkgs.xterm ]; + displayManager.gdm.enable = true; + displayManager.gdm.wayland = true; + # displayManager.sddm.enable = true; + displayManager.sessionPackages = [ pkgs.sway ]; + displayManager.defaultSession = "sway"; + libinput.enable = true; + desktopManager.gnome = { + enable = true; + extraGSettingsOverrides = '' + [org.gnome.desktop.interface] + gtk-theme='Dracula' + ''; + }; + }; + + services.teamviewer.enable = true; + services.gnome.gnome-keyring.enable = true; + + environment.systemPackages = with pkgs; [ + alsaUtils + alacritty + bitwarden + chromium + cryptomator + dbeaver + dbus-sway-environment + dracula-theme + gcc + git + glib + gimp + gnome.seahorse + gnome3.adwaita-icon-theme + grim + jmeter + libreoffice + mako + networkmanagerapplet + nextcloud-client + obs-studio + pavucontrol + pcmanfm + pinentry + rbw + rofi-rbw + rustdesk + slurp + sway + sway-launcher-desktop + swayidle + swaylock + # thunderbird + tor-browser-bundle-bin + unzip + vlc + waybar + wayland + wl-clipboard + wofi + wtype + apache-directory-studio + firefox + + + onlyoffice-bin + # kexi + ]; + + # nixpkgs.config.permittedInsecurePackages = [ + # "qtwebkit-5.212.0-alpha4" + # "electron-13.6.9" + # ]; + # nixpkgs.config.allowBroken = true; + + programs._1password-gui = { + enable = true; + polkitPolicyOwners = [ "dominik" ]; + }; + + programs.light.enable = true; + + fonts.fonts = with pkgs; [ + noto-fonts + noto-fonts-cjk + noto-fonts-emoji + nerdfonts + ]; + + security.rtkit.enable = true; + services.pipewire = { + enable = true; + alsa.enable = true; + alsa.support32Bit = true; + pulse.enable = true; + jack.enable = true; + }; + + programs.sway = { + enable = true; + wrapperFeatures.gtk = true; + }; + + environment.etc = { + "sway/config".source = "/etc/nixos/modules/sway/sway.conf"; + "wofi/style.css".source = "/etc/nixos/modules/sway/wofi.css"; + "xdg/waybar/config".source = "/etc/nixos/modules/sway/waybar.conf"; + "xdg/waybar/style.css".source = "/etc/nixos/modules/sway/waybar.css"; + }; +} diff --git a/utils/modules/sway/thunderbird.nix b/utils/modules/sway/thunderbird.nix new file mode 100644 index 0000000..88210d0 --- /dev/null +++ b/utils/modules/sway/thunderbird.nix @@ -0,0 +1,39 @@ + +{ pkgs, ... }: +let + thunderbirdWorkDesktopItem = pkgs.makeDesktopItem { + name = "thunderbird-work"; + desktopName = "Thunderbird Work"; + icon = "thunderbird"; + exec = "thunderbird -P Work"; + }; + thunderbirdCloonarDesktopItem = pkgs.makeDesktopItem { + name = "thunderbird-cloonar"; + desktopName = "Thunderbird Cloonar"; + icon = "thunderbird"; + exec = "thunderbird -P Cloonar"; + }; +in +{ + + nixpkgs.overlays = [ + (self: super: + { + thunderbird-bin-unwrapped = super.thunderbird-bin-unwrapped.overrideAttrs ( old: rec { + version = "112.0b7"; + name = "thunderbird-bin"; + src = super.fetchurl { + url = "https://download-installer.cdn.mozilla.net/pub/thunderbird/releases/${version}/linux-x86_64/en-US/thunderbird-${version}.tar.bz2"; + sha256 = "30d23df34834096a79261439d5ea6d78d44921f0218893f100596ee6296cd806"; + }; + }); + } + ) + ]; + environment.systemPackages = [ + # (import ../../pkgs/thunderbird.nix) + pkgs.thunderbird-bin-unwrapped + thunderbirdWorkDesktopItem + thunderbirdCloonarDesktopItem + ]; +} diff --git a/utils/modules/sway/waybar.conf b/utils/modules/sway/waybar.conf new file mode 100644 index 0000000..504fae6 --- /dev/null +++ b/utils/modules/sway/waybar.conf @@ -0,0 +1,132 @@ +// -*- mode: json -*- + +{ +"layer": "top", +"position": "top", + +"modules-left": [ +"sway/workspaces", +"custom/right-arrow-dark" +], +"modules-center": [ +"custom/left-arrow-dark", +"clock#1", +"custom/left-arrow-light", +"custom/left-arrow-dark", +"clock#2", +"custom/right-arrow-dark", +"custom/right-arrow-light", +"clock#3", +"custom/right-arrow-dark" +], +"modules-right": [ +"custom/left-arrow-dark", +"network", +"custom/left-arrow-light", +"custom/left-arrow-dark", +"pulseaudio", +"custom/left-arrow-light", +"custom/left-arrow-dark", +"memory", +"custom/left-arrow-light", +"custom/left-arrow-dark", +"cpu", +"custom/left-arrow-light", +"custom/left-arrow-dark", +"battery", +"custom/left-arrow-light", +"custom/left-arrow-dark", +"disk", +"custom/left-arrow-light", +"custom/left-arrow-dark", +"tray" +], + +"custom/left-arrow-dark": { +"format": "", +"tooltip": false +}, +"custom/left-arrow-light": { +"format": "", +"tooltip": false +}, +"custom/right-arrow-dark": { +"format": "", +"tooltip": false +}, +"custom/right-arrow-light": { +"format": "", +"tooltip": false +}, + +"sway/workspaces": { +"disable-scroll": true, +"format": "{name}" +}, + +"clock#1": { +"format": "{:%a}", +"tooltip": false +}, +"clock#2": { +"format": "{:%H:%M}", +"tooltip": false +}, +"clock#3": { +"format": "{:%d.%m}", +"tooltip": false +}, + + "pulseaudio": { + "format": "{icon}{volume}%", + "format-muted": "ﱝ", + "format-icons": { + "phone": ["奄", "奔", "墳"], + "default": ["奄", "奔", "墳"] + }, + "scroll-step": 5, + "on-click": "pavucontrol", + "tooltip": false + }, + "network": { + // "interface": "wlp2*", // (Optional) To force the use of this interface + "format-wifi": "{essid} ({signalStrength}%) ", + "format-ethernet": "{ipaddr}/{cidr} ", + "tooltip-format": "{ifname} via {gwaddr} ", + "format-linked": "{ifname} (No IP) ", + "format-disconnected": "Disconnected ", + "format-alt": "{ifname}: {ipaddr}/{cidr}" + }, +"memory": { +"interval": 5, +"format": "{}%" +}, +"cpu": { +"interval": 5, +"format": "﬙{usage:2}%" +}, +"battery": { +"states": { +"good": 95, +"warning": 30, +"critical": 15 +}, +"format": "{icon} {capacity}%", +"format-icons": [ +"", +"", +"", +"", +"" +] +}, +"disk": { +"interval": 5, +"format": "{percentage_used:2}%", +"path": "/home/" +}, +"tray": { +"icon-size": 20 +} +} + diff --git a/utils/modules/sway/waybar.css b/utils/modules/sway/waybar.css new file mode 100644 index 0000000..c927754 --- /dev/null +++ b/utils/modules/sway/waybar.css @@ -0,0 +1,79 @@ +* { + font-size: 20px; + font-family: monospace; +} + +window#waybar { + background: #282a36; + color: #f8f8f2; +} + +#custom-right-arrow-dark, +#custom-left-arrow-dark { + color: #252525; +} +#custom-right-arrow-light, +#custom-left-arrow-light { + color: #282a36; + background: #252525; +} + +#workspaces, +#clock.1, +#clock.2, +#clock.3, +#network, +#pulseaudio, +#memory, +#cpu, +#battery, +#disk, +#tray { + background: #252525; +} + +#workspaces button { + padding: 0 2px; + color: #f8f8f2; +} +#workspaces button.focused { + color: #50fa7b; +} +#workspaces button:hover { + box-shadow: inherit; + text-shadow: inherit; +} +#workspaces button:hover { + background: #252525; + border: #252525; + padding: 0 3px; +} + +#network { + color: #8be9fd; +} +#pulseaudio { + color: #6272a4; +} +#memory { + color: #f8f8f2; +} +#cpu { + color: #bd93f9; +} +#battery { + color: #f1fa8c; +} +#disk { + color: #ffb86c; +} + +#clock, +#network, +#pulseaudio, +#memory, +#cpu, +#battery, +#disk { + padding: 0 10px; +} diff --git a/utils/modules/sway/wofi.css b/utils/modules/sway/wofi.css new file mode 100644 index 0000000..322997f --- /dev/null +++ b/utils/modules/sway/wofi.css @@ -0,0 +1,40 @@ +window { + margin: 0px; + border: 1px solid #bd93f9; + background-color: #282a36; +} + +#input { + margin: 5px; + border: none; + color: #f8f8f2; + background-color: #44475a; +} + +#inner-box { + margin: 5px; + border: none; + background-color: #282a36; +} + +#outer-box { + margin: 5px; + border: none; + background-color: #282a36; +} + +#scroll { + margin: 0px; + border: none; +} + +#text { + margin: 5px; + border: none; + color: #f8f8f2; +} + +#entry:selected { + background-color: #44475a; +} + diff --git a/utils/modules/tang.nix b/utils/modules/tang.nix new file mode 100644 index 0000000..99ea3c6 --- /dev/null +++ b/utils/modules/tang.nix @@ -0,0 +1,81 @@ +{ config, pkgs, ... }: +let + user = "tang"; + group = "tang"; +in { + + environment.systemPackages = with pkgs; [ + jose + tang + ]; + + systemd.paths.tangd-update = { + pathConfig = { + PathChanged = "/var/db/tang"; + MakeDirectory = true; + DirectoryMode = "0700"; + }; + }; + + systemd.services.tangd-update = { + description = "Tang update"; + path = [ pkgs.jose ]; + serviceConfig = { + Type = "oneshot"; + StandardError = "journal"; + ExecStart = "${pkgs.tang}/libexec/tangd-update /var/db/tang /var/cache/tang"; + }; + }; + + systemd.services.tangd-keygen = { + description = "Tang keygen"; + documentation = [ "man:tang(8)" ]; + path = [ pkgs.jose ]; + serviceConfig = { + Type = "oneshot"; + StandardError = "journal"; + ExecStart = "${pkgs.tang}/libexec/tangd-keygen /var/db/tang"; + }; + }; + + systemd.services."tangd@" = { + description = "Tang Server"; + documentation = [ "man:tang(8)" ]; + path = [ pkgs.jose ]; + serviceConfig = { + + StandardInput = "socket"; + StandardOutput = "socket"; + StandardError = "journal"; + ExecStart = "${pkgs.tang}/libexec/tangd /var/cache/tang"; + }; + }; + + systemd.sockets.tangd = { + description = "Tang Server socket"; + documentation = [ "man:tang(8)" ]; + requires = [ + "tangd-keygen.service" + "tangd-update.service" + "tangd-update.path" + ]; + after = [ + "tangd-keygen.service" + "tangd-update.service" + ]; + wantedBy = [ "multi-user.target" ]; + socketConfig = { + ListenStream = 8000; + Accept = true; + }; + }; + + # users.groups.tang = {}; + # users.users.tang = { + # isSystemUser = true; + # group = "tang"; + # home = "/var/db/tang"; + # createHome = true; + # description = "Tang system user"; + # }; +} diff --git a/utils/modules/tuxedo.nix b/utils/modules/tuxedo.nix new file mode 100644 index 0000000..c1da746 --- /dev/null +++ b/utils/modules/tuxedo.nix @@ -0,0 +1,13 @@ +{ config, pkgs, ... }: +let + tuxedo = import (builtins.fetchTarball "https://github.com/blitz/tuxedo-nixos/archive/master.tar.gz"); +in { + + # ... + + imports = [ + tuxedo.module + ]; + + hardware.tuxedo-control-center.enable = true; +} diff --git a/utils/modules/vpn/epicenter.works/default.nix b/utils/modules/vpn/epicenter.works/default.nix new file mode 100644 index 0000000..be0fd7d --- /dev/null +++ b/utils/modules/vpn/epicenter.works/default.nix @@ -0,0 +1,19 @@ +{ pkgs, ... }: + +{ + services.resolved.enable = true; + services.openvpn.servers = { + epicenterWorks = { + config = '' + config /etc/nixos/modules/vpn/epicenter.works/vpn.conf + + script-security 2 + up ${pkgs.update-systemd-resolved}/libexec/openvpn/update-systemd-resolved + up-restart + down ${pkgs.update-systemd-resolved}/libexec/openvpn/update-systemd-resolved + down-pre + ''; + }; + }; +} + diff --git a/utils/modules/vpn/epicenter.works/hetzner.nix b/utils/modules/vpn/epicenter.works/hetzner.nix new file mode 100644 index 0000000..666be79 --- /dev/null +++ b/utils/modules/vpn/epicenter.works/hetzner.nix @@ -0,0 +1,40 @@ +{ pkgs, ... }: + +{ + networking.firewall = { + allowedUDPPorts = [ 51820 ]; # Clients and peers can use the same port, see listenport + }; + # Enable WireGuard + networking.wireguard.interfaces = { + # "wg0" is the network interface name. You can name the interface arbitrarily. + wg0 = { + # Determines the IP address and subnet of the client's end of the tunnel interface. + ips = [ "10.50.60.6/24" ]; + listenPort = 51820; # to match firewall allowedUDPPorts (without this wg uses random port numbers) + + privateKeyFile = "/run/secrets/wg_private_key"; + + postSetup = ''printf "search epicenter.works\nnameserver 10.25.0.10" | ${pkgs.openresolv}/bin/resolvconf -a wg0 -m 0''; + + postShutdown = "${pkgs.openresolv}/bin/resolvconf -d wg0"; + + + peers = [ + # For a client configuration, one peer entry for the server will suffice. + { + # Public key of the server (not a file path). + publicKey = "T7jPGSapSudtKyWwi2nu+2hjjse96I4U3lccRHZWd2s="; + presharedKeyFile = "/run/secrets/wg_preshared_key"; + + allowedIPs = [ "10.50.60.0/24" "10.25.0.0/24" ]; + + # Set this to the server IP and port. + endpoint = "5.9.131.17:51821"; # ToDo: route to endpoint not automatically configured https://wiki.archlinux.org/index.php/WireGuard#Loop_routing https://discourse.nixos.org/t/solved-minimal-firewall-setup-for-wireguard-client/7577 + + # Send keepalives every 25 seconds. Important to keep NAT tables alive. + persistentKeepalive = 25; + } + ]; + }; + }; +} diff --git a/utils/modules/vpn/epicenter.works/vpn.conf b/utils/modules/vpn/epicenter.works/vpn.conf new file mode 100644 index 0000000..c224107 --- /dev/null +++ b/utils/modules/vpn/epicenter.works/vpn.conf @@ -0,0 +1,14 @@ +dev tun +persist-tun +persist-key +cipher AES-128-GCM +auth RSA-SHA256 +client +resolv-retry infinite +remote vpn.epicenter.works 1195 udp +lport 0 +verify-x509-name "C=AT, ST=Vienna, L=Vienna, O=epicenter_works, emailAddress=team@epicenter.works, CN=epicenter.works VPN Server" subject +remote-cert-tls server +ca /run/secrets/epicenter_vpn_ca +cert /run/secrets/epicenter_vpn_cert +key /run/secrets/epicenter_vpn_key diff --git a/utils/modules/web/cloonar.dev.nix b/utils/modules/web/cloonar.dev.nix new file mode 100644 index 0000000..1038a5f --- /dev/null +++ b/utils/modules/web/cloonar.dev.nix @@ -0,0 +1,135 @@ +{ pkgs, lib, config, ... }: +let + domain = "cloonar.dev"; + dataDir = "/var/www/${domain}"; +in { + systemd.services."phpfpm-${domain}".serviceConfig.ProtectHome = lib.mkForce false; + + services.phpfpm.pools."${domain}" = { + user = domain; + settings = { + "listen.owner" = config.services.nginx.user; + "pm" = "dynamic"; + "pm.max_children" = 32; + "pm.max_requests" = 500; + "pm.start_servers" = 2; + "pm.min_spare_servers" = 2; + "pm.max_spare_servers" = 5; + "php_admin_value[error_log]" = "stderr"; + "php_admin_flag[log_errors]" = true; + "catch_workers_output" = true; + "access.log" = "/var/log/$pool.access.log"; + }; + phpPackage = pkgs.php81; + phpEnv."PATH" = lib.makeBinPath [ pkgs.php81 ]; + }; + + services.nginx.virtualHosts."${domain}" = { + forceSSL = true; + enableACME = true; + acmeRoot = null; + root = "${dataDir}"; + + locations."/favicon.ico".extraConfig = '' + log_not_found off; + access_log off; + ''; + + # TYPO3 - Rule for versioned static files, configured through: + # - $GLOBALS['TYPO3_CONF_VARS']['BE']['versionNumberInFilename'] + # - $GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename'] + + extraConfig = '' + if (!-e $request_filename) { + rewrite ^/(.+)\.(\d+)\.(php|js|css|png|jpg|gif|gzip)$ /$1.$3 last; + } + ''; + + # TYPO3 - Block access to composer files + locations."~* composer\.(?:json|lock)".extraConfig = '' + deny all; + ''; + + + # TYPO3 - Block access to flexform files + locations."~* flexform[^.]*\.xml".extraConfig = '' + deny all; + ''; + + # TYPO3 - Block access to language files + locations."~* locallang[^.]*\.(?:xml|xlf)$".extraConfig = '' + deny all; + ''; + + # TYPO3 - Block access to static typoscript files + locations."~* ext_conf_template\.txt|ext_typoscript_constants\.txt|ext_typoscript_setup\.txt".extraConfig = '' + deny all; + ''; + + # TYPO3 - Block access to miscellaneous protected files + locations."~* /.*\.(?:bak|co?nf|cfg|ya?ml|ts|typoscript|tsconfig|dist|fla|in[ci]|log|sh|sql|sqlite)$".extraConfig = '' + deny all; + ''; + + # TYPO3 - Block access to recycler and temporary directories + locations."~ _(?:recycler|temp)_/".extraConfig = '' + deny all; + ''; + + # TYPO3 - Block access to configuration files stored in fileadmin + locations."~ fileadmin/(?:templates)/.*\.(?:txt|ts|typoscript)$".extraConfig = '' + deny all; + ''; + + + # TYPO3 - Block access to libraries, source and temporary compiled data + locations."~ ^(?:vendor|typo3_src|typo3temp/var)".extraConfig = '' + deny all; + ''; + + + # TYPO3 - Block access to protected extension directories + locations."~ (?:typo3conf/ext|typo3/sysext|typo3/ext)/[^/]+/(?:Configuration|Resources/Private|Tests?|Documentation|docs?)/".extraConfig = '' + deny all; + ''; + + locations."/".extraConfig = '' + index index.php index.html; + try_files $uri $uri/ /index.php$is_args$args; + ''; + + # TYPO3 Backend URLs + locations."/typo3".extraConfig = '' + rewrite ^ /typo3/; + ''; + + locations."/typo3/".extraConfig = '' + try_files $uri /typo3/index.php$is_args$args; + ''; + + locations."~ [^/]\.php(/|$)".extraConfig = '' + fastcgi_split_path_info ^(.+?\.php)(/.*)$; + if (!-f $document_root$fastcgi_script_name) { + return 404; + } + include ${pkgs.nginx}/conf/fastcgi_params; + include ${pkgs.nginx}/conf/fastcgi.conf; + fastcgi_buffer_size 32k; + fastcgi_buffers 8 16k; + fastcgi_connect_timeout 240s; + fastcgi_read_timeout 240s; + fastcgi_send_timeout 240s; + fastcgi_pass unix:${config.services.phpfpm.pools."${domain}".socket}; + fastcgi_index index.php; + ''; + }; + users.users."${domain}" = { + isSystemUser = true; + createHome = true; + home = dataDir; + homeMode= "770"; + #home = "/home/${domain}"; + group = "nginx"; + }; + users.groups.${domain} = {}; +} diff --git a/utils/modules/web/default.nix b/utils/modules/web/default.nix new file mode 100644 index 0000000..b3075f1 --- /dev/null +++ b/utils/modules/web/default.nix @@ -0,0 +1,56 @@ +{ config, pkgs, ... }: + +{ + + imports = [ + ./cloonar.dev.nix + ./diabetes-austria.cloonar.dev.nix + ]; + + environment.systemPackages = with pkgs; [ + imagemagick + ghostscript + ]; + + systemd.services.nginx.serviceConfig.ProtectHome = "read-only"; + + services.nginx = { + enable = true; + + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + + sslCiphers = "AES256+EECDH:AES256+EDH:!aNULL"; + + commonHttpConfig = '' + # Add HSTS header with preloading to HTTPS requests. + # Adding this header to HTTP requests is discouraged + map $scheme $hsts_header { + https "max-age=31536000; includeSubdomains; preload"; + } + add_header Strict-Transport-Security $hsts_header; + + # Enable CSP for your services. + #add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always; + + # Minimize information leaked to other domains + add_header 'Referrer-Policy' 'origin-when-cross-origin'; + + # Disable embedding as a frame + add_header X-Frame-Options DENY; + + # Prevent injection of code in other mime types (XSS Attacks) + add_header X-Content-Type-Options nosniff; + + # Enable XSS protection of the browser. + # May be unnecessary when CSP is configured properly (see above) + add_header X-XSS-Protection "1; mode=block"; + + # This might create errors + proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; + ''; + }; + +} diff --git a/utils/modules/web/diabetes-austria.cloonar.dev.nix b/utils/modules/web/diabetes-austria.cloonar.dev.nix new file mode 100644 index 0000000..bdf513d --- /dev/null +++ b/utils/modules/web/diabetes-austria.cloonar.dev.nix @@ -0,0 +1,135 @@ +{ pkgs, lib, config, ... }: +let + domain = "diabetes-austria.cloonar.dev"; + dataDir = "/var/www/${domain}"; +in { + systemd.services."phpfpm-${domain}".serviceConfig.ProtectHome = lib.mkForce false; + + services.phpfpm.pools."${domain}" = { + user = domain; + settings = { + "listen.owner" = config.services.nginx.user; + "pm" = "dynamic"; + "pm.max_children" = 32; + "pm.max_requests" = 500; + "pm.start_servers" = 2; + "pm.min_spare_servers" = 2; + "pm.max_spare_servers" = 5; + "php_admin_value[error_log]" = "stderr"; + "php_admin_flag[log_errors]" = true; + "catch_workers_output" = true; + "access.log" = "/var/log/$pool.access.log"; + }; + phpPackage = pkgs.php81; + phpEnv."PATH" = lib.makeBinPath [ pkgs.php81 ]; + }; + + services.nginx.virtualHosts."${domain}" = { + forceSSL = true; + enableACME = true; + acmeRoot = null; + root = "${dataDir}"; + + locations."/favicon.ico".extraConfig = '' + log_not_found off; + access_log off; + ''; + + # TYPO3 - Rule for versioned static files, configured through: + # - $GLOBALS['TYPO3_CONF_VARS']['BE']['versionNumberInFilename'] + # - $GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename'] + + extraConfig = '' + if (!-e $request_filename) { + rewrite ^/(.+)\.(\d+)\.(php|js|css|png|jpg|gif|gzip)$ /$1.$3 last; + } + ''; + + # TYPO3 - Block access to composer files + locations."~* composer\.(?:json|lock)".extraConfig = '' + deny all; + ''; + + + # TYPO3 - Block access to flexform files + locations."~* flexform[^.]*\.xml".extraConfig = '' + deny all; + ''; + + # TYPO3 - Block access to language files + locations."~* locallang[^.]*\.(?:xml|xlf)$".extraConfig = '' + deny all; + ''; + + # TYPO3 - Block access to static typoscript files + locations."~* ext_conf_template\.txt|ext_typoscript_constants\.txt|ext_typoscript_setup\.txt".extraConfig = '' + deny all; + ''; + + # TYPO3 - Block access to miscellaneous protected files + locations."~* /.*\.(?:bak|co?nf|cfg|ya?ml|ts|typoscript|tsconfig|dist|fla|in[ci]|log|sh|sql|sqlite)$".extraConfig = '' + deny all; + ''; + + # TYPO3 - Block access to recycler and temporary directories + locations."~ _(?:recycler|temp)_/".extraConfig = '' + deny all; + ''; + + # TYPO3 - Block access to configuration files stored in fileadmin + locations."~ fileadmin/(?:templates)/.*\.(?:txt|ts|typoscript)$".extraConfig = '' + deny all; + ''; + + + # TYPO3 - Block access to libraries, source and temporary compiled data + locations."~ ^(?:vendor|typo3_src|typo3temp/var)".extraConfig = '' + deny all; + ''; + + + # TYPO3 - Block access to protected extension directories + locations."~ (?:typo3conf/ext|typo3/sysext|typo3/ext)/[^/]+/(?:Configuration|Resources/Private|Tests?|Documentation|docs?)/".extraConfig = '' + deny all; + ''; + + locations."/".extraConfig = '' + index index.php index.html; + try_files $uri $uri/ /index.php$is_args$args; + ''; + + # TYPO3 Backend URLs + locations."/typo3".extraConfig = '' + rewrite ^ /typo3/; + ''; + + locations."/typo3/".extraConfig = '' + try_files $uri /typo3/index.php$is_args$args; + ''; + + locations."~ [^/]\.php(/|$)".extraConfig = '' + fastcgi_split_path_info ^(.+?\.php)(/.*)$; + if (!-f $document_root$fastcgi_script_name) { + return 404; + } + include ${pkgs.nginx}/conf/fastcgi_params; + include ${pkgs.nginx}/conf/fastcgi.conf; + fastcgi_buffer_size 32k; + fastcgi_buffers 8 16k; + fastcgi_connect_timeout 240s; + fastcgi_read_timeout 240s; + fastcgi_send_timeout 240s; + fastcgi_pass unix:${config.services.phpfpm.pools."${domain}".socket}; + fastcgi_index index.php; + ''; + }; + users.users."${domain}" = { + isSystemUser = true; + createHome = true; + home = dataDir; + homeMode= "770"; + #home = "/home/${domain}"; + group = "nginx"; + }; + users.groups.${domain} = {}; +} diff --git a/utils/modules/zammad/default.nix b/utils/modules/zammad/default.nix new file mode 100644 index 0000000..664af08 --- /dev/null +++ b/utils/modules/zammad/default.nix @@ -0,0 +1,51 @@ +{ config, pkgs, ... }: + +{ + services.zammad = { + enable = true; + port = 3010; + secretKeyBaseFile = config.sops.secrets.zammad-key-base.path; + database = { + createLocally = true; + }; + }; + + services.nginx.enable = true; + services.nginx.virtualHosts."support.cloonar.com" = { + forceSSL = true; + enableACME = true; + acmeRoot = null; + locations."/" = { + proxyPass = "http://127.0.0.1:3010"; + proxyWebsockets = true; + extraConfig = + "proxy_connect_timeout 300;" + + "proxy_send_timeout 300;" + + "proxy_read_timeout 300;" + + "send_timeout 300;" + ; + }; + locations."/ws" = { + proxyPass = "http://127.0.0.1:6042"; + proxyWebsockets = true; + extraConfig = + "proxy_read_timeout 86400;" + + "send_timeout 300;" + ; + }; + }; + + sops.secrets = { + zammad-db-password = { + sopsFile = ./secrets.yaml; + owner = "zammad"; + }; + zammad-key-base = { + sopsFile = ./secrets.yaml; + owner = "zammad"; + }; + }; + + services.postgresqlBackup.enable = true; + services.postgresqlBackup.databases = [ "zammad" ]; +} diff --git a/utils/modules/zammad/secrets.yaml b/utils/modules/zammad/secrets.yaml new file mode 100644 index 0000000..441d27a --- /dev/null +++ b/utils/modules/zammad/secrets.yaml @@ -0,0 +1,31 @@ +zammad-db-password: ENC[AES256_GCM,data:FFsTnwQcL8V1ZWvZ9a15FWcHnsrC7nuDW155reSmfg/IRhRfrtnvbCDQ0N3AMh7TBiyG3x5za/6orV04CplUgQ==,iv:inQXkwlTbGaKgU3nfOtIYMcheBdGv8xa7dCad8WrGEc=,tag:fxjNRCUpS6RMipk4D08new==,type:str] +zammad-key-base: ENC[AES256_GCM,data:z2v1GrjRFoaDY9tPaAsUJPVRHZhSOrXWCZhhm5E6rmH4s6QWU1EW7aY4PPgditdcathLRWkDlBT5c3SQ8Cd2DPLp/SVn9Xd8w8g/lrplhNC2sJXUyB+CUgdEnBBN0XPMsFWNx9EIrqGrF/A8js5eKtQON9fCNytaHMOsCCc0rNE=,iv:oHKiXE9U0h846XVpCrcD/dFJ1MAXCYrnM80CwaWgALc=,tag:W88DsRWvdudMscH+UBPy/Q==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age16veg3fmvpfm7a89a9fc8dvvsxmsthlm70nfxqspr6t8vnf9wkcwsvdq38d + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpK2E1R1ByNVdxNVJxR3NV + Smc1eHhPL2syb0RSQXFaNDlzdG9LaDh6YkIwClFQVFVJNHZ1V214NVBjZXlaYTJH + QkRRdSt5MkZGV2JKU2tvQUY5Um90Q1UKLS0tIEhjMnlwTURDbWtXMEFxSUVQQzln + VjVadHM1byt0M0ZtdVUzVjc3OEFLOXMKBJ1XviMhDv+QzxsIXRgkhts3yfjQ0aK4 + ADlt2DyQ3nJzmOZe9NzBUAAMzftdcOxDCc42kqOhJlWLwV25Nock9w== + -----END AGE ENCRYPTED FILE----- + - recipient: age1y6lvl5jkwc47p5ae9yz9j9kuwhy7rtttua5xhygrgmr7ehd49svsszyt42 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEOXpTZFdqcE9IaFBIYS9M + eU5jL1lVTjFtOWJXVjRVbE1XMkxNSzA0c2tzCkd5L285NDRXekE5ZHBvUGw2RHV0 + cmlTRm13MVhoRlc0S1YxSUpSandydUEKLS0tIC96V2pQVC9EL1pXdXdMTHhrVTBL + RUxmbjFEcEdmNzlaSERGdnBEM05CSXMKwENxkq1yu4TDiwFhDsk25pvcLjg3m4MR + qVgMbMx+kzROf9UpbArEjcVFJ9eGwo5f/fEsRDEf03VObWRiLTmucQ== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2022-11-29T10:54:56Z" + mac: ENC[AES256_GCM,data:OX49RTucGWdH1RkbXfkiMLH2Lj65v554WSfJxkCkIu/dFagCH90QSRiX/15HTsI//ffwKVurDivC6H6OByK2eWdaeCYTEn2029GjdL4RhJhXy0RLXEq5D/KVRu73O9Xe6M36asc/OenzPcmbHAvddD14y9vaOsVTL0H15ydVrwg=,iv:+uBt1Mvj+WMM4CvAOwmOXhZJVZBXVDCXA8iSXpdjktU=,tag:AeipsBJ8PA22OfUxXA8bIA==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.7.3