From 8d9c226543c1acf4be7b5b707f421397ee00270a Mon Sep 17 00:00:00 2001 From: Ardis Lu Date: Sun, 7 Sep 2025 23:36:01 -0700 Subject: [PATCH] Initial commit --- README.md | 119 ++++++++++++++++++ etc/caddy/Caddyfile | 31 +++++ etc/gitweb.conf | 16 +++ etc/systemd/system/git-daemon.service | 23 ++++ setup.sh | 52 ++++++++ srv/git/.hushlogin | 0 srv/git/.ssh/authorized_keys | 1 + srv/git/git-shell-commands/init | 75 +++++++++++ .../git-shell-commands/no-interactive-login | 4 + srv/git/git-shell-commands/rm | 26 ++++ 10 files changed, 347 insertions(+) create mode 100644 README.md create mode 100644 etc/caddy/Caddyfile create mode 100644 etc/gitweb.conf create mode 100644 etc/systemd/system/git-daemon.service create mode 100644 setup.sh create mode 100644 srv/git/.hushlogin create mode 100644 srv/git/.ssh/authorized_keys create mode 100644 srv/git/git-shell-commands/init create mode 100644 srv/git/git-shell-commands/no-interactive-login create mode 100644 srv/git/git-shell-commands/rm diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4806d7 --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# git.ardis.lu + +Configurations and scripts for my self-hosted git server. + +Tested and running on Debian but these directions should work on any similar `systemd` distribution. + +Code repository mirrors: [GitHub](https://github.com/ardislu/git.ardis.lu), [Codeberg](https://codeberg.org/ardislu/git.ardis.lu), [git.ardis.lu](https://git.ardis.lu/git.ardis.lu) + +## Setup + +1. Install required software: + +``` +sudo apt install git gitweb highlight ufw fcgiwrap +``` + +See [Caddy: Install](https://caddyserver.com/docs/install) for directions on installing `caddy`. + +Explanation: +- `git`, `gitweb` are naturally required. +- `highlight` for code syntax highlighting on the web view. +- `ufw` for setting up firewall rules (can substitute with whatever you like). +- `fcgiwrap` to call the `gitweb` and `git-http-backend` FastCGI scripts. +- `caddy` for the web server to serve the web view and repos over HTTPS. + +2. Clone this repo somewhere on the server. + +3. Update `./srv/git/.ssh/authorized_keys` to include all admin SSH keys that should have **write** access to repos. + +4. Run `setup.sh` to create or update the `git` user, git scripts, the git repo directory `/srv/git`, and `gitweb` configurations: + +``` +sudo ./setup.sh +``` + +5. Open port 443 to allow access over HTTPS: + +``` +sudo ufw allow "WWW Secure" +``` + +## View and clone repos + +Browse existing repos using the web view: [https://git.ardis.lu](https://git.ardis.lu) + +Support for read/write: + +Transport protocol | Read | Write | Authorization required? +--- | --- | --- | --- +`ssh://` | Yes | Yes | Yes +`https://` | Yes | No | No +`http://` | No | No | N/A +`git://` | No | No | N/A + +`http://` and `git://` are disabled because (1) it simplifies server administration and (2) the lack of integrity checks or encryption makes them subject to tampering. This configuration matches [GitHub's](https://github.blog/security/application-security/improving-git-protocol-security-github/). + +URLs to clone individual repos can be found within the repo page on the web view. + +## Helper scripts + +A minimal set of administrative commands. + +### `init` + +Create a new repo and optionally set its description for the web view. + +``` +ssh git@git.ardis.lu init [repository_description] +``` + +### `rm` + +Delete a repo. + +``` +ssh git@git.ardis.lu rm +``` + +## Uninstall + +To reverse the steps in `setup.sh`. + +3. (From another computer) Use the `rm` helper script to delete all repos: + +``` +ssh git@git.ardis.lu rm +``` + +4. Delete the `git` user and its home directory `/srv/git`: + +``` +sudo deluser --remove-home git +``` + +5. (Optional) Delete `git-shell` from `/etc/shells`: + +``` +sudo vim /etc/shells +``` + +### Disable `git-daemon` + +These steps are for reference only, they are not needed because `git-daemon` is not enabled on the server. + +1. Delete the `ufw` rule to open port 9418 for `git-daemon`: + +``` +sudo ufw delete limit 9418/tcp +``` + +2. Stop and delete `git-daemon.service`: + +``` +sudo systemctl stop git-daemon +sudo systemctl disable git-daemon +sudo rm /etc/systemd/system/git-daemon.service +sudo systemctl daemon-reload +sudo systemctl reset-failed +``` diff --git a/etc/caddy/Caddyfile b/etc/caddy/Caddyfile new file mode 100644 index 0000000..35d763d --- /dev/null +++ b/etc/caddy/Caddyfile @@ -0,0 +1,31 @@ +git.ardis.lu { + root /usr/share/gitweb + encode + file_server + + # For root, serve gitweb for human-friendly browsing + reverse_proxy / unix//run/fcgiwrap.socket { + transport fastcgi { + env SCRIPT_FILENAME /usr/share/gitweb/gitweb.cgi + } + } + + # For /[name] and /[name].git WITHOUT additional paths, redirect to /?p=[name].git for human-friendly browsing + @git_bare path_regexp ^(\/)([A-Za-z0-9_.-]+?)(\.git)?$ + redir @git_bare /?p={re.2}.git + + # For /[name].git WITH additional paths, serve git-http-backend to support git operations + # {method}, {path}, {query} are magic Caddy variables which correspond to the URL + # components from the request. + @git_backend path_regexp ^\/[A-Za-z0-9_.-]+\.git\/.*$ + reverse_proxy @git_backend unix//run/fcgiwrap.socket { + transport fastcgi { + env SCRIPT_FILENAME /usr/lib/git-core/git-http-backend + env GIT_PROJECT_ROOT /srv/git + env GIT_HTTP_EXPORT_ALL 1 # Can be any value, but it must be set to allow serving over HTTP + env REQUEST_METHOD {method} + env PATH_INFO {path} + env QUERY_STRING {query} + } + } +} \ No newline at end of file diff --git a/etc/gitweb.conf b/etc/gitweb.conf new file mode 100644 index 0000000..1e605ea --- /dev/null +++ b/etc/gitweb.conf @@ -0,0 +1,16 @@ +$projectroot = '/srv/git'; +$git_temp = '/tmp'; + +# https://git-scm.com/docs/gitweb.conf#_changing_gitwebs_look +$site_name = 'git.ardis.lu'; +$projects_list_description_width = 120; # Increase project description character limit from 25 +$default_projects_order = 'age'; # Sort by last commit, default is sort by project name + +# https://git-scm.com/docs/gitweb.conf#_features_in_feature +$feature{'highlight'}{'default'} = [1]; # Syntax highlighting. Assumes `highlight` is available on $PATH. +$feature{'blame'}{'default'} = [1]; # Enable full git blame +$feature{'timed'}{'default'} = [1]; # Enable footer stats +$feature{'snapshot'}{'default'} = ['zip', 'tgz']; # Enable .zip (gzip) and .tgz (tar) archive download + +# https://git-scm.com/docs/gitweb.conf#_some_optional_features_and_policies +$omit_owner = 1; # Not needed since all repos are implied to be owned by me \ No newline at end of file diff --git a/etc/systemd/system/git-daemon.service b/etc/systemd/system/git-daemon.service new file mode 100644 index 0000000..900382f --- /dev/null +++ b/etc/systemd/system/git-daemon.service @@ -0,0 +1,23 @@ +# This file is for reference purposes only, it is not actually used on the server. +# Use this systemd unit to enable cloning via `git://` protocol. + +[Unit] +Description=Git Daemon +Documentation=man:git-daemon(1) +Wants=network-online.target +After=network-online.target + +[Service] +User=git +Group=git +ExecStart=/usr/bin/git daemon --reuseaddr --base-path=/srv/git +Restart=on-failure + +# Security hardening +PrivateTmp=true +ProtectSystem=full +ProtectHome=true +NoNewPrivileges=true + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..a01a99f --- /dev/null +++ b/setup.sh @@ -0,0 +1,52 @@ +#!/bin/sh + +git_shell_path="$(command -v git-shell || true)" + +if [ -z "$git_shell_path" ]; then + echo "Error: git-shell not found in PATH. Install git or check its path." >&2 + exit 1 +fi + +# Add git-shell to /etc/shells if not already there +if ! grep -qxF "$git_shell_path" /etc/shells; then + echo "$git_shell_path" >> /etc/shells +fi + +# Setup files in /srv/git +mkdir -p /srv/git/.ssh +cp -rf ./srv/git /srv + +# Create git user if it does not already exist +id -u git &>/dev/null || useradd --home-dir /srv/git --shell "$git_shell_path" --no-create-home --skel /dev/null git + +# Set permissions for /srv/git +chown -R git:git /srv/git +chmod -R 755 /srv/git +chmod 700 /srv/git/.ssh +chmod 600 /srv/git/.ssh/authorized_keys + +# Globally disable safe directory checks because only I will be writing to this server. This step is required +# or else there will be "fatal: detected dubious ownership" errors when the web server attempts to +# serve the repos as a user other than "git". +# See https://stackoverflow.com/a/71904131/21084807 +git config --system safe.directory '*' + +# Otherwise HEAD will point to "master" when using init, even if the pushed repo has renamed main branch +git config --system init.defaultBranch main + +# Setup gitweb +systemctl enable --now fcgiwrap +cp -f ./etc/caddy/Caddyfile /etc/caddy +systemctl restart caddy + +# Below steps for configuring git-daemon are for reference only. +# It is commented out because the git:// protocol's lack of encryption or integrity checks make it +# subject to tampering. + +# Add or update git-daemon.service, which enables read-only access via git:// protocol (port 9418) +# cp -f ./etc/systemd/system/git-daemon.service /etc/systemd/system +# systemctl daemon-reload +# systemctl enable --now git-daemon.service + +# Configure ufw to open port 9418 with rate limiting +# ufw limit 9418/tcp comment 'rate-limited git-daemon' diff --git a/srv/git/.hushlogin b/srv/git/.hushlogin new file mode 100644 index 0000000..e69de29 diff --git a/srv/git/.ssh/authorized_keys b/srv/git/.ssh/authorized_keys new file mode 100644 index 0000000..281e9d1 --- /dev/null +++ b/srv/git/.ssh/authorized_keys @@ -0,0 +1 @@ +no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAgbXIdAZ9p+GMj2TngGaF4X//OfzSm56eCcNuMAllcK \ No newline at end of file diff --git a/srv/git/git-shell-commands/init b/srv/git/git-shell-commands/init new file mode 100644 index 0000000..7ed11ea --- /dev/null +++ b/srv/git/git-shell-commands/init @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +# Require a name to be provided +if [ $# -lt 1 ]; then + echo "Usage: init [repository_description]" >&2 + exit 1 +fi + +user='git' +hostname='git.ardis.lu' + +name="$1" + +# Interpret all remaining arguments as the description. Importantly, passing a command like: +# ssh git@git.ardis.lu init example_name "example description with spaces" +# will NOT preserve the quotes when passed to the bash command (i.e., each word is interpreted +# as a separate argument). As a workaround, just interpret all remaining arguments as the description. +# See https://stackoverflow.com/a/53472697/21084807 +shift 1 +description="$@" + +# Check repo name is valid using the same convention as GitHub +# See https://stackoverflow.com/a/59082561/21084807 +pattern='^[A-Za-z0-9_\.\-]+$' +if ! [[ "$name" =~ $pattern ]]; then + echo "Error: invalid repository name '$name'" >&2 + exit 1 +fi + +path="$HOME/$name.git" + +# Prevent overwriting an existing repo +if [ -e "$path" ]; then + echo "Error: repository '$name' already exists" >&2 + exit 1 +fi + +# Instantiate the repo +mkdir -p "$path" +cd "$path" +git init --bare --quiet + +# Schedule and enable maintenance jobs +# Commented out to simplify administration (not dealing with cron jobs to remove repos) +# git maintenance start + +# Create post-update hook for dumb HTTP transport and general maintenance +# See https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols#_dumb_http) +# See https://git-scm.com/docs/git-maintenance +# See https://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery +cat > "$path/hooks/post-update" << EOL +#!/bin/sh +git update-server-info +git maintenance run +EOL +chmod a+x "$path/hooks/post-update" + +# Add git-daemon-export-ok file to support git protocol +# Commented out because git-daemon is not enabled on the server +# See https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols#_the_git_protocol +# touch "$path/git-daemon-export-ok" + +# Set clone URLs to show on the git web view +cat > "$path/cloneurl" << EOL +https://$hostname/$name.git +ssh://$user@$hostname/~/$name.git +$user@$hostname:$name.git +EOL + +# Set description if it was provided +if [ -n "$description" ]; then + echo "$description" >"$path/description" +fi + +echo "Repository '$name' created" diff --git a/srv/git/git-shell-commands/no-interactive-login b/srv/git/git-shell-commands/no-interactive-login new file mode 100644 index 0000000..3960294 --- /dev/null +++ b/srv/git/git-shell-commands/no-interactive-login @@ -0,0 +1,4 @@ +#!/bin/sh +GREEN='\033[32m' +RESET='\033[0m' +printf "🎉 ${GREEN}Success!${RESET} Your SSH key works.\n" diff --git a/srv/git/git-shell-commands/rm b/srv/git/git-shell-commands/rm new file mode 100644 index 0000000..a614989 --- /dev/null +++ b/srv/git/git-shell-commands/rm @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +# Require a name to be provided +if [ $# -lt 1 ]; then + echo "Usage: rm " >&2 + exit 1 +fi + +name="$1" +path="$HOME/$name.git" + +if ! [ -e "$path" ]; then + echo "Error: repository '$name' does not exist" >&2 + exit 1 +fi + +# Remove scheduled maintenance from init script +# Commented out because git maintenance start is not automatically started +# cd "$path" +# git maintenance stop +# git maintenance unregister --force +# cd "$HOME" + +rm -r "$path" + +echo "Repository '$name' deleted" -- 2.47.3