ssh-git-repo-docker.html 9.7 KB

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