diff --git a/.gitignore b/.gitignore index c035cc9..4bf8bdd 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,5 @@ config.tf.json nixos-template # Possibly sensitive for public repo -lib/ips.nix -lib/constants.nix +# lib/ips.nix +# lib/constants.nix diff --git a/README.md b/README.md index 9b66f82..f5a9538 100644 --- a/README.md +++ b/README.md @@ -7,46 +7,63 @@ It's supposed to be used from an host where `nixos-rebuild` is available. - Uses `nix-community/generators` to build a LXC template with a base NixOS container - Uses `terranix` to build the infra definition and `opentofu` to deploy it on Proxmox - Uses `nixos-rebuild` to deploy the configuration on the container + - Uses `agenix` to avoid having too many secrets in clear My main objective was to have a "light" definition for the containers and to be able to use Nix to factorize configuration. -# Usage +Sadly as there is quite a bit of self/cross-references, I "had" to frequently split the configs in two definitions (which pretty much defeats my initial purpose). -## Prepare the infra constants -- `cp lib/constants.nix.template lib/constants.nix` -- adapt `lib/constants.nix` to match your needs -- touch `lib/ips.nix` -- remove both these files from `.gitignore` and `git add` them. +# Architecture -## Build NixOS template -- modify `lib/lxc-template.nix` as needed -- run `build-template` -- template available in `nixos-template/tarball/` -(.tar.xz to be uploaded to Proxmox) +- `config/_globals.nix` => Global values used in a lot of places +- `config/_ids.nix` => hostname -> container ID mapping +- `config/_keys.nix` => hostname -> public ssh key mapping (for secrets, built with ssh-keyscan {ip}) +- `config/_passwords.nix` => hostname -> db password... +- `containers/{name}.nix` => main definition of the container with my custom module +- `modules/containers.nix` => container module definition & evaluation +- `modules/tools.nix` => some utilities using global config etc. Also the main logic to convert a container ID to an IP +- `config/{name}-*.nix` => additional configuration if needed, imported with the whole config available for cross-references. +- `secrets/` => Agenix secrets -TODO Script the Proxmox Template upload if possible. +# My Stack -## Prepare Terraform -- create a user/role etc on Proxmox (see [the provider documentation](https://registry.terraform.io/providers/Telmate/proxmox/latest/docs)) -- `cp terraform.tfvars.example terraform.tfvars` -- edit `terraform.tfvars` to fill in values -- adapt the terraform base config as needed in `infra/main.nix` -- run `tofu init` +- DNS: AdGuardHome with Unbound +- Reverse Proxy: Traefik +- Auth: Authentik - mostly manual for now, I'd like to add Terraform... +- DB: Postgres +- Monitoring: Prometheus + Loki + Grafana and Alloy agents on hosts -## Adapt NixOS / Terraform modules building -- edit `lib/container_build.nix` to change how a container definition is translated to TF / NixOS config (in particular check the template name) +# Setting up -## Create containers definitions -- run `add-lxc [name] [id]` -- edit `lxc/#NAME#.nix` as needed -- run `build-terraform-json` -- run `tofu plan` and review the plan -- run `tofu apply`, hopefully without errors -- run `deploy-lxc #NAME#` +1. Build your `config/_globals.nix` using the example +2. Fill in `config/_ids.nix` - probably starting with your existing and relevant hosts/IP +3. Adapt `modules/tools.nix` to be relevant with your setup +4. Adapt `modules/lxc-template.nix` as needed +5. Run `build-template` (= `nix build .#lxc-template -o nixos-template`) to build the Proxmox LXC template. +6. Upload the template to Proxmox (TODO Try to automate this) +7. Create a user/role etc on Proxmox (see [the provider documentation](https://registry.terraform.io/providers/Telmate/proxmox/latest/docs)) +8. Create `terraform.tfvars` and adapt it with the relevant infos +9. Run `tofu init` -## Update container -- edit `lxc/#NAME#.nix` as needed -- if the container specs have changed, do all as above -- otherwise you can just run `deploy-lxc #NAME#` +Probably remove all files in `containers/` that you don't need, or move them to another folder. Another option is to set `my-lxc.[name].container.enable = false;`. +Also it is tailored for my use and was built iteratively so I guess if you want to replicate my setup you'll have to disable things to make it work and iteratively add them back. First I'd disable `logging.enable` in every `my-lxc` module. + +# Adding a container + +To add a container, the main workflow is: +- run `add-lxc {name} {id}` +- edit `containers/{name}.nix` + - adapt container specs + - setup NixOS config + - setup DB if needed + - enable logging, routing, auth, ... +- if needed, add additional definition in `config/{name}-{service}.nix` and reference it in `containers/{name}.nix` +- add needed secrets in `secrets/secrets.nix` and use `cd secrets && agenix -e {secret-name}.age` to create the encrypted files (see agenix doc) +- run `build-terraform-json` to build the Terraform definition +- run `tofu apply`, REVIEW the plan, and confirm if it's OK +- start the container on Proxmox (probably should add the option in terraform but I prefer to have control...) +- run `ssh-keyscan {ip}` and add the public SSH key to `config/_keys.nix` for the secrets decoding - run `cd secrets && agenix -r` to re-build the secrets for your new key +- run `deploy-lxc {name}` to deploy the NixOS setup to Proxmox +- if needed, run `deploy-lxc {proxy}` to enable routing to your new container diff --git a/config/_globals.nix.example b/config/_globals.nix.example new file mode 100644 index 0000000..899a5b0 --- /dev/null +++ b/config/_globals.nix.example @@ -0,0 +1,44 @@ +{ ... }: +{ + globals = { + ip_prefix = "10.0.0."; + cidr = 24; + gateway = "10.0.0.1"; + domains = { + internal = ".local"; + external = ".example.tld"; + }; + master = { + login = "me"; + email = "my@self"; + initial_htpasswd = "######"; + public_ssh_key = "ssh-ed25519 ############"; + }; + default_tz = "UTC"; + country_code = "eu"; + currency = "EUR"; + dns_provider = "cloudflare"; + + other_hosts = [ + { + hostname = "homeassistant"; + private = false; + auth = false; + addr = "http://10.0.0.42:8123/"; + } + { + hostname = "proxmox"; + private = true; + auth = false; + addr = "https://10.0.0.69:8006/"; + useCustomCA = true; + } + { + hostname = "nas"; + private = true; + auth = false; + addr = "http://10.0.0.99:5000/"; + } + ]; + }; +} diff --git a/config/_ids.nix.example b/config/_ids.nix.example new file mode 100644 index 0000000..df8e7c8 --- /dev/null +++ b/config/_ids.nix.example @@ -0,0 +1,17 @@ +{ ... }: +{ + id = { + dns = 1002; + proxy = 1003; + auth = 1004; + db = 1005; + metrics = 1006; + monitoring = 1007; + vault = 1008; + + nas = 99; + proxmox = 69; + homeassistant = 42; + camera = 200; + }; +} diff --git a/lib/config/alloy/config.alloy.nix b/config/alloy/config.alloy.nix similarity index 54% rename from lib/config/alloy/config.alloy.nix rename to config/alloy/config.alloy.nix index 45f1e50..59c29a8 100644 --- a/lib/config/alloy/config.alloy.nix +++ b/config/alloy/config.alloy.nix @@ -1,6 +1,7 @@ -let - infra = import ../../constants.nix; -in +{ + tools, + ... +}: { out = '' logging { @@ -8,7 +9,7 @@ in } loki.write "grafana_loki" { endpoint { - url = "http://${infra.loki_addr}/loki/api/v1/push" + url = "http://${tools.loki_addr}/loki/api/v1/push" } } ''; diff --git a/config/alloy/default-journal-logger.alloy.nix b/config/alloy/default-journal-logger.alloy.nix new file mode 100644 index 0000000..5cfb02f --- /dev/null +++ b/config/alloy/default-journal-logger.alloy.nix @@ -0,0 +1,44 @@ +{ + tools, + container, + service, + additional_stages, + ... +}: +let + hostname = tools.build_hostname container; + ip = tools.build_ip container; + prefix = "${container}_${service}"; +in +{ + out = '' + loki.relabel "${prefix}_journal" { + forward_to = [] + rule { + source_labels = ["__journal__priority_keyword"] + target_label = "level" + } + rule { + source_labels = ["__journal__SYSLOG_IDENTIFIER"] + target_label = "app" + } + } + loki.source.journal "${prefix}_journal_scrape" { + forward_to = [loki.process.${prefix}_router.receiver] + matches = "_SYSTEMD_UNIT=${service}.service" + relabel_rules = loki.relabel.${prefix}_journal.rules + labels = { + service = "${service}", + host = "${hostname}", + host_ip = "${ip}", + } + } + + loki.process "${prefix}_router" { + + ${additional_stages} + + forward_to = [loki.write.grafana_loki.receiver] + } + ''; +} diff --git a/config/alloy/matrix-mas.alloy.nix b/config/alloy/matrix-mas.alloy.nix new file mode 100644 index 0000000..17f93de --- /dev/null +++ b/config/alloy/matrix-mas.alloy.nix @@ -0,0 +1,51 @@ +{ ip, domainname, ... }: +{ + out = '' + loki.relabel "mas_journal" { + forward_to = [] + rule { + source_labels = ["__journal__priority_keyword"] + target_label = "level" + } + rule { + source_labels = ["__journal__SYSLOG_IDENTIFIER"] + target_label = "app" + } + } + loki.source.journal "mas_journal_scrape" { + forward_to = [loki.process.mas_router.receiver] + matches = "_SYSTEMD_UNIT=matrix-authentication-service.service" + relabel_rules = loki.relabel.mas_journal.rules + labels = { + service = "matrix-authentication-service", + host = "${domainname}", + host_ip = "${ip}", + } + } + + loki.process "mas_router" { + stage.regex { + expression = "^(?P\\S+) (?P\\S+) (?P\\S+) (?P\\S+) - (?P.*)$" + } + + stage.timestamp { + source = "timestamp" + format = "RFC3339Nano" + } + + stage.labels { + values = { + level = "", + facility = "", + worker = "", + } + } + + stage.output { + source = "message" + } + + forward_to = [loki.write.grafana_loki.receiver] + } + ''; +} diff --git a/lib/config/alloy/metrics.alloy.nix b/config/alloy/metrics.alloy.nix similarity index 83% rename from lib/config/alloy/metrics.alloy.nix rename to config/alloy/metrics.alloy.nix index b964439..9c5b21c 100644 --- a/lib/config/alloy/metrics.alloy.nix +++ b/config/alloy/metrics.alloy.nix @@ -1,7 +1,9 @@ -{ container_id, ... }: -let - infra = import ../../constants.nix; -in +{ + config, + tools, + container, + ... +}: { out = '' prometheus.exporter.unix "default" { @@ -33,14 +35,14 @@ in action = "replace" regex = "127\\.0\\.0\\.1" target_label = "instance" - replacement = "${infra.build_ip container_id}" + replacement = "${tools.build_ip container}" } forward_to = [prometheus.remote_write.metrics_service.receiver] } prometheus.remote_write "metrics_service" { endpoint { - url = "http://${infra.prometheus_addr}/api/v1/write" + url = "http://${tools.metrics_addr}/api/v1/write" } } ''; diff --git a/config/alloy/proxy-traefik.alloy.nix b/config/alloy/proxy-traefik.alloy.nix new file mode 100644 index 0000000..17691ee --- /dev/null +++ b/config/alloy/proxy-traefik.alloy.nix @@ -0,0 +1,54 @@ +{ config, tools, ... }: +let + hostname = tools.build_hostname "proxy"; + ip = tools.build_ip "proxy"; +in +{ + out = '' + loki.relabel "trf_journal" { + forward_to = [] + rule { + source_labels = ["__journal__priority_keyword"] + target_label = "level" + } + rule { + source_labels = ["__journal__SYSLOG_IDENTIFIER"] + target_label = "app" + } + } + + loki.source.journal "trf_journal_scrape" { + forward_to = [loki.process.trf_router.receiver] + matches = "_SYSTEMD_UNIT=traefik.service" + relabel_rules = loki.relabel.trf_journal.rules + labels = { + service = "traefik", + host = "${hostname}", + host_ip = "${ip}", + } + } + loki.process "trf_router" { + stage.regex { + expression = "^(?P\\S+) (?P\\w{3}) (?P.*)$" + } + stage.timestamp { + source = "datetime" + format = "2006-01-02 15:04:05-07:00" + } + stage.replace { + source = "level" + expression = "INF" + replace = "INFO" + } + stage.labels { + values = { + level = "level", + } + } + stage.output { + source = "message" + } + forward_to = [loki.write.grafana_loki.receiver] + } + ''; +} diff --git a/config/auth-authentik.nix b/config/auth-authentik.nix new file mode 100644 index 0000000..0930c0e --- /dev/null +++ b/config/auth-authentik.nix @@ -0,0 +1,30 @@ +{ tools, config, ... }: +let + hostname = tools.build_hostname "auth"; +in +{ + # Doesn't seem to like having the path directly in the params below?! + environment.etc = { + "authentik/ldap-secrets.env".source = config.age.secrets.auth-authentik-ldap-secrets.path; + "authentik/proxy-secrets.env".source = config.age.secrets.auth-authentik-proxy-secrets.path; + "authentik/secrets.env".source = config.age.secrets.auth-authentik-secrets.path; + }; + services = { + authentik = { + enable = true; + environmentFile = "/etc/authentik/secrets.env"; + nginx = { + enable = true; + host = hostname; + }; + }; + authentik-ldap = { + enable = true; + environmentFile = "/etc/authentik/ldap-secrets.env"; + }; + authentik-proxy = { + enable = true; + environmentFile = "/etc/authentik/proxy-secrets.env"; + }; + }; +} diff --git a/config/db-postgres.nix b/config/db-postgres.nix new file mode 100644 index 0000000..1f31774 --- /dev/null +++ b/config/db-postgres.nix @@ -0,0 +1,18 @@ +{ + pkgs, + config, + tools, + ... +}: +{ + services.postgresql = { + enable = true; + enableTCPIP = true; + package = pkgs.postgresql_18; + authentication = '' + host all all ${tools.mask_cidr} md5 + ''; + checkConfig = true; + initialScript = config.age.secrets.db-postgres-initscript.path; + }; +} diff --git a/config/dns-adguardhome.nix b/config/dns-adguardhome.nix new file mode 100644 index 0000000..7440437 --- /dev/null +++ b/config/dns-adguardhome.nix @@ -0,0 +1,107 @@ +{ + config, + tools, + pkgs, + ... +}: +let + lib = pkgs.lib; + master_login = config.globals.master.login; + master_pass = config.globals.master.initial_htpasswd; + ip = tools.build_ip; + proxy_addr = ip "proxy"; + domain_ext = config.globals.domains.external; + domain_int = config.globals.domains.internal; +in +{ + environment.etc."alloy/logs-adguardhome.alloy".text = + (import ./alloy/default-journal-logger.alloy.nix { + inherit tools; + container = "dns"; + service = "adguardhome"; + additional_stages = '' + stage.regex { + expression = "^(?P\\S+ \\S+) \\[(?P\\w+)\\] (?P.*)$" + } + + stage.timestamp { + source = "timestamp" + format = "2006-01-02 15:04:05" + } + + stage.labels { + values = { + level = "level", + } + } + + stage.output { + source = "message" + } + ''; + }).out; + services.adguardhome = { + enable = true; + host = "0.0.0.0"; + port = 80; + openFirewall = true; + mutableSettings = true; # ?? + settings = { + http = { + address = "0.0.0.0:80"; + session_ttl = "720h"; + }; + users = [ + { + name = master_login; + password = master_pass; + } + ]; + filters = [ + { + enabled = true; + url = "https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt"; + name = "AdGuard DNS filter"; + id = 1; + } + { + enabled = true; + url = "https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt"; + name = "AdAway Default Blocklist"; + id = 2; + } + ]; + + auth_attempts = 5; + block_auth_min = 15; + language = "fr"; + dns = { + bind_hosts = [ "0.0.0.0" ]; + port = 53; + upstream_dns = [ + "127.0.0.1:5335" + "https://dns10.quad9.net/dns-query" + ]; + trusted_proxies = [ + "127.0.0.0/8" + "::1/128" + proxy_addr + ]; + }; + filtering = { + safe_search.enabled = false; + blocking_mode = "nxdomain"; + rewrites = [ + { + domain = "*${domain_ext}"; + answer = proxy_addr; + } + ] + ++ (lib.mapAttrsToList (d: id: { + domain = "${d}${domain_int}"; + answer = "${ip d}"; + }) config.id); + }; + }; + }; +} diff --git a/config/dns-unbound.nix b/config/dns-unbound.nix new file mode 100644 index 0000000..d8ec63a --- /dev/null +++ b/config/dns-unbound.nix @@ -0,0 +1,63 @@ +{ + config, + tools, + ... +}: +let + mask_cidr = tools.mask_cidr; # build_ip_cidr 0 config.globals.cidr; +in +{ + environment.etc."alloy/logs-adguardhome.alloy".text = + (import ./alloy/default-journal-logger.alloy.nix { + inherit tools; + container = "dns"; + service = "unbound"; + additional_stages = '' + stage.pattern { + pattern = "[<_>] : " + } + + stage.labels { + values = { + level = "level", + } + } + + stage.output { + source = "message" + } + ''; + }).out; + services.unbound = { + enable = true; + settings = { + remote-control = { + control-enable = true; + control-interface = "/run/unbound/unbound.ctl"; + }; + server = { + auto-trust-anchor-file = "/var/lib/unbound/root.key"; + interface = "0.0.0.0"; + port = "5335"; + hide-identity = true; + hide-version = true; + harden-referral-path = true; + cache-min-ttl = 300; + cache-max-ttl = 14400; + serve-expired = true; + serve-expired-ttl = 3600; + prefetch = true; + prefetch-key = true; + private-address = [ + mask_cidr + ]; + do-ip6 = false; + so-sndbuf = 0; + access-control = [ + "${mask_cidr} allow" + "127.0.0.1/32 allow" + ]; + }; + }; + }; +} diff --git a/config/finances-fireflyiii.nix b/config/finances-fireflyiii.nix new file mode 100644 index 0000000..94729b4 --- /dev/null +++ b/config/finances-fireflyiii.nix @@ -0,0 +1,29 @@ +{ + config, + tools, + ... +}: +let + name = "finances"; + hostname = tools.build_hostname name; + ip = tools.build_ip name; +in +{ + services.firefly-iii = { + enable = true; + enableNginx = true; + settings = { + SITE_OWNER = config.globals.master.email; + DB_CONNECTION = "pgsql"; + DB_HOST = ip; + DB_PORT = 5432; + DB_DATABASE = hostname; + DB_USERNAME = hostname; + DB_PASSWORD = config.my-lxc.finances.db.password; + AUTHENTICATION_GUARD = "remote_user_guard"; + AUTHENTICATION_GUARD_HEADER = "HTTP_REMOTE_EMAIL"; + AUTHENTICATION_GUARD_EMAIL = "HTTP_REMOTE_EMAIL"; + APP_KEY_FILE = config.age.secrets.finances-app-key.path; + }; + }; +} diff --git a/config/frigate-frigate.nix b/config/frigate-frigate.nix new file mode 100644 index 0000000..b51c6fd --- /dev/null +++ b/config/frigate-frigate.nix @@ -0,0 +1,53 @@ +{ config, tools, ... }: +let + hostname = tools.build_hostname "frigate"; + mask_cidr = tools.mask_cidr; + camera = tools.build_ip "camera"; + user = "admin"; # use yours + pass = "admin"; # use yours +in +{ + services.frigate = { + enable = true; + hostname = hostname; + checkConfig = false; + settings = { + auth = { + trusted_proxies = [ + mask_cidr + ]; + }; + proxy = { + header_map = { + user = "X-authentik-name"; + role = "X-authentik-groups"; + }; + separator = "|"; + default_role = "admin"; + }; + cameras = { + front = { + enabled = true; + ffmpeg.inputs = [ + { + # TODO: Move this elsewhere + path = "rtsp://${user}:${pass}@${camera}:554/?streamtype=0&subtype=1"; + roles = [ + "audio" + "detect" + "record" + ]; + } + ]; + onvif = { + host = camera; + port = 8899; + user = user; + password = pass; + }; + }; + }; + }; + # vaapiDriver ... + }; +} diff --git a/config/grocy-grocy.nix b/config/grocy-grocy.nix new file mode 100644 index 0000000..0085258 --- /dev/null +++ b/config/grocy-grocy.nix @@ -0,0 +1,26 @@ +{ + config, + tools, + pkgs, + ... +}: +let + lib = pkgs.lib; +in +{ + services.grocy = { + enable = true; + hostName = tools.build_hostname "grocy"; + settings = { + calendar.firstDayOfWeek = 1; + culture = config.globals.country_code; + currency = config.globals.currency; + }; + nginx.enableSSL = false; + }; + environment.etc."grocy/config.php".text = lib.mkAfter '' + // Arbitrary PHP code in grocy's configuration file + Setting('AUTH_CLASS', 'Grocy\Middleware\ReverseProxyAuthMiddleware'); + Setting('REVERSE_PROXY_AUTH_HEADER', 'REMOTE_USER'); + ''; +} diff --git a/config/matrix-element.config.nix b/config/matrix-element.config.nix new file mode 100644 index 0000000..395c65c --- /dev/null +++ b/config/matrix-element.config.nix @@ -0,0 +1,54 @@ +{ config, tools, ... }: +let + hostname = tools.build_hostname "matrix"; +in +{ + "default_server_config" = { + "m.homeserver" = { + "base_url" = "https://${hostname}"; + "server_name" = "My HomeServer"; + }; + "m.identity_server" = { + "base_url" = "https://vector.im"; + }; + }; + "disable_custom_urls" = false; + "disable_guests" = false; + "disable_login_language_selector" = false; + "disable_3pid_login" = false; + "force_verification" = false; + "brand" = "Element"; + "integrations_ui_url" = "https://scalar.vector.im/"; + "integrations_rest_url" = "https://scalar.vector.im/api"; + "integrations_widgets_urls" = [ + "https://scalar.vector.im/_matrix/integrations/v1" + "https://scalar.vector.im/api" + "https://scalar-staging.vector.im/_matrix/integrations/v1" + "https://scalar-staging.vector.im/api" + ]; + "default_widget_container_height" = 280; + "default_country_code" = "GB"; + "show_labs_settings" = false; + "features" = { }; + "default_federate" = true; + "default_theme" = "light"; + "room_directory" = { + "servers" = [ "matrix.org" ]; + }; + "enable_presence_by_hs_url" = { + "https://matrix.org" = false; + "https://matrix-client.matrix.org" = false; + }; + "setting_defaults" = { + "breadcrumbs" = true; + }; + "jitsi" = { + "preferred_domain" = "meet.element.io"; + }; + "element_call" = { + "url" = "https://call.element.io"; + "participant_limit" = 8; + "brand" = "Element Call"; + }; + "map_style_url" = "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"; +} diff --git a/config/matrix-mas.config.nix b/config/matrix-mas.config.nix new file mode 100644 index 0000000..cb2e01a --- /dev/null +++ b/config/matrix-mas.config.nix @@ -0,0 +1,104 @@ +{ config, tools, ... }: +let + mask = tools.mask_cidr; + db_host = tools.build_ip "db"; + db_pass = (import ../config/_passwords.nix).matrix; + + hostname = tools.build_hostname "matrix"; + auth = tools.build_hostname "auth"; + sec = import ../config/_matrix_secrets.nix; +in +{ + http = { + listeners = [ + { + name = "web"; + resources = [ + { name = "discovery"; } + { name = "human"; } + { name = "oauth"; } + { name = "compat"; } + { name = "graphql"; } + { name = "assets"; } + ]; + binds = [ + { address = "[::]:8080"; } + ]; + proxy_protocol = false; + } + { + name = "internal"; + resources = [ + { name = "health"; } + ]; + binds = [ + { + host = "localhost"; + port = 8081; + } + ]; + proxy_protocol = false; + } + ]; + trusted_proxies = [ + mask + "127.0.0.1/8" + ]; + public_base = "http://[::]:8080/"; + issuer = "http://[::]:8080/"; + database = { + uri = "postgresql://matrix:${db_pass}@${db_host}:5432/matrix_mas"; + max_connections = 10; + min_connections = 0; + connect_timeout = 30; + idle_timeout = 600; + max_lifetime = 1800; + }; + email = { + from = "\"Authentication Service\" "; + reply_to = "\"Authentication Service\" "; + transport = "blackhole"; + }; + secrets = sec.mas; + passwords = { + enabled = true; + schemes = [ + { + version = 1; + algorithm = "bcrypt"; + minimum_complexity = 3; + } + ]; + }; + matrix = { + kind = "synapse"; + homeserver = hostname; + secret = sec.mas_secret; + endpoint = "http://localhost:8008/"; + upstream_oauth2 = { + providers = [ + { + id = sec.oidc_provider_id; + synapse_idp_id = "oidc-authentik"; + issuer = "https://${auth}"; + client_id = sec.oidc_client_id; + client_secret = sec.oidc_client_secret; + scope = "openid profile email"; + discovery_mode = "insecure"; + claims_imports = { + localpart = { + action = "require"; + template = "{{ user.preferred_username }}"; + on_conflicts = "add"; + }; + displayname = { + action = "suggest"; + template = "{{ user.name }}"; + }; + }; + } + ]; + }; + }; + }; +} diff --git a/config/matrix-mas.nix b/config/matrix-mas.nix new file mode 100644 index 0000000..3639f55 --- /dev/null +++ b/config/matrix-mas.nix @@ -0,0 +1,31 @@ +{ + config, + tools, + pkgs, + ... +}: +let + yaml = pkgs.format.yaml { }; +in +{ + environment.systemPackages = [ + pkgs.matrix-authentication-service + ]; + environment.etc = { + "mas/config.yaml".source = yaml.generate "mas-config.yaml" ( + import ./matrix-mas.config.yaml { inherit config tools; } + ); + "alloy/logs-mas.alloy".text = (import ./alloy/matrix-mas.alloy.nix { inherit config tools; }).out; + }; + systemd.services.matrix-authentication-service = { + enable = true; + description = "Matrix Authentication Service"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = "${pkgs.matrix-authentication-service}/bin/mas-cli server --config /etc/mas/config.yaml"; + Restart = "on-failure"; + RestartSec = 5; + }; + }; +} diff --git a/config/matrix-nginx.nix b/config/matrix-nginx.nix new file mode 100644 index 0000000..24602a3 --- /dev/null +++ b/config/matrix-nginx.nix @@ -0,0 +1,63 @@ +{ + config, + tools, + pkgs, + ... +}: +let + json = pkgs.formats.json { }; +in +{ + environment = { + systemPackages = [ + pkgs.element-web + pkgs.synapse-admin-etkecc + ]; + etc."alloy/logs-nginx.alloy".text = + (import ./alloy/default-journal-logger.alloy.nix { + inherit tools; + container = "matrix"; + service = "nginx"; + }).out; + }; + services.nginx = { + enable = true; + recommendedTlsSettings = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; + virtualHosts.element-web = { + root = pkgs.element-web; + locations = { + "/" = { + tryFiles = "$uri $uri/ /index.html?$query_string"; + index = "index.html"; + }; + "= /config.json" = { + alias = json.generate "element.config.json" ( + import ./config/matrix-element.config.nix { inherit tools config; } + ); + }; + }; + }; + virtualHosts.synapse-admin = { + root = pkgs.synapse-admin-etkecc; + listen = [ + { + addr = "0.0.0.0"; + port = 5173; + } + ]; + locations = { + "/" = { + tryFiles = "$uri $uri/ /index.html?$query_string"; + index = "index.html"; + }; + "= /config.json" = { + alias = json.generate "synapse-admin.config.json" ( + import ./config/matrix-synapse-admin.config.nix { inherit tools config; } + ); + }; + }; + }; + }; +} diff --git a/config/matrix-synapse-admin.config.nix b/config/matrix-synapse-admin.config.nix new file mode 100644 index 0000000..1a4d897 --- /dev/null +++ b/config/matrix-synapse-admin.config.nix @@ -0,0 +1,10 @@ +{ + tools, + ... +}: +let + hostname = tools.build_hostname "matrix"; +in +{ + "restrictBaseUrl" = "https://${hostname}/"; +} diff --git a/config/matrix-synapse.nix b/config/matrix-synapse.nix new file mode 100644 index 0000000..fac3a1e --- /dev/null +++ b/config/matrix-synapse.nix @@ -0,0 +1,114 @@ +{ + config, + tools, + pkgs, + ... +}: +let + container = "matrix"; + hostname = tools.build_hostname container; + admin_handle = "@${config.globals.master.login}:${hostname}"; + db_host = tools.build_hostname "db"; + auth_host = tools.build_hostname "auth"; + db_pass = config.my-lxc.matrix.db.password; + sec = import ../config/_matrix_secrets.nix; +in +{ + environment = { + etc."alloy/logs-synapse.alloy".text = + (import ./alloy/default-journal-logger.alloy.nix { + inherit tools container; + service = "matrix-synapse"; + additional_stages = '' + stage.regex { + expression = "^(?P\\S+): \\[(?P[^\\]]+)\\] (?P.*)$" + } + stage.labels { + values = { + facility = "", + worker = "", + } + } + stage.output { + source = "message" + } + ''; + }).out; + }; + services.matrix-synapse = { + enable = true; + extras = [ + "oidc" + "postgres" + "systemd" + "url-preview" + ]; + settings = { + admin_users = [ + admin_handle + ]; + enable_metrics = true; + server_name = hostname; + database = { + name = "psycopg2"; + args = { + user = container; + password = db_pass; + database = container; + host = db_host; + port = 5432; + cp_min = 5; + cp_max = 10; + }; + allow_unsafe_locale = true; + }; + listeners = [ + { + bind_addresses = [ "0.0.0.0" ]; + port = 8008; + resources = [ + { + compress = true; + names = [ + "client" + "federation" + ]; + } + ]; + tls = false; + type = "http"; + x_forwarded = true; + } + ]; + matrix-authentication-service = { + enable = true; + endpoint = "http://localhost:8080/"; + secret = sec.mas_secret; + }; + jwt_config = { + enabled = true; + secret = sec.jwt_secret; + algorithm = sec.jwt_algo; + }; + oidc_providers = [ + { + idp_id = "authentik"; + idp_name = "authentik"; + discover = true; + issuer = "https://${auth_host}/application/o/chat/"; + client_id = sec.oidc_client_id; + client_secret = sec.oidc_client_secret; + scopes = [ + "openid" + "profile" + "email" + ]; + user_mapping_provider.config = { + localpart_template = "{{ user.preferred_username }}"; + display_name_template = "{{ user.name }}"; + }; + } + ]; + }; + }; +} diff --git a/config/metrics-prometheus.nix b/config/metrics-prometheus.nix new file mode 100644 index 0000000..18619cc --- /dev/null +++ b/config/metrics-prometheus.nix @@ -0,0 +1,43 @@ +{ + config, + tools, + pkgs, + ... +}: +let + lib = pkgs.lib; +in +{ + services.prometheus = { + enable = true; + extraFlags = [ + "--web.enable-otlp-receiver" + "--web.enable-remote-write-receiver" + ]; + globalConfig = { + scrape_interval = "30s"; + }; + scrapeConfigs = [ + { + job_name = "prometheus"; + static_configs = [ + { targets = [ "localhost:9090" ]; } + ]; + } + ] + ++ (lib.filter (sc: sc.static_configs != [ ]) ( + lib.mapAttrsToList ( + container: def: + let + container_ip = tools.build_ip container; + in + { + job_name = container; + static_configs = map (port: { + targets = [ "${container_ip}:${toString port}" ]; + }) def.logging.prometheusPorts; + } + ) config.my-lxc + )); + }; +} diff --git a/config/monitoring-grafana.nix b/config/monitoring-grafana.nix new file mode 100644 index 0000000..8827f17 --- /dev/null +++ b/config/monitoring-grafana.nix @@ -0,0 +1,75 @@ +{ + config, + tools, + pkgs, + ... +}: +let + container = "monitoring"; + hostname = tools.build_hostname container; +in +{ + services.grafana = { + enable = true; + openFirewall = true; + declarativePlugins = [ + pkgs.grafanaPlugins.grafana-mqtt-datasource + pkgs.grafanaPlugins.grafana-lokiexplore-app + pkgs.grafanaPlugins.grafana-metricsdrilldown-app + ]; + provision = { + enable = true; + alerting = { }; + dashboards = { }; + datasources.settings.datasources = [ + { + name = "Prometheus"; + type = "prometheus"; + url = "http://${tools.metrics_addr}"; + jsonData = { + prometheusType = "Prometheus"; + timeInterval = "30s"; + }; + } + { + name = "Loki"; + type = "loki"; + url = "http://localhost:3100/"; + } + ]; + }; + + settings = { + analytics = { + feedback_links_enabled = false; + reporting_enabled = false; + check_for_plugin_updates = false; + check_for_updates = false; + }; + database = { + host = tools.build_ip "db"; + name = container; + password = config.my-lxc.monitoring.db.password; + # ssl_mode = "require" ? + type = "postgres"; + user = container; + }; + security = { + # CSP? + admin_email = config.globals.master.email; + admin_user = config.globals.master.login; + cookie_secure = true; + data_source_proxy_whitelist = [ + (tools.build_ip "auth") + ]; + }; + server = { + enable_gzip = true; + root_url = "https://${hostname}/"; + http_addr = tools.build_ip container; + http_port = 3000; + protocol = "http"; + }; + }; + }; +} diff --git a/config/monitoring-loki.nix b/config/monitoring-loki.nix new file mode 100644 index 0000000..7d224ef --- /dev/null +++ b/config/monitoring-loki.nix @@ -0,0 +1,37 @@ +{ + config, + tools, + pkgs, + ... +}: +{ + services.loki = { + enable = true; + configuration = { + auth_enabled = false; + server.http_listen_port = 3100; + analytics.reporting_enabled = false; + schema_config.configs = [ + { + from = "2020-05-15"; + store = "tsdb"; + object_store = "filesystem"; + schema = "v13"; + index = { + prefix = "index_"; + period = "24h"; + }; + } + ]; + common = { + ring = { + instance_addr = "127.0.0.1"; + kvstore.store = "inmemory"; + }; + replication_factor = 1; + path_prefix = "/tmp/loki"; + }; + storage_config.filesystem.directory = "/tmp/loki/chunks"; + }; + }; +} diff --git a/config/power-ups.nix b/config/power-ups.nix new file mode 100644 index 0000000..0d8034c --- /dev/null +++ b/config/power-ups.nix @@ -0,0 +1,37 @@ +{ + pkgs, + config, + tools, + ... +}: +{ + power.ups = { + enable = true; + mode = "standalone"; + openFirewall = true; # -> 80 + ups.eaton5e = { + driver = "usbhid-ups"; + port = "auto"; + summary = '' + vendorid = "0463" + productid = "FFFF" + product = "Eaton 5E" + serial = "BLANK" + vendor = "EATON" + bus = "002" + ''; + }; + upsmon = { + monitor.eaton5e = { + user = "nut"; + powerValue = 1; + system = "eaton5e"; + }; + }; + users.nut = { + passwordFile = config.age.secrets.power-password-file.path; + actions = [ "SET" ]; + instcmds = [ "ALL" ]; + }; + }; +} diff --git a/config/proxy-ovh-config.env b/config/proxy-ovh-config.env new file mode 100644 index 0000000..a0866a1 --- /dev/null +++ b/config/proxy-ovh-config.env @@ -0,0 +1,4 @@ +OVH_ENDPOINT=ovh-eu +OVH_APPLICATION_KEY=19f98c7e330b04fc +OVH_APPLICATION_SECRET=40518f33ae70e5e8ef368be9561994aa +OVH_CONSUMER_KEY=ff8851d2b96c6f99f732d5d0b78e542a diff --git a/config/proxy-traefik.nix b/config/proxy-traefik.nix new file mode 100644 index 0000000..c9dff06 --- /dev/null +++ b/config/proxy-traefik.nix @@ -0,0 +1,241 @@ +{ + tools, + config, + pkgs, + ... +}: +let + lib = pkgs.lib; + ip = h: tools.build_ip h; + dmn = config.globals.domains.external; + internal = "&& ClientIP(`${tools.mask_cidr}`)"; + mergeConf = c: (lib.foldl' (acc: e: lib.recursiveUpdate acc e) { } c); + # NOTE: For now this is built manually on the host. + customCAs = [ + "/var/lib/traefik/ca.public.crt" + ]; +in +{ + services = { + traefik = { + enable = true; + environmentFiles = [ + config.age.secrets.proxy-dns-provider-config.path + ]; + staticConfigOptions = { + api.insecure = true; + log.level = "INFO"; + entryPoints = { + web.address = ":80"; + websecure.address = ":443"; + traefik.address = ":8080"; + metrics.address = ":8082"; + dbsecure.address = ":5432"; + }; + certificatesResolvers = { + letsencrypt = { + acme = { + email = config.globals.master.email; + storage = "/var/lib/traefik/acme"; + httpChallenge = { + entryPoint = "web"; + }; + dnsChallenge = { + provider = config.globals.dns_provider; + }; + }; + }; + }; + tls = { + options = { + modern = { + minVersion = "VersionTLS13"; + }; + intermediate = { + minVersion = "VersionTLS12"; + cipherSuites = [ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305" + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305" + ]; + }; + }; + }; + metrics = { + prometheus = { + entryPoint = "metrics"; + addEntryPointsLabels = true; + addRoutersLabels = true; + addServicesLabels = true; + }; + }; + }; + dynamicConfigOptions = { + tcp = { + # TODO: Build both routers+services in 1 pass. + routers = mergeConf ( + lib.concatLists ( + lib.mapAttrsToList ( + ct: def: + (map ( + d: + lib.optionalAttrs (d.raw_tcp == true) { + ${d.subdomain} = { + rule = ( + if (d.customRule != null) then + (lib.replaceStrings [ "#DOMAIN#" ] [ dmn ] d.customRule) + else + ("HostSNI(`${d.subdomain}${dmn}`) " + (if (d.private == true) then internal else "")) + ); + service = "${d.subdomain}-service"; + entryPoints = [ "dbsecure" ]; # not really flexible + middlewares = [ ]; + tls.certResolver = "letsencrypt"; + }; + } + ) def.otherDomains) + ) config.my-lxc + ) + ); + services = mergeConf ( + lib.concatLists ( + lib.mapAttrsToList ( + ct: def: + (map ( + d: + lib.optionalAttrs (d.raw_tcp == true) { + "${d.subdomain}-service" = { + loadBalancer.servers = [ + { address = "${ip ct}:${toString d.port}"; } + ]; + }; + } + ) def.otherDomains) + ) config.my-lxc + ) + ); + }; + http = { + middlewares = { + authentik.forwardAuth = { + address = "http://${ip "auth"}:9000/outpost.goauthentik.io/auth/traefik"; + trustForwardHeader = true; + authResponseHeaders = [ + "X-authentik-username" + "X-authentik-groups" + "X-authentik-entitlements" + "X-authentik-email" + "X-authentik-name" + "X-authentik-uid" + "X-authentik-jwt" + "X-authentik-meta-jwks" + "X-authentik-meta-outpost" + "X-authentik-meta-provider" + "X-authentik-meta-app" + "X-authentik-meta-version" + "Remote-User" + "Remote-Group" + "Remote-Email" + "Remote-Name" + ]; + }; + }; + routers = mergeConf ( + lib.concatLists ( + (lib.mapAttrsToList ( + ct: def: + (map ( + d: + lib.optionalAttrs (d.raw_tcp == false) { + ${d.subdomain} = { + rule = ( + if (d.customRule != null) then + (lib.replaceStrings [ "#DOMAIN#" ] [ dmn ] d.customRule) + else + ("Host(`${d.subdomain}${dmn}`) " + (if (d.private == true) then internal else "")) + ); + service = "${d.subdomain}-service"; + entryPoints = [ "websecure" ]; + middlewares = if (d.auth) then [ "authentik" ] else [ ]; + tls.certResolver = "letsencrypt"; + }; + } + ) def.otherDomains) + ++ [ + (lib.optionalAttrs (def.system.port != null) { + ${ct} = { + rule = "Host(`${ct}${dmn}`) " + (if (def.private == true) then internal else ""); + service = "${ct}-service"; + entryPoints = [ "websecure" ]; + middlewares = if (def.auth) then [ "authentik" ] else [ ]; + tls.certResolver = "letsencrypt"; + }; + }) + ] + ) config.my-lxc) + ++ [ + (map (h: { + ${h.hostname} = { + rule = "Host(`${h.hostname}${dmn}`) " + (if (h.private == true) then internal else ""); + service = "${h.hostname}-service"; + entryPoints = [ "websecure" ]; + middlewares = if (h.auth) then [ "authentik" ] else [ ]; + tls.certResolver = "letsencrypt"; + }; + }) config.globals.other_hosts) + ] + ) + ); + services = mergeConf ( + lib.concatLists ( + (lib.mapAttrsToList ( + ct: def: + (map (d: { + "${d.subdomain}-service" = { + loadBalancer.servers = [ + { url = "http://${ip ct}:${toString d.port}/"; } + ]; + }; + }) def.otherDomains) + ++ [ + ( + (lib.optionalAttrs (def.system.port != null) { + "${ct}-service" = { + loadBalancer.servers = [ { url = "http://${ip ct}:${toString def.system.port}/"; } ]; + }; + }) + ) + ] + ) config.my-lxc) + ++ [ + (map (h: { + "${h.hostname}-service" = { + loadBalancer = { + servers = [ { url = h.addr; } ]; + } + // (lib.optionalAttrs (h.useCustomCA) { + serversTransport = "${h.hostname}-transport"; + }); + }; + }) config.globals.other_hosts) + ] + ) + ); + serversTransports = mergeConf ( + (map ( + h: + lib.optionalAttrs (h.useCustomCA) { + "${h.hostname}-transport" = { + rootCAs = customCAs; + }; + } + ) config.globals.other_hosts) + ); + }; + }; + }; + }; +} diff --git a/config/vault-vaultwarden.nix b/config/vault-vaultwarden.nix new file mode 100644 index 0000000..0aeae24 --- /dev/null +++ b/config/vault-vaultwarden.nix @@ -0,0 +1,29 @@ +{ + pkgs, + config, + tools, + ... +}: +let + container = "vault"; + hostname = tools.build_hostname container; + db_host = tools.build_ip "db"; + db_password = config.my-lxc.vault.db.password; +in +{ + services.vaultwarden = { + enable = true; + config = { + DISABLE_ADMIN_TOKEN = true; + ROCKET_ADDRESS = "0.0.0.0"; + ROCKET_PORT = config.my-lxc.vault.system.port; + DOMAIN = "https://${hostname}"; + SIGNUPS_ALLOWED = false; + DATABASE_URL = "postgresql://${container}:${db_password}@${db_host}:5432/${container}"; + WEB_VAULT_ENABLED = true; + INVITATIONS_ENABLED = true; + ORG_CREATION_USERS = config.globals.master.email; + }; + dbBackend = "postgresql"; + }; +} diff --git a/config/yarrr-arr.nix b/config/yarrr-arr.nix new file mode 100644 index 0000000..040c0a3 --- /dev/null +++ b/config/yarrr-arr.nix @@ -0,0 +1,68 @@ +{ + config, + pkgs, + tools, + ... +}: +{ + environment.etc."yarrr.env".source = config.age.secrets.yarrr-env; + + services = { + bazarr = { + enable = true; + openFirewall = true; # 6767 + user = "root"; + group = "root"; + }; + lidarr = { + enable = true; + openFirewall = true; # 8686 + user = "root"; + group = "root"; + environmentFiles = [ "/etc/yarrr.env" ]; + dataDir = "/mnt/nas/app-data/lidarr"; # TODO: Manual bind-mount in Proxmox + }; + radarr = { + enable = true; + openFirewall = true; # 7878 + user = "root"; + group = "root"; + environmentFiles = [ "/etc/yarrr.env" ]; + }; + sonarr = { + enable = true; + openFirewall = true; # 8989 + user = "root"; + group = "root"; + environmentFiles = [ "/etc/yarrr.env" ]; + }; + readarr = { + enable = true; + openFirewall = true; # 8787 + user = "root"; + group = "root"; + environmentFiles = [ "/etc/yarrr.env" ]; + }; + prowlarr = { + enable = true; + openFirewall = true; # 9696 + }; + recyclarr = { + enable = true; + }; + prometheus.exporters = { + exportarr-bazarr.enable = true; + exportarr-bazarr.openFirewall = true; + exportarr-lidarr.enable = true; + exportarr-lidarr.openFirewall = true; + exportarr-prowlarr.enable = true; + exportarr-prowlarr.openFirewall = true; + exportarr-radarr.enable = true; + exportarr-radarr.openFirewall = true; + exportarr-readarr.enable = true; + exportarr-readarr.openFirewall = true; + exportarr-sonarr.enable = true; + exportarr-sonarr.openFirewall = true; + }; + }; +} diff --git a/containers/_cont.tmpl b/containers/_cont.tmpl new file mode 100644 index 0000000..bdf87f1 --- /dev/null +++ b/containers/_cont.tmpl @@ -0,0 +1,28 @@ +{ ... }: +let + db_pass = import ../config/_passwords.nix; +in +{ + my-lxc.#name# = { + container = { + cores = 1; + memory = 512; + disk = "4G"; + swap = 512; + }; + db = { + enable = true; + password = db_pass.#name#; + }; + system = { + port = 80; # open in firewall + expose on proxy + services.nginx.enable = true; + }; + logging = { + enable = true; + metricsEnable = true; + }; + private = true; # available only on private lan + auth = true; # auth overlay + }; +} diff --git a/containers/auth.nix b/containers/auth.nix new file mode 100644 index 0000000..484b1fe --- /dev/null +++ b/containers/auth.nix @@ -0,0 +1,44 @@ +{ ... }: +let + db_pass = import ../config/_passwords.nix; +in +{ + my-lxc.auth = { + container = { + cores = 2; + memory = 1024; + disk = "8G"; + swap = 1024; + }; + system = { + port = 80; + additionalPorts = [ + 443 + 389 + 636 + 9000 + 9443 + 3389 + 6636 + 9300 + 9303 + ]; + udpPorts = [ + 1812 + ]; + importConfig = [ + ../config/auth-authentik.nix + ]; + }; + logging = { + enable = true; + metricsEnable = true; + }; + db = { + enable = true; + password = db_pass.auth; + }; + private = false; + auth = false; + }; +} diff --git a/containers/db.nix b/containers/db.nix new file mode 100644 index 0000000..e93982c --- /dev/null +++ b/containers/db.nix @@ -0,0 +1,41 @@ +{ ... }: +{ + my-lxc.db = { + container = { + cores = 2; + memory = 2048; + disk = "16G"; + swap = 512; + }; + system = { + additionalPorts = [ + 9187 + 5432 + ]; + importConfig = [ + ../config/db-postgres.nix + ]; + services.prometheus.exporters.postgres = { + enable = true; + listenAddress = "0.0.0.0"; + port = 9187; + }; + }; + logging = { + enable = true; + metricsEnable = true; + prometheusPorts = [ 9187 ]; + }; + private = true; + auth = true; + otherDomains = [ + { + subdomain = "db"; + port = 5432; + private = true; + auth = false; + raw_tcp = true; + } + ]; + }; +} diff --git a/containers/default.nix b/containers/default.nix new file mode 100644 index 0000000..cff7801 --- /dev/null +++ b/containers/default.nix @@ -0,0 +1,14 @@ +{ pkgs, ... }: +let + lib = pkgs.lib; + + containersFiles = builtins.readDir ./.; + containers = lib.filter (v: v != null) ( + (lib.mapAttrsToList ( + name: type: + if type == "regular" && name != "default.nix" && lib.hasSuffix ".nix" name then ./${name} else null + )) + containersFiles + ); +in +containers diff --git a/containers/dns.nix b/containers/dns.nix new file mode 100644 index 0000000..0dd8ecc --- /dev/null +++ b/containers/dns.nix @@ -0,0 +1,31 @@ +{ + ... +}: +{ + my-lxc.dns = { + container = { + cores = 2; + memory = 1024; + disk = "4G"; + swap = 512; + }; + system = { + port = 80; + additionalPorts = [ + 53 + ]; + udpPorts = [ 53 ]; + importConfig = [ + ../config/dns-adguardhome.nix + ../config/dns-unbound.nix + ]; + services.resolved.enable = false; + }; + logging = { + enable = true; + metricsEnable = true; + }; + private = true; + auth = true; + }; +} diff --git a/containers/finances.nix b/containers/finances.nix new file mode 100644 index 0000000..8835534 --- /dev/null +++ b/containers/finances.nix @@ -0,0 +1,32 @@ +{ + ... +}: +let + db_pass = import ../config/_passwords.nix; +in +{ + my-lxc.finances = { + container = { + cores = 1; + memory = 512; + disk = "4G"; + swap = null; + }; + system = { + port = 80; + importConfig = [ + ../config/finances-fireflyiii.nix + ]; + }; + logging = { + enable = true; + metricsEnable = true; + }; + db = { + enable = true; + password = db_pass.finances; + }; + private = true; + auth = true; + }; +} diff --git a/containers/frigate.nix b/containers/frigate.nix new file mode 100644 index 0000000..c365926 --- /dev/null +++ b/containers/frigate.nix @@ -0,0 +1,23 @@ +{ ... }: +{ + my-lxc.frigate = { + container = { + cores = 4; + memory = 2048; + disk = "12G"; + swap = 1024; + }; + system = { + port = 80; + importConfig = [ + ../config/frigate-frigate.nix + ]; + }; + logging = { + enable = true; + metricsEnable = true; + }; + private = false; + auth = true; + }; +} diff --git a/containers/grocy.nix b/containers/grocy.nix new file mode 100644 index 0000000..a4d10a1 --- /dev/null +++ b/containers/grocy.nix @@ -0,0 +1,23 @@ +{ ... }: +{ + my-lxc.grocy = { + container = { + cores = 1; + memory = 512; + disk = "4G"; + swap = 512; + }; + system = { + port = 80; + importConfig = [ + ../config/grocy-grocy.nix + ]; + }; + logging = { + enable = true; + metricsEnable = true; + }; + private = false; + auth = true; + }; +} diff --git a/containers/matrix.nix b/containers/matrix.nix new file mode 100644 index 0000000..2cefbe4 --- /dev/null +++ b/containers/matrix.nix @@ -0,0 +1,70 @@ +{ + ... +}: +let + db_pass = import ../config/_passwords.nix; +in +{ + my-lxc.matrix = { + container = { + cores = 2; + memory = 2048; + disk = "4G"; + swap = 512; + }; + system = { + additionalPorts = [ + 80 + 8008 + 8080 + 5173 + ]; + importConfig = [ + ../config/matrix-synapse.nix + ../config/matrix-mas.nix + ../config/matrix-nginx.nix + ]; + }; + db = { + enable = true; + password = db_pass.matrix; + additionalDB = [ + "matrix_mas" + ]; + }; + logging = { + enable = true; + metricsEnable = true; + }; + private = false; + auth = false; + otherDomains = [ + { + subdomain = "chat"; + port = 80; + private = false; + auth = false; + } + { + subdomain = "matrix"; + port = 8008; + private = false; + auth = false; + customRule = "Host(`matrix#DOMAIN#`) && !(PathPrefix(`/_matrix/client/*/login`) || PathPrefix(`/_matrix/client/*/logout`) || PathPrefix(`/_matrix/client/*/refresh`))"; + } + { + subdomain = "matrix_auth"; + port = 8080; + private = false; + auth = false; + customRule = "Host(`matrix#DOMAIN#`) && (PathPrefix(`/_matrix/client/*/login`) || PathPrefix(`/_matrix/client/*/logout`) || PathPrefix(`/_matrix/client/*/refresh`))"; + } + { + subdomain = "matrix-admin"; + port = 5173; + private = true; + auth = false; + } + ]; + }; +} diff --git a/containers/media.nix b/containers/media.nix new file mode 100644 index 0000000..0040f69 --- /dev/null +++ b/containers/media.nix @@ -0,0 +1,53 @@ +{ ... }: +let + db_pass = import ../config/_passwords.nix; +in +{ + my-lxc.media = { + container = { + cores = 4; + memory = 4096; + disk = "12G"; + swap = 1024; + }; + db = { + enable = true; + password = db_pass.media; + }; + system = { + port = 8096; # jellyfin default http + additionalPorts = [ 5055 ]; # jellyseerr default + services = { + jellyfin = { + enable = true; + openFirewall = true; + # TODO: Manual bind-mount in proxmox + dataDir = "/mnt/nas/app-data/jellyfin"; + logDir = "/var/log/jellyfin"; + user = "root"; + group = "root"; + }; + jellyseerr = { + enable = true; + openFirewall = true; + # TODO: Same... + configDir = "/mnt/nas/app-data/jellyseerr"; + }; + }; + }; + logging = { + enable = true; + metricsEnable = true; + }; + private = true; + auth = true; + otherDomains = [ + { + subdomain = "flix"; + port = 5055; + private = true; + auth = true; + } + ]; + }; +} diff --git a/containers/metrics.nix b/containers/metrics.nix new file mode 100644 index 0000000..aab3c9a --- /dev/null +++ b/containers/metrics.nix @@ -0,0 +1,23 @@ +{ ... }: +{ + my-lxc.metrics = { + container = { + cores = 1; + memory = 1024; + disk = "10G"; + swap = 512; + }; + system = { + additionalPorts = [ 9090 ]; + importConfig = [ + ../config/metrics-prometheus.nix + ]; + }; + logging = { + enable = true; + metricsEnable = true; + }; + private = true; + auth = true; # unused anyway + }; +} diff --git a/containers/monitoring.nix b/containers/monitoring.nix new file mode 100644 index 0000000..ad4d9f0 --- /dev/null +++ b/containers/monitoring.nix @@ -0,0 +1,34 @@ +{ ... }: +let + db_pass = import ../config/_passwords.nix; +in +{ + my-lxc.monitoring = { + container = { + cores = 2; + memory = 1024; + disk = "10G"; + swap = 512; + }; + system = { + port = 3000; # grafana + additionalPorts = [ + 3100 # loki + ]; + importConfig = [ + ../config/monitoring-grafana.nix + ../config/monitoring-loki.nix + ]; + }; + db = { + enable = true; + password = db_pass.monitoring; + }; + logging = { + enable = true; + metricsEnable = true; + }; + private = true; + auth = true; + }; +} diff --git a/containers/music.nix b/containers/music.nix new file mode 100644 index 0000000..7148bb8 --- /dev/null +++ b/containers/music.nix @@ -0,0 +1,46 @@ +{ nixpkgs, system, ... }: +let + pkgs = nixpkgs.legacyPackages.${system}; +in +{ + my-lxc.music = { + container = { + cores = 2; + memory = 2048; + disk = "6G"; + swap = 512; + }; + system = { + port = 8095; + additionalPorts = [ + 8097 + ]; + services.music-assistant = { + enable = true; + providers = [ + "builtin" + "builtin_player" + "chromecast" + "deezer" + "dlna" + "filesystem_local" + "filesystem_smb" + "hass_players" + "jellyfin" + "player_group" + "ytmusic" + ]; + }; + packages = with pkgs; [ + cifs-utils + util-linux + ]; + }; + logging = { + enable = true; + metricsEnable = true; + }; + private = true; + auth = false; + }; +} diff --git a/containers/power.nix b/containers/power.nix new file mode 100644 index 0000000..d484931 --- /dev/null +++ b/containers/power.nix @@ -0,0 +1,23 @@ +{ ... }: +{ + # TODO: Manual bind-mount /dev/bus/usb/{bus}/{device} # check with lsusb + my-lxc.power = { + container = { + cores = 1; + memory = 512; + disk = "4G"; + swap = 512; + }; + system = { + importConfig = [ + ../config/power-ups.nix + ]; + }; + logging = { + enable = true; + metricsEnable = true; + }; + private = true; + auth = true; + }; +} diff --git a/containers/proxy.nix b/containers/proxy.nix new file mode 100644 index 0000000..06e2a65 --- /dev/null +++ b/containers/proxy.nix @@ -0,0 +1,33 @@ +{ ... }: +{ + my-lxc.proxy = { + container = { + cores = 2; + memory = 512; + disk = "5G"; + swap = 512; + }; + system = { + port = 8080; + additionalPorts = [ + 80 + 443 + 8082 + ]; + udpPorts = [ 443 ]; + importConfig = [ + ../config/proxy-traefik.nix + ]; + }; + logging = { + enable = true; + metricsEnable = true; + alloyConfig = { + # probably move to default-journal... + "logs-traefik" = ../config/alloy/proxy-traefik.alloy.nix; + }; + }; + private = true; + auth = true; + }; +} diff --git a/containers/vault.nix b/containers/vault.nix new file mode 100644 index 0000000..da3c87f --- /dev/null +++ b/containers/vault.nix @@ -0,0 +1,30 @@ +{ ... }: +let + db_pass = import ../config/_passwords.nix; +in +{ + my-lxc.vault = { + container = { + cores = 1; + memory = 512; + disk = "4G"; + swap = 512; + }; + db = { + enable = true; + password = db_pass.vault; + }; + system = { + port = 8000; + importConfig = [ + ../config/vault-vaultwarden.nix + ]; + }; + logging = { + enable = true; + metricsEnable = true; + }; + private = false; + auth = false; + }; +} diff --git a/containers/yarrr.nix b/containers/yarrr.nix new file mode 100644 index 0000000..299823b --- /dev/null +++ b/containers/yarrr.nix @@ -0,0 +1,65 @@ +{ ... }: +let + db_pass = import ../config/_passwords.nix; +in +{ + my-lxc.yarrr = { + container = { + enable = false; + cores = 4; + memory = 2048; + disk = "8G"; + swap = 512; + protection = false; + }; + db = { + enable = true; + password = db_pass.yarrr; + additionalDB = [ + "yarrr_radarr" + "yarrr_sonarr" + "yarrr_readarr" + "yarrr_lidarr" + ]; + }; + system = { + importConfig = [ + ../config/yarrr-arr.nix + ]; + }; + logging = { + enable = true; + metricsEnable = true; + prometheusPorts = [ + 9708 + ]; + }; + otherDomains = [ + { + subdomain = "bazarr"; + port = 6767; + } + { + subdomain = "lidarr"; + port = 8686; + auth = false; + } + { + subdomain = "radarr"; + port = 7878; + } + { + subdomain = "readarr"; + port = 8787; + } + { + subdomain = "sonarr"; + port = 8989; + } + { + subdomain = "prowlarr"; + port = 9696; + } + ]; + }; +} diff --git a/flake.lock b/flake.lock index d455ad6..9d5f49f 100644 --- a/flake.lock +++ b/flake.lock @@ -1,12 +1,80 @@ { "nodes": { + "agenix": { + "inputs": { + "darwin": [], + "home-manager": "home-manager", + "nixpkgs": [ + "nixpkgs" + ], + "systems": "systems" + }, + "locked": { + "lastModified": 1761656077, + "narHash": "sha256-lsNWuj4Z+pE7s0bd2OKicOFq9bK86JE0ZGeKJbNqb94=", + "owner": "ryantm", + "repo": "agenix", + "rev": "9ba0d85de3eaa7afeab493fed622008b6e4924f5", + "type": "github" + }, + "original": { + "owner": "ryantm", + "repo": "agenix", + "type": "github" + } + }, + "authentik-nix": { + "inputs": { + "authentik-src": "authentik-src", + "flake-compat": "flake-compat", + "flake-parts": "flake-parts", + "flake-utils": "flake-utils", + "napalm": "napalm", + "nixpkgs": "nixpkgs", + "pyproject-build-systems": "pyproject-build-systems", + "pyproject-nix": "pyproject-nix", + "systems": "systems_2", + "uv2nix": "uv2nix" + }, + "locked": { + "lastModified": 1761726959, + "narHash": "sha256-SGndrZx7I0z4vITH1Arf60OTSfkQVMZRTcRgtPIBVtg=", + "owner": "nix-community", + "repo": "authentik-nix", + "rev": "ea1e06f9fe7cbf59c61b2ec4f2979801ff395d8e", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "authentik-nix", + "type": "github" + } + }, + "authentik-src": { + "flake": false, + "locked": { + "lastModified": 1759190535, + "narHash": "sha256-pIzDaoDWc58cY/XhsyweCwc4dfRvkaT/zqsV1gDSnCI=", + "owner": "goauthentik", + "repo": "authentik", + "rev": "8d3a289d12c7de2f244c76493af7880f70d08af2", + "type": "github" + }, + "original": { + "owner": "goauthentik", + "ref": "version/2025.8.4", + "repo": "authentik", + "type": "github" + } + }, "cachix": { "inputs": { "devenv": [ "devenv" ], "flake-compat": [ - "devenv" + "devenv", + "flake-compat" ], "git-hooks": [ "devenv", @@ -18,11 +86,11 @@ ] }, "locked": { - "lastModified": 1752264895, - "narHash": "sha256-1zBPE/PNAkPNUsOWFET4J0cjlvziH8DOekesDmjND+w=", + "lastModified": 1760971495, + "narHash": "sha256-IwnNtbNVrlZIHh7h4Wz6VP0Furxg9Hh0ycighvL5cZc=", "owner": "cachix", "repo": "cachix", - "rev": "47053aef762f452e816e44eb9a23fbc3827b241a", + "rev": "c5bfd933d1033672f51a863c47303fc0e093c2d2", "type": "github" }, "original": { @@ -35,18 +103,18 @@ "devenv": { "inputs": { "cachix": "cachix", - "flake-compat": "flake-compat", - "flake-parts": "flake-parts", + "flake-compat": "flake-compat_2", + "flake-parts": "flake-parts_2", "git-hooks": "git-hooks", "nix": "nix", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1761091275, - "narHash": "sha256-SIiugXvSuI2WFedt1NyDj8yHsSDntsO/JWKyEZ+mI50=", + "lastModified": 1761922975, + "narHash": "sha256-j4EB5ku/gDm7h7W7A+k70RYj5nUiW/l9wQtXMJUD2hg=", "owner": "cachix", "repo": "devenv", - "rev": "a795c32dc826b51d12706f27fb344f966bb2b084", + "rev": "c9f0b47815a4895fadac87812de8a4de27e0ace1", "type": "github" }, "original": { @@ -71,19 +139,32 @@ "type": "github" } }, + "flake-compat_2": { + "flake": false, + "locked": { + "lastModified": 1761588595, + "narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "flake-parts": { "inputs": { - "nixpkgs-lib": [ - "devenv", - "nixpkgs" - ] + "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1756770412, - "narHash": "sha256-+uWLQZccFHwqpGqr2Yt5VsW/PbeJVTn9Dk6SHWhNRPw=", + "lastModified": 1760948891, + "narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "4524271976b625a4a605beefd893f270620fd751", + "rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04", "type": "github" }, "original": { @@ -93,6 +174,27 @@ } }, "flake-parts_2": { + "inputs": { + "nixpkgs-lib": [ + "devenv", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1760948891, + "narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_3": { "inputs": { "nixpkgs-lib": [ "terranix", @@ -115,7 +217,28 @@ }, "flake-utils": { "inputs": { - "systems": "systems" + "systems": [ + "authentik-nix", + "systems" + ] + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_3" }, "locked": { "lastModified": 1731533236, @@ -134,7 +257,7 @@ "generators": { "inputs": { "nixlib": "nixlib", - "nixpkgs": "nixpkgs_2" + "nixpkgs": "nixpkgs_3" }, "locked": { "lastModified": 1751903740, @@ -163,11 +286,11 @@ ] }, "locked": { - "lastModified": 1758108966, - "narHash": "sha256-ytw7ROXaWZ7OfwHrQ9xvjpUWeGVm86pwnEd1QhzawIo=", + "lastModified": 1760663237, + "narHash": "sha256-BflA6U4AM1bzuRMR8QqzPXqh8sWVCNDzOdsxXEguJIc=", "owner": "cachix", "repo": "git-hooks.nix", - "rev": "54df955a695a84cd47d4a43e08e1feaf90b1fd9b", + "rev": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37", "type": "github" }, "original": { @@ -198,6 +321,53 @@ "type": "github" } }, + "home-manager": { + "inputs": { + "nixpkgs": [ + "agenix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1745494811, + "narHash": "sha256-YZCh2o9Ua1n9uCvrvi5pRxtuVNml8X2a03qIFfRKpFs=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "abfad3d2958c9e6300a883bd443512c55dfeb1be", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "napalm": { + "inputs": { + "flake-utils": [ + "authentik-nix", + "flake-utils" + ], + "nixpkgs": [ + "authentik-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1725806412, + "narHash": "sha256-lGZjkjds0p924QEhm/r0BhAxbHBJE1xMOldB/HmQH04=", + "owner": "willibutz", + "repo": "napalm", + "rev": "b492440d9e64ae20736d3bec5c7715ffcbde83f5", + "type": "github" + }, + "original": { + "owner": "willibutz", + "ref": "avoid-foldl-stack-overflow", + "repo": "napalm", + "type": "github" + } + }, "nix": { "inputs": { "flake-compat": [ @@ -224,16 +394,16 @@ ] }, "locked": { - "lastModified": 1758763079, - "narHash": "sha256-Bx1A+lShhOWwMuy3uDzZQvYiBKBFcKwy6G6NEohhv6A=", + "lastModified": 1761648602, + "narHash": "sha256-H97KSB/luq/aGobKRuHahOvT1r7C03BgB6D5HBZsbN8=", "owner": "cachix", "repo": "nix", - "rev": "6f0140527c2b0346df4afad7497baa08decb929f", + "rev": "3e5644da6830ef65f0a2f7ec22830c46285bfff6", "type": "github" }, "original": { "owner": "cachix", - "ref": "devenv-2.30.5", + "ref": "devenv-2.30.6", "repo": "nix", "type": "github" } @@ -255,11 +425,42 @@ }, "nixpkgs": { "locked": { - "lastModified": 1758532697, - "narHash": "sha256-bhop0bR3u7DCw9/PtLCwr7GwEWDlBSxHp+eVQhCW9t4=", + "lastModified": 1761114652, + "narHash": "sha256-f/QCJM/YhrV/lavyCVz8iU3rlZun6d+dAiC3H+CDle4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "01f116e4df6a15f4ccdffb1bcd41096869fb385c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1754788789, + "narHash": "sha256-x2rJ+Ovzq0sCMpgfgGaaqgBSwY+LST+WbZ6TytnT9Rk=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "a73b9c743612e4244d865a2fdee11865283c04e6", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1761313199, + "narHash": "sha256-wCIACXbNtXAlwvQUo1Ed++loFALPjYUA3dpcUJiXO44=", "owner": "cachix", "repo": "devenv-nixpkgs", - "rev": "207a4cb0e1253c7658c6736becc6eb9cace1f25f", + "rev": "d1c30452ebecfc55185ae6d1c983c09da0c274ff", "type": "github" }, "original": { @@ -269,7 +470,7 @@ "type": "github" } }, - "nixpkgs_2": { + "nixpkgs_3": { "locked": { "lastModified": 1736657626, "narHash": "sha256-FWlPMUzp0lkQBdhKlPqtQdqmp+/C+1MBiEytaYfrCTY=", @@ -285,13 +486,13 @@ "type": "github" } }, - "nixpkgs_3": { + "nixpkgs_4": { "locked": { - "lastModified": 1760878510, - "narHash": "sha256-K5Osef2qexezUfs0alLvZ7nQFTGS9DL2oTVsIXsqLgs=", + "lastModified": 1762111121, + "narHash": "sha256-4vhDuZ7OZaZmKKrnDpxLZZpGIJvAeMtK6FKLJYUtAdw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5e2a59a5b1a82f89f2c7e598302a9cacebb72a67", + "rev": "b3d51a0365f6695e7dd5cdf3e180604530ed33b4", "type": "github" }, "original": { @@ -301,7 +502,7 @@ "type": "github" } }, - "nixpkgs_4": { + "nixpkgs_5": { "locked": { "lastModified": 1728956102, "narHash": "sha256-J8zo+UYNjHATsxn2/ROl8iaji2RgLm+sG7b3VcD36YM=", @@ -316,12 +517,64 @@ "type": "github" } }, + "pyproject-build-systems": { + "inputs": { + "nixpkgs": [ + "authentik-nix", + "nixpkgs" + ], + "pyproject-nix": [ + "authentik-nix", + "pyproject-nix" + ], + "uv2nix": [ + "authentik-nix", + "uv2nix" + ] + }, + "locked": { + "lastModified": 1759113590, + "narHash": "sha256-fgxP2RCN4cg0jYiMYoETYc7TZ2JjgyvJa2y9l8oSUFE=", + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "rev": "dbfc0483b5952c6b86e36f8b3afeb9dde30ea4b5", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "type": "github" + } + }, + "pyproject-nix": { + "inputs": { + "nixpkgs": [ + "authentik-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1760402624, + "narHash": "sha256-jF6UKLs2uGc2rtved8Vrt58oTWjTQoAssuYs/0578Z4=", + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "rev": "84c4ea102127c77058ea1ed7be7300261fafc7d2", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "type": "github" + } + }, "root": { "inputs": { + "agenix": "agenix", + "authentik-nix": "authentik-nix", "devenv": "devenv", - "flake-utils": "flake-utils", + "flake-utils": "flake-utils_2", "generators": "generators", - "nixpkgs": "nixpkgs_3", + "nixpkgs": "nixpkgs_4", "terranix": "terranix" } }, @@ -341,6 +594,36 @@ } }, "systems_2": { + "locked": { + "lastModified": 1689347949, + "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", + "owner": "nix-systems", + "repo": "default-linux", + "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default-linux", + "type": "github" + } + }, + "systems_3": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_4": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", @@ -357,16 +640,16 @@ }, "terranix": { "inputs": { - "flake-parts": "flake-parts_2", - "nixpkgs": "nixpkgs_4", - "systems": "systems_2" + "flake-parts": "flake-parts_3", + "nixpkgs": "nixpkgs_5", + "systems": "systems_4" }, "locked": { - "lastModified": 1757278723, - "narHash": "sha256-hTMi6oGU+6VRnW9SZZ+muFcbfMEf2ajjOp7Z2KM5MMY=", + "lastModified": 1762161791, + "narHash": "sha256-J1L1yP29NVBJO04LA/JGM6kwhnjeNhEsX0tLFnuN3FI=", "owner": "terranix", "repo": "terranix", - "rev": "924573fa6587ac57b0d15037fbd2d3f0fcdf17fb", + "rev": "a79a47b4617dfb92184e2e5b8f5aa6fc06c659c8", "type": "github" }, "original": { @@ -374,6 +657,31 @@ "repo": "terranix", "type": "github" } + }, + "uv2nix": { + "inputs": { + "nixpkgs": [ + "authentik-nix", + "nixpkgs" + ], + "pyproject-nix": [ + "authentik-nix", + "pyproject-nix" + ] + }, + "locked": { + "lastModified": 1761101082, + "narHash": "sha256-4Kt3RsfJgg6HzmDCc44ZN//xB8n7KGEGxxt9dNjqPQc=", + "owner": "pyproject-nix", + "repo": "uv2nix", + "rev": "e6e728d9719e989c93e65145fe3f9e0c65a021a2", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "uv2nix", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index d44c6c9..14d4296 100644 --- a/flake.nix +++ b/flake.nix @@ -7,11 +7,12 @@ generators.url = "github:nix-community/nixos-generators"; terranix.url = "github:terranix/terranix"; devenv.url = "github:cachix/devenv"; - }; - - nixConfig = { - extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw="; - extra-substituters = "https://devenv.cachix.org"; + authentik-nix.url = "github:nix-community/authentik-nix"; + agenix = { + url = "github:ryantm/agenix"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.darwin.follows = ""; + }; }; outputs = @@ -22,6 +23,8 @@ generators, terranix, devenv, + authentik-nix, + agenix, ... }@inputs: let @@ -29,47 +32,65 @@ pkgs = nixpkgs.legacyPackages.${system}; lib = pkgs.lib; - containersMapping = import ./lib/ips.nix; - - containers = import ./lxc { inherit pkgs containersMapping; }; - - lxc-def = import ./lib/lxc-template.nix; - - infra = import ./lib/constants.nix; - - nixosConfigurations = lib.mapAttrs ( - _: def: - nixpkgs.lib.nixosSystem { - inherit system; - modules = [ def.nixosModule ]; + finalModule = ( + lib.evalModules { + modules = [ + { + _module.args.nixpkgs = nixpkgs; + _module.args.system = system; + } + ./modules/containers.nix + ./config/_globals.nix + ./config/_ids.nix + ] + ++ (import ./containers { inherit pkgs; }); } - ) containers; - - terraformCfg = import ./lib/infra.nix; - - terraformResources = { - resource.proxmox_lxc = lib.mapAttrs (_: def: def.terraformResource) containers; - }; + ); + nixosModules = finalModule.config.nixosModule; + terraformConfig = finalModule.config.tf; + # lxc-def = import ./modules/lxc-template.nix; + terraformBase = import ./modules/terraform-base.nix; + inherit (import ./config/_globals.nix { }) globals; in { packages.${system} = { lxc-template = generators.nixosGenerate { - system = "x86_64-linux"; - modules = [ lxc-def ]; + inherit system; format = "proxmox-lxc"; + modules = [ + ./modules/lxc-template.nix + ]; + }; + + kiosk-iso = generators.nixosGenerate { + inherit system; + format = "iso"; + modules = [ + ./modules/nixos-kiosk-iso.nix + ]; }; terraform-json = terranix.lib.terranixConfiguration { inherit system; modules = [ - terraformResources - terraformCfg + terraformBase + terraformConfig ]; }; }; - nixosConfigurations = nixosConfigurations; + nixosConfigurations = lib.mapAttrs ( + name: module: + nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + agenix.nixosModules.default + authentik-nix.nixosModules.default + module + ]; + } + ) nixosModules; devShells.${system}.default = devenv.lib.mkShell { inherit inputs pkgs; @@ -84,6 +105,10 @@ echo 'Template should be available at nixos-template/tarball/*.tar.xz' ''; + scripts.build-kiosk-iso.exec = '' + nix build .#kiosk-iso -o kiosk.iso + ''; + scripts.build-terraform-json.exec = '' nix build .#terraform-json -o config.tf.json echo 'Terraform build available as config.tf.json' @@ -93,33 +118,34 @@ if ! [[ "$2" =~ ^[0-9]+$ ]]; then echo "Error: invalid container ID '$2', should be a number" && exit fi - if ! [ -f lib/ips.nix ]; then - echo "{" > lib/ips.nix - echo "}" >> lib/ips.nix + if ! [ -f config/_ids.nix ]; then + echo "{ ... }: { id = {" > config/_ids.nix + echo "};\n}" >> config/_ids.nix fi - if ! [[ -z "`grep "[^0-9]$2[^0-9]" lib/ips.nix`" ]]; then + if ! [[ -z "`grep "[^0-9]$2[^0-9]" config/_ids.nix`" ]]; then echo "Error: container ID '$2' already used" && exit fi - if [ -f lxc/$1.nix ]; then + if [ -f containers/$1.nix ]; then echo "Error: container definition '$1' already exists" && exit fi - sed -i "s#}# $1 = $2;#" lib/ips.nix - echo "}" >> lib/ips.nix - cp lib/container.nix.template lxc/$1.nix - git add lxc/$1.nix - echo "Entry added to infra/ips.nix" - echo "Container template copied to lxc/$1.nix, please edit it" + sed -i "s#};# $1 = $2;\n };#" config/_ids.nix + cp containers/_cont.tmpl containers/$1.nix + sed -i "s/#name#/$1/g" containers/$1.nix + git add containers/$1.nix + echo "Entry added to config/_ids.nix" + echo "Container template copied to containers/$1.nix, please edit it" ''; scripts.deploy-lxc.exec = '' - if [ -f lxc/$1.nix ]; then - CONTID=`grep -E "$1 ?=" lib/ips.nix | cut -d '=' -f 2 | grep -o '\<[0-9]*\>' ` + if [ -f containers/$1.nix ]; then + CONTID=`grep -E "$1 ?=" config/_ids.nix | cut -d '=' -f 2 | grep -o '\<[0-9]*\>' ` + IP_SUFFIX=$((CONTID - 1000)) # TODO Verify mapping exists... echo "Redeploying LXC on container '$1' ('$CONTID')" - nixos-rebuild switch --flake .#$1 --target-host root@${infra.ip_prefix}$CONTID + nixos-rebuild switch --flake .#$1 --target-host root@${globals.ip_prefix}$IP_SUFFIX echo "Done." else - echo "Error: Container definition 'lxc/$1.nix' not found!" + echo "Error: Container definition 'containers/$1.nix' not found!" fi ''; diff --git a/lib/constants.nix.template b/lib/constants.nix.template deleted file mode 100644 index 319a277..0000000 --- a/lib/constants.nix.template +++ /dev/null @@ -1,33 +0,0 @@ -let - ip_prefix = "10.0.0."; -in -{ - # Centralizes the IP to the gateway for the containers. - gateway_ip = "10.0.0.1"; - - # Builders for IP addresses, given a container id. - ip_prefix = ip_prefix; - cidr = "24"; - build_ip = id: "${ip_prefix}${toString id}"; - build_ip_cidr = id: "${ip_prefix}${toString id}/${cidr}"; - - loki_addr = "10.0.0.42:3100"; - prometheus_addr = "10.0.0.42:9090"; - reverse_proxy_addr = "10.0.0.50"; - - domains = { - exposed = ".mydomain.tld"; - internal = ".local"; - }; - - # Your deployer's host - master_login = "admin"; - master_htpasswd = "$2$10$pouet.pouet"; - master_public_ssh_key = "ssh-ed25519 [...] me@here"; - - # Default timezone for the containers - default_tz = "UTC"; - - # NixOS template build name => see `ls nixos-template/tarball/` - nixos_template_name = "nixos-image-lxc-proxmox-25.11pre-git-x86_64-linux"; -} diff --git a/lib/container.nix.template b/lib/container.nix.template deleted file mode 100644 index 2be342b..0000000 --- a/lib/container.nix.template +++ /dev/null @@ -1,60 +0,0 @@ -{ pkgs, containersMapping, ... }: -let - infra = import ../lib/constants.nix; -in -{ - # OPTIONAL int cores: number of CPU (default = 1) - cores = 2; - - # OPTIONAL int memory: RAM memory (default 512) - memory = 512; - - # OPTIONAL string disk: disk space (default "4G") - beware, NixOS is greedy - disk = "4G"; - - # OPTIONAL string swap: swap space (default null) - swap = null; - - # OPTIONAL list of int tcp_ports: TCP ports to open (default []) - tcp_ports = [ 80 ]; - - # OPTIONAL list of int udp_ports: UDP ports to open (default []) - udp_ports = [ ]; - - # OPTIONAL submodule services: services to be passed to the NixOS Module (default {}) - services = { - nginx.enable = true; - }; - - # OPTIONAL list of pkgs other_packages: packages to add to eenvironment.systemPackages (default []) - other_packages = [ pkgs.hello ]; - - # OPTIONAL submodule etc: files contents to pass to eenvironment.etc - etc."alloy/log-myservice.alloy" = '' - # logger_ip = ${infra.build_ip containersMapping.grafana} - # prometheus = ${infra.build_ip containersMapping.prometheus} - ''; - - # OPTIONAL bool logging.enable: whether to enable the Alloy configuration (=> Loki) - # Need further configuration in etc."alloy/log-myservice.alloy" - logging.enable = true; - - # OPTIONAL bool logging.metrics.enable: whether to enable the Alloy metrics configuration (=> Prometheus) - logging.metrics.enable = true; - - # OPTIONAL string template: template file to use (default defined in infra/constants.nix) - template = null; - - # OPTIONAL bool unprivileged: whether the container should be unprivileged (default true) - unprivileged = true; - - # OPTIONAL string tags: ';'-separated tags, appended to "terraform" (default empty) - tags = ""; - - # OPTIONAL list of paths additional_tf_modules: list of modules to merge into the tf ressource module (default []) - # Not implemented - additional_tf_modules = []; - - # OPTIONAL bool exposed: whether this host should be exposed by the reverse proxy. - exposed = false; -} diff --git a/lib/container_build.nix b/lib/container_build.nix deleted file mode 100644 index 228eb6f..0000000 --- a/lib/container_build.nix +++ /dev/null @@ -1,86 +0,0 @@ -{ def, lib, ... }: -let - infra = import ./constants.nix; - - hostname = def.hostname; - memory = def.memory or 512; - cores = def.cores or 1; - container_id = def.container_id; - disk = def.disk or "4G"; - swap = def.swap or 512; - services = def.services or { }; - tcp_ports = def.tcp_ports or [ ]; - udp_ports = def.udp_ports or [ ]; - other_packages = def.other_packages or [ ]; - etc = def.etc or { }; - logging_enabled = def.logging.enable or false; # TODO: Implement - logging_metrics_enabled = def.logging.metrics.enable or false; - extraModules = def.extraModules or [ ]; - template = def.template or infra.nixos_template_name; - unprivileged = def.unprivileged or true; - tags = def.tags or ""; - additional_tf_modules = def.additional_tf_modules or [ ]; -in -{ - terraformResource = { - hostname = hostname; - memory = memory; - cores = cores; - ostemplate = "local:vztmpl/${template}.tar.xz"; - unprivileged = unprivileged; - password = "changeme"; - features.nesting = true; - target_node = "\${var.pve_node}"; - network = { - name = "eth0"; - bridge = "vmbr0"; - ip = infra.build_ip_cidr container_id; - gw = infra.gateway_ip; - type = "veth"; - }; - rootfs = { - storage = "local-lvm"; - size = disk; - }; - swap = swap; - vmid = container_id; - tags = "terraform;${tags}"; - }; # // each additional_tf_modules ? - - nixosModule = - { config, pkgs, ... }: - { - imports = [ - ./lxc-template.nix - ] - ++ extraModules; - networking.hostName = hostname; - networking.firewall = { - enable = true; - allowedTCPPorts = tcp_ports; - allowedUDPPorts = udp_ports; - }; - services = - services - // lib.optionalAttrs (logging_enabled) { - alloy = { - enable = true; - extraFlags = [ - "--server.http.listen-addr=0.0.0.0:12345" - "--disable-reporting" - ]; - }; - }; - environment.etc = - etc - // lib.optionalAttrs (logging_enabled) { - "alloy/config.alloy".text = (import ./config/alloy/config.alloy.nix).out; - "alloy/metrics.alloy".text = - if (logging_metrics_enabled) then - (import ./config/alloy/metrics.alloy.nix { inherit container_id; }).out - else - ""; - }; - environment.systemPackages = other_packages; - }; -} diff --git a/lib/infra.nix b/lib/infra.nix deleted file mode 100644 index 6bb60ba..0000000 --- a/lib/infra.nix +++ /dev/null @@ -1,21 +0,0 @@ -{ lib, ... }: -{ - terraform.required_providers = { - proxmox = { - source = "Telmate/proxmox"; - version = "~> 2.9.11"; - }; - }; - - provider.proxmox = { - pm_api_url = "\${var.pm_api_url}"; - pm_api_token_id = "\${var.pm_api_token_id}"; - pm_api_token_secret = "\${var.pm_api_token_secret}"; - pm_tls_insecure = false; - }; - - variable.pm_api_url.type = "string"; - variable.pm_api_token_id.type = "string"; - variable.pm_api_token_secret.type = "string"; - variable.pve_node.type = "string"; -} diff --git a/lxc/default.nix b/lxc/default.nix deleted file mode 100644 index b02564d..0000000 --- a/lxc/default.nix +++ /dev/null @@ -1,36 +0,0 @@ -{ pkgs, containersMapping, ... }: -let - lib = pkgs.lib; - - containerBuild = import ../lib/container_build.nix; - - containersFiles = builtins.readDir ./.; - - containers = lib.filterAttrs (_: v: v != null) ( - lib.mapAttrs ( - name: type: - if type == "regular" && name != "default.nix" && lib.hasSuffix ".nix" name then - import ./${name} { inherit name containersMapping pkgs; } - else - null - ) containersFiles - ); - - cleanedName = lib.listToAttrs (lib.mapAttrsToList (name: def: mkContainer name def) containers); - - mkContainer = - name: raw_def: - let - hostname = lib.removeSuffix ".nix" name; - def = raw_def // { - hostname = hostname; - container_id = containersMapping.${hostname}; - }; - result = containerBuild { inherit def lib; }; - in - { - name = hostname; - value = result; - }; -in -cleanedName diff --git a/lxc/dns.nix b/lxc/dns.nix deleted file mode 100644 index 813df35..0000000 --- a/lxc/dns.nix +++ /dev/null @@ -1,43 +0,0 @@ -{ - pkgs, - name, - containersMapping, - ... -}: -let - hostname = pkgs.lib.removeSuffix ".nix" name; - infra = import ../lib/constants.nix; - container_id = containersMapping.${hostname}; - ip = infra.build_ip container_id; - domainname = "${hostname}${infra.domains.internal}"; -in -{ - cores = 2; - memory = 1024; - disk = "4G"; - swap = 512; - tcp_ports = [ - 80 - 53 - 12345 - ]; - udp_ports = [ 53 ]; - exposed = false; - services = { - resolved.enable = false; - adguardhome = import ./dns/adguardhome-config.nix { inherit infra ip domainname; }; - unbound = { - enable = true; - }; - }; - logging.enable = true; - logging.metrics.enable = true; - etc."alloy/logs-adguard.alloy".text = - (import ./dns/logs-adguard.alloy.nix { - inherit ip domainname; - }).out; - etc."alloy/logs-unbound.alloy".text = - (import ./dns/logs-unbound.alloy.nix { - inherit ip domainname; - }).out; -} diff --git a/lxc/dns/adguardhome-config.nix b/lxc/dns/adguardhome-config.nix deleted file mode 100644 index fa2dc1d..0000000 --- a/lxc/dns/adguardhome-config.nix +++ /dev/null @@ -1,74 +0,0 @@ -{ - infra, - ip, - domainname, - ... -}: -{ - enable = true; - host = "0.0.0.0"; - port = 80; - openFirewall = true; - mutableSettings = true; - settings = { - http = { - address = "${ip}:80"; - session_ttl = "720h"; - }; - users = [ - { - name = infra.master_login; - password = infra.master_htpasswd; - } - ]; - filters = [ - { - enabled = true; - url = "https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt"; - name = "AdGuard DNS filter"; - id = 1; - } - { - enabled = true; - url = "https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt"; - name = "AdAway Default Blocklist"; - id = 2; - } - ]; - - auth_attempts = 5; - block_auth_min = 15; - language = "fr"; - dns = { - bind_hosts = [ ip ]; - port = 53; - upstream_dns = [ - "127.0.0.1:5335" - "https://dns10.quad9.net/dns-query" - ]; - }; - filtering = { - safe_search = { - enabled = true; - bing = true; - duckduckgo = true; - ecosia = true; - google = true; - pixabay = true; - yandex = true; - youtube = true; - }; - rewrites = [ - { - domain = "*${infra.domains.exposed}"; - answer = infra.reverse_proxy_addr; - } - { - domain = domainname; - answer = ip; - } - # add internal domains for all containers? - ]; - }; - }; -} diff --git a/lxc/dns/logs-adguard.alloy.nix b/lxc/dns/logs-adguard.alloy.nix deleted file mode 100644 index 8fc1c41..0000000 --- a/lxc/dns/logs-adguard.alloy.nix +++ /dev/null @@ -1,49 +0,0 @@ -{ ip, domainname, ... }: -{ - out = '' - loki.relabel "agh_journal" { - forward_to = [] - rule { - source_labels = ["__journal__priority_keyword"] - target_label = "level" - } - rule { - source_labels = ["__journal__SYSLOG_IDENTIFIER"] - target_label = "app" - } - } - loki.source.journal "agh_journal_scrape" { - forward_to = [loki.process.agh_router.receiver] - matches = "_SYSTEMD_UNIT=adguardhome.service" - relabel_rules = loki.relabel.agh_journal.rules - labels = { - service = "adguardhome", - host = "${domainname}", - host_ip = "${ip}", - } - } - - loki.process "agh_router" { - stage.regex { - expression = "^(?P\\S+ \\S+) \\[(?P\\w+)\\] (?P.*)$" - } - - stage.timestamp { - source = "timestamp" - format = "2006-01-02 15:04:05" - } - - stage.labels { - values = { - level = "level", - } - } - - stage.output { - source = "message" - } - - forward_to = [loki.write.grafana_loki.receiver] - } - ''; -} diff --git a/lxc/dns/logs-unbound.alloy.nix b/lxc/dns/logs-unbound.alloy.nix deleted file mode 100644 index 9af7cf8..0000000 --- a/lxc/dns/logs-unbound.alloy.nix +++ /dev/null @@ -1,44 +0,0 @@ -{ ip, domainname, ... }: -{ - out = '' - loki.relabel "unbd_journal" { - forward_to = [] - rule { - source_labels = ["__journal__priority_keyword"] - target_label = "level" - } - rule { - source_labels = ["__journal__SYSLOG_IDENTIFIER"] - target_label = "app" - } - } - loki.source.journal "unbd_journal_scrape" { - forward_to = [loki.process.unbd_router.receiver] - matches = "_SYSTEMD_UNIT=unbound.service" - relabel_rules = loki.relabel.unbd_journal.rules - labels = { - service = "unbound", - host = "${domainname}", - host_ip = "${ip}", - } - } - - loki.process "unbd_router" { - stage.pattern { - pattern = "[<_>] : " - } - - stage.labels { - values = { - level = "level", - } - } - - stage.output { - source = "message" - } - - forward_to = [loki.write.grafana_loki.receiver] - } - ''; -} diff --git a/modules/containers-nixos-logging.nix b/modules/containers-nixos-logging.nix new file mode 100644 index 0000000..ed55aea --- /dev/null +++ b/modules/containers-nixos-logging.nix @@ -0,0 +1,49 @@ +{ + config, + tools, + container, + def, + pkgs, + ... +}: +let + lib = pkgs.lib; + mergeConf = c: (lib.foldl' (acc: e: lib.recursiveUpdate acc e) { } c); +in +{ + services.alloy = { + enable = true; + extraFlags = [ + "--server.http.listen-addr=0.0.0.0:12345" + "--disable-reporting" + ]; + }; + environment.etc = mergeConf ( + [ + { + "alloy/config.alloy".text = (import ../config/alloy/config.alloy.nix { inherit config tools; }).out; + "alloy/metrics.alloy".text = + if (def.logging.metricsEnable) then + (import ../config/alloy/metrics.alloy.nix { inherit config tools container; }).out + else + ""; + } + ] + ++ (lib.mapAttrsToList (filename: file: { + "alloy/${filename}.alloy".text = (import file { inherit config tools container; }).out; + }) def.logging.alloyConfig) + ++ (lib.mapAttrsToList (service: additional_stages: { + "alloy/${container}-${service}.alloy".text = + import ../config/alloy/default-journal-logger.alloy.nix + { + inherit + tools + container + service + additional_stages + ; + }; + }) def.logging.journalLoggers) + ); + +} diff --git a/modules/containers-terraform-authentik.nix b/modules/containers-terraform-authentik.nix new file mode 100644 index 0000000..c0d1988 --- /dev/null +++ b/modules/containers-terraform-authentik.nix @@ -0,0 +1,8 @@ +{ + config, + tools, + lib, + ... +}: +{ +} diff --git a/modules/containers-terraform-postgres.nix b/modules/containers-terraform-postgres.nix new file mode 100644 index 0000000..2fe92a2 --- /dev/null +++ b/modules/containers-terraform-postgres.nix @@ -0,0 +1,35 @@ +{ config, lib, ... }: +let + cfg = config.my-lxc; +in +{ + postgresql_role = lib.filterAttrs (_: v: v != { }) ( + lib.mapAttrs ( + containerName: def: + lib.optionalAttrs (def.db.enable) { + name = containerName; + login = true; + password = def.db.password; + } + ) cfg + ); + postgresql_database = lib.foldl' (acc: elem: acc // elem) { } ( + lib.mapAttrsToList ( + containerName: def: + lib.optionalAttrs (def.db.enable) ( + # mkIf ? + lib.listToAttrs ( + lib.map (db: { + name = db; + value = { + name = db; + owner = containerName; + lc_ctype = "C"; + lc_collate = "C"; + }; + }) (def.db.additionalDB ++ [ containerName ]) + ) + ) + ) cfg + ); +} diff --git a/modules/containers-terraform-proxmox.nix b/modules/containers-terraform-proxmox.nix new file mode 100644 index 0000000..6f28842 --- /dev/null +++ b/modules/containers-terraform-proxmox.nix @@ -0,0 +1,45 @@ +{ + config, + tools, + lib, + ... +}: +let + cfg = config.my-lxc; +in +{ + proxmox_lxc = lib.mapAttrs ( + name: def: + let + c = def.container; + in + lib.mkIf (c.enable) { + hostname = name; + memory = c.memory; + cores = c.cores; + ostemplate = "local:vztmpl/\${var.ostemplate}.tar.xz"; + unprivileged = true; + password = "changeme"; + features.nesting = true; + target_node = "\${var.pve_node}"; + network = { + name = "eth0"; + bridge = "vmbr0"; + ip = tools.build_ip_cidr name; + gw = config.globals.gateway; + type = "veth"; + }; + protection = c.protection; + onboot = true; + rootfs = { + storage = "local-lvm"; + size = c.disk; + }; + swap = c.swap; + vmid = config.id.${name}; + tags = lib.strings.join ";" ([ "terraform" ] ++ c.tags); + } + // c.overrides + ) cfg; + +} diff --git a/modules/containers.nix b/modules/containers.nix new file mode 100644 index 0000000..0425acd --- /dev/null +++ b/modules/containers.nix @@ -0,0 +1,364 @@ +{ + config, + system, + nixpkgs, + ... +}: +let + pkgs = nixpkgs.legacyPackages.${system}; + lib = pkgs.lib; + inherit (lib) types mkOption mkEnableOption; + cfg = config.my-lxc or { }; + realcfg = config; + nixosTemplate = import ./lxc-template.nix { inherit pkgs; }; + tools = import ./tools.nix { inherit config lib; }; + mergeConf = c: (lib.foldl' (acc: e: lib.recursiveUpdate acc e) { } c); + + secrets = import ../secrets/secrets.nix; +in +{ + options = with types; { + # Inputs + my-lxc = mkOption { + type = attrsOf (submodule { + options = { + container = mkOption { + type = submodule { + options = { + enable = mkOption { + type = bool; + description = "Enable the container terraform generation"; + default = true; + }; + cores = mkOption { + type = int; + description = "Number of CPU"; + default = 1; + }; + memory = mkOption { + type = int; + description = "Ram in MB"; + default = 512; + }; + unprivileged = mkOption { + type = bool; + default = true; + description = "Make an unprivileged container"; + }; + disk = mkOption { + type = str; + description = "Disk size"; + default = "4G"; + }; + swap = mkOption { + type = nullOr int; + description = "Optional swap size in MB"; + default = null; + }; + tags = mkOption { + type = listOf str; + description = "Container tags (terraform is automatically added)"; + default = [ ]; + }; + overrides = mkOption { + type = attrs; + description = "Overrides to the Proxmox LXC Terraform resource"; + default = { }; + }; + protection = mkOption { + type = bool; + description = "Whether container should be protected against changes."; + default = true; + }; + # TODO: Device passthrough & mount points => use overrides for now - or do it manually... + # => in /etc/pve/lxc/{id}.conf + # mp{number}: /pve/path,mp=/container/path # <- for bind-mount + # dev{number}: /dev/{...} # <- for device pass-through + }; + }; + }; + system = mkOption { + type = submodule { + options = { + port = mkOption { + type = nullOr int; + description = "Main exposed HTTP port"; + default = null; + }; + additionalPorts = mkOption { + type = listOf int; + description = "TCP ports to open"; + default = [ ]; + }; + udpPorts = mkOption { + type = listOf int; + description = "UDP ports to open"; + default = [ ]; + }; + services = mkOption { + type = attrs; + description = "NixOS services to enable"; + default = { }; + }; + packages = mkOption { + type = listOf package; + description = "NixOS packages to add"; + default = [ ]; + }; + etcFiles = mkOption { + type = attrs; + description = "Files to create on NixOS (=> environment.etc)"; + default = { }; + }; + additional = mkOption { + type = attrs; + description = "Additional NixOS definitions to be merged to nixosModule"; + default = { }; + }; + importConfig = mkOption { + type = listOf path; + description = "Modules to import and merge to NixOS module"; + default = [ ]; + }; + }; + }; + }; + db = mkOption { + type = submodule { + options = { + enable = mkEnableOption "Create a DB"; + password = mkOption { type = str; }; + additionalDB = mkOption { + type = listOf str; + default = [ ]; + }; + }; + }; + default = { }; + }; + logging = mkOption { + type = submodule { + options = { + enable = mkEnableOption "Whether to enable default logs collection"; + metricsEnable = mkEnableOption "Whether to enable default metrics collection"; + prometheusPorts = mkOption { + type = listOf int; + description = "Ports of Prometheus Exporters"; + default = [ ]; + }; + alloyConfig = mkOption { + type = attrsOf path; + description = "Name => paths to add to Alloy"; + default = { }; + }; + journalLoggers = mkOption { + type = attrsOf (nullOr str); + description = "Service => routing stages (if any) as str"; + default = { }; + }; + }; + }; + }; + private = mkOption { + type = bool; + description = "Should be only exposed to the private network"; + default = true; + }; + auth = mkOption { + type = bool; + description = "Should be accessed through the auth middleware"; + default = true; + }; + otherDomains = mkOption { + type = listOf (submodule { + options = { + subdomain = mkOption { type = str; }; + port = mkOption { type = int; }; + private = mkOption { + type = bool; + default = true; + }; + auth = mkOption { + type = bool; + default = true; + }; + customRule = mkOption { + type = nullOr str; + default = null; + }; + raw_tcp = mkOption { + type = bool; + default = false; + }; + }; + }); + default = [ ]; + }; + sso = mkOption { + type = attrs; + description = "SSO parameters (TBD)"; + default = { }; + }; + secrets = mkOption { + type = attrs; + description = "Secrets to import {name}: {path}"; + default = { }; + }; + }; + }); + }; + + # in _id.nix + id = mkOption { + type = attrsOf int; + }; + # in _config.nix + globals = mkOption { + type = submodule { + options = { + ip_prefix = mkOption { type = str; }; + cidr = mkOption { type = int; }; + gateway = mkOption { type = str; }; + domains = mkOption { + type = submodule { + options = { + internal = mkOption { type = str; }; + external = mkOption { type = str; }; + }; + }; + }; + master = mkOption { + type = submodule { + options = { + login = mkOption { type = str; }; + email = mkOption { type = str; }; + public_ssh_key = mkOption { type = str; }; + initial_htpasswd = mkOption { type = str; }; + }; + }; + }; + default_tz = mkOption { type = str; }; + country_code = mkOption { type = str; }; + currency = mkOption { type = str; }; + services = mkOption { + type = submodule { + log_sink = mkOption { type = str; }; # ip:port + metrics_sink = mkOption { type = str; }; # ip:port + }; + }; + + dns_provider = mkOption { type = str; }; + + other_hosts = mkOption { + type = listOf (submodule { + options = { + hostname = mkOption { type = str; }; + private = mkOption { + type = bool; + default = true; + }; + auth = mkOption { + type = bool; + default = true; + }; + addr = mkOption { + type = str; + description = "ip:port for the service"; + }; + useCustomCA = mkOption { + type = bool; + default = false; + description = "Whether to use a custom CA (pretty much hardcoded)"; + }; + }; + }); + default = [ ]; + }; + }; + }; + }; + + # Outputs + tf = mkOption { + type = types.attrs; + description = "Terraform resources"; + default = { }; + }; + nixosModule = mkOption { + type = types.attrs; + description = "NixOS system"; + default = { }; + }; + }; + + config = { + + tf.resource = mergeConf [ + (import ./containers-terraform-postgres.nix { inherit config tools lib; }) + (import ./containers-terraform-proxmox.nix { inherit config tools lib; }) + (import ./containers-terraform-authentik.nix { inherit config tools lib; }) + ]; + + nixosModule = lib.mapAttrs ( + container: def: + { config, pkgs, ... }: + let + keys = import ../config/_keys.nix; + ownKey = if (lib.hasAttr container keys) then keys.${container} else null; + in + mergeConf [ + (mergeConf ( + lib.attrValues ( + lib.mapAttrs ( + secretName': _: + let + secretName = lib.removeSuffix ".age" secretName'; + in + { + age.secrets.${secretName}.file = ../secrets/${secretName'}; + } + ) (lib.filterAttrs (_: entry: builtins.elem ownKey entry.publicKeys) secrets) + ) + )) + nixosTemplate + (lib.optionalAttrs (def.logging.enable) import ./containers-nixos-logging.nix { + inherit + config + tools + container + def + pkgs + ; + }) + { + networking.hostName = container; + networking.firewall = { + enable = true; + allowedTCPPorts = + def.system.additionalPorts + ++ (if (def.system.port != null) then [ def.system.port ] else [ ]) + ++ (if (def.logging.enable) then [ 12345 ] else [ ]); + allowedUDPPorts = def.system.udpPorts; + }; + services = def.system.services; + environment.etc = def.system.etcFiles; + environment.systemPackages = def.system.packages; + } + def.system.additional + (mergeConf ( + map ( + p: + import p { + inherit pkgs tools; + config = mergeConf [ + realcfg + config + ]; + } + ) def.system.importConfig + )) + ] + + ) cfg; + }; +} diff --git a/lib/lxc-template.nix b/modules/lxc-template.nix similarity index 80% rename from lib/lxc-template.nix rename to modules/lxc-template.nix index afad949..c01ce7a 100644 --- a/lib/lxc-template.nix +++ b/modules/lxc-template.nix @@ -1,11 +1,11 @@ { pkgs, - lib, - modulesPath, ... }: let - infra = import ./constants.nix; + lib = pkgs.lib; + modulesPath = pkgs.path + "/nixos/modules"; + config = import ../config/_globals.nix { }; in { imports = [ @@ -38,11 +38,11 @@ in options = "--delete-older-than 7d"; }; - time.timeZone = infra.default_tz; + time.timeZone = config.globals.default_tz; users.users.root = { openssh.authorizedKeys.keys = [ - infra.master_public_ssh_key + config.globals.master.public_ssh_key ]; initialPassword = "nixos"; }; diff --git a/modules/nixos-kiosk-iso.nix b/modules/nixos-kiosk-iso.nix new file mode 100644 index 0000000..c01ce7a --- /dev/null +++ b/modules/nixos-kiosk-iso.nix @@ -0,0 +1,53 @@ +{ + pkgs, + ... +}: +let + lib = pkgs.lib; + modulesPath = pkgs.path + "/nixos/modules"; + config = import ../config/_globals.nix { }; +in +{ + imports = [ + (modulesPath + "/virtualisation/proxmox-lxc.nix") + ]; + + boot.isContainer = true; + + systemd.suppressedSystemUnits = [ + "dev-mqueue.mount" + "sys-kernel-debug.mount" + "sys-fs-fuse-connections.mount" + ]; + environment.systemPackages = with pkgs; [ + vim + openssl + coreutils + ]; + services.openssh.enable = true; + nix.settings = { + experimental-features = [ + "nix-command" + "flakes" + ]; + auto-optimise-store = true; + }; + nix.gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 7d"; + }; + + time.timeZone = config.globals.default_tz; + + users.users.root = { + openssh.authorizedKeys.keys = [ + config.globals.master.public_ssh_key + ]; + initialPassword = "nixos"; + }; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + + system.stateVersion = "25.11"; +} diff --git a/modules/terraform-base.nix b/modules/terraform-base.nix new file mode 100644 index 0000000..fdf23f5 --- /dev/null +++ b/modules/terraform-base.nix @@ -0,0 +1,42 @@ +{ lib, ... }: +{ + terraform.required_providers = { + proxmox = { + source = "Telmate/proxmox"; + version = "~> 2.9.11"; + }; + + postgresql = { + source = "cyrilgdn/postgresql"; + version = "~> 1.26.0"; + }; + }; + + provider.proxmox = { + pm_api_url = "\${var.pm_api_url}"; + pm_api_token_id = "\${var.pm_api_token_id}"; + pm_api_token_secret = "\${var.pm_api_token_secret}"; + pm_tls_insecure = "\${var.pm_tls_insecure}"; + }; + + variable.pm_api_url.type = "string"; + variable.pm_api_token_id.type = "string"; + variable.pm_api_token_secret.type = "string"; + variable.pm_tls_insecure.type = "bool"; + variable.pve_node.type = "string"; + variable.ostemplate.type = "string"; + + provider.postgresql = { + host = "\${var.pg_host}"; + port = 5432; + database = "postgres"; + username = "\${var.pg_user}"; + password = "\${var.pg_pass}"; + sslmode = "disable"; + connect_timeout = 15; + }; + + variable.pg_host.type = "string"; + variable.pg_user.type = "string"; + variable.pg_pass.type = "string"; +} diff --git a/modules/tools.nix b/modules/tools.nix new file mode 100644 index 0000000..3455491 --- /dev/null +++ b/modules/tools.nix @@ -0,0 +1,31 @@ +{ + config, + lib, + ... +}: +let + build_ip = + arg: + ( + if (!lib.strings.isString arg) then + "${config.globals.ip_prefix}${toString arg}" + else + let + id = config.id.${arg}; + ip = if (id > 1000) then id - 1000 else id; + in + "${config.globals.ip_prefix}${toString ip}" + ); + build_ip_cidr = arg: "${build_ip arg}/${toString config.globals.cidr}"; + mask_cidr = build_ip_cidr 0; + build_hostname = arg: "${arg}${config.globals.domains.external}"; +in +{ + build_ip = build_ip; + build_ip_cidr = build_ip_cidr; + mask_cidr = mask_cidr; + build_hostname = build_hostname; + + loki_addr = "${build_ip "monitoring"}:3100"; + metrics_addr = "${build_ip "metrics"}:9090"; +} diff --git a/secrets/auth-authentik-ldap-secrets.age b/secrets/auth-authentik-ldap-secrets.age new file mode 100644 index 0000000..1eda0ed Binary files /dev/null and b/secrets/auth-authentik-ldap-secrets.age differ diff --git a/secrets/auth-authentik-proxy-secrets.age b/secrets/auth-authentik-proxy-secrets.age new file mode 100644 index 0000000..f17a17d Binary files /dev/null and b/secrets/auth-authentik-proxy-secrets.age differ diff --git a/secrets/auth-authentik-secrets.age b/secrets/auth-authentik-secrets.age new file mode 100644 index 0000000..cdd7229 Binary files /dev/null and b/secrets/auth-authentik-secrets.age differ diff --git a/secrets/db-postgres-initscript.age b/secrets/db-postgres-initscript.age new file mode 100644 index 0000000..b667b03 Binary files /dev/null and b/secrets/db-postgres-initscript.age differ diff --git a/secrets/finances-app-key.age b/secrets/finances-app-key.age new file mode 100644 index 0000000..f1d609f --- /dev/null +++ b/secrets/finances-app-key.age @@ -0,0 +1,8 @@ +age-encryption.org/v1 +-> ssh-ed25519 jxhkLg aQoOlZUoNaXXxfkMlkGx9zJDKQh+zlLyYrXuX+LEcFw +9c/dFd+LYdnb2TUm5+lxcPmFW8STMq6UALHlClL85jc +-> ssh-ed25519 UJuwpQ hnsSFl7MIkaG0DmCzZKoUtDLj/ey+YZ7Af4gEiPNtkc +2bmkqUGoh2kAW03X//iq/mlzOZeoS1PpmAmLWcAR48k +--- yMItyu2jgirF9YB+u26yykPuqEVz7T46oi6EDZ8rXYs +•û6v%aÇKFÛÞ1×49<$kHüC²bÄvÊ#dÜ¿$ë‰4 +õF5k*Ct¯±qUH%¶¶~ÇÓEíŒú³Å:e:÷d½¶èä·´k \ No newline at end of file diff --git a/secrets/power-password-file.age b/secrets/power-password-file.age new file mode 100644 index 0000000..3ca795b --- /dev/null +++ b/secrets/power-password-file.age @@ -0,0 +1,7 @@ +age-encryption.org/v1 +-> ssh-ed25519 jxhkLg +kc3WvRZu+M7FPObE9sUEBrRZUjaKQ3uDX01e30bvH4 +jp7GGPCdUHMFYAdZ6eHlb2Rpjbr7fgxO5i5A4JCuBFQ +-> ssh-ed25519 DVDL4g u3KhmxBa+ycZKj6g9/p9VfdWJe3sXNIYWqvnxS0LOFk ++6czbSa2PsgCNrsWFYtFJpW6YRttVpC3tlJpvMyKVlo +--- 6giEp6Qr8xXyII1KyBbEtT0a4qUkYtvby2NVshaHvK8 +©³˜Üú+& °ê³PWtÙFApOL¦ '¸¨ñ¬gÖ×Q[ä \ No newline at end of file diff --git a/secrets/proxy-dns-provider-config.age b/secrets/proxy-dns-provider-config.age new file mode 100644 index 0000000..c3cbcf7 Binary files /dev/null and b/secrets/proxy-dns-provider-config.age differ diff --git a/secrets/secrets.nix b/secrets/secrets.nix new file mode 100644 index 0000000..684ac63 --- /dev/null +++ b/secrets/secrets.nix @@ -0,0 +1,36 @@ +let + config = (import ../config/_globals.nix { }).globals; + users = [ + config.master.public_ssh_key + ]; + + keys = import ../config/_keys.nix; + common = builtins.attrValues (keys); +in +{ + # TODO: Probably there would be a way to guess the default service key from the secret prefix + "auth-authentik-ldap-secrets.age".publicKeys = users ++ [ + keys.auth + ]; + "auth-authentik-proxy-secrets.age".publicKeys = users ++ [ + keys.auth + ]; + "auth-authentik-secrets.age".publicKeys = users ++ [ + keys.auth + ]; + "db-postgres-initscript.age".publicKeys = users ++ [ + keys.db + ]; + "finances-app-key.age".publicKeys = users ++ [ + keys.finances + ]; + "power-password-file.age".publicKeys = users ++ [ + keys.power + ]; + "proxy-dns-provider-config.age".publicKeys = users ++ [ + keys.proxy + ]; + "yarrr-env.age".publicKeys = users ++ [ + keys.yarrr + ]; +} diff --git a/secrets/yarrr-env.age b/secrets/yarrr-env.age new file mode 100644 index 0000000..3608f6a Binary files /dev/null and b/secrets/yarrr-env.age differ diff --git a/terraform.tfvars.example b/terraform.tfvars.example index 3f362a3..a0c72ec 100644 --- a/terraform.tfvars.example +++ b/terraform.tfvars.example @@ -2,3 +2,9 @@ pm_api_url = "https://proxmox:8006/api2/json" pm_api_token_id = "terraform@pve" pm_api_token_secret = "secr3t" pve_node = "proxmox" + +pg_host = "10.0.0." +pg_user = "tf" +pg_pass = "terraf0rm" + +ostemplate = "nixos-image-lxc-proxmox-..."