Compare commits

...

1 Commits

Author SHA1 Message Date
h7x4 b9c7e0f40f
WIP 2024-08-13 19:31:39 +02:00
5 changed files with 207 additions and 1 deletions

View File

@ -6,7 +6,8 @@ let
in {
imports = [
./ci.nix
./import-users.nix
./import-users
./web-secret-provider
];
sops.secrets = {

View File

@ -0,0 +1,100 @@
{ config, pkgs, lib, ... }:
let
sops.secrets = {
"gitea/web-secret-provider/Drift" = {
owner = "gitea";
group = "gitea";
restartUnits = [ "gitea-web-secret-provider@Drift" ];
};
"gitea/web-secret-provider/Projects" = {
owner = "gitea";
group = "gitea";
restartUnits = [ "gitea-web-secret-provider@Projects" ];
};
"gitea/web-secret-provider/Kurs" = {
owner = "gitea";
group = "gitea";
restartUnits = [ "gitea-web-secret-provider@Kurs" ];
};
cfg = config.services.gitea;
program = pkgs.writers.writePython3 "gitea-web-secret-provider" {
libraries = with pkgs.python3Packages; [ requests ];
makeWrapperArgs = [
"--prefix PATH : ${(lib.makeBinPath [ pkgs.openssh ])}"
];
} (builtins.readFile ./gitea-web-secret-provider.py);
in
{
# https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#Specifiers
# %i - instance name (after the @)
# %d - secrets directory
# %s - /var/lib
systemd.services = {
"gitea-web-secret-provider@" = {
description = "Gitea web secret provider";
wantedBy = [ "multi-user.target" ];
requires = [ "gitea.service" "network.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = let
args = lib.cli.toGNUCommandLineShell { } {
org = "%i";
token-path = "%d/token";
api-url = "${cfg.settings.server.ROOT_URL}api/v1";
key-dir = "%s/%i/keys";
authorized-keys-path = "%s/gitea-web/authorized_keys.d/%i";
rrsync-path = "${pkgs.rrsync}/bin/rrsync";
web-dir = "%s/gitea-web/web";
};
in "${program} ${args}";
User = "gitea";
Group = "gitea";
Restart = "always";
StateDir = "%i";
WorkingDirectory = "%s/%i";
# Hardening
NoNewPrivileges = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectSystem = true;
ProtectHome = true;
ProtectControlGroups = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
RestrictRealtime = true;
RestrictSUIDSGID = true;
MemoryDenyWriteExecute = true;
LockPersonality = true;
};
};
}
//
builtins.listToAttrs (map (org: lib.nameValuePair "gitea-web-secret-provider@${org}" {
serviceConfig.LoadCredential = [
"token:${config.sops.secrets."gitea/web-secret-provider/${org}".path}"
];
}));
systemd.timers = {
"gitea-web-secret-provider@" = {
description = "Run the Gitea web secret provider";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "daily";
RandomizedDelaySec = "1h";
Persistent = true;
Unit = "gitea-web-secret-provider@%i.service";
};
};
}
//
builtins.listToAttrs (map (org: lib.nameValuePair "gitea-web-secret-provider@${org}" { }));
# services.nginx.virtualHosts.
}

View File

@ -0,0 +1,105 @@
#!/usr/bin/env nix-shell
#!nix-shell -i python3 -p "python3.withPackages(ps: with ps; [ requests ])" openssh
import argparse
import hashlib
import os
import requests
import subprocess
def parse_args():
parser = argparse.ArgumentParser(description="Generate SSH keys for Gitea repositories and add them as secrets")
parser.add_argument("--org", required=True, help="The organization to generate keys for")
parser.add_argument("--token-path", metavar='PATH', required=True, help="Path to a file containing the Gitea API token")
parser.add_argument("--api-url", metavar='URL', help="The URL of the Gitea API", default="https://git.pvv.ntnu.no/api/v1")
parser.add_argument("--key-dir", metavar='PATH', help="The directory to store the generated keys in", default="/run/gitea-web-secret-provider")
parser.add_argument("--authorized-keys-path", metavar='PATH', help="The path to the resulting authorized_keys file", default="/etc/ssh/authorized_keys.d/gitea-web-secret-provider")
parser.add_argument("--rrsync-path", metavar='PATH', help="The path to the rrsync binary", default="/run/current-system/sw/bin/rrsync")
parser.add_argument("--web-dir", metavar='PATH', help="The directory to sync the repositories to", default="/var/www")
parser.add_argument("--force", action="store_true", help="Overwrite existing keys")
return parser.parse_args()
def add_secret(args, token, repo, name, secret):
result = requests.put(
f"{args.api_url}/repos/{args.org}/{repo}/actions/secrets/{name}",
json = { 'data': secret },
headers = { 'Authorization': 'token ' + token },
)
if result.status_code not in (201, 204):
raise Exception(f"Failed to add secret: {result.json()}")
def get_org_repo_list(args, token):
result = requests.get(
f"{args.api_url}/orgs/{args.org}/repos",
headers = { 'Authorization': 'token ' + token },
)
return [repo["name"] for repo in result.json()]
def generate_ssh_key(args, repository: str):
keyname = hashlib.sha256(args.org.encode() + repository.encode()).hexdigest()
if not os.path.exists(os.path.join(args.key_dir, keyname)) or args.force:
subprocess.run(
[
"ssh-keygen",
*("-t", "ed25519"),
*("-b", "4096"),
*("-f", os.path.join(args.key_dir, keyname)),
*("-N", ""),
*("-C", f"{args.org}/{repository}"),
],
check=True,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
print(f"Generated SSH key for `{args.org}/{repository}`")
with open(os.path.join(args.key_dir, keyname), "r") as f:
private_key = f.read()
with open(os.path.join(args.key_dir, keyname + ".pub"), "r") as f:
public_key = f.read()
return private_key, public_key
def generate_authorized_keys(args, repo_public_keys: list[tuple[str, str]]):
result = ""
for repo, public_key in repo_public_keys:
result += f"""
command="{args.rrsync_path} -wo {args.web_dir}/{args.org}/{repo}",restrict,no-agent-forwarding,no-port-forwarding,no-pty,no-X11-forwarding {public_key}
""".strip() + "\n"
with open(args.authorized_keys_path, "w") as f:
f.write(result)
def main():
args = parse_args()
with open(args.token_path, "r") as f:
token = f.read().strip()
os.makedirs(args.key_dir, 0o700, exist_ok=True)
repos = get_org_repo_list(args, token)
print(f'Found {len(repos)} repositories in `{args.org}`')
repo_public_keys = []
for repo in repos:
print(f"Locating key for `{args.org}/{repo}`")
private_key, public_key = generate_ssh_key(args, repo)
add_secret(args, token, repo, "WEB_SYNC_SSH_KEY", private_key)
repo_public_keys.append((repo, public_key))
generate_authorized_keys(args, repo_public_keys)
print(f"Wrote authorized_keys file to `{args.authorized_keys_path}`")
if __name__ == "__main__":
main()