Table of Contents
Home page: http://www.lshift.net/mercurial-server.html
mercurial-server gives your developers remote read/write access to centralized Mercurial repositories using SSH public key authentication; it provides convenient and fine-grained key management and access control.
Though mercurial-server is currently targeted at Debian-based systems such as Ubuntu, other users have reported success getting it running on other Unix-based systems such as Red Hat. Running it on a non-Unix system such as Windows is not supported. You will need root privileges to install it.
mercurial-server authenticates users not using passwords but using SSH public keys; everyone who wants access to a mercurial-server repository will need such a key. In combination with ssh-agent (or equivalents such as the Windows program Pageant), this means that users will not need to type in a password to access the repository. If you're not familiar with SSH public keys, the OpenSSH Public Key Authentication tutorial may be helpful.
In what follows, we assume that your username is jay
, that you usually sit at a machine called
spoon
and you have
installed mercurial-server on jeeves
using the package management system (see the README for more on installation). We assume that you have created your SSH public key, set up your SSH agent with this key, and that this key gives you access to jeeves
.
jay@spoon:~$
ssh -A jeeves
jay@jeeves:~$
ssh-add -L > my-key
jay@jeeves:~$
sudo mkdir -p /etc/mercurial-server/keys/root/jay
jay@jeeves:~$
sudo cp my-key /etc/mercurial-server/keys/root/jay/spoon
jay@jeeves:~$
sudo -u hg /usr/share/mercurial-server/refresh-auth
jay@jeeves:~$
exit
Connection to jeeves closed. jay@spoon:~$
You can now create repositories on the remote machine and have complete read-write access to all of them.
To store a repository on the server, clone it over.
jay@spoon:~$
cd myproj
jay@spoon:~/myproj$
hg clone . ssh://hg@jeeves/jays/project
searching for changes remote: adding changesets remote: adding manifests remote: adding file changes remote: added 119 changesets with 284 changes to 61 files jay@spoon:~/myproj$
hg pull ssh://hg@jeeves/jays/project
pulling from ssh://hg@jeeves/jays/project searching for changes no changes found
jay@spoon:~/myproj$
cd ..
jay@spoon:~$
At this stage, no-one but you has any access to any repositories you
create on this system. In order to give anyone else access, you'll need a
copy of their SSH public key; we'll assume you have that key in
~/sam-saucer-key.pub
. To manage access, you make changes to the special hgadmin
repository.
jay@spoon:~$
hg clone ssh://hg@jeeves/hgadmin
destination directory: hgadmin no changes found updating working directory 0 files updated, 0 files merged, 0 files removed, 0 files unresolved jay@spoon:~$
cd hgadmin
jay@spoon:~/hgadmin$
mkdir -p keys/users/sam
jay@spoon:~/hgadmin$
cp ~/sam-saucer-key.pub keys/users/sam/saucer
jay@spoon:~/hgadmin$
hg add
adding keys/users/sam/saucer jay@spoon:~/hgadmin$
hg commit -m "Add Sam's key"
jay@spoon:~/hgadmin$
hg push
pushing to ssh://hg@jeeves/hgadmin searching for changes remote: adding changesets remote: adding manifests remote: adding file changes remote: added 1 changesets with 1 changes to 1 files jay@spoon:~/hgadmin$
Sam can now read and write to your
ssh://hg@jeeves/jays/project
repository.
Most other changes to access control can be made simply by making and
pushing changes to hgadmin
, and you can use Mercurial to
cooperate with other root users in the normal way.
If you prefer, you could give them access by
logging into jeeves
,
putting the key in the right place under /etc/mercurial-server/keys
, and re-running
sudo -u hg /usr/share/mercurial-server/refresh-auth
.
However, using hgadmin
is usually more convenient if you need to make more than a very few changes; it also makes it easier to share administration with others and provides a log of all changes.
Out of the box, mercurial-server supports two kinds of users: "root" users and normal users. If you followed the steps above, you are a "root" user because your key is under keys/root
, while the other user you gave access to is a normal user since their key is under keys/users
. Keys that are not in either of these directories will by default have no access to anything.
Root users can edit hgadmin
, create new repositories and read and write to existing ones. Normal users cannot access hgadmin
or create new repositories, but they can read and write to any other repository.
mercurial-server offers much more fine-grained access control than this division into two classes of users. Let's suppose you wish to give Pat access to the widget
repository, but no other. We first copy Pat's SSH public key into the keys/pat
directory in hgadmin
. This tells mercurial-server about Pat's key, but gives Pat no access to anything because the key is not under either keys/root
or keys/users
. To grant this key access, we must give mercurial-server a new access rule, so we create a file in hgadmin
called access.conf
, with the following contents:
# Give Pat access to the "widget" repository write repo=widget user=pat/*
Pat will have read and write access to the widget
repository as soon as we add, commit, and push these files.
Each line of access.conf
has the following syntax:
rule
condition
condition...
Blank lines and lines that start with #
are ignored. Rule is
one of
init
: allow reads, writes, and the creation of new repositories
write
: allow reads and writes
read
: allow only read operations
deny
: deny all requests
A condition is a globpattern matched against a relative path. The two most important conditions are
user=globpattern
: path to the user's key
repo=globpattern
: path to the repository
*
only matches one directory level, where **
matches as many as you want. More precisely, *
matches zero or
more characters not including /
while **
matches
zero or more characters including /
. So
projects/*
matches projects/foo
but not projects/foo/bar
, while
projects/**
matches both.
When considering a request, mercurial-server steps through all the rules in
/etc/mercurial-server/access.conf
and then all the
rules in access.conf
in hgadmin
looking for a rule which matches on every condition. The first match
determines whether the request will be allowed; if there is no match in
either file, the request will be denied.
By default, /etc/mercurial-server/access.conf
has the
following rules:
init user=root/** deny repo=hgadmin write user=users/**
These rules ensure that root users can do any operation on any repository,
that no other users can access the hgadmin
repository,
and that those with keys in keys/users
can read or write to any repository
but not create repositories. Some examples of how these rules work:
root/jay
creates a repository
foo/bar/baz
. This matches the first
rule and so will be allowed.
root/jay
changes repository
hgadmin
. Again, this matches the
first rule and so will be allowed; later rules have no effect.
users/sam
tries to read
repository hgadmin
. This does not
match the first rule, but matches the second, and so will be denied.
users/sam
tries to create
repository sams-project
. This does
not match the first two rules, but matches the third; this is a
write
rule, which doesn't grant the privilege to create
repositories, so the request will be denied.
users/sam
writes to existing
repository projects/main
. Again,
this matches the third rule, which allows the request.
pat
tries to write to existing
repository widget
. Until we change
the access.conf
file in hgadmin
, this will match no rule, and so will
be denied.
keys
directory at all will always be denied,
no matter what rules are in effect; because of the way SSH authentication
works, they will be prompted to enter a password, but no password will
work. This can't be changed.
mercurial-server consults two distinct locations to collect information about what to allow: /etc/mercurial-server
and its own hgadmin
repository. This is useful for several reasons:
/etc/mercurial-server
may offer a simpler route.
/etc/mercurial-server
is suitable
for management with tools such as Puppethgadmin
leaves you "locked out", /etc/mercurial-server
allows you a way back in.
Rules in /etc/mercurial-server/access.conf
are checked before those in hgadmin
, and keys in /etc/mercurial-server/keys
will be present no matter how hgadmin
changes.
We anticipate that once mercurial-server is successfully installed and
working you will usually want to use hgadmin
for most
access control tasks. Once you have the right keys and
access.conf
set up in hgadmin
, you
can delete /etc/mercurial-server/access.conf
and all
of /etc/mercurial-server/keys
,
turning control entirely over to hgadmin
.
/etc/mercurial-server/remote-hgrc.d
is in the
HGRCPATH
for all remote access to mercurial-server
repositories. This directory contains the hooks that mercurial-server uses for
access control and logging. You can add hooks to this directory, but obviously
breaking the existing hooks will disable the relevant functionality and
isn't advisable.
mercurial-server supports file and branch conditions, which restrict an operation depending on what files it modifies and what branch the work is on.
File and branch conditions are added to the conditions against which a rule matches, just like user and repo conditions; they have this form:
file=globpattern
: file within the repo
branch=globpattern
: Mercurial branch name
However, in order to understand what effect adding these conditions will have, it helps to understand how and when these rules are applied.
The rules file is used to make three decisions:
When the first two of these decisions are being made, nothing is known
about any changsets that might be pushed, and so all file and branch
conditions automatically succeed for the purpose of such decisions. For the
third condition, every file changed in the changeset must be allowed by a
write
or init
rule for the changeset
to be allowed.
This means that doing tricky things with file conditions can have counterintuitive consequences:
You cannot limit read access to a subset of a repository with a read
rule and a file condition: any user who has access to a repository can read
all of it and its full history. Such a rule can only have the effect of
masking a later write
rule, as in this example:
read repo=specialrepo file=dontwritethis write repo=specialrepo
allows all users to read specialrepo
, and to write to all files
except that any changeset which writes to
dontwritethis
will be rejected.
init
rules file conditions.
Don't try to deny write access to a particular file on a particular branch—a developer can write to the file on another branch and then merge it in. Either deny all writes to the branch from that user, or allow them to write to all the files they can write to on any branch.
write user=docs/* branch=docs file=docs/*
This rule grants users whose keys are in the docs
subdirectory the power to push changesets
into any repository only if those changesets are on the
docs
branch and they affect only those files directly
under the docs
directory. However,
the rules below have more counterintuitive consequences.
write user=docs/* branch=docs write user=docs/* file=docs/* read user=docs/*
These rules grant users whose keys are in the docs
subdirectory the power to change any file directly under the docs
directory, or any file at all in the docs
branch. Indirectly, however, this adds up to the power to change any file on any branch, simply by making the change on the docs branch and then merging the change into another branch.
All of the repositories controlled by mercurial-server are owned by a
single user, the hg
user, which is why all URLs for
mercurial-server repositories start with ssh://hg@...
.
Each SSH key that has access to the repository has an entry in
~hg/.ssh/authorized_keys
; this is how the SSH daemon
knows to give that key access. When the user connects over SSH, their
commands are run in a custom restricted shell; this shell knows which key
was used to connect, determines what the user is trying to do, checks the
access rules to decide whether to allow it, and if allowed invokes
Mercurial internally, without forking.
This restricted shell also ensures that certain Mercurial extensions are loaded when the user acts on a repository; these extensions check the access control rules for any changeset that the user tries to commit, and log all pushes and pulls into a per-repository access log.
refresh-auth recurses through the /etc/mercurial-server/keys
and the keys
directory in the
hgadmin
repository, creating an entry in
~hg/.ssh/authorized_keys
for each one. This is redone
automatically whenever a change is pushed to hgadmin
.
mercurial-server relies entirely on sshd to grant access to remote users.
As a result, it runs no daemons, installs no setuid programs, and no part
of it runs as root
except the install process: all programs run as the user
hg
. Any attack on mercurial-server can only be started if the attacker
already has a public key in ~hg/.ssh/authorized_keys
,
otherwise sshd will bar the way.
No matter what command the user tries to run on the remote system via SSH, mercurial-server is run. It parses the command line the user asked for, and interprets and runs the corresponding operation itself if access is allowed, so users can only read and add to history within repositories; they cannot run any other command. In addition, every push and pull is logged with a datestamp, changeset ID and the key that performed the operation.
However, while the first paragraph holds no matter what bugs mercurial-server contains, the second depends on the relevant code being correct; though the entire codebase is short, like all software mercurial-server may harbour bugs. Backups are essential!
Every successful access is logged in a file called
~hg/repos/
. This file is in YAML format for easy parsing, but if you don't like YAML, simply treat each line as a JSON data structure prepended with repository
/.hg/mercurial-server.log-
. The log records the time as a
UTC ISO 8601 time, the operation ("push" or "pull"), the path to the key as
used in the access rules, the SSH connection information (including the source IP address), and the hex changeset IDs.
For security reasons, all mercurial-server code runs as the hg
user. The first thing this code reads when it starts is ~hg/.mercurial-server
; if this file is absent or corrupt the code won't run. This file specifies all of the file paths that mercurial-server uses. In particular, it specifies that mercurial-server always uses HGRCPATH = /etc/mercurial-server/remote-hgrc.d
for remote operations, overriding any system HGRCPATH
.
By creating such a file with suitable entries, you can run mercurial-server as a user other than hg
, or install it without root privileges; however I strongly recommend that if you need to do this, you use a user account that is used for no other purpose, and take the time to thoroughly understand how mercurial-server works before you attempt it.
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Thanks for reading this far. If you use mercurial-server, please tell me about it.
Paul Crowley, <paul@lshift.net>
, 2010