Skip to content

Commit

Permalink
mcaptcha package and module with nixos tests
Browse files Browse the repository at this point in the history
This completes #17

Co-authored-by: Shahar "Dawn" Or <[email protected]>
Co-authored-by: Rohit <[email protected]>
Co-authored-by: Matúš Ferech <[email protected]>
Co-authored-by: Alejandro Sanchez Medina <[email protected]>
  • Loading branch information
5 people committed Sep 27, 2023
1 parent da86490 commit adf9abb
Show file tree
Hide file tree
Showing 14 changed files with 5,799 additions and 0 deletions.
2 changes: 2 additions & 0 deletions all-packages.nix
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

libgnunetchat = callPackage ./pkgs/libgnunetchat {};
librecast = callPackage ./pkgs/librecast {inherit lcrq;};
mcaptcha = callPackage ./pkgs/mcaptcha {};
mcaptcha-cache = callPackage ./pkgs/mcaptcha-cache {};
pretalx = callPackage ./pkgs/pretalx {};
pretalx-frontend = callPackage ./pkgs/pretalx/frontend.nix {};
pretalx-full = callPackage ./pkgs/pretalx {
Expand Down
1 change: 1 addition & 0 deletions modules/all-modules.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Refer to <https://github.com/ngi-nix/ngipkgs/issues/40>.
#liberaforms = import ./liberaforms.nix;
flarum = import ./flarum.nix;
mcaptcha = import ./mcaptcha.nix;
pretalx = import ./pretalx.nix;
rosenpass = import ./rosenpass.nix;
unbootable = import ./unbootable.nix;
Expand Down
268 changes: 268 additions & 0 deletions modules/mcaptcha.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
{
config,
lib,
options,
pkgs,
...
}:
with builtins;
with lib; let
cfg = config.services.mcaptcha;

# mCaptcha has no support for defaults. Every option must be specified.
# The module-provided defaults below are based on
# https://github.com/mCaptcha/mCaptcha/blob/f337ee0643d88723776e1de4e5588dfdb6c0c574/config/default.toml
settings = {
debug = false;
source_code = "https://github.com/mCaptcha/mCaptcha";
commercial = false;
allow_demo = false;
allow_registration = true;

server = {
port = cfg.server.port;
domain = cfg.server.host;
ip = cfg.server.bindAddress;
proxy_has_tls = false;
};

database =
{
pool = 4;
database_type = "postgres";
}
// lib.optionalAttrs (!cfg.database.createLocally) {
username = cfg.database.user;
hostname = cfg.database.host;
port = cfg.database.port;
name = cfg.database.name;
};

captcha = {
gc = 30;
runners = 4;
queue_length = 2000;
enable_stats = true;

default_difficulty_strategy = {
avg_traffic_difficulty = 50000;
peak_sustainable_traffic_difficulty = 3000000;
broke_my_site_traffic_difficulty = 5000000;
duration = 30;
};
};

redis = {
pool = 4;
};
};

