diff --git a/default.nix b/default.nix index 6e0834dd..76178382 100644 --- a/default.nix +++ b/default.nix @@ -8,16 +8,19 @@ inherit system; }, lib ? import "${sources.nixpkgs}/lib", + name ? "web-security-tracker", }: rec { - inherit pkgs; + inherit pkgs name; inherit (pkgs) python3; localPythonPackages = import ./pkgs { inherit pkgs python3; }; # For exports. overlays = [ overlay ]; + # TODO: `callPackage` the derivation here instead of splicing through + # overlays, it's needlessly hard to follow package = pkgs.web-security-tracker; - module = import ./nix/web-security-tracker.nix; + module = ./nix/web-security-tracker.nix; dev-container = import ./infra/container.nix; dev-setup = import ./nix/dev-setup.nix; @@ -137,6 +140,7 @@ rec { GH_ISSUES_REPO = "sectracker-testing"; GH_SECURITY_TEAM = "setracker-testing-security"; GH_COMMITTERS_TEAM = "sectracker-testing-committers"; + STATIC_ROOT = "${toString ./src/website/static}"; }; }; @@ -166,5 +170,5 @@ rec { ''; }; - tests = pkgs.callPackage ./nix/tests.nix { }; + tests = pkgs.callPackage ./nix/tests.nix { application = name; }; } diff --git a/infra/configuration.nix b/infra/configuration.nix index e926241b..e76c5547 100644 --- a/infra/configuration.nix +++ b/infra/configuration.nix @@ -137,7 +137,7 @@ in values = [ "unix_timestamp" ]; }; }; - connections = [ "postgres://postgres@/web-security-tracker?host=/run/postgresql" ]; + connections = [ "postgres://postgres@/${application}?host=/run/postgresql" ]; interval = "1h"; }; }; diff --git a/infra/container.nix b/infra/container.nix index 22c5ed13..0b9e5059 100644 --- a/infra/container.nix +++ b/infra/container.nix @@ -5,11 +5,12 @@ ... }: let + application = "web-security-tracker"; sectracker = import ../. { }; secretsPath = "/etc/secrets"; secretsGuestPath = "/mnt/secrets"; secretsHostPath = toString ../.credentials; - cfg = config.containers.nix-security-tracker; + cfg = config.containers.${application}; in { /** @@ -18,17 +19,17 @@ in The container can be managed at runtime with [`nixos-container`](https://nixos.org/manual/nixos/unstable/#sec-imperative-containers). */ - users.users.web-security-tracker = { + users.users.${application} = { isSystemUser = true; - group = "web-security-tracker"; + group = application; }; - users.groups.web-security-tracker = { }; - systemd.services."container@nix-security-tracker" = { + users.groups.${application} = { }; + systemd.services."container@${application}" = { serviceConfig = { TimeoutStartSec = lib.mkForce "15m"; }; }; - containers.nix-security-tracker = { + containers.${application} = { autoStart = true; privateNetwork = true; # local address range that is unlikely to collide with something else @@ -56,7 +57,7 @@ in # which almost certainly won't be the same as the user under which the service runs boot.postBootCommands = '' cp -r ${secretsGuestPath}/* ${secretsPath} - chown web-security-tracker ${secretsPath} + chown ${application} ${secretsPath} ''; networking.firewall.allowedTCPPorts = map (forward: forward.containerPort) ( lib.filter (forward: forward.protocol == "tcp") cfg.forwardPorts @@ -67,7 +68,7 @@ in imports = [ sectracker.module ]; - services.web-security-tracker = { + services.${application} = { enable = true; domain = "sectracker.local"; production = false; diff --git a/infra/sectracker.nix b/infra/sectracker.nix index b49eaf7f..ef48f858 100644 --- a/infra/sectracker.nix +++ b/infra/sectracker.nix @@ -51,7 +51,7 @@ in 80 443 ]; - services.web-security-tracker = { + services.${sectracker.name} = { enable = true; production = true; domain = "tracker.security.nixos.org"; diff --git a/nix/tests.nix b/nix/tests.nix index 41f3428d..286a5c62 100644 --- a/nix/tests.nix +++ b/nix/tests.nix @@ -1,7 +1,9 @@ -{ lib, pkgs }: +{ + lib, + pkgs, + application, +}: let - # TODO: specify project/service name globally - application = "web-security-tracker"; defaults = { documentation.enable = lib.mkDefault false; diff --git a/nix/web-security-tracker.nix b/nix/web-security-tracker.nix index 65ee7252..0435dfc8 100644 --- a/nix/web-security-tracker.nix +++ b/nix/web-security-tracker.nix @@ -18,8 +18,10 @@ let recursiveUpdate optionalString ; + # TODO: make it somehow configurable from the outside... modular services anyone? + application = "web-security-tracker"; inherit (pkgs) writeScriptBin writeShellApplication stdenv; - cfg = config.services.web-security-tracker; + cfg = config.services.${application}; pythonFmt = pkgs.formats.pythonVars { }; settingsFile = pythonFmt.generate "wst-settings.py" cfg.settings; @@ -50,15 +52,15 @@ let text = '' sudo="exec" - if [[ "$USER" != "web-security-tracker" ]]; then - sudo='exec /run/wrappers/bin/sudo -u web-security-tracker --preserve-env --preserve-env=PYTHONPATH' + if [[ "$USER" != "${application}" ]]; then + sudo='exec /run/wrappers/bin/sudo -u ${application} --preserve-env --preserve-env=PYTHONPATH' fi export PYTHONPATH=${toString cfg.package.pythonPath} $sudo ${cfg.package}/bin/manage.py "$@" ''; }; credentials = mapAttrsToList (name: secretPath: "${name}:${secretPath}") cfg.secrets; - databaseUrl = "postgres:///web-security-tracker"; + databaseUrl = "postgres:///${application}"; environment = { DATABASE_URL = databaseUrl; @@ -75,9 +77,9 @@ let --collect \ --service-type=exec \ --unit "wst-manage.service" \ - --property "User=web-security-tracker" \ - --property "Group=web-security-tracker" \ - --property "WorkingDirectory=/var/lib/web-security-tracker" \ + --property "User=${application}" \ + --property "Group=${application}" \ + --property "WorkingDirectory=${cfg.stateDir}/${application}" \ ${concatStringsSep "\n" (map (cred: "--property 'LoadCredential=${cred}' \\") credentials)} --property "Environment=${ toString (lib.mapAttrsToList (name: value: "${name}=${value}") environment) @@ -86,9 +88,10 @@ let ''; in { - options.services.web-security-tracker = { + options.services.${application} = { enable = mkEnableOption "web security tracker for Nixpkgs and similar monorepo"; + # TODO: `callPackage` the derivation here instead of splicing through overlays, it's needlessly hard to follow package = mkPackageOption pkgs "web-security-tracker" { }; production = mkOption { type = types.bool; @@ -108,9 +111,21 @@ in type = types.nullOr types.str; default = null; }; - env = mkOption { + stateDir = mkOption { + description = "directory for keeping file system state"; + type = types.path; + default = "/var/lib"; + }; + env = mkOption rec { + description = '' + Django configuration via environment variables, see `settings.py` for options. + ''; type = types.attrsOf types.anything; - default = { }; + default = { + STATIC_ROOT = "${cfg.stateDir}/${application}/static/"; + }; + # only override defaults with explicit values + apply = lib.recursiveUpdate default; }; settings = mkOption { type = types.attrsOf types.anything; @@ -151,9 +166,7 @@ in environment.systemPackages = [ wstExternalManageScript ]; services = { # TODO(@fricklerhandwerk): move all configuration over to pydantic-settings - web-security-tracker.settings = { - STATIC_ROOT = mkDefault "/var/lib/web-security-tracker/static"; - DEBUG = mkDefault false; + ${application}.settings = { ALLOWED_HOSTS = mkDefault [ (with cfg; if production then domain else "*") "localhost" @@ -161,10 +174,10 @@ in "[::1]" ]; CSRF_TRUSTED_ORIGINS = mkDefault [ "https://${cfg.domain}" ]; - EVALUATION_GC_ROOTS_DIRECTORY = mkDefault "/var/lib/web-security-tracker/gc-roots"; - EVALUATION_LOGS_DIRECTORY = mkDefault "/var/log/web-security-tracker/evaluation"; - LOCAL_NIXPKGS_CHECKOUT = mkDefault "/var/lib/web-security-tracker/nixpkgs-repo"; - CVE_CACHE_DIR = mkDefault "/var/lib/web-security-tracker/cve-cache"; + EVALUATION_GC_ROOTS_DIRECTORY = mkDefault "${cfg.stateDir}/${application}/gc-roots"; + EVALUATION_LOGS_DIRECTORY = mkDefault "${cfg.stateDir}/${application}/evaluation"; + LOCAL_NIXPKGS_CHECKOUT = mkDefault "${cfg.stateDir}/${application}/nixpkgs-repo"; + CVE_CACHE_DIR = mkDefault "${cfg.stateDir}/${application}/cve-cache"; ACCOUNT_DEFAULT_HTTP_PROTOCOL = mkDefault (with cfg; if production then "https" else "http"); }; @@ -174,7 +187,7 @@ in { locations = { "/".proxyPass = "http://localhost:${toString cfg.wsgi-port}"; - "/static/".alias = "/var/lib/web-security-tracker/static/"; + "/static/".alias = cfg.env.STATIC_ROOT; }; } // lib.optionalAttrs cfg.production { @@ -187,19 +200,19 @@ in postgresql = { ensureUsers = [ { - name = "web-security-tracker"; + name = application; ensureDBOwnership = true; } ]; - ensureDatabases = [ "web-security-tracker" ]; + ensureDatabases = [ application ]; }; }; - users.users.web-security-tracker = { + users.users.${application} = { isSystemUser = true; - group = "web-security-tracker"; + group = application; }; - users.groups.web-security-tracker = { }; + users.groups.${application} = { }; systemd.services = let @@ -210,18 +223,18 @@ in pkgs.nix-eval-jobs ]; serviceConfig = { - User = "web-security-tracker"; - WorkingDirectory = "/var/lib/web-security-tracker"; - StateDirectory = "web-security-tracker"; - RuntimeDirectory = "web-security-tracker"; - LogsDirectory = "web-security-tracker"; + User = application; + WorkingDirectory = "${cfg.stateDir}/${application}"; + StateDirectory = application; + RuntimeDirectory = application; + LogsDirectory = application; LoadCredential = credentials; }; inherit environment; }; in mapAttrs (_: recursiveUpdate defaults) { - web-security-tracker-server = { + "${application}-server" = { description = "A web security tracker ASGI server"; after = [ "network.target" @@ -235,7 +248,7 @@ in }; preStart = '' # Auto-migrate on first run or if the package has changed - versionFile="/var/lib/web-security-tracker/package-version" + versionFile="${cfg.stateDir}/${application}/package-version" if [[ $(cat "$versionFile" 2>/dev/null) != ${cfg.package} ]]; then wst-manage migrate --no-input wst-manage collectstatic --no-input --clear @@ -256,12 +269,12 @@ in ''; }; - web-security-tracker-worker = { + "${application}-worker" = { description = "Web security tracker - background job processor"; after = [ "network.target" "postgresql.service" - "web-security-tracker-server.service" + "${application}-server.service" ]; requires = [ "postgresql.service" ]; wantedBy = [ "multi-user.target" ]; @@ -274,13 +287,13 @@ in ''; }; - web-security-tracker-fetch-all-channels = { + "${application}-fetch-all-channels" = { description = "Web security tracker - refresh all channels and start nixpkgs evaluation"; after = [ "network.target" "postgresql.service" - "web-security-tracker-server.service" + "${application}-server.service" ]; requires = [ "postgresql.service" ]; @@ -294,12 +307,12 @@ in startAt = "*-*-* 04:00:00"; }; - web-security-tracker-delta = { + "${application}-delta" = { description = "Web security tracker catch up with CVEs"; after = [ "network.target" "postgresql.service" - "web-security-tracker-server.service" + "${application}-server.service" ]; requires = [ "postgresql.service" ]; serviceConfig.Type = "oneshot"; diff --git a/src/website/tracker/settings.py b/src/website/tracker/settings.py index 6b9c76d2..909df086 100644 --- a/src/website/tracker/settings.py +++ b/src/website/tracker/settings.py @@ -38,6 +38,11 @@ class Settings(BaseSettings): class DjangoSettings(BaseModel): # SECURITY WARNING: don't run with debug turned on in production! DEBUG: bool = False + STATIC_ROOT: Path = Field( + description=""" + Writeable directory for compilimg static files, such as stylesheets, when running `manage collectstatic`. + """ + ) SYNC_GITHUB_STATE_AT_STARTUP: bool = Field( description=""" Connect to GitHub when the service is started and update