ssh-git-repo-docker.html 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. <!DOCTYPE html>
  2. <html lang="en-us">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="generator" content="Hugo 0.92.0" />
  6. <meta name="viewport" content="width=device-width, initial-scale=1">
  7. <link rel="stylesheet" href="/assets/css/theme.css">
  8. <link rel="alternate" href="/rss.xml" type="application/rss+xml" title="Pleasant Programmer">
  9. <script type="text/javascript" src="//use.typekit.net/iwm5axp.js"></script>
  10. <script type="text/javascript">try{Typekit.load();}catch(e){}</script>
  11. <title>SSH Access to Git Repository in Docker - Pleasant Programmer</title>
  12. </head>
  13. <body>
  14. <header id="header" role="banner">
  15. <div id="thomas">
  16. <img src="/assets/img/thomas.gif" alt="DJ THOMAS IN DA HAUS">
  17. <img src="/assets/img/thomas.png" alt="Pleasant Programmer">
  18. </div>
  19. <h1 class="site-title"><a href="/">Pleasant Programmer</a></h1>
  20. <nav id="menu" role="navigation">
  21. <ul>
  22. <li class="twitter">
  23. <a href="http://twitter.com/pleasantprog">@pleasantprog</a>
  24. </li>
  25. <li><a href="/pages/projects.html">projects</a></li>
  26. <li><a href="/posts.html">archives</a></li>
  27. <li><a href="/tags.html">tags</a></li>
  28. <li><a href="/rss.xml">rss</a></li>
  29. </ul>
  30. </nav>
  31. </header>
  32. <div id="container">
  33. <main id="content" role="main">
  34. <article itemscope itemtype="http://schema.org/BlogPosting">
  35. <h1 class="p-name entry-title" itemprop="headline name">
  36. <a href="/posts/ssh-git-repo-docker.html">SSH Access to Git Repository in Docker</a></h1>
  37. <small>
  38. <span class="dateline">Posted: <time itemprop="datePublished" datetime="2022-01-23">2022-01-23</time></span>
  39. | More posts about
  40. <a class="tag p-category" href="/tags/sysadmin.html" rel="tag">
  41. sysadmin
  42. </a>
  43. </small>
  44. <div class="e-content entry-content" itemprop="entry-text">
  45. <p>With the likes of <a href="https://gogs.io/">Gogs</a> and <a href="https://gitea.io">Gitea</a>,
  46. self-hosting a personal git service has become quite common. It&rsquo;s also not
  47. unlikely that the software is run via docker but this brings a problem with
  48. regards to SSH access.</p>
  49. <p>Ideally, the git service container just exposes port 22 but this would conflict
  50. with the host&rsquo;s own SSH service. One solution would be to just use different
  51. ports for the git SSH and host SSH and that&rsquo;s perfectly fine. But we can also
  52. just have the host SSH service forward the request to the git service itself
  53. using the <code>command</code> option in <code>authorized_keys</code>. And as we&rsquo;ll find out later,
  54. the git service itself is using this functionality.</p>
  55. <h2 id="how-git-over-ssh-works">How git over SSH works</h2>
  56. <p>When you do a <code>git push</code>, what actually happens is it runs <code>git-send-pack</code> which
  57. runs <code>git-receive-pack &lt;directory&gt;</code> on the remote using SSH. The actual
  58. communication then just simply happens via stdin/stdout. Conversely, doing a
  59. <code>git pull</code> just runs <code>git-fetch-pack</code> and the somewhat confusingly named
  60. <code>git-upload-pack</code> on the remote.</p>
  61. <p>So far so good, but did you notice that when cloning via SSH, the remote is
  62. typically <code>git@github.com:org/repo</code>? If everyone SSHs in as the <code>git</code> user, how
  63. does the git service know which user is which? And how does it prevent users
  64. from accessing each other&rsquo;s repositories?</p>
  65. <p>One typically thinks of <code>authorized_keys</code> as just a list of allowed SSH keys but
  66. it can do <a href="https://linux.die.net/man/8/sshd">much more than that</a>. Of particular
  67. interest is the <code>command</code> directive which gets run instead of the user supplied
  68. command. The original command is passed in as an environment variable
  69. <code>SSH_ORIGINAL_COMMAND</code> which can be used to check if we allow it to be run or
  70. not.</p>
  71. <p>So with an <code>authorized_keys</code> file like so:</p>
  72. <div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">command=&#34;verify-user user-1&#34; ssh-rsa ...
  73. command=&#34;verify-user user-1&#34; ssh-ed25519 ...
  74. command=&#34;verify-user user-2&#34; ssh-rsa ...
  75. command=&#34;verify-user user-3&#34; ssh-rsa ...
  76. </code></pre></div><p>Each key is tied to a particular user by virtue of <code>command</code>. And <code>verify-user</code>
  77. can check the <code>SSH_ORIGINAL_COMMAND</code> if the particular user is allowed access to
  78. the particular repository. You can also implement additional restrictions like
  79. pull-only or push-only permissions with this setup. This is how both Gogs and
  80. Gitea work.</p>
  81. <h2 id="forwarding-git-ssh-to-docker">Forwarding git SSH to Docker</h2>
  82. <p>When running Gogs on the host, it&rsquo;s typically run as the <code>git</code> user and when an
  83. SSH key is added or removed, it simply rewrites <code>~git/.ssh/authorized_keys</code>.
  84. Thus it just works with the host SSH service without problems. When running
  85. inside docker, one thing we can do is bind mount the host&rsquo;s <code>~git/.ssh</code> folder
  86. into the docker container so that the host can authorize the SSH connections.</p>
  87. <p>The problem lies with the <code>command</code> which only exists inside the docker
  88. container itself. So any user trying to connect can authenticate successfully
  89. but will get a <code>command not found</code> error. For the Gogs docker image, the command
  90. looks like <code>/app/gogs/gogs serv key-1</code>. So we can just make <code>/app/gogs/gogs</code>
  91. available on the host and forward the command to the docker container.</p>
  92. <p><a href="https://docs.gitea.io/en-us/install-with-docker/#ssh-container-passthrough">Most</a>
  93. <a href="http://www.ateijelo.com/blog/2016/07/09/share-port-22-between-docker-gogs-ssh-and-local-system">instructions</a>
  94. I&rsquo;ve seen with regards to this involves using <code>ssh</code> to connect to the internal
  95. docker SSH service but this just seems overly complicated to me. If you
  96. remember, at it&rsquo;s core git really only communicates over stdin/stdout and SSH is
  97. just a means to get that.</p>
  98. <p>If all we need is for our shim <code>/app/gogs/gogs</code> to be able to run a command
  99. inside the docker container with stdin/stdout attached, then we can actually
  100. just do that with <code>docker exec</code>. So it can be something like this:</p>
  101. <div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#0f0;font-weight:bold">#!/usr/bin/env bash
  102. </span><span style="color:#0f0;font-weight:bold"></span>
  103. <span style="color:#007f7f"># Requires the following in sudoers</span>
  104. <span style="color:#007f7f"># git ALL=(ALL) NOPASSWD: /app/gogs/gogs</span>
  105. <span style="color:#007f7f"># Defaults:git env_keep=SSH_ORIGINAL_COMMAND</span>
  106. GOGS_CONTAINER=git-gogs-1
  107. <span style="color:#fff;font-weight:bold">if</span> [[ $EUID -ne <span style="color:#ff0;font-weight:bold">0</span> ]]; <span style="color:#fff;font-weight:bold">then</span>
  108. <span style="color:#fff;font-weight:bold">exec</span> sudo <span style="color:#0ff;font-weight:bold">&#34;</span>$0<span style="color:#0ff;font-weight:bold">&#34;</span> <span style="color:#0ff;font-weight:bold">&#34;</span>$@<span style="color:#0ff;font-weight:bold">&#34;</span>
  109. <span style="color:#fff;font-weight:bold">fi</span>
  110. <span style="color:#fff;font-weight:bold">if</span> [ <span style="color:#0ff;font-weight:bold">&#34;</span>$1<span style="color:#0ff;font-weight:bold">&#34;</span> != <span style="color:#0ff;font-weight:bold">&#34;serv&#34;</span> ]; <span style="color:#fff;font-weight:bold">then</span>
  111. <span style="color:#fff;font-weight:bold">exit</span> <span style="color:#ff0;font-weight:bold">1</span>
  112. <span style="color:#fff;font-weight:bold">fi</span>
  113. <span style="color:#fff;font-weight:bold">exec</span> docker <span style="color:#fff;font-weight:bold">exec</span> -i -u git -e <span style="color:#0ff;font-weight:bold">&#34;SSH_ORIGINAL_COMMAND=</span>$SSH_ORIGINAL_COMMAND<span style="color:#0ff;font-weight:bold">&#34;</span> <span style="color:#0ff;font-weight:bold">&#34;</span>$GOGS_CONTAINER<span style="color:#0ff;font-weight:bold">&#34;</span> /app/gogs/gogs <span style="color:#0ff;font-weight:bold">&#34;</span>$@<span style="color:#0ff;font-weight:bold">&#34;</span>
  114. </code></pre></div><p>So for git SSH access to Gogs running in docker, the necessary steps here are:</p>
  115. <ol>
  116. <li>Have a <code>git</code> user on the host</li>
  117. <li>Bind mount <code>~git/.ssh</code> to <code>/data/git/.ssh</code> in the Gogs container</li>
  118. <li>Add the shim script to <code>/app/gogs/gogs</code> (make sure it&rsquo;s owned by root and
  119. is chmod-ed <code>0755</code>)</li>
  120. <li>Add the listed sudoers rules</li>
  121. </ol>
  122. </div>
  123. <aside class="postpromonav">
  124. <nav>
  125. <ul class="pager clearfix">
  126. <li class="previous">
  127. <a href="/posts/android-multisim.html" rel="prev" title="Android Multisim Pre-5.1">&larr; Previous post</a>
  128. </li>
  129. </ul>
  130. </nav>
  131. </aside>
  132. <section class="comments">
  133. <script
  134. data-isso="https://isso.pleasantprogrammer.com/"
  135. data-isso-require-author="true"
  136. data-isso-vote="false"
  137. src="https://isso.pleasantprogrammer.com/js/embed.min.js">
  138. </script>
  139. <section id="isso-thread"></section>
  140. </section>
  141. </article>
  142. </main>
  143. <footer id="footer" role="contentinfo">
  144. <p>
  145. <a rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/deed.en_US">
  146. <img alt="CC-BY-SA" style="border-width:0" src="https://licensebuttons.net/l/by-sa/3.0/80x15.png">
  147. </a> &copy; 2020 Thomas Dy - Powered by <a href="http://gohugo.io">Hugo</a></p>
  148. </footer>
  149. </div>
  150. <script src="/assets/js/konami.js"></script>
  151. <script>
  152. var easter_egg = new Konami();
  153. easter_egg.code = function() {
  154. var el = document.getElementById('thomas');
  155. if(el.className == "whoa") {
  156. el.className = "";
  157. }
  158. else {
  159. el.className = "whoa";
  160. }
  161. document.body.scrollTop = document.documentElement.scrollTop = 0;
  162. }
  163. easter_egg.load();
  164. </script>
  165. </body>
  166. </html>