Skip to content

Instantly share code, notes, and snippets.

@m-bartlett
Last active June 26, 2021 06:28
Show Gist options
  • Save m-bartlett/ab10fc4a4aacbeae5874497e154c7ccb to your computer and use it in GitHub Desktop.
Save m-bartlett/ab10fc4a4aacbeae5874497e154c7ccb to your computer and use it in GitHub Desktop.
How to configure a local-network machine that permits SSH access to automatically create git repos if it receives a `git push`.

Easy and Automatic Local Area Network Git Mirroring via SSH

Mirroring Remote Origin

A git user may add or modify a remote in their repository via the git remote subcommand. Most commonly (e.g. in the case the user has cloned from a popular git service such as GitLab or GitHub) git repositories have a remote named origin which is the default remote operated on. We can add extra URLs that point to other servers hosting a copy of the repository via git remote set-url --add origin <url>.

Alternatively, one can also manually edit .git/config in the root of the repository. An example of what may be in the config regarding the remote origin is

[remote "origin"]
  url = [email protected]:<user>/<repo_name>.git
  fetch = +refs/heads/*:refs/remotes/origin/*

E.g. if we wanted to mirror the above config to GitLab, we would execute git remote set-url --add origin [email protected]:<user>/<repo_name>.git in the repository. The config would then read similar to:

[remote "origin"]
  url = [email protected]:<user>/<repo_name>.git
  fetch = +refs/heads/*:refs/remotes/origin/*
  url = [email protected]:<user>/<repo_name>.git

and git remote -v will list the extra url:

$ git remote -v
origin  [email protected]:<user>/<repo_name>.git (fetch)
origin  [email protected]:<user>/<repo_name>.git (push)
origin  [email protected]:<user>/<repo_name>.git (push)

Use a Local Network Git Remote via SSH

One can easily set a machine on their local network that they have SSH access to as a remote, and consequently push/pull from that machine.

For the following example code blocks, let's assume there is a machine on the LAN which resolves through local DNS with the hostname gitserver. Furthermore, let's assume there is a user on this system named git who accepts one of our public keys as an authorized key (and thus, we are able to authenticate and SSH into the system as git with ssh git@gitserver).

In order for this to work normally, we must first SSH into the machine and create a repository somewhere. Let's make a repo named example in /git. First we must create a directory in /git that is suffixed with .git. Then we must initialize this directory as a git repository in order for it to accept our git commands via SSH:

git@gitserver$ mkdir -p /git/example.git
git@gitserver$ cd /git/example.git
git@gitserver$ git init --bare
Initialized empty Git repository in /git/example.git/

Note that --bare will not create a .git hidden directory in this directory like git init would. The git-related files/folders normally in .git will exist one directory up, in the directory itself. In this case, instead of /git/example.git/.git/config it will be /git/example.git/config

Now our repository example on gitserver is ready to be used as a remote url. Logging out of gitserver and back on our original machine we can now run:

$ git remote set-url --add origin git@gitserver:/git/example.git

and then if we git push --verbose we should see something like:

Pushing to github.com:<user>/example.git
To github.com:<user>/example.git
 = [up to date]      main -> main
updating local tracking ref 'refs/remotes/origin/main'
Everything up-to-date
Pushing to gitlab.com:<user>/example.git
To gitlab.com:<user>/example.git
 = [up to date]      main -> main
updating local tracking ref 'refs/remotes/origin/main'
Everything up-to-date
Pushing to gitserver:/git/example.git
Enumerating objects: 136, done.
Counting objects: 100% (136/136), done.
Delta compression using up to 4 threads
Compressing objects: 100% (125/125), done.
Writing objects: 100% (136/136), 123.27 MiB | 3.16 MiB/s, done.
Total 136 (delta 7), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (7/7), done.

Note the Pushing to gitserver:/git/example.git

Automating Git Repository Creation on Authenticated git push Event on Our Local Git SSH Server

One thing to emphasize here is we would need to do this procedure of git initing every repository we would ever want to be git pushed to our local mirror server. This may be very minor in the grand scheme of things, and you might even write a git alias or something similar which runs the mkdir and git init in a brief ssh invocation. However, it is possible to do this server side and therefore any potential clients (for example, you but on a different machine on your network) will enjoy this automation.

As hopefully made obvious by this point, we're using SSH as the underlying protocol. As such, there are a few mechanisms we can take advantage of to achieve this in a standard POSIX implementation of SSH. Primarily we will use the command="..." prefix which can be placed in the authorized keys of the server prior to our public key (in our example case, /home/git/.ssh/authorized_keys). This is the command that is used for any SSH connection to this user (and should end with launching a login shell if you are seeking to not distrub the default behavior, otherwise if not it will disable any interactive shell access to the system).

  • Note: not used here, but ~/.ssh/rc on the server-side user can be used for SSH-only interactive shell environment configuration

More saliently, in ~/.ssh/authorized_keys of the git users on gitserver we should have a line as such which contains our SSH public key contents:

command="/path/to/command" ssh-rsa AAAA...

The specific SSH command script is included as a separate file with this README. It will intercept SSH-invoked calls to git-upload-pack and git-receive-pack and check the given filesystem path argument. If the argument is not absolute, it will modify it to point the repository to git's home directory. Then, it will check if the directory exists--if not, it will create it and run git init --bare for us. Some important details here are:

  • This will only happen if done via an authenticated SSH call, so any risk of malicious actors exploiting this is equal to them exploiting your underlying SSH configuration.
  • The command used in command=... in authorized_keys should not print anything to stdout or stderr manually. This can and likely will introduce errors in the SSH text protocol being used by git, resulting in no git actions being possible. The included script makes sure not to print anything unexpected as to cause errors in the protocol.

I have named this command pre-receive-push to fit the naming conventions of standard git hooks such as pre-fetch and pre-receive.

I have also placed this command in the global git-hook directory as configured in git global config core.hooksPath (which is /home/git/.hooks/ in my configuration) for the git user on gitserver to keep this hook logically grouped with other potential global git hooks. So specifically, the absolute path of this script is /home/git/.hooks/pre-receive-push with this string configured in /home/git/.ssh/authorized_keys:

command="/home/git/.hooks/pre-receive-push" ssh-rsa AAAA...

So after creating this script (and ensuring it has execution permissions) and adding it to authorized_keys, if we test pushing a brand new repository named test from our local machine we should see our git mirror server accept the push without any errors and without requiring us to explicitly SSH and run git init:

$ cd $(mktemp -d)   # make a temporary directory as a workspace

$ git init
Initialized empty Git repository in /tmp/tmp.P2RNFycF2m/.git/

$ git remote add origin git@gitserver:test

$ touch test

$ git add test

$ git commit -m "initial"
[main (root-commit) 04fe861] test
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 test

$ git push --set-upstream origin main
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 206 bytes | 206.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote: Reinitialized existing Git repository in /home/git/test/
To gitserver:test
 * [new branch]      main -> main

Voilà! Now we can conveniently call git remote set-url --add git@gitserver:<repo_name> in any git repository we wish to mirror on our local server and the server will automatically handle the rest.

# This configuration isn't essential, it just logically groups our SSH hook with other global git hooks
[core]
hooksPath = /home/git/.hooks/
#!/bin/bash
# $SSH_ORIGINAL_COMMAND contains the actual string command sent by the client
SSH_COMMAND_PARTS=( $SSH_ORIGINAL_COMMAND )
GIT_COMMAND=${SSH_COMMAND_PARTS[0]}
case $GIT_COMMAND in
git-receive-pack|git-upload-pack) # These commands indicate a git-push
path="${SSH_COMMAND_PARTS[1]//\'/}" # remove apostrophes from client's path
# If client path arg isnt absolute, prefix it with $HOME
if [[ "${path:0:1}" != '/' ]]; then path="$HOME/$path"; fi
SSH_NEW_COMMAND="$GIT_COMMAND '$path'"
# If the directory doesnt exist, make it and git init it
[ -d "$path" ] || (
mkdir -p $path ; cd $path ; git init --bare &>/dev/null
)
# Run the client original command in the repo directory
pushd $path &>/dev/null
eval "$SSH_NEW_COMMAND"
popd &>/dev/null
;;
# Run an interactive shell if this isnt a git-push
*) bash -il
esac
command="/home/git/.hooks/pre-receive-push" ssh-rsa AAAAYourLocalMachineSSHPublicKey...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment