RemoteRepo
Summary
This document describes a way to configure hosting git/got repos for users. The goal is to provide shared hosting of repos, from multiple users, without needing to grant shared group privileges or even accounts on the public server. This maximises the isolation, for security reasons, between users and the public facing server. Users will store their repos in their home directory (~/public/gitgot/) and push/pull changes to it locally or via ssh as normal (details below).
Two scripts will be used to accomplish this; gitgot-staging.pl will run on the User Host, and gitgot-deploy.pl will run on the Public Host. They will need to run as root via a cron job at regular intervals (5 - 15 min). The gitgot-staging.pl script will rsync repos from each user's home directory to the staging area on the User Host. It'll then rsync the User Host staging area to the Public Host's staging area. The gitgot-deploy.pl script will rsync the files from the staging area to the web server's chroot for deployment.
The staging area is needed for security reasons. The script needs access to rsync the files between hosts without a password which would be a security concern if it can directly modify other areas on the Public Host. It's on both the the User and Public Host to allow the same scripts to be used even for user accounts hosted on the Public Host. The scripts will need an account (gitgot) on each machine and have an ssh key allowing the User Host to connect to the Public Host without a password. The staging area on both hosts will be owned by the gitgot user. The repos will be owned by the web server user once deployed.
The repo will be staged in /var/gitgot/user/repo directory structure.
1. Setup the Hosts
This configuration assumes two hosts, but can also be run on a single or multiple hosts. The host where the user accounts and home directories are will be known as the User Host, and the host where the web server runs will be known as the Public Host in this configuration. You can have multiple User Hosts that all use the same Public Host, and the Public Host can also have user accounts on it. Advanced configurations could also have multiple public hosts with a load balancing configuration, but that is beyond the scope of this document.
1.1 User Host
The configuration of the User Host consists of creating the ~/public/gitgot directory where users will put the repos they wish to publish. The gitgot-staging.pl script can be configured to create this folder for you if it is missing for existing users or you can use other means to setup the directory.
1.1.1 Modify /etc/skel
For new accounts, setting up the ~/public/gitgot directory can be automated by adding it to /etc/skel directory like this:
doas mkdir -p /etc/skel/public/gitgot doas chown root.wheel /etc/skel/public/gitgot doas chmod 755 /etc/skel/public/gitgot
1.1.2 Create the gitgot user
The gitgot-staging.pl script will move repos from the users directory to the staging area and change ownership to the gitgot user. It skips empty directories. Both the user Host and Public Host will need a gitgot user created like this:
doas useradd -m gitgot doas su - gitgot
1.1.3 Generate an SSH Key
The User Host will need an ssh key so it can send the changes to the Public Host.
ssh-keygen -t ed25519 exit
Use the default path for the key file and leave the password empty. The output will look something like this:
Generating public/private ed25519 key pair. Enter file in which to save the key (/home/gitgot/.ssh/id_ed25519): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /home/gitgot/.ssh/id_ed25519 Your public key has been saved in /home/gitgot/.ssh/id_ed25519.pub The key fingerprint is: SHA256:lyrfN6pDSRuVI112nfZiMh02aOoHxvgq5Y5jyY2QKpI gitgot@planetofnix.com The key's randomart image is: +--[ED25519 256]--+ | . oo....| | . =.o.+o.| | = + o.o.| | + =.o + .| | . .SBo. + . | | o =oo . | | . . o.B.. . | |E . B+=. o | |.. ..++ooo . | +----[SHA256]-----+
You'll need to do the same on the Public Host and copy the public key over. Run the following, but change <adminUser> and <publicHost> as needed. Doas is broken and wont let you cat >> so a few extra steps to do this as a copy/paste...
NOTE: The <adminUser> has to exist on the Public Host and needs to have write permission on /home/gitgot/.ssh/authorized_keys for this to work. You can use other methods to get the gitgot public key onto the Public Host.
1.1.4 Create gitgot User on Public Host
ssh_as='<adminUser>@<publicHost>' ssh $ssh_as doas useradd -m gitgot doas cat /home/gitgot/.ssh/id_ed25519.pub | ssh $ssh_as 'cat >>~/doasbroken.tmp'
1.1.5 Copy User Host Public Key to Public Host
# Log in to the Public Host ssh $ssh_as # Copy paste these separately after logging in doas su cat doasbroken.tmp >>/home/gitgot/.ssh/authorized_keys rm doasbroken.tmp exit exit
1.1.6 The gitgot-staging.pl script
We'll put the script for copying the files in /home/gitgot/bin/gitgot-staging.pl This script is also available via a git repo.
mkdir /home/gitgot/bin vim /home/gitgot/bin/gitgot-staging.pl
#!/usr/bin/perl use strict; use warnings; my $config={ remoteHost=>'got.ircnow.org', remoteUser=>'gitgot', remoteStage=>'/var/gitgot', repoDir=>'public/gitgot', createRepoDir=>1, stageDir=>'/var/gitgot', gitgotUser=>'gitgot', key_path=>'/home/gitgot/.ssh/id_ed25519', rsync_cmd=>'openrsync', }; # verify we have a local staging area if (not -d $config->{stageDir}) { my (undef,undef,$uid,$gid) = getpwnam($config->{gitgotUser}) or die "User " . $config->{gitgotUser} . " doesn't exist"; mkdir $config->{stageDir}, 0750; chown $uid,$gid,$config->{stageDir}; } opendir (my $DH, '/home/') || die "Can't opendir /home : $!"; while (my $user = readdir($DH)) { if (-d "/home/$user/" . $config->{repoDir}) { # Skip if no repos opendir (my $RDH, "/home/$user/" . $config->{repoDir}) or die "Can't opendir /home/$user/ : $!"; my @repos = readdir($RDH); next unless (scalar @repos > 2); # Make sure user has a staging directory if(not -d $config->{stageDir} . "/$user") { mkdir ($config->{stageDir} . "/$user"); } # rsync files to local staging area my $src=qq(/home/$user/$config->{repoDir}/*); my $dst=qq($config->{stageDir}/$user/); my $rv=system(qq{ openrsync --rsync-path=openrsync -a --delete $src $dst }); warn "rsync failed $user\n$!\n" if ($rv != 0); } elsif ($config->{createRepoDir}) { # create missing repo dirs # skip dirs without users. my (undef,undef,$uid,$gid) = getpwnam($user) or next; my $repoPath="/home/$user"; for my $dir (split('/', $config->{repoDir})) { $repoPath.="/$dir"; unless (-e $repoPath) { mkdir $repoPath, 0755; chown $uid,$gid,$repoPath; } } } } # rsync to remote staging area if we have a remote host if (defined $config->{remoteHost}) { # Only sync if we have repos to deploy opendir (my $SDH, $config->{stageDir}) or die "Can't opendir ". $config->{stageDir} . " : $!"; my @repos = readdir($SDH); if(scalar @repos > 2) { my $src=$config->{stageDir} . "/*"; my $dst=$config->{gitgotUser}. "@" . $config->{remoteHost} . ":" . $config->{remoteStage} . "/"; my $rv=system(qq{ openrsync --rsync-path=openrsync -ae "ssh -i $config->{key_path}" --delete $src $dst }); } }
1.2 Public Host
The Public Host is where the web server runs. It will run the gitgot-deploy.pl script from a cron job as root to copy the files from the staging area to the proper location for the web server.
NOTE: This part of the documentation is a bit in flux because the current server doesn't have a way to publish repos in user folders. This is a project for another day, hopefully not too far into the future. As a work around, the users name is added as a prefix to the repo name to avoid one user's repo clobbering another users repo with the same name. Hopefully this will be fixed soon.
1.2.1 The gitgot-deploy.pl Script
#!/usr/bin/perl use strict; use warnings; my $config={ stageDir=>'/var/gitgot', gitgotUser=>'gitgot', gotwebDir=>'/var/www/got/public', }; # Sync changes from stageDir to the webdir opendir (my $DH, $config->{stageDir}) or die "Can't opendir " . $config->{stageDir} . " : $!"; while (my $user = readdir($DH)) { next if ($user =~ /^\./); # skip . files if (-d $config->{stageDir} . "/$user") { # Need the name of each repo so we can add username ass prefix opendir (my $uDH, $config->{stageDir} . "/$user") or die "Can't opendir ". $config->{stageDir} . "/$user/ : $!"; while (my $repo = readdir($uDH)) { next if ($repo =~ /^\./); # skip . files # need destination folder name to change to prefix # the username to it to prevent one user from clobbering # another users repos. Can't do a rename of the folder # with rsync so have to create the destination folder then # rsync the contents of it. my @stat=stat($config->{stageDir} . "/$user/$repo"); my $prefixName="$user-$repo"; my $dst=qq($config->{gotwebDir}/$prefixName/); mkdir $dst,$stat[2]; #stat[2] is mode of original dir opendir (my $rDH, $config->{stageDir} . "/$user/$repo") or die "Can't opendir ". $config->{stageDir} . "/$user/$repo : $!"; while (my $git = readdir($rDH)) { next if ($git eq '.' or $git eq '..'); # skip . and .. my $src=qq($config->{stageDir}/$user/$repo/$git); my $rv=system(qq{ openrsync --rsync-path=openrsync -a --delete $src $dst }); warn "rsync failed $prefixName\n$!\n" if ($rv != 0); } } } }
2.0 Using the Repo
NOTE: For most of these commands, git and got can be used interchangeably. Got is a clone of git after all.
Git repos exist in two forms; working or bare. A bare repo contains no working tree and is generally used for published versions of the repo. The bare version is really just the contents of the .git directory in your working tree. It is usually the repo name with a .git extension by convention. So a repo named my myProgram would be published in the directory ~/public/gitgot/myProgram.git.
Your working repo is where you make your changes. It has a hidden directory of .git which contains the bare repo contents. If you are using your shell account on the User Host machine for your working repo, you are a local user. If you are using a remote computer for your working repo then you'll use the remote User commands to access the public repo. The examples below will assume you are using a working directory of ~/myProjects/ where you'll put the working versions of your repos.
2.1 Creating a New Repo
A new repo is created using the git init command. Its easiest if you create the bare repo first since cloning sets up the remote details for you.
2.1.1 Local User
cd ~/myProjects/ git init --bare ~/public/gitgot/myProgram.git git clone ~/public/gitgot/myProgram.git git push
2.1.2 Remote User
cd ~/myProjects/ ssh username@example.com git init --bare ~/public/gitgot/myProgram.git git clone ssh://username@example.com/~/public/gitgot/myProgram.git git push
2.2 Pushing an Existing Repo
If you have an existing working repo that you want to publish you first need to create an empty bare repo then add a remote url to your working repo to push to like this:
2.2.1 Local User
cd ~/myProjects/myProgram git init --bare ~/public/gitgot/myProgram.git git remote add origin ~/public/gitgot/myProgram.git git push --set-upstream origin master # After the first push you only need this git push
2.2.2 Remote User
cd ~/myProjects/myProgram ssh username@example.com git init --bare ~/public/gitgot/myProgram.git git remote add origin ssh://username@example.com/~/public/gitgot/myProgram.git git push --set-upstream origin master # After the first push you only need this git push
2.3 Clone a Bare Repo
Another way to create a bare repo of an existing repo is to clone it as a --bare repo like this:
2.3.1 Local User
cd ~/public/gitgot/ git clone --bare ~/myProjects/myProgram cd ~/myProjects/myProgram git remote add origin ~/public/gitgot/myProgram.git git push --set-upstream origin master # After the first push you only need this git push
2.3.2 Remote User
Its harder to do a clone of your working repo from remote since you probably don't have an easy way to access it from remote. We'll assume you have the ability to ssh
cd ~/public/gitgot/ git clone --bare ssh://username@example.com/~/myProjects/myProgram # On your local machine cd ~/myProjects/myProgram git remote add origin ssh://username@UserHost.com/~/public/gitgot/myProgram.git git push --set-upstream origin master # After the first push you only need this git push