configFile = (pkgs.formats.toml {}).generate "mcaptcha.config.toml" (lib.recursiveUpdate settings cfg.extraSettings);
in {
options.services.mcaptcha.enable = mkEnableOption "Enable mCaptcha server.";
options.services.mcaptcha.package = mkPackageOption pkgs "mcaptcha" {};

options.services.mcaptcha.extraSettings = mkOption {
type = types.attrs;
description = ''
Extra settings. Best sources of documentation for settings seem to be
https://github.com/mCaptcha/mCaptcha/blob/master/config/default.toml
https://github.com/mCaptcha/mCaptcha/blob/master/docs/CONFIGURATION.md
'';
default = {};
};

options.services.mcaptcha.user = mkOption {
type = types.str;
description = "User account to run under.";
default = "mcaptcha";
};

options.services.mcaptcha.group = mkOption {
type = types.str;
description = "Group for the user mCaptcha runs under.";
default = "mcaptcha";
};

options.services.mcaptcha.database.createLocally = mkOption {
type = types.bool;
description = "Whether to create and use a local databse instance";
default = false;
};

options.services.mcaptcha.database.passwordFile = mkOption {
type = types.nullOr types.path;
description = ''
Path to a file containing a database password.
Ignored when `database.createLocally`.
'';
default = null;
example = "/run/secrets/mcaptcha/database";
};

options.services.mcaptcha.database.name = mkOption {
type = types.str;
description = "Applies both when `database.createLocally` is set and not.";
default = "mcaptcha";
};

options.services.mcaptcha.database.user = mkOption {
type = types.str;
description = "Ignored when `database.createLocally`.";
example = "mcaptcha";
};

options.services.mcaptcha.database.host = mkOption {
type = types.str;
description = "Ignored when `database.createLocally`.";
example = "localhost";
};

options.services.mcaptcha.database.port = mkOption {
type = types.int;
description = "Ignored when `database.createLocally`.";
example = 5432;
};

options.services.mcaptcha.server.cookieSecretFile = mkOption {
type = types.path;
description = "Path to a file containing a cookie secret.";
example = "/run/secrets/mcaptcha/cookie-secret";
};

options.services.mcaptcha.captcha.saltFile = mkOption {
type = types.path;
description = "Path to a file containing a salt.";
example = "/run/secrets/mcaptcha/salt";
};

options.services.mcaptcha.redis.createLocally = mkOption {
type = types.bool;
description = "Whether to create a Redis instance locally.";
default = false;
};

options.services.mcaptcha.redis.host = mkOption {
type = types.str;
description = "Ignored when `redis.createLocally`.";
example = "redis.example.com";
};

options.services.mcaptcha.redis.port = mkOption {
type = types.int;
description = "Applies both when `redis.createLocally` is set and not.";
default = 6379;
};

options.services.mcaptcha.redis.user = mkOption {
type = types.str;
description = "Ignored when `redis.createLocally`.";
default = "default";
example = "mcaptcha";
};

options.services.mcaptcha.redis.passwordFile = mkOption {
type = types.path;
description = ''
Path to a file containing the Redis server password.
Ignored when `redis.createLocally`.";
'';
example = "/run/secrets/mcaptcha/redis-secret";
};

options.services.mcaptcha.server.port = mkOption {
type = types.int;
description = "Web server port.";
default = 7000;
};

options.services.mcaptcha.server.host = mkOption {
type = types.str;
description = "Web server host.";
default = "localhost";
example = "example.com";
};

options.services.mcaptcha.server.bindAddress = mkOption {
type = types.str;
description = "Web server IP addresses to bind to.";
default = "127.0.0.1";
example = "0.0.0.0";
};

config = mkIf cfg.enable {
systemd.services.mcaptcha.description = "mCaptcha: a CAPTCHA system that gives attackers a run for their money";

systemd.services.mcaptcha.script = let
serverCookieSecret = "export MCAPTCHA_SERVER_COOKIE_SECRET=$(< ${cfg.server.cookieSecretFile})";
captchaSalt = "export MCAPTCHA_CAPTCHA_SALT=$(< ${cfg.captcha.saltFile})";
databaseLocalUrl = ''export DATABASE_URL="postgres:///${cfg.database.name}?host=/run/postgresql"'';
databasePassword = "export MCAPTCHA_DATABASE_PASSWORD=$(< ${cfg.database.passwordFile})";
redisLocalUrl = ''export MCAPTCHA_REDIS_URL="redis://${cfg.redis.host}:${builtins.toString cfg.redis.port}"'';
redisRemoteUrl = ''
redis_user=$(${pkgs.urlencode}/bin/urlencode -e userinfo ${lib.escapeShellArg cfg.redis.user})
redis_pass=$(${pkgs.urlencode}/bin/urlencode -e userinfo < ${cfg.redis.passwordFile})
export MCAPTCHA_REDIS_URL="redis://$redis_user:$redis_pass@${cfg.redis.host}:${builtins.toString cfg.redis.port}"
'';
exec = "exec ${cfg.package}/bin/mcaptcha";
in
concatStringsSep "\n" [
serverCookieSecret
captchaSalt
(
if cfg.database.createLocally
then databaseLocalUrl
else databasePassword
)
(
if cfg.redis.createLocally
then redisLocalUrl
else redisRemoteUrl
)
exec
];

systemd.services.mcaptcha.environment.MCAPTCHA_CONFIG = builtins.toString configFile;
systemd.services.mcaptcha.after = ["syslog.target"] ++ lib.optionals cfg.database.createLocally ["postgresql.service"];
systemd.services.mcaptcha.bindsTo = lib.optionals cfg.database.createLocally ["postgresql.service"];
systemd.services.mcaptcha.wants = ["network-online.target"];
systemd.services.mcaptcha.wantedBy = ["multi-user.target"];
# Settings modeled after https://github.com/mCaptcha/mCaptcha/blob/f337ee0643d88723776e1de4e5588dfdb6c0c574/docs/DEPLOYMENT.md#6-systemd-service-configuration
systemd.services.mcaptcha.serviceConfig.User = cfg.user;
systemd.services.mcaptcha.serviceConfig.Type = "simple";
systemd.services.mcaptcha.serviceConfig.Restart = "on-failure";
systemd.services.mcaptcha.serviceConfig.RestartSec = 1;
systemd.services.mcaptcha.serviceConfig.SuccessExitStatus = "3 4";
systemd.services.mcaptcha.serviceConfig.RestartForceExitStatus = "3 4";
systemd.services.mcaptcha.serviceConfig.SystemCallArchitectures = "native";
systemd.services.mcaptcha.serviceConfig.MemoryDenyWriteExecute = true;
systemd.services.mcaptcha.serviceConfig.NoNewPrivileges = true;

users.users."${cfg.user}" = {
isSystemUser = true;
group = cfg.group;
};

users.groups."${cfg.group}" = {};

services.postgresql = lib.mkIf cfg.database.createLocally {
enable = true;
ensureDatabases = [cfg.database.name];
ensureUsers = [
{
name = cfg.user;
ensurePermissions = {"DATABASE ${cfg.database.name}" = "ALL PRIVILEGES";};
}
];
};

services.redis.servers.mcaptcha = lib.mkIf cfg.redis.createLocally {
enable = true;
port = cfg.redis.port;
extraParams = ["--loadmodule" "${pkgs.mcaptcha-cache}/lib/libcache.so"];
};
services.mcaptcha.redis.host = lib.mkIf cfg.redis.createLocally "127.0.0.1";
};
}
Loading

0 comments on commit adf9abb

Please sign in to comment.