Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sourcehut: include module #65109

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@
./services/misc/siproxd.nix
./services/misc/snapper.nix
./services/misc/sonarr.nix
./services/misc/sourcehut
./services/misc/spice-vdagentd.nix
./services/misc/ssm-agent.nix
./services/misc/sssd.nix
Expand Down
168 changes: 168 additions & 0 deletions nixos/modules/services/misc/sourcehut/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
{ config, pkgs, lib, ... }:

with lib;

let
cfg = config.services.sourcehut;
cfgIni = cfg.settings;

# Specialized python containing all the modules
python = pkgs.sourcehut.python.withPackages (ps: with ps; [
gunicorn
# Sourcehut services
buildsrht dispatchsrht gitsrht hgsrht listssrht mansrht
metasrht pastesrht todosrht
]);
in {
imports =
[
./git.nix
./hg.nix
./todo.nix
./man.nix
./meta.nix
./paste.nix
];

options.services.sourcehut = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Enable sourcehut - git hosting, continuous integration, mailing list, ticket tracking,
task dispatching, wiki and account management services.
'';
};

services = mkOption {
type = types.nonEmptyListOf (types.enum [ "builds" "dispatch" "git" "hg" "lists" "man" "meta" "paste" "todo" ]);
default = [ "builds" "dispatch" "git" "hg" "lists" "man" "meta" "paste" "todo" ];
description = ''
Services to enable on the sourcehut network.
'';
};

address = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
'';
};

python = mkOption {
internal = true;
type = types.package;
default = python;
description = ''
The python package to use. It should contain references to the *srht modules and also
gunicorn.
'';
};

statePath = mkOption {
type = types.path;
default = "/var/sourcehut";
description = ''
Root state path for the sourcehut network.
'';
};

settings = mkOption {
type = with types; attrsOf (attrsOf (nullOr (either bool (either int (either float (either str path))))));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will produce a crazy type like the one in #86402. Can you add a decription with // { description = "..."; }?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't there already a description for this in line 73?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think he meant an example maybe?

default = {};
description = ''
The configuration for the sourcehut network.
'';
};
};

config = mkIf cfg.enable {
assertions =
[
{ assertion = with cfgIni."sr.ht"; secret-key != null && stringLength secret-key == 32;
message = "sr.ht's secret key must be defined and of a 32 byte length."; }

# Is it always 44 characters...? At least from the times I've generated one...
{ assertion = with cfgIni.webhooks; private-key != null && stringLength private-key == 44;
message = "The webhook's private key must be defined."; }

{ assertion = hasAttrByPath [ "meta.sr.ht" "origin" ] cfgIni && cfgIni."meta.sr.ht".origin != null;
message = "meta.sr.ht's origin must be defined."; }
];

environment.etc."sr.ht/config.ini".text = let
mkKeyValue = key: v: let
isPath = v: builtins.typeOf v == "path";

value =
if null == v then ""
else if true == v then "yes"
else if false == v then "no"
else if isInt v then toString v
else if isString v then toString v
else if isPath v then toString v
else abort "sourcehut.mkKeyValue: unexpected type (v = ${v})";
in "${toString key}=${value}";
in generators.toINI { inherit mkKeyValue; } cfg.settings;

environment.systemPackages = [ python ];

# PostgreSQL server
services.postgresql.enable = true;
# Mail server
services.postfix.enable = mkOverride 999 true;
# Cron daemon
services.cron.enable = mkOverride 999 true;
# Redis server
services.redis = {
enable = true;
port = mkDefault 6379;
# TODO: localhost
bind = mkDefault "127.0.0.1";
# TODO: More like 2?
databases = mkDefault 8;
};

services.sourcehut.settings = {
# The name of your network of sr.ht-based sites
"sr.ht".site-name = mkDefault "sourcehut";
# The top-level info page for your site
"sr.ht".site-info = mkDefault "https://sourcehut.org";
# {{ site-name }}, {{ site-blurb }}
"sr.ht".site-blurb = mkDefault "the hacker's forge";
# If this != production, we add a banner to each page
"sr.ht".environment = mkDefault "development";
# Contact information for the site owners
"sr.ht".owner-name = mkDefault "Drew DeVault";
"sr.ht".owner-email = mkDefault "sir@cmpwn.com";
# The source code for your fork of sr.ht
"sr.ht".source-url = mkDefault "https://git.sr.ht/~sircmpwn/srht";
# A secret key to encrypt session cookies with
"sr.ht".secret-key = mkDefault null;

# Outgoing SMTP settings
mail.smtp-host = mkDefault null;
mail.smtp-port = mkDefault null;
mail.smtp-user = mkDefault null;
mail.smtp-password = mkDefault null;
mail.smtp-from = mkDefault null;
# Application exceptions are emailed to this address
mail.error-to = mkDefault null;
mail.error-from = mkDefault null;
# Your PGP key information (DO NOT mix up pub and priv here)
# You must remove the password from your secret key, if present.
# You can do this with gpg --edit-key [key-id], then use the passwd
# command and do not enter a new password.
mail.pgp-privkey = mkDefault null;
mail.pgp-pubkey = mkDefault null;
mail.pgp-key-id = mkDefault null;

# base64-encoded Ed25519 key for signing webhook payloads. This should be
# consistent for all *.sr.ht sites, as we'll use this key to verify signatures
# from other sites in your network.
#
# Use the srht-webhook-keygen command to generate a key.
webhooks.private-key = mkDefault null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Serious security problem here.
Any idea? I thought of just appending this part from the content of a secure file, creating a third, non-store config for the service to use.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC services e.g. ssh generate keys on service start if they're not present...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appending won't fix the problem, it'll still end up in the store. If you don't want it to be in the store, the only option is to have as an external file and you're better off setting it up yourself since I can't access the settings unless I parse the file's contents.

I'm pretty sure the generated key isn't suppose to be used for ssh connections.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why wont appending fix the problem? I dont think you understood what I meant. The file we're appending from is not in the store, and neiter is the file were appending to. Only the 'base' file (the one created here, without private keys) would be in the store.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I know, the only configuration file for sourcehut lies in /etc/sr.ht/config.ini since the relative config.ini is out of the question unless one starts overriding the derivation.

a = 1;
${builtins.readFile path_to_external_file}

Would have the file contents of path_to_external_file in the file in the store.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This really is a sourcehut bug. Secrets dont go in config files.

};
};
}
168 changes: 168 additions & 0 deletions nixos/modules/services/misc/sourcehut/git.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
{ config, lib, pkgs, ... }:

