Exercises in Restful Integration and Continuous Delivery

Musing on Software Development and Technologies.

Fri 01 February 2019

Git Repo in Shared Hosting #4 - Git Full Service Via SSH

Posted by Mikko Koivunalho in How-To   

Git Logo

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:

  1. Via local git client, i.e. calling git clone or git pull/push.
  2. Connecting to the account via SSH and executing one command and then returning immediately, e.g. ssh [-i ~/.ssh/shared-repos] <USER>@<HOST> list.
  3. 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