about summary refs log blame commit diff
path: root/configuration.nix
blob: be24ec050c4572badf6cc561cc98554ec62fd2e3 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17















                                                                     
                                                                                




                                                                   
                          

















                                                                                                           
                                  
                                                                        

                                                                  

                                                                          



                                                                           
           
                               
                                      
                                                   
                                  
                                                                                     
                                                                                  

                                                                                  
        

                                                  
                                  
                                                                                         
                                                                          

                                             








                                                                             








                                                                    








                                                                                                                          
                                                                                                                                                                                                    


                                
                  
                                                                                                                                                           

                                                                                                            



                                                                 


                                                                                     
                      

                            






                                                      
                                        




                             
                                               
                                     
                                         
                                          
                                        
                                     
                                                               






                                                                                                


                                                                          




                                        

































                                                                     


        
kittybox:
{ config, pkgs, lib, ... }:
with lib;
let
  cfg = config.services.kittybox;
in {
  options = {
    services.kittybox = {
      enable = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Whether to enable Kittybox, the IndieWeb blogging solution.
        '';
      };
      package = mkOption {
        type = types.package;
        default = kittybox.packages.${pkgs.stdenv.hostPlatform.system}.kittybox;
        defaultText = "<kittybox package from the upstream flake>";
        description = "Which Kittybox derivation to use.";
      };

      bind = mkOption {
        type = types.nullOr types.str;
        default = "[::1]";
        description = "The host for Kittybox to bind to.";
        example = "192.168.1.100";
      };
      port = mkOption {
        type = types.int;
        default = 8080;
        description = "The port for Kittybox to listen at.";
        example = 16420;
      };
      logLevel = mkOption {
        type = types.str;
        default = "warn";
        example = "info";
        description = "Specify the server verbosity level. Uses RUST_LOG environment variable internally.";
      };
      backendUri = mkOption {
        type = types.str;
        default = "file:///var/lib/kittybox/data";
        example = "redis://192.168.1.200:6379";
        description = lib.mdDoc ''
          Set the backend used for storing data. Available backends are:
           - `postgres://` - PostgreSQL backend (recommended)
           - `file://`     - static folder backend
           - `redis://`    - Redis backend (currently unavailable)

          Make sure that if you are using the file backend, the state
          directory is accessible by Kittybox. By default, the unit config
          uses DynamicUser=true and heavy sandboxing options which prevent
          the unit from accessing data outside of its directory. It is
          recommended to reconfigure the sandboxing or use a bind-mount to
          `/var/lib/private/kittybox` if you require the state directory to
          reside elsewhere.
        '';
      };
      blobstoreUri = mkOption {
        type = types.nullOr types.str;
        default = "file:///var/lib/kittybox/media";
        description = lib.mdDoc ''
          Set the backend used for the media endpoint storage. Available options are:
            - `file://` - content-addressed storage using flat files (recommended)

          When using the file backend, check notes in the `backendUri` option too.
        '';
      };
      authstoreUri = mkOption {
        type = types.nullOr types.str;
        default = "file:///var/lib/kittybox/auth";
        description = lib.mdDoc ''
          Set the backend used for persisting authentication data. Available options are:
           - `file://` - flat files. Codes are stored globally, tokens and
             credentials are stored per-site.
        '';
      };
      jobQueueUri = mkOption {
        type = types.nullOr types.str;
        default = "postgres://localhost/kittybox?host=/run/postgresql";
        description = lib.mdDoc ''
          Set the job queue backend. Available options are:
           - `postgres://` - PostgreSQL based job queue. It shares the schema
             with the Kittybox PostgreSQL backend, so Kittybox can reuse the
             same database for both.
        '';
      };
      microsubServer = mkOption {
        type = types.nullOr types.str;
        default = null;
        example = "https://aperture.p3k.io/microsub/69420";
        description = ''
          The URL of your Microsub server, which saves feeds for you
          and allows you to browse Web content from one place. Try
          https://aperture.p3k.io/ if you don't have one yet!
        '';
      };
      internalTokenFile = mkOption {
        type = types.nullOr types.str;
        default = null;
        example = "/run/secrets/kittybox-shared-secret";
        description = "A shared secret that will, when passed, allow unlimited editing access to database. Keep it safe.";
      };
      cookieSecretFile = mkOption {
        type = types.str;
        default = "/var/lib/kittybox/cookie_secret_key";
        example = "/run/secrets/kittybox-cookie-secret";
        description = "A secret file to encrypt cookies with the contents of. Should be at least 32 bytes in length. A random persistent file will be generated if it doesn't exist or is invalid.";
      };
    };
  };
  config = lib.mkIf cfg.enable {
    assertions = [
      {
        assertion = (lib.strings.hasPrefix cfg.backendUri "postgres://" || lib.strings.hasPrefix cfg.jobQueueUri "postgres://") -> cfg.package.hasPostgres;
        message = "To use the Postgres backend, Kittybox has to be compiled with Postgres support enabled.";
      }
    ];
    systemd.sockets.kittybox = {
      description = config.systemd.services.kittybox.description;
      wantedBy = [ "sockets.target" ];

      restartTriggers = [ cfg.bind cfg.port ];
      listenStreams = lib.mkMerge [
        [ (lib.mkIf (cfg.bind == null) (builtins.toString cfg.port)) ]
        [ (lib.mkIf (cfg.bind != null) "${cfg.bind}:${builtins.toString cfg.port}") ]
      ];
      socketConfig = {
        BindIPv6Only = true;
      };
    };
    systemd.services.kittybox = {
      description = "An IndieWeb-enabled blog engine";

      wantedBy = [ "multi-user.target" ];
      after = [ "network.target" ];

      restartTriggers = [
        cfg.package
        cfg.backendUri cfg.blobstoreUri
        cfg.authstoreUri cfg.jobQueueUri
        cfg.internalTokenFile
        cfg.bind cfg.port
        cfg.cookieSecretFile
      ];

      environment = {
        MICROSUB_ENDPOINT = cfg.microsubServer;
        BACKEND_URI = cfg.backendUri;
        BLOBSTORE_URI = cfg.blobstoreUri;
        AUTH_STORE_URI = cfg.authstoreUri;
        JOB_QUEUE_URI = cfg.jobQueueUri;
        RUST_LOG = "${cfg.logLevel}";
        # TODO: consider hardening by using systemd credentials
        COOKIE_KEY_FILE = "${cfg.cookieSecretFile}";
      };

      script = ''
        ${lib.optionalString (cfg.internalTokenFile != null) ''
          if [[ -f ${cfg.internalTokenFile} ]]; then
            export KITTYBOX_INTERNAL_TOKEN=$(${pkgs.coreutils}/bin/cat ${cfg.internalTokenFile})
          fi
        ''}
        if [[ ! -e "$COOKIE_KEY_FILE" ]]; then
            dd if=/dev/urandom bs=64 count=1 | base64 > "$COOKIE_KEY_FILE"
        fi
        export COOKIE_KEY="$(cat "$COOKIE_KEY_FILE")"
        exec ${cfg.package}/bin/kittybox
      '';

      serviceConfig = {
        DynamicUser = true;
        StateDirectory = "kittybox";
        # Hardening
        NoNewPrivileges = true;
        CapabilityBoundingSet = "";
        ProtectSystem = "strict";
        ProtectHome = true;
        ProtectHostname = true;
        ProtectClock = true;
        ProtectKernelTunables = true;
        ProtectKernelModules = true;
        ProtectKernelLogs = true;
        ProtectControlGroups = true;
        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
        RestrictNamespaces = true;
        LockPersonality = true;
        MemoryDenyWriteExecute = true;
        RestrictRealtime = true;
        RestrictSUIDSGID = true;
        RemoveIPC = true;
        ProtectProc = "invisible";
        SystemCallArchitectures = "native";
        SystemCallFilter = [
          "@aio"
          "@basic-io"
          "@file-system"
          "@io-event"
          "@network-io"
          "@sync"
          "@system-service"
          "~@resources"
          "~@privileged"
        ];
        PrivateDevices = true;
        DeviceAllow = [ "" ];
        UMask = "0077";
        IPAddressDeny = [ "link-local" "multicast" ];
      };
    };
  };
}