Git Repo in Shared Hosting #4 - Git Full Service Via SSH
Posted by Mikko Koivunalho in How-To
In this series of four articles we show how to share a Git repository via a Linux shell account and a shared hosting Apache HTTP server without root access.
In the first article we learned how we can share our Git repository with others securily and yet allow them to commit (push) to the repo.
In the second article we set up the web-based collaborative code review tool Review Board to work with our repository and make it accessible for users via our homepage.
In the third article we made our repository even more secure and usable with the help of a Git hooks framework, the Perl based Git::Hooks
In this fourth article we will now use SSH connection and SSH public keys to give access and also limit access to our repositories.
Prerequisites
- Shell account on a Linux server. All shell commands are executed in bash.
- Git.
- Perl.
- The need to work collaboratively! Need is the greatest innovator.
- The repositories we set up in the first article.
Git Via SSH
We need to share a repository but what if we don't have a web server at our disposal? We still have SSH or Secure Shell access to a remote Linux server. We have an account there, and enough storage for the repository. Anybody with access to our account can clone and push repositories like this:
git clone <USER>@<HOST>/<REPOSITORIES PATH>/shared/shared-repo.git
This is a normal SSH connection. If user can access a repository with Git, he can equally well access everything else under the user's account.
If we trust our Git contributors - trust 100% - then we can share the account by sharing the user credentials or an SSH key. However, since we don't live in a perfect world, and have at least a little bit of distrust towards other human beings, we need a way to share the repositories and only them. Not our whole Linux account!
There is a way! We configure the incoming SSH connection using
the file ${HOME}/.ssh/authorized_keys
at the server.
It contains the public keys for SSH public key authentication.
Besides the keys, we can set
connection parameters,
including the command which is executed during connection.
In a normal and plain connection SSH starts the login shell and directs
the session to it. This is the normal security in Linux.
But we need something we can control better and create limits
for what our logged in users can do.
Git Shell
Git command git-shell is a restricted login shell for Git-only SSH access. It permits execution only of server-side Git commands implementing the pull/push functionality, plus custom commands present in a subdirectory named git-shell-commands in the user’s home directory.
Git shell accepts the following commands after the -c
option:
These are the corresponding server-side commands to support the client’s git push, git fetch (including clone and pull), or git archive --remote request.
We want to give the user an interface to do the above plus get a list
of all available Git repositories. We also want to limit user's actions
to subdirectory ${REPOS_PATH}/shared
(created in the first article of the series).
git-shell
in itself does not contain any security constraints.
We need to go around this by creating a wrapper to git-shell
to check the arguments user gives.
SSH configuration
Ask user for SSH public key. It can be generated easily.
ssh-keygen -t ed25519 -f ~/.ssh/shared-repos
The above command generates a private/public key pair. I am using the new Ed25519 public key type instead of older DSA and RSA types. If your SSH does not support Ed25519, just use the older types.
User will send the public key file shared-repos.pub
.
Every user should have a nick. Let's identify our user as userone.
Add the user's public key to authorized keys. The public key is a string that looks like this: ssh-ed25519 BB55C3NzaC1lZDI1NTE5BAEDINo4+/19/mPi+6JsBqWdFgT5E3JfGhvqEhYuHkJNO78T userone@hostname
export GIT_SHARED_USER="userone"
cat >>${HOME}/.ssh/authorized_keys <<EOF
command="export USER=${GIT_SHARED_USER}; perl -T ${HOME}/bin/git-shell-wrapper.pl" <PUBLIC KEY>
EOF
Whenever that key is used, SSH will execute the command string
which 1) creates environment variable USER with our designated user name
and 2) executes command bin/git-shell-wrapper.pl
. Variable USER is important
because it can be used together with Git::Hooks framework
(introduced in the previous article) to separate
users into restricted access users and administrators who have more
powers, such as ability to force push (rewrite branch history).
Git Shell Wrapper
The Git Shell wrapper is the most complicated part of this project.
It is our
Kerberos, the
gateway guardian.
So let's take all precautions! Did you notice the perl
command
executing it? perl -T <FULL PATH>/git-shell-wrapper.pl
!
We use full file path, not dependent upon PATH variable and
we use the switch -T
for tainted check.
Perl is a language often used in web development and system administration, and because of that Perl has many features to force security and guide programmer into noticing some problem cases. The tainted check ensures that all input from user is laundered, i.e. always filtered and/or checked to contain only what is expected. Naturally, it is programmer's responsibility to do this.
Create directory ${USER}/bin
if you don't already have it.
Copy the following script as git-shell-wrapper.pl
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | #!/usr/bin/env perl
use strict;
use warnings;
use feature q{say};
my $GIT_COMMANDS = q{git-receive-pack|git-upload-pack|git-upload-archive};
my $OUR_COMMANDS = q{help|list};
my $GIT_SHELL = q{/usr/bin/git-shell};
local ($ENV{HOME}) = $ENV{HOME} =~ m/^(.*)$/msx; # Untaint path
sub log_info { ## no critic (Subroutines::RequireArgUnpacking)
open my $logfile, '>>', $ENV{HOME} . q{/bin/git-shell-wrapper.log} or die "Could not open log file!\n";
say {$logfile} @_; ## no critic (InputOutput::RequireCheckedSyscalls)
my $ok = close $logfile;
return;
}
sub allowed_repositories {
my $list_cmd = $ENV{HOME} . q{/git-shell-commands/list};
my @paths = `$list_cmd`; ## no critic (InputOutput::ProhibitBacktickOperators)
return map { m/^([^\n]+)$/msx; } @paths;
}
sub path_is_allowed {
my $path = shift;
return (grep { /^$path$/msx } allowed_repositories() ) == 1;
}
my $user = $ENV{USER};
log_info( q{User '}, $user, q{' logged in.} );
log_info( q{SSH_ORIGINAL_COMMAND: '}, $ENV{SSH_ORIGINAL_COMMAND}//q{}, q{'.} );
local ($ENV{PATH}) = $ENV{PATH} =~ m/^(.*)$/msx; # Untaint path
my ($arg_cmd, $arg_par) = split q{ }, $ENV{SSH_ORIGINAL_COMMAND}//q{};
log_info( q{arg_cmd:}, $arg_cmd//q{} );
log_info( q{arg_par:}, $arg_par//q{} );
if( defined $arg_cmd ) {
if( $arg_cmd =~ m/^(?:$GIT_COMMANDS)$/msx ) {
my ($git_cmd) = $arg_cmd =~ m/^($GIT_COMMANDS)$/msx; # Untaint command
if( defined $arg_par ) {
my ($git_cmd_arg) = $arg_par =~ m/^(.+)$/msx; # Untaint
my ($repo_path_candidate) = $git_cmd_arg =~ m/^'?([^']+)'?$/msx; # Can have ' around path.
my ($repo_path) = grep { /^$repo_path_candidate$/msx } allowed_repositories();
if( defined $repo_path ) {
log_info( q{repo_path: '}, $repo_path, q{'.} );
exec $GIT_SHELL, q{-c}, "$git_cmd '$repo_path'"; # Yes, git-shell wants the params as one!
} else {
die q{Repository '}, $repo_path_candidate, q{' not available!}, qq{\n};
}
} else {
die q{Command '}, $arg_cmd, q{' missing parameter <repo_path>!}, qq{\n};
}
} elsif( $arg_cmd =~ m/^(?:$OUR_COMMANDS)$/msx ) {
my ($our_cmd) = $arg_cmd =~ m/^($OUR_COMMANDS)$/msx; # Untaint command
exec $GIT_SHELL, q{-c}, $our_cmd;
} else {
die q{Command '}, $arg_cmd, q{' not allowed!}, qq{\n};
}
} else {
exec $GIT_SHELL;
}
|
Make the script secure. Do not make it executable because it is supposed
to be called only by sshd
(Secure Shell Daemon)!
chmod 600 ${HOME}/bin/git-shell-wrapper.pl
If you want to run it from the command line, e.g. to test and debug it,
execute it with command perl -T ${HOME}/bin/git-shell-wrapper.pl
.
Because of how command /usr/bin/env
works, the -T
flag cannot be used
in the shebang line.
All access is logged to file ${HOME}/bin/git-shell-wrapper.log
.
As you can see, I use Perl Critic quite a lot.
All environment variables get examined and untainted.
This script allows three different ways of using git-shell
:
- Via local git client, i.e. calling
git clone
orgit pull/push
. - Connecting to the account via SSH and executing one command and then returning immediately, e.g.
ssh [-i ~/.ssh/shared-repos] <USER>@<HOST> list
. - Connecting to the account via SSH and getting an interactive Git shell, e.g.
ssh [-i ~/.ssh/shared-repos] <USER>@<HOST>
.
In the first case, the allowed Git commands are listed in the variable $GIT_COMMANDS. When any of these is recognized, the following parameter is assumed to be a file system path. This path is compared with the list of allowed paths, i.e. the available Git repositories. It must match with exactly one repository to continue to execute the command. If not, the script aborts.
In the second and third case, we are setting up a user friendly expansion to Git Shell.
The executable files in directory ${HOME}/git-shell-commands
are
commands user can execute either interactively or one in an SSH session.
In order for the above script to work, you need to copy the commands from
https://github.com/git/git/tree/master/contrib/git-shell-commands (Git official repo)
and make them secure and executable. These commands are listed in the variable $OUR_COMMANDS.
You can easily create more of them.
Modify the command list
so it returns only the repositories we want.
perl -p -i -e 's/find\s-type/find <FULLPATH>\/repos\/shared/msx;' ${HOME}/git-shell-commands/list
Congratulations! Now your repositories are available and your Linux account is still secure!
Comments