<?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>2008-2010</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 gives your developers remote read/write access tocentralized <link xlink:href="http://hg-scm.org/">Mercurial</link>repositories using SSH public key authentication; it provides convenientand fine-grained key management and access control.</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><section><title>Step by step</title><para>mercurial-server authenticates users not using passwords but using SSHpublic keys; everyone who wants access to a mercurial-server repositorywill need such a key. 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. If you're not familiar with SSH public keys, the <linkxlink:href="http://sial.org/howto/openssh/publickey-auth/">OpenSSH PublicKey Authentication tutorial</link> may be helpful.</para><section><title>Initial access to mercurial-server</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">spoon</systemitem> and you haveinstalled mercurial-server on <systemitemclass="systemname">jeeves</systemitem> 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 <systemitemclass="systemname">jeeves</systemitem>. </para><screen><computeroutput>jay@spoon:~$ </computeroutput><userinput>ssh -A jeeves</userinput><computeroutput>jay@jeeves:~$ </computeroutput><userinput>ssh-add -L > my-key</userinput><computeroutput>jay@jeeves:~$ </computeroutput><userinput>sudo mkdir -p /etc/mercurial-server/keys/root/jay</userinput><computeroutput>jay@jeeves:~$ </computeroutput><userinput>sudo cp my-key /etc/mercurial-server/keys/root/jay/spoon</userinput><computeroutput>jay@jeeves:~$ </computeroutput><userinput>sudo -u hg /usr/share/mercurial-server/refresh-auth</userinput><computeroutput>jay@jeeves:~$ </computeroutput><userinput>exit</userinput><computeroutput>Connection to jeeves closed.jay@spoon:~$ </computeroutput></screen><para>You can now create repositories on the remote machine and have completeread-write access to all of them.</para></section><section><title>Creating repositories</title><para>To store a repository on the server, clone it over.</para><screen><computeroutput>jay@spoon:~$ </computeroutput><userinput>cd myproj</userinput><computeroutput>jay@spoon:~/myproj$ </computeroutput><userinput>hg clone . ssh://hg@jeeves/jays/project</userinput><computeroutput>searching for changesremote: adding changesetsremote: adding manifestsremote: adding file changesremote: added 119 changesets with 284 changes to 61 filesjay@spoon:~/myproj$ </computeroutput><userinput>hg pull ssh://hg@jeeves/jays/project</userinput><computeroutput>pulling from ssh://hg@jeeves/jays/projectsearching for changesno changes found<computeroutput>jay@spoon:~/myproj$ </computeroutput><userinput>cd ..</userinput>jay@spoon:~$ </computeroutput></screen></section><section><title>Adding other users</title><para>At this stage, 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-saucer-key.pub</filename>. To manage access, you make changes to the special <filenameclass='directory'>hgadmin</filename> repository.</para><screen><computeroutput>jay@spoon:~$ </computeroutput><userinput>hg clone ssh://hg@jeeves/hgadmin</userinput><computeroutput>destination directory: hgadminno changes foundupdating working directory0 files updated, 0 files merged, 0 files removed, 0 files unresolvedjay@spoon:~$ </computeroutput><userinput>cd hgadmin</userinput><computeroutput>jay@spoon:~/hgadmin$ </computeroutput><userinput>mkdir -p keys/users/sam</userinput><computeroutput>jay@spoon:~/hgadmin$ </computeroutput><userinput>cp ~/sam-saucer-key.pub keys/users/sam/saucer</userinput><computeroutput>jay@spoon:~/hgadmin$ </computeroutput><userinput>hg add</userinput><computeroutput>adding keys/users/sam/saucerjay@spoon:~/hgadmin$ </computeroutput><userinput>hg commit -m "Add Sam's key"</userinput><computeroutput>jay@spoon:~/hgadmin$ </computeroutput><userinput>hg push</userinput><computeroutput>pushing to ssh://hg@jeeves/hgadminsearching for changesremote: adding changesetsremote: adding manifestsremote: adding file changesremote: added 1 changesets with 1 changes to 1 filesjay@spoon:~/hgadmin$ </computeroutput></screen><para>Sam can now read and write to your<uri>ssh://hg@jeeves/jays/project</uri> repository.Most other changes to access control can be made simply by making andpushing changes to <filenameclass='directory'>hgadmin</filename>, and you can use Mercurial tocooperate with other root users in the normal way.</para><para>If you prefer, you could give them access bylogging into <systemitem class="systemname">jeeves</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, using <filenameclass='directory'>hgadmin</filename> 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.</para></section></section><section><title>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 <filenameclass='directory'>hgadmin</filename>, create new repositories and read and write to existing ones. Normal users cannot access <filenameclass='directory'>hgadmin</filename> or create new repositories, but they can read and write to any other repository.</para><section><title>Using access.conf</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 <filenameclass='directory'>widget</filename> repository, but no other. We first copy Pat's SSH public key into the <filenameclass='directory'>keys/pat</filename> directory in <filenameclass='directory'>hgadmin</filename>. This tells mercurial-server about Pat's key, but gives 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 <filenameclass='directory'>hgadmin</filename> called <filename>access.conf</filename>, with the following contents:</para><programlisting># Give Pat access to the "widget" repositorywrite repo=widget user=pat/*</programlisting><para>Pat will have read and write access to the <filenameclass='directory'>widget</filename> repository 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 <code>#</code> are ignored. Rule isone 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>A condition is a globpattern matched against a relative path. The two mostimportant conditions are</para><itemizedlist><listitem><code>user=<replaceable>globpattern</replaceable></code>: path to the user's key</listitem><listitem><code>repo=<replaceable>globpattern</replaceable></code>: path to the repository</listitem></itemizedlist><para><code>*</code> only matches one directory level, where <code>**</code>matches as many as you want. More precisely, <code>*</code> matches zero ormore characters not including <code>/</code> while <code>**</code> matcheszero or more characters including <code>/</code>. So<code>projects/*</code> matches <filenameclass='directory'>projects/foo</filename> but not <filenameclass='directory'>projects/foo/bar</filename>, while<code>projects/**</code> matches both.</para><para>When considering a request, mercurial-server steps through all the rules in<filename>/etc/mercurial-server/access.conf</filename> and then all therules in <filename>access.conf</filename> in <filenameclass='directory'>hgadmin</filename>looking for a rule which matches on every condition. The first matchdetermines whether the request will be allowed; if there is no match ineither file, the request will be denied.</para><para>By default, <filename>/etc/mercurial-server/access.conf</filename> has thefollowing rules:</para><programlisting>init user=root/**deny repo=hgadminwrite user=users/**</programlisting><para>These rules ensure that root users can do any operation on any repository,that no other users can access the <filenameclass='directory'>hgadmin</filename> repository,and that those with keys in <filenameclass='directory'>keys/users</filename> can read or write to any repositorybut not create repositories. Some examples of how these rules work:</para><itemizedlist><listitem>User <filename class='directory'>root/jay</filename> creates a repository<filename class='directory'>foo/bar/baz</filename>. This matches the firstrule and so will be allowed.</listitem><listitem>User <filename class='directory'>root/jay</filename> changes repository<filename class='directory'>hgadmin</filename>. Again, this matches thefirst rule and so will be allowed; later rules have no effect.</listitem><listitem>User <filename class='directory'>users/sam</filename> tries to readrepository <filename class='directory'>hgadmin</filename>. This does notmatch the first rule, but matches the second, and so will be denied.</listitem><listitem>User <filename class='directory'>users/sam</filename> tries to createrepository <filename class='directory'>sams-project</filename>. This doesnot match the first two rules, but matches the third; this is a<literal>write</literal> rule, which doesn't grant the privilege to createrepositories, so the request will be denied.</listitem><listitem>User <filename class='directory'>users/sam</filename> writes to existingrepository <filename class='directory'>projects/main</filename>. Again,this matches the third rule, which allows the request.</listitem><listitem>User <filename class='directory'>pat</filename> tries to write to existingrepository <filename class='directory'>widget</filename>. Until we changethe <filename>access.conf</filename> file in <filenameclass='directory'>hgadmin</filename>, this will match no rule, and so willbe denied.</listitem><listitem>Any request from a user whose key not under the <filenameclass='directory'>keys</filename> directory at all will always be denied,no matter what rules are in effect; because of the way SSH authenticationworks, they will be prompted to enter a password, but no password willwork. This can't be changed.</listitem></itemizedlist></section><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 <filenameclass='directory'>hgadmin</filename> repository. This is useful for several reasons:</para><itemizedlist><listitem>Some users may not need the convenience of access control via mercurial; for these users updating <filenameclass='directory'>/etc/mercurial-server</filename> may offer a simpler route.</listitem><listitem><filename class='directory'>/etc/mercurial-server</filename> is suitablefor management with tools such as <linkxlink:href="http://reductivelabs.com/products/puppet">Puppet</link></listitem><listitem>If a change to <filenameclass='directory'>hgadmin</filename> 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> are checked before those in <filenameclass='directory'>hgadmin</filename>, and keys in <filename class='directory'>/etc/mercurial-server/keys</filename> will be present no matter how <filenameclass='directory'>hgadmin</filename> changes.</para><para>We anticipate that once mercurial-server is successfully installed andworking you will usually want to use <filenameclass='directory'>hgadmin</filename> for mostaccess control tasks. Once you have the right keys and<filename>access.conf</filename> set up in <filenameclass='directory'>hgadmin</filename>, youcan delete <filename>/etc/mercurial-server/access.conf</filename> and allof <filename class='directory'>/etc/mercurial-server/keys</filename>,turning control entirely over to <filenameclass='directory'>hgadmin</filename>.</para><para><filename>/etc/mercurial-server/remote-hgrc.d</filename> is in the<systemitem>HGRCPATH</systemitem> for all remote access to mercurial-serverrepositories. This directory contains the hooks that mercurial-server uses foraccess control and logging. You can add hooks to this directory, 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. Unlessyou need what they provide, ignore this section, stick to user and repoconditions, and then things are likely to work the way you would expect. Ifyou do need what they provide, read what follows very carefully.</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>file=<replaceable>globpattern</replaceable></code>: file within the repo</listitem><listitem><code>branch=<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</listitem></itemizedlist><para>When the first two of these decisions are being made, nothing is knownabout any changsets that might be pushed, and so all file and branchconditions automatically succeed for the purpose of such decisions. For thethird condition, every file changed in the changeset must be allowed by a<literal>write</literal> or <literal>init</literal> rule for the changesetto be allowed.</para><para>This means that doing tricky things with file conditions can havecounterintuitive consequences:</para><itemizedlist><listitem><para>You cannot limit read access to a subset of a repository with a <literal>read</literal>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 <literal>write</literal> rule, as in this example:</para><programlisting>read repo=specialrepo file=dontwritethiswrite repo=specialrepo</programlisting><para>allows all users to read <literal>specialrepo</literal>, 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.</para><programlisting>write user=docs/* branch=docs file=docs/*</programlisting><para>This rule grants users whose keys are in the <filenameclass='directory'>docs</filename> subdirectory the power to push changesetsinto any repository only if those changesets are on the<literal>docs</literal> branch and they affect only those files directlyunder the <filename class='directory'>docs</filename> directory. However,the rules below have more counterintuitive consequences.</para><programlisting>write user=docs/* branch=docswrite user=docs/* file=docs/*read user=docs/*</programlisting><para>These rules grant users whose keys are in the <filenameclass='directory'>docs</filename> subdirectory the power to change any file directly under the <filename class='directory'>docs</filename> directory, or any file at all in the <literal>docs</literal> 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.</para></listitem></itemizedlist></section></section><section><title>In detail</title><section><title>How mercurial-server works</title><para>All of the repositories controlled by mercurial-server are owned by asingle user, the <systemitemclass="username">hg</systemitem> user, which is why all URLs formercurial-server repositories start with <uri>ssh://hg@...</uri>.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 custom restricted shell; this shell knows which keywas used to connect, determines what the user is trying to do, checks theaccess rules to decide whether to allow it, and if allowed invokesMercurial internally, without forking.</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<filenameclass='directory'>hgadmin</filename> repository, creating an entry in<filename>~hg/.ssh/authorized_keys</filename> for each one. This is redoneautomatically whenever a change is pushed to <filenameclass='directory'>hgadmin</filename>.</para></section><section><title>Security</title><para>mercurial-server relies entirely on <command>sshd</command> to grant access to remote users.As a result, it runs no daemons, installs no setuid programs, and no partof it runs as <systemitemclass="username">root</systemitem> except the install process: all programs run as the user<systemitemclass="username">hg</systemitem>. Any attack on mercurial-server can only be started if the attackeralready has a public key in <filename>~hg/.ssh/authorized_keys</filename>,otherwise <command>sshd</command> 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 operation itself if access isallowed, so users can only read and add to history within repositories;they cannot run any other 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, like all software mercurial-server may harbour bugs. Backups are essential!</para></section><section><title>Logging</title><para>Every successful access is logged in a file called<filename>~hg/repos/<replaceable>repository</replaceable>/.hg/mercurial-server.log</filename>. 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 <code>- </code>. The log records the time as aUTC ISO 8601 time, the operation ("push" or "pull"), the path to the key asused in the access rules, the SSH connection information (including the source IP address), and the hex changeset IDs.</para></section><section><title>Paths and configuration</title><para>For security reasons, all mercurial-server code runs as the <systemitemclass="username">hg</systemitem> user. The first thing this code reads when it starts is <filename>~hg/.mercurial-server</filename>; 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 <code>HGRCPATH = /etc/mercurial-server/remote-hgrc.d</code> for remote operations, overriding any system <code>HGRCPATH</code>.</para><para>By creating such a file with suitable entries, you can run mercurial-server as a user other than <systemitemclass="username">hg</systemitem>, 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.</para></section><section><title>License</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><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>, 2010</para></section></section></article>