with lib;

let
cfg = config.services.sourcehut;
scfg = cfg.git;
iniKey = "git.sr.ht";

rcfg = config.services.redis;
drv = pkgs.sourcehut.gitsrht;
in {
options.services.sourcehut.git = {
user = mkOption {
type = types.str;
visible = false;
internal = true;
readOnly = true;
default = "git";
description = ''
User for git.sr.ht.
'';
};

port = mkOption {
type = types.int;
default = 5001;
description = ''
'';
};

database = mkOption {
type = types.str;
default = "git.sr.ht";
description = ''
PostgreSQL database name for git.sr.ht.
'';
};

statePath = mkOption {
type = types.path;
default = "${cfg.statePath}/gitsrht";
description = ''
State path for git.sr.ht.
'';
};
};

config = with scfg; lib.mkIf (cfg.enable && elem "git" cfg.services) {
# sshd refuses to run with `Unsafe AuthorizedKeysCommand ... bad ownership or modes for directory /nix/store`
environment.etc."ssh/gitsrht-dispatch" = {
mode = "0755";
text = ''
#! ${pkgs.stdenv.shell}
${cfg.python}/bin/gitsrht-dispatch $@
'';
};

# Needs this in the $PATH when sshing into the server
environment.systemPackages = [ pkgs.git ];

users = {
users = [
{ name = user;
group = user;
# https://stackoverflow.com/questions/22314298/git-push-results-in-fatal-protocol-error-bad-line-length-character-this
# Probably could use gitsrht-shell if output is restricted to just parameters...
shell = "${pkgs.bash}/bin/bash";
description = "git.sr.ht user"; }
];

groups = [
{ name = user; }
];
};

services = {
cron.systemCronJobs = [ "*/20 * * * * ${cfg.python}/bin/gitsrht-periodic" ];
fcgiwrap.enable = true;

openssh.extraConfig = ''
AuthorizedKeysCommand /etc/ssh/gitsrht-dispatch "%u" "%h" "%t" "%k"
AuthorizedKeysCommandUser root
PermitUserEnvironment SRHT_*
'';

postgresql = {
authentication = ''
local ${database} ${user} trust
'';
ensureDatabases = [ database ];
ensureUsers = [
{ name = user;
ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; }; }
];
};
};

systemd = {
tmpfiles.rules = [
# /var/log is owned by root
"f /var/log/git-srht-shell 0644 ${user} ${user} -"

"d ${statePath} 0750 ${user} ${user} -"
"d ${cfg.settings."${iniKey}".repos} 2755 ${user} ${user} -"
];

services = {
gitsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
after = [ "redis.service" "postgresql.service" "network.target" ];
requires = [ "redis.service" "postgresql.service" ];
wantedBy = [ "multi-user.target" ];

# Needs internally to create repos at the very least
path = [ pkgs.git ];
description = "git.sr.ht website service";

serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
};

gitsrht-webhooks = {
after = [ "postgresql.service" "network.target" ];
requires = [ "postgresql.service" ];
wantedBy = [ "multi-user.target" ];

description = "git.sr.ht webhooks service";
serviceConfig = {
Type = "simple";
User = user;
Restart = "always";
};

serviceConfig.ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
};
};
};

services.sourcehut.settings = {
# URL git.sr.ht is being served at (protocol://domain)
"git.sr.ht".origin = mkDefault "http://git.sr.ht.local";
# Address and port to bind the debug server to
"git.sr.ht".debug-host = mkDefault "0.0.0.0";
"git.sr.ht".debug-port = mkDefault port;
# Configures the SQLAlchemy connection string for the database.
"git.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
# Set to "yes" to automatically run migrations on package upgrade.
"git.sr.ht".migrate-on-upgrade = mkDefault "yes";
# The redis connection used for the webhooks worker
"git.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/1";
# A post-update script which is installed in every git repo.
"git.sr.ht".post-update-script = mkDefault "${cfg.python}/bin/gitsrht-update-hook";
# git.sr.ht's OAuth client ID and secret for meta.sr.ht
# Register your client at meta.example.org/oauth
"git.sr.ht".oauth-client-id = mkDefault null;
"git.sr.ht".oauth-client-secret = mkDefault null;
# Path to git repositories on disk
"git.sr.ht".repos = mkDefault "/var/lib/git";

# The authorized keys hook uses this to dispatch to various handlers
# The format is a program to exec into as the key, and the user to match as the
# value. When someone tries to log in as this user, this program is executed
# and is expected to omit an AuthorizedKeys file.
#
# Uncomment the relevant lines to enable the various sr.ht dispatchers.
"git.sr.ht::dispatch"."/run/current-system/sw/bin/gitsrht-keys" = mkDefault "${user}:${user}";
};
};
}
Loading