<?xml version="1.0" encoding="utf-8"?><article xmlns="http://docbook.org/ns/docbook" version="5.0" xml:lang="en" xmlns:xlink="http://www.w3.org/1999/xlink"><info> <title>Sharing Mercurial repositories with mercurial-server</title> <author><firstname>Paul</firstname><surname>Crowley</surname></author> <copyright><year>2009</year><holder>Paul Crowley, LShift Ltd</holder></copyright></info><section><title>About mercurial-server</title><para>Home page: <link xlink:href="http://www.lshift.net/mercurial-server.html"/></para><para>mercurial-server is software for Debian and Ubuntu systems which gives yourdevelopers remote read/write access to <linkxlink:href="http://hg-scm.org/">Mercurial</link> repositories using SSH publickey authentication; it provides convenient and fine-grained key managementand access control.</para><para>mercurial-server is the easiest and most secure way for several developersto have read/write access to a central repository, but that's not the onlyway for several people to work on the same project using Mercurial; youshould be familiar with the <linkxlink:href="http://mercurial.selenic.com/wiki/MultipleCommitters">other ways ofhandling multiple commiters</link> before deciding to use this.</para><para>Though mercurial-server is currently targeted at Debian-based systems suchas Ubuntu, other users have reported success getting it running on otherUnix-based systems such as Red Hat. Running it on a non-Unix system such asWindows is not supported. You will need root privileges to install it.</para><section><title>Legalese</title><para>This program is free software; you can redistribute it and/or modify itunder the terms of the GNU General Public License as published by the FreeSoftware Foundation; either version 2 of the License, or (at your option)any later version.</para><para>This program is distributed in the hope that it will be useful, butWITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITYor FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License formore details.</para><para>You should have received a copy of the GNU General Public License alongwith this program; if not, write to the Free Software Foundation, Inc., 51Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.</para></section></section><section><title>Step by step</title><para>mercurial-server authenticates users not using passwords but using <linkxlink:href="http://sial.org/howto/openssh/publickey-auth/">SSH publickeys</link>; everyone who wants access to a mercurial-server repositorywill need such a key, so you'll need to familiarize yourself with thembefore proceeding. In combination with <command>ssh-agent</command> (orequivalents such as the Windows program <linkxlink:href="http://the.earth.li/~sgtatham/putty/0.60/htmldoc/Chapter9.html#pageant">Pageant</link>),this means that users will not need to type in a password to access therepository.</para><section><title>Creating a repository host</title><para>In what follows, we assume that your username is <systemitemclass="username">jay</systemitem>, that you usually sit at a machine called<systemitem class="systemname">my-workstation</systemitem> and you wish toinstall mercurial-server on <systemitemclass="systemname">repository-host</systemitem>. First, you'll need tocreate an SSH public key if you haven't already. You should consult yoursystem documentation on how to do this, but it should look something likethis.</para><screen><computeroutput>jay@my-workstation:~$ </computeroutput><userinput>ssh-keygen</userinput><computeroutput>Generating public/private rsa key pair.Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /home/jay/.ssh/id_rsa.Your public key has been saved in /home/jay/.ssh/id_rsa.pub.The key fingerprint is:8b:aa:0a:98:fe:e7:84:48:a3:fe:5f:31:4b:16:e6:0b jay@my-workstationjay@my-workstation:~$ </computeroutput><userinput>ssh-add</userinput><computeroutput>Enter passphrase for /home/jay/.ssh/id_rsa: Identity added: /home/jay/.ssh/id_rsa (/home/jay/.ssh/id_rsa)jay@my-workstation:~$ </computeroutput></screen><para>Now copy the files you're going to need over to your target system, and install mercurial-server</para><screen><computeroutput>jay@my-workstation:~$ </computeroutput><userinput>ssh-copy-id repository-host</userinput><computeroutput>jay@repository-host's password:Now try logging into the machine, with "ssh 'repository-host'", and check in: .ssh/authorized_keysto make sure we haven't added extra keys that you weren't expecting.jay@my-workstation:~$ </computeroutput><userinput>scp mercurial-server_0.6.1_amd64.deb repository-host:</userinput><computeroutput>mercurial-server_0.6.1_amd64.deb 100%jay@my-workstation:~$ </computeroutput><userinput>ssh -A repository-host</userinput><computeroutput>jay@repository-host:~$ </computeroutput><userinput>sudo dpkg -i ../mercurial-server_0.6.1_amd64.deb</userinput><computeroutput>[sudo] password for jay: Selecting previously deselected package mercurial-server.(Reading database ... 144805 files and directories currently installed.)Unpacking mercurial-server (from .../mercurial-server_0.6.1_amd64.deb) ...Setting up mercurial-server (0.6.1) ...jay@repository-host:~$ </computeroutput></screen><para>mercurial-server is now installed on the repository host. Next, we need to give you permission to access its repositories.</para><screen><computeroutput>jay@repository-host:~$ </computeroutput><userinput>ssh-add -L > my-key</userinput><computeroutput>jay@repository-host:~$ </computeroutput><userinput>sudo mkdir -p /etc/mercurial-server/keys/root/jay</userinput><computeroutput>jay@repository-host:~$ </computeroutput><userinput>sudo cp my-key /etc/mercurial-server/keys/root/jay/my-workstation</userinput><computeroutput>jay@repository-host:~$ </computeroutput><userinput>sudo -u hg /usr/share/mercurial-server/refresh-auth</userinput><computeroutput>jay@repository-host:~$ </computeroutput><userinput>exit</userinput><computeroutput>Connection to shell closed.jay@my-workstation:~$ </computeroutput></screen><para>You can now create repositories on the remote machine and have completeread-write access to all of them; you need never log on to <systemitemclass="systemname">repository-host</systemitem> again.</para></section><section><title>Creating repositories</title><screen><computeroutput>jay@my-workstation:~$ </computeroutput><userinput>cd my-mercurial-project</userinput><computeroutput>jay@my-workstation:~/my-mercurial-project$ </computeroutput><userinput>hg clone . ssh://hg@repository-host/repository/name</userinput><computeroutput>searching for changesremote: adding changesetsremote: adding manifestsremote: adding file changesremote: added 119 changesets with 284 changes to 61 filesjay@my-workstation:~/my-mercurial-project$ </computeroutput><userinput>hg pull ssh://hg@repository-host/repository/name</userinput><computeroutput>pulling from ssh://hg@repository-host/repository/namesearching for changesno changes foundjay@my-workstation:~/my-mercurial-project$ </computeroutput></screen></section><section><title>Adding other users</title><para>As things stand, no-one but you has any access to any repositories youcreate on this system. In order to give anyone else access, you'll need acopy of their SSH public key; we'll assume you have that key in<filename>~/sam-key.pub</filename>. You could give them access bylogging into <systemitem class="systemname">repository-host</systemitem>,putting the key in the right place under <filenameclass='directory'>/etc/mercurial-server/keys</filename>, and re-running<userinput>sudo -u hg /usr/share/mercurial-server/refresh-auth</userinput>.However, there's a more convenient way.</para><screen><computeroutput>jay@my-workstation:~/my-mercurial-project$ </computeroutput><userinput>cd ..</userinput><computeroutput>jay@my-workstation:~$ </computeroutput><userinput>hg clone ssh://hg@repository-host/hgadmin</userinput><computeroutput>destination directory: hgadminno changes foundupdating working directory0 files updated, 0 files merged, 0 files removed, 0 files unresolvedjay@my-workstation:~$ </computeroutput><userinput>cd hgadmin</userinput><computeroutput>jay@my-workstation:~/hgadmin$ </computeroutput><userinput>mkdir -p keys/users/sam</userinput><computeroutput>jay@my-workstation:~/hgadmin$ </computeroutput><userinput>cp ~/other-users-key.pub keys/users/sam/their-workstation</userinput><computeroutput>jay@my-workstation:~/hgadmin$ </computeroutput><userinput>hg add</userinput><computeroutput>adding keys/users/sam/their-workstationjay@my-workstation:~/hgadmin$ </computeroutput><userinput>hg commit -m "Add Sam's key'"</userinput><computeroutput>jay@my-workstation:~/hgadmin$ </computeroutput><userinput>hg push</userinput><computeroutput>pushing to ssh://hg@repository-host/hgadminsearching for changesremote: adding changesetsremote: adding manifestsremote: adding file changesremote: added 1 changesets with 1 changes to 1 filesjay@my-workstation:~/hgadmin$ </computeroutput></screen><para>Sam can now read and write to your<literal>ssh://hg@repository-host/repository/name</literal> repository.Most other changes to access control can be made simply by making andpushing changes to <literal>hgadmin</literal>, and you can use Mercurial tocooperate with other root users in the normal way.</para></section><section><title>Basic access control</title><para>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 <filename class='directory'>keys/root</filename>, while the other user you gave access to is a normal user since their key is under <filename class='directory'>keys/users</filename>. Keys that are not in either of these directories will by default have no access to anything.</para><para>Root users can edit <literal>hgadmin</literal>, create new repositories and read and write to existing ones. Normal users cannot access <literal>hgadmin</literal> or create new repositories, but they can read and write to any other repository. This is only the default configuration; for more advanced configuration read <xref linkend="accesscontrol"/>.</para></section></section><section id="accesscontrol"><title>Access control</title><para>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 <literal>widget</literal> repository, but no other. We first copy Pat's SSH public key into the <filenameclass='directory'>keys/widget/pat</filename> directory in <literal>hgadmin</literal>. Now mercurial-server knows about Pat's key, but will give Pat no access to anything because the key is not under either <filenameclass='directory'>keys/root</filename> or <filenameclass='directory'>keys/users</filename>. To grant this key access, we must give mercurial-server a new access rule, so we create a file in <literal>hgadmin</literal> called <filename>access.conf</filename>, with the following contents:</para><programlisting> write repo=widget user=widget/**</programlisting><para>Pat will have read and write access as soon as we add, commit, and push these files.</para><para>Each line of <filename>access.conf</filename> has the following syntax:</para><programlisting><replaceable>rule</replaceable> <replaceable>condition</replaceable> <replaceable>condition...</replaceable></programlisting><para>Blank lines and lines that start with <literal>#</literal> are ignored. Rule is one of</para><itemizedlist><listitem><literal>init</literal>: allow reads, writes, and the creation of new repositories</listitem><listitem><literal>write</literal>: allow reads and writes</listitem><listitem><literal>read</literal>: allow only read operations</listitem><listitem><literal>deny</literal>: deny all requests</listitem></itemizedlist><para>When considering a request, mercurial-server steps through all the rules in <filename>/etc/mercurial-server/access.conf</filename> and then all the rules in <filename>access.conf</filename> in <literal>hgadmin</literal> looking for a rule which matches on every condition. If it does not find such a rule, it denies the request; otherwise it checks whether the rule grants sufficient privilege to allow it.</para><para>By default, <filename>/etc/mercurial-server/access.conf</filename> has the following rules:</para><programlisting> init user=root/** deny repo=hgadmin write user=users/**</programlisting><para>These rules ensure that root users can do any operation on any repository, that no other users can access the <literal>hgadmin</literal> repository, and that those with keys in <filename class='directory'>keys/users</filename> can read or write to any repository but not create repositories.</para><para>A condition is a globpattern matched against a relative path. The two mostimportant conditions are</para><itemizedlist><listitem><code><literal>user=</literal><replaceable>globpattern</replaceable></code>: path to the user's key</listitem><listitem><code><literal>repo=</literal><replaceable>globpattern</replaceable></code>: path to the repository</listitem></itemizedlist><para>"*" only matches one directory level, where "**" matches as many as youwant. More precisely, "*" matches zero or more characters not including "/"while "**" matches zero or more characters including "/".</para><section><title>/etc/mercurial-server and hgadmin</title><para>mercurial-server consults two distinct locations to collect information about what to allow: <filenameclass='directory'>/etc/mercurial-server</filename> and its own <literal>hgadmin</literal> repository. This is useful for several reasons:</para><itemizedlist><listitem>Users may not need the sophistication of access control via mercurial; for these users updating <filenameclass='directory'>/etc/mercurial-server</filename> may offer a simpler route.</listitem><listitem><filenameclass='directory'>/etc/mercurial-server</filename> is suitable for management by some other route, such as with <linkxlink:href="http://reductivelabs.com/products/puppet">Puppet</link></listitem><listitem>If a change to <literal>hgadmin</literal> leaves you "locked out", <filenameclass='directory'>/etc/mercurial-server</filename> allows you a way back in.</listitem><listitem>At install time, all users are "locked out", and so some mechanism to allow some users in is needed.</listitem></itemizedlist><para>Rules in <filename>/etc/mercurial-server/access.conf</filename> take precedence over those in <literal>hgadmin</literal>, and obviously keys in <filename class='directory'>/etc/mercurial-server/keys</filename> cannot be affected by changes to <literal>hgadmin</literal>.</para><para>We anticipate that once mercurial-server is successfully installed andworking most users will want to use <literal>hgadmin</literal> for mostaccess control tasks. Once you have the right keys and<filename>access.conf</filename> set up in <literal>hgadmin</literal>, youcan delete <filename>/etc/mercurial-server/access.conf</filename> and allof <filename class='directory'>/etc/mercurial-server/keys</filename>,turning control entirely over to <literal>hgadmin</literal>.</para><para><filename>/etc/mercurial-server/remote-hgrc</filename> is in the<systemitem>HGRCPATH</systemitem> for all remote access to mercurial-serverrepositories. This file contains the hooks that mercurial-server uses foraccess control and logging. You can add hooks to this file, but obviouslybreaking the existing hooks will disable the relevant functionality andisn't advisable.</para></section><section><title>File and branch conditions</title><para>mercurial-server supports file and branch conditions, which restrict anoperation depending on what files it modifies and what branch the work ison. </para><caution>The way these conditions work is subtle and can be counterintuitive - ifyou want to keep things simple, stick to user and repo conditions, and thenthings are likely to work the way you would expect.</caution><para>File and branch conditions are added to the conditions against which a rulematches, just like user and repo conditions; they have this form:</para><itemizedlist><listitem><code><literal>file=</literal><replaceable>globpattern</replaceable></code>: file within the repo</listitem><listitem><code><literal>branch=</literal><replaceable>globpattern</replaceable></code>: Mercurial branch name</listitem></itemizedlist><para>However, in order to understand what effect adding these conditions willhave, it helps to understand how and when these rules are applied.</para><para>The rules file is used to make three decisions:</para><itemizedlist><listitem>Whether to allow a repository to be created</listitem><listitem>Whether to allow any access to a repository</listitem><listitem>Whether to allow a changeset, which is on a some branch</listitem><listitem>Whether to allow a changeset which changes a particular file</listitem></itemizedlist><para>When the first two of these decisions are being made, nothing is knownabout what files might be changed, and so all file and branch conditionsautomatically succeed for the purpose of such decisions. This means thatdoing tricky things with file conditions can have counterintuitiveconsequences:</para><itemizedlist><listitem><para>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 readall of it and its full history. Such a rule can only have the effect ofmasking a later "write" rule, as in this example:</para><programlisting> read repo=specialrepo file=dontwritethis write repo=specialrepo</programlisting><para>allows all users to read specialrepo, and to write to all files<emphasis>except</emphasis> that any changeset which writes to<filename>dontwritethis</filename> will be rejected.</para></listitem><listitem>For similar reasons, don't give <literal>init</literal> rules file conditions.</listitem><listitem><para>Don't try to deny write access to a particular file on a particularbranch - a developer can write to the file on another branch and then mergeit in. Either deny all writes to the branch from that user, or allow themto write to all the files they can write to on any branch. In other words,something like this will have the intended effect:</para><programlisting> write user=docs/* branch=docs file=docs/*</programlisting><para>But something like this will not have the intended effect; it willeffectively allow these users to write to any file on any branch, bywriting it to "docs" first:</para><programlisting> write user=docs/* branch=docs write user=docs/* file=docs/* read user=docs/*</programlisting></listitem></itemizedlist></section></section><section><title>How mercurial-server works</title><para>All of the repositories controlled by mercurial-server are owned by asingle user, the <literal>hg</literal> user, which is why all URLs formercurial-server repositories start with <literal>ssh://hg@...</literal>.Each SSH key that has access to the repository has an entry in<filename>~hg/.ssh/authorized_keys</filename>; this is how the SSH daemonknows to give that key access. When the user connects over SSH, theircommands are run in a specially crafted restricted shell; this shell knowswhich key was used to connect, determines what the user is trying to do,and checks the access rules to decide whether to allow it. </para><para>This restricted shell also ensures that certain Mercurial extensions areloaded when the user acts on a repository; these extensions check theaccess control rules for any changeset that the user tries to commit, andlog all pushes and pulls into a per-repository access log.</para><para><command>refresh-auth</command> recurses through the <filenameclass='directory'>/etc/mercurial-server/keys</filename> and the <filenameclass='directory'>keys</filename> directory in the<literal>hgadmin</literal> repository, creating an entry in<filename>~hg/.ssh/authorized_keys</filename> for each one. This is redoneautomatically whenever a change is pushed to <literal>hgadmin</literal>.</para></section><section><title>Security</title><para>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 partof it runs as root except the install process: all programs run as the userhg. And any attack on mercurial-server can only be started if the Bad Guysalready have a public key in <filename>~hg/.ssh/authorized_keys</filename>,otherwise sshd will bar the way.</para><para>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, andinterprets and runs the corresponding hg operation itself if access isallowed, so users can only read and add to history within repositories;they cannot run any other hg command. In addition, every push and pull islogged with a datestamp, changeset ID and the key that performed theoperation.</para><para>However, while the first paragraph holds no matter what bugsmercurial-server contains, the second depends on the relevant code beingcorrect; though the entire codebase is short, mercurial-server is a fairlynew program and may harbour bugs. Backups are essential!</para></section><section><title>Thanks</title><para>Thanks for reading this far. If you use mercurial-server, please tell me aboutit.</para><para>Paul Crowley, <email>paul@lshift.net</email>, 2009</para></section></article>