Browse Source

Add SSH Git Repo Docker post

Thomas Dy 2 years ago
parent
commit
f9c61ab1d4

+ 105 - 0
content/posts/ssh-git-repo-docker.md

@@ -0,0 +1,105 @@
++++
+title = "SSH Access to Git Repository in Docker"
+date = 2022-01-23T15:06:47+09:00
+tags = ["sysadmin"]
++++
+
+With the likes of [Gogs](https://gogs.io/) and [Gitea](https://gitea.io),
+self-hosting a personal git service has become quite common. It's also not
+unlikely that the software is run via docker but this brings a problem with
+regards to SSH access.
+
+Ideally, the git service container just exposes port 22 but this would conflict
+with the host's own SSH service. One solution would be to just use different
+ports for the git SSH and host SSH and that's perfectly fine. But we can also
+just have the host SSH service forward the request to the git service itself
+using the `command` option in `authorized_keys`. And as we'll find out later,
+the git service itself is using this functionality.
+
+## How git over SSH works
+
+When you do a `git push`, what actually happens is it runs `git-send-pack` which
+runs `git-receive-pack <directory>` on the remote using SSH. The actual
+communication then just simply happens via stdin/stdout. Conversely, doing a
+`git pull` just runs `git-fetch-pack` and the somewhat confusingly named
+`git-upload-pack` on the remote.
+
+So far so good, but did you notice that when cloning via SSH, the remote is
+typically `git@github.com:org/repo`? If everyone SSHs in as the `git` user, how
+does the git service know which user is which? And how does it prevent users
+from accessing each other's repositories?
+
+One typically thinks of `authorized_keys` as just a list of allowed SSH keys but
+it can do [much more than that](https://linux.die.net/man/8/sshd). Of particular
+interest is the `command` directive which gets run instead of the user supplied
+command. The original command is passed in as an environment variable
+`SSH_ORIGINAL_COMMAND` which can be used to check if we allow it to be run or
+not.
+
+So with an `authorized_keys` file like so:
+
+```text
+command="verify-user user-1" ssh-rsa ...
+command="verify-user user-1" ssh-ed25519 ...
+command="verify-user user-2" ssh-rsa ...
+command="verify-user user-3" ssh-rsa ...
+```
+
+Each key is tied to a particular user by virtue of `command`. And `verify-user`
+can check the `SSH_ORIGINAL_COMMAND` if the particular user is allowed access to
+the particular repository. You can also implement additional restrictions like
+pull-only or push-only permissions with this setup. This is how both Gogs and
+Gitea work.
+
+## Forwarding git SSH to Docker
+
+When running Gogs on the host, it's typically run as the `git` user and when an
+SSH key is added or removed, it simply rewrites `~git/.ssh/authorized_keys`.
+Thus it just works with the host SSH service without problems. When running
+inside docker, one thing we can do is bind mount the host's `~git/.ssh` folder
+into the docker container so that the host can authorize the SSH connections.
+
+The problem lies with the `command` which only exists inside the docker
+container itself. So any user trying to connect can authenticate successfully
+but will get a `command not found` error. For the Gogs docker image, the command
+looks like `/app/gogs/gogs serv key-1`. So we can just make `/app/gogs/gogs`
+available on the host and forward the command to the docker container.
+
+[Most](https://docs.gitea.io/en-us/install-with-docker/#ssh-container-passthrough)
+[instructions](http://www.ateijelo.com/blog/2016/07/09/share-port-22-between-docker-gogs-ssh-and-local-system)
+I've seen with regards to this involves using `ssh` to connect to the internal
+docker SSH service but this just seems overly complicated to me. If you
+remember, at it's core git really only communicates over stdin/stdout and SSH is
+just a means to get that.
+
+If all we need is for our shim `/app/gogs/gogs` to be able to run a command
+inside the docker container with stdin/stdout attached, then we can actually
+just do that with `docker exec`. So it can be something like this:
+
+```shell
+#!/usr/bin/env bash
+
+# Requires the following in sudoers
+# git ALL=(ALL) NOPASSWD: /app/gogs/gogs
+# Defaults:git env_keep=SSH_ORIGINAL_COMMAND
+
+GOGS_CONTAINER=git-gogs-1
+
+if [[ $EUID -ne 0 ]]; then
+  exec sudo "$0" "$@"
+fi
+
+if [ "$1" != "serv" ]; then
+  exit 1
+fi
+
+exec docker exec -i -u git -e "SSH_ORIGINAL_COMMAND=$SSH_ORIGINAL_COMMAND" "$GOGS_CONTAINER" /app/gogs/gogs "$@"
+```
+
+So for git SSH access to Gogs running in docker, the necessary steps here are:
+
+ 1. Have a `git` user on the host
+ 2. Bind mount `~git/.ssh` to `/data/git/.ssh` in the Gogs container
+ 3. Add the shim script to `/app/gogs/gogs` (make sure it's owned by root and
+    is chmod-ed `0755`)
+ 4. Add the listed sudoers rules

+ 95 - 148
output/index.html

@@ -36,6 +36,101 @@
 <main id="content" role="main">
 <div class="postindex">
 	
+	<article class="h-entry post-text" itemscope itemtype="http://schema.org/Blog">
+		<header>
+			<h1 class="p-name entry-title" itemprop="headline">
+				<a href="/posts/ssh-git-repo-docker.html" class="u-url">SSH Access to Git Repository in Docker</a>
+			</h1>
+		</header>
+		<div class="e-content entry-content">
+			<p>With the likes of <a href="https://gogs.io/">Gogs</a> and <a href="https://gitea.io">Gitea</a>,
+self-hosting a personal git service has become quite common. It&rsquo;s also not
+unlikely that the software is run via docker but this brings a problem with
+regards to SSH access.</p>
+<p>Ideally, the git service container just exposes port 22 but this would conflict
+with the host&rsquo;s own SSH service. One solution would be to just use different
+ports for the git SSH and host SSH and that&rsquo;s perfectly fine. But we can also
+just have the host SSH service forward the request to the git service itself
+using the <code>command</code> option in <code>authorized_keys</code>. And as we&rsquo;ll find out later,
+the git service itself is using this functionality.</p>
+<h2 id="how-git-over-ssh-works">How git over SSH works</h2>
+<p>When you do a <code>git push</code>, what actually happens is it runs <code>git-send-pack</code> which
+runs <code>git-receive-pack &lt;directory&gt;</code> on the remote using SSH. The actual
+communication then just simply happens via stdin/stdout. Conversely, doing a
+<code>git pull</code> just runs <code>git-fetch-pack</code> and the somewhat confusingly named
+<code>git-upload-pack</code> on the remote.</p>
+<p>So far so good, but did you notice that when cloning via SSH, the remote is
+typically <code>git@github.com:org/repo</code>? If everyone SSHs in as the <code>git</code> user, how
+does the git service know which user is which? And how does it prevent users
+from accessing each other&rsquo;s repositories?</p>
+<p>One typically thinks of <code>authorized_keys</code> as just a list of allowed SSH keys but
+it can do <a href="https://linux.die.net/man/8/sshd">much more than that</a>. Of particular
+interest is the <code>command</code> directive which gets run instead of the user supplied
+command. The original command is passed in as an environment variable
+<code>SSH_ORIGINAL_COMMAND</code> which can be used to check if we allow it to be run or
+not.</p>
+<p>So with an <code>authorized_keys</code> file like so:</p>
+<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 ...
+command=&#34;verify-user user-1&#34; ssh-ed25519 ...
+command=&#34;verify-user user-2&#34; ssh-rsa ...
+command=&#34;verify-user user-3&#34; ssh-rsa ...
+</code></pre></div><p>Each key is tied to a particular user by virtue of <code>command</code>. And <code>verify-user</code>
+can check the <code>SSH_ORIGINAL_COMMAND</code> if the particular user is allowed access to
+the particular repository. You can also implement additional restrictions like
+pull-only or push-only permissions with this setup. This is how both Gogs and
+Gitea work.</p>
+<h2 id="forwarding-git-ssh-to-docker">Forwarding git SSH to Docker</h2>
+<p>When running Gogs on the host, it&rsquo;s typically run as the <code>git</code> user and when an
+SSH key is added or removed, it simply rewrites <code>~git/.ssh/authorized_keys</code>.
+Thus it just works with the host SSH service without problems. When running
+inside docker, one thing we can do is bind mount the host&rsquo;s <code>~git/.ssh</code> folder
+into the docker container so that the host can authorize the SSH connections.</p>
+<p>The problem lies with the <code>command</code> which only exists inside the docker
+container itself. So any user trying to connect can authenticate successfully
+but will get a <code>command not found</code> error. For the Gogs docker image, the command
+looks like <code>/app/gogs/gogs serv key-1</code>. So we can just make <code>/app/gogs/gogs</code>
+available on the host and forward the command to the docker container.</p>
+<p><a href="https://docs.gitea.io/en-us/install-with-docker/#ssh-container-passthrough">Most</a>
+<a href="http://www.ateijelo.com/blog/2016/07/09/share-port-22-between-docker-gogs-ssh-and-local-system">instructions</a>
+I&rsquo;ve seen with regards to this involves using <code>ssh</code> to connect to the internal
+docker SSH service but this just seems overly complicated to me. If you
+remember, at it&rsquo;s core git really only communicates over stdin/stdout and SSH is
+just a means to get that.</p>
+<p>If all we need is for our shim <code>/app/gogs/gogs</code> to be able to run a command
+inside the docker container with stdin/stdout attached, then we can actually
+just do that with <code>docker exec</code>. So it can be something like this:</p>
+<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
+</span><span style="color:#0f0;font-weight:bold"></span>
+<span style="color:#007f7f"># Requires the following in sudoers</span>
+<span style="color:#007f7f"># git ALL=(ALL) NOPASSWD: /app/gogs/gogs</span>
+<span style="color:#007f7f"># Defaults:git env_keep=SSH_ORIGINAL_COMMAND</span>
+
+GOGS_CONTAINER=git-gogs-1
+
+<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>
+  <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>
+<span style="color:#fff;font-weight:bold">fi</span>
+
+<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>
+  <span style="color:#fff;font-weight:bold">exit</span> <span style="color:#ff0;font-weight:bold">1</span>
+<span style="color:#fff;font-weight:bold">fi</span>
+
+<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>
+</code></pre></div><p>So for git SSH access to Gogs running in docker, the necessary steps here are:</p>
+<ol>
+<li>Have a <code>git</code> user on the host</li>
+<li>Bind mount <code>~git/.ssh</code> to <code>/data/git/.ssh</code> in the Gogs container</li>
+<li>Add the shim script to <code>/app/gogs/gogs</code> (make sure it&rsquo;s owned by root and
+is chmod-ed <code>0755</code>)</li>
+<li>Add the listed sudoers rules</li>
+</ol>
+
+		</div>
+		<small class="dateline">Posted: <time class="published dt-published" itemprop="datePublished" datetime="2022-01-23">2022-01-23</time></small>
+		| <small class="commentline"><a href="/posts/ssh-git-repo-docker.html#isso-thread">Comments</a></small>
+	</article>
+	</article>
+	
 	<article class="h-entry post-text" itemscope itemtype="http://schema.org/Blog">
 		<header>
 			<h1 class="p-name entry-title" itemprop="headline">
@@ -173,154 +268,6 @@ want to contribute music or sound effects, I&rsquo;d gladly appreciate it.</p>
 	</article>
 	</article>
 	
-	<article class="h-entry post-text" itemscope itemtype="http://schema.org/Blog">
-		<header>
-			<h1 class="p-name entry-title" itemprop="headline">
-				<a href="/posts/openpreppad.html" class="u-url">OpenPrepPad</a>
-			</h1>
-		</header>
-		<div class="e-content entry-content">
-			<p>Smart electronics and IoT (Internet of Things) are all the rage these days. You
-have a lot of companies sprout up trying to make the next big thing, which also
-leads to a lot of failures big and small. Pebble, the maker of my smartwatch,
-got bought out by Fitbit recently. This left watch owners without any official
-support, but thankfully, community members <a href="http://rebble.io/">stepped up</a> to continue
-maintaining it.</p>
-<p>Another casualty of the IoT boom was the <a href="http://theorangechef.com/">Orange Chef</a> <a href="https://www.amazon.com/Orange-Chef-Smart-Scale-Silver/dp/B00KFW8L90">Prep Pad</a>. It&rsquo;s a
-bluetooth connected weighing scale to make it easy to track your calories and
-carb/fat/protein intake. My dad bought it last year only to find out that the
-app was incredibly buggy. The search function doesn&rsquo;t work which makes the whole
-thing practically useless. I also found out later that you can&rsquo;t even download
-the app to use the scale anymore.</p>
-<p><strong>Note</strong> I just found out as I was writing this post that it <em>may</em> get supported
-by <a href="http://www.prnewswire.com/news-releases/perfect-company-acquires-orange-chefs-prep-pad-related-ip-continues-momentum-in-the-connected-kitchen-300383178.html">another company</a>.</p>
-<p>So the app is useless, but at least you can use it as a scale, right?</p>
-<p><img src="/galleries/openpreppad/preppad.jpg" alt="Prep Pad"></p>
-<p>Nope. The device has no display whatsoever. The only controls on it are the
-on/off button and a green LED that isn&rsquo;t even that useful at telling you whether
-it&rsquo;s on or not. At this point, it&rsquo;s just a giant paperweight.</p>
-<h2 id="reverse-engineering">Reverse Engineering</h2>
-<p>Since I essentially had nothing to lose, I tried poking at the thing to figure
-out how it works. I didn&rsquo;t really have experience with bluetooth besides trying
-to get my bluetooth mouse connected on Linux. The main thing I used then was
-<code>bluetoothctl</code> which is essentially a CLI for managing bluetooth devices so I
-started there.</p>
-<p>I started up <code>bluetoothctl</code> and turned on the Prep Pad. And it showed up!</p>
-<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">[bluetooth]# power on
-[CHG] Controller ... Class: 0x00010c
-Changing power on succeeded
-[CHG] Controller ... Powered: yes
-[bluetooth]# scan on
-Discovery started
-[CHG] Device 1C:BA:8C:21:7C:BB RSSI: -51
-[CHG] Device 1C:BA:8C:21:7C:BB Name: CHSLEEV_00
-[CHG] Device 1C:BA:8C:21:7C:BB Alias: CHSLEEV_00
-</code></pre></div><p>I then connected to it, which was surprisingly easy.</p>
-<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">[bluetooth]# connect 1C:BA:8C:21:7C:BB
-Attempting to connect to 1C:BA:8C:21:7C:BB
-[CHG] Device 1C:BA:8C:21:7C:BB Connected: yes
-[CHG] Device 1C:BA:8C:21:7C:BB Name: CH BTScale_00
-[CHG] Device 1C:BA:8C:21:7C:BB Alias: CH BTScale_00
-</code></pre></div><p>Now normally, when you turn the device on, the green light flashes occasionally.
-Once I connected to it, the green light stayed on permanently. Clearly, I was
-making progress. A lot of services were also discovered but I had no idea what
-those things were at that point.</p>
-<p>After a lot of poking around, I could check the general device information. You
-could get the hardware, software and firmware version. There&rsquo;s also the device
-serial number which was nowhere on the actual physical device.</p>
-<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">[CHSLEEV_00]# select-attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0010/char0017
-[CH BTScale_00:/service0010/char0017]# attribute-info
-Characteristic - Firmware Revision String
-	UUID: 00002a26-0000-1000-8000-00805f9b34fb
-	Service: /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0010
-	Value: 0x31
-	Value: 0x2e
-	Value: 0x31
-	Value: 0x33
-	Value: 0x41
-	Value: 0x00
-	Flags: read
-[CH BTScale_00:/service0010/char0017]# read
-Attempting to read /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0010/char0017
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0010/char0017 Value: 0x31
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0010/char0017 Value: 0x2e
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0010/char0017 Value: 0x31
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0010/char0017 Value: 0x33
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0010/char0017 Value: 0x41
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0010/char0017 Value: 0x00
-  31 2e 31 33 41 00                                1.13A.
-[CH BTScale_00:/service0010/char0017]#
-</code></pre></div><p>There was also a service which contained Accel Enable, Accel Range, Accel
-X-Coordinate, Accel Y-Coordinate, and Accel Z-Coordinate. I guess it stands for
-accelerometer, which is probably what it uses to weigh things.</p>
-<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">[CHSLEEV_00]# select-attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026
-[CH BTScale_00:/service0023/char0024/desc0026]# read
-Attempting to read /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x41
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x63
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x63
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x65
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x6c
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x20
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x45
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x6e
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x61
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x62
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x6c
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x65
-  41 63 63 65 6c 20 45 6e 61 62 6c 65              Accel Enable
-</code></pre></div><p>I couldn&rsquo;t read from any of the Accel Coordinates. It kept saying permission
-denied. I could however, notify on them. But that didn&rsquo;t yield anything as well.
-What I <em>could</em> read was Accel Enable, which was set to 00. I guess that means it
-was off. After writing 01 to Accel Enable, I found I could get values out of
-Accel X-Coordinate! Also, the green LED which was permanently on turned off.</p>
-<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">[CHSLEEV_00]# select-attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024
-[CH BTScale_00:/service0023/char0024]# write 01
-Attempting to write /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024
-[CH BTScale_00:/service0023/char0024]# select-attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a
-[CH BTScale_00:/service0023/char002a]# notify on
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Notifying: yes
-Notify started
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0x5b
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0xa3
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0x02
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0x00
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0x55
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0xa3
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0x02
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0x00
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0x59
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0xa3
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0x02
-[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0x00
-</code></pre></div><p>I tried pressing the scale down a few times, and the values changed accordingly.
-Now, I just had to figure out how to convert the values into grams. It looked
-like the values were 32-bit integers sent as 4 bytes. In the above example it
-would be <code>0x0002a35b</code>, <code>0x0002a355</code>, <code>0x0002a359</code> or 172891, 172855, 172899. The
-values also decrease as you exert more effort on the scale. So assuming you take
-the initial value as <em>tare</em>, you simply subtract any succeeding value from that
-<em>tare</em> and you get the &ldquo;weight&rdquo;.</p>
-<p>The values I got didn&rsquo;t seem to be in grams though. After weighing some things
-on an actual scale and comparing the values I got, I found I can just divide the
-values by 14 and get something in grams. That 14 is entirely a magic number
-though and I have no idea whether other Prep Pad&rsquo;s would have the same constant.</p>
-<h2 id="openpreppad">OpenPrepPad</h2>
-<p>With all that figured out, I went ahead and made a <a href="https://github.com/thatsmydoing/openpreppad">simple CLI application</a>
-to interface with the Prep Pad. Ironically, node was the simplest thing I found
-that had <a href="https://github.com/sandeepmistry/noble">nice bluetooth library support</a> so that&rsquo;s what I wrote it in. I
-also added most of the technical details in the README for that as well.</p>
-<p>While this is all well and cool, I doubt the intersection of Linux users and
-people who <!-- raw HTML omitted -->got ripped off<!-- raw HTML omitted --> bought the Prep Pad is anyone besides me. In
-light of that, I&rsquo;m in the process of making a React Native version of the app,
-but that&rsquo;s still a work in progress. Who knows, if the new owners of Prep Pad
-are good, I might not even need to finish it.</p>
-
-		</div>
-		<small class="dateline">Posted: <time class="published dt-published" itemprop="datePublished" datetime="2017-01-15">2017-01-15</time></small>
-		| <small class="commentline"><a href="/posts/openpreppad.html#isso-thread">Comments</a></small>
-	</article>
-	</article>
-	
 </div>
 <nav class="postindexpager">
 	<ul class="pager clearfix">

+ 148 - 20
output/page/2.html

@@ -36,6 +36,154 @@
 <main id="content" role="main">
 <div class="postindex">
 	
+	<article class="h-entry post-text" itemscope itemtype="http://schema.org/Blog">
+		<header>
+			<h1 class="p-name entry-title" itemprop="headline">
+				<a href="/posts/openpreppad.html" class="u-url">OpenPrepPad</a>
+			</h1>
+		</header>
+		<div class="e-content entry-content">
+			<p>Smart electronics and IoT (Internet of Things) are all the rage these days. You
+have a lot of companies sprout up trying to make the next big thing, which also
+leads to a lot of failures big and small. Pebble, the maker of my smartwatch,
+got bought out by Fitbit recently. This left watch owners without any official
+support, but thankfully, community members <a href="http://rebble.io/">stepped up</a> to continue
+maintaining it.</p>
+<p>Another casualty of the IoT boom was the <a href="http://theorangechef.com/">Orange Chef</a> <a href="https://www.amazon.com/Orange-Chef-Smart-Scale-Silver/dp/B00KFW8L90">Prep Pad</a>. It&rsquo;s a
+bluetooth connected weighing scale to make it easy to track your calories and
+carb/fat/protein intake. My dad bought it last year only to find out that the
+app was incredibly buggy. The search function doesn&rsquo;t work which makes the whole
+thing practically useless. I also found out later that you can&rsquo;t even download
+the app to use the scale anymore.</p>
+<p><strong>Note</strong> I just found out as I was writing this post that it <em>may</em> get supported
+by <a href="http://www.prnewswire.com/news-releases/perfect-company-acquires-orange-chefs-prep-pad-related-ip-continues-momentum-in-the-connected-kitchen-300383178.html">another company</a>.</p>
+<p>So the app is useless, but at least you can use it as a scale, right?</p>
+<p><img src="/galleries/openpreppad/preppad.jpg" alt="Prep Pad"></p>
+<p>Nope. The device has no display whatsoever. The only controls on it are the
+on/off button and a green LED that isn&rsquo;t even that useful at telling you whether
+it&rsquo;s on or not. At this point, it&rsquo;s just a giant paperweight.</p>
+<h2 id="reverse-engineering">Reverse Engineering</h2>
+<p>Since I essentially had nothing to lose, I tried poking at the thing to figure
+out how it works. I didn&rsquo;t really have experience with bluetooth besides trying
+to get my bluetooth mouse connected on Linux. The main thing I used then was
+<code>bluetoothctl</code> which is essentially a CLI for managing bluetooth devices so I
+started there.</p>
+<p>I started up <code>bluetoothctl</code> and turned on the Prep Pad. And it showed up!</p>
+<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">[bluetooth]# power on
+[CHG] Controller ... Class: 0x00010c
+Changing power on succeeded
+[CHG] Controller ... Powered: yes
+[bluetooth]# scan on
+Discovery started
+[CHG] Device 1C:BA:8C:21:7C:BB RSSI: -51
+[CHG] Device 1C:BA:8C:21:7C:BB Name: CHSLEEV_00
+[CHG] Device 1C:BA:8C:21:7C:BB Alias: CHSLEEV_00
+</code></pre></div><p>I then connected to it, which was surprisingly easy.</p>
+<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">[bluetooth]# connect 1C:BA:8C:21:7C:BB
+Attempting to connect to 1C:BA:8C:21:7C:BB
+[CHG] Device 1C:BA:8C:21:7C:BB Connected: yes
+[CHG] Device 1C:BA:8C:21:7C:BB Name: CH BTScale_00
+[CHG] Device 1C:BA:8C:21:7C:BB Alias: CH BTScale_00
+</code></pre></div><p>Now normally, when you turn the device on, the green light flashes occasionally.
+Once I connected to it, the green light stayed on permanently. Clearly, I was
+making progress. A lot of services were also discovered but I had no idea what
+those things were at that point.</p>
+<p>After a lot of poking around, I could check the general device information. You
+could get the hardware, software and firmware version. There&rsquo;s also the device
+serial number which was nowhere on the actual physical device.</p>
+<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">[CHSLEEV_00]# select-attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0010/char0017
+[CH BTScale_00:/service0010/char0017]# attribute-info
+Characteristic - Firmware Revision String
+	UUID: 00002a26-0000-1000-8000-00805f9b34fb
+	Service: /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0010
+	Value: 0x31
+	Value: 0x2e
+	Value: 0x31
+	Value: 0x33
+	Value: 0x41
+	Value: 0x00
+	Flags: read
+[CH BTScale_00:/service0010/char0017]# read
+Attempting to read /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0010/char0017
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0010/char0017 Value: 0x31
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0010/char0017 Value: 0x2e
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0010/char0017 Value: 0x31
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0010/char0017 Value: 0x33
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0010/char0017 Value: 0x41
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0010/char0017 Value: 0x00
+  31 2e 31 33 41 00                                1.13A.
+[CH BTScale_00:/service0010/char0017]#
+</code></pre></div><p>There was also a service which contained Accel Enable, Accel Range, Accel
+X-Coordinate, Accel Y-Coordinate, and Accel Z-Coordinate. I guess it stands for
+accelerometer, which is probably what it uses to weigh things.</p>
+<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">[CHSLEEV_00]# select-attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026
+[CH BTScale_00:/service0023/char0024/desc0026]# read
+Attempting to read /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x41
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x63
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x63
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x65
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x6c
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x20
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x45
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x6e
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x61
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x62
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x6c
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024/desc0026 Value: 0x65
+  41 63 63 65 6c 20 45 6e 61 62 6c 65              Accel Enable
+</code></pre></div><p>I couldn&rsquo;t read from any of the Accel Coordinates. It kept saying permission
+denied. I could however, notify on them. But that didn&rsquo;t yield anything as well.
+What I <em>could</em> read was Accel Enable, which was set to 00. I guess that means it
+was off. After writing 01 to Accel Enable, I found I could get values out of
+Accel X-Coordinate! Also, the green LED which was permanently on turned off.</p>
+<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">[CHSLEEV_00]# select-attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024
+[CH BTScale_00:/service0023/char0024]# write 01
+Attempting to write /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char0024
+[CH BTScale_00:/service0023/char0024]# select-attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a
+[CH BTScale_00:/service0023/char002a]# notify on
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Notifying: yes
+Notify started
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0x5b
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0xa3
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0x02
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0x00
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0x55
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0xa3
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0x02
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0x00
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0x59
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0xa3
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0x02
+[CHG] Attribute /org/bluez/hci0/dev_1C_BA_8C_21_7C_BB/service0023/char002a Value: 0x00
+</code></pre></div><p>I tried pressing the scale down a few times, and the values changed accordingly.
+Now, I just had to figure out how to convert the values into grams. It looked
+like the values were 32-bit integers sent as 4 bytes. In the above example it
+would be <code>0x0002a35b</code>, <code>0x0002a355</code>, <code>0x0002a359</code> or 172891, 172855, 172899. The
+values also decrease as you exert more effort on the scale. So assuming you take
+the initial value as <em>tare</em>, you simply subtract any succeeding value from that
+<em>tare</em> and you get the &ldquo;weight&rdquo;.</p>
+<p>The values I got didn&rsquo;t seem to be in grams though. After weighing some things
+on an actual scale and comparing the values I got, I found I can just divide the
+values by 14 and get something in grams. That 14 is entirely a magic number
+though and I have no idea whether other Prep Pad&rsquo;s would have the same constant.</p>
+<h2 id="openpreppad">OpenPrepPad</h2>
+<p>With all that figured out, I went ahead and made a <a href="https://github.com/thatsmydoing/openpreppad">simple CLI application</a>
+to interface with the Prep Pad. Ironically, node was the simplest thing I found
+that had <a href="https://github.com/sandeepmistry/noble">nice bluetooth library support</a> so that&rsquo;s what I wrote it in. I
+also added most of the technical details in the README for that as well.</p>
+<p>While this is all well and cool, I doubt the intersection of Linux users and
+people who <!-- raw HTML omitted -->got ripped off<!-- raw HTML omitted --> bought the Prep Pad is anyone besides me. In
+light of that, I&rsquo;m in the process of making a React Native version of the app,
+but that&rsquo;s still a work in progress. Who knows, if the new owners of Prep Pad
+are good, I might not even need to finish it.</p>
+
+		</div>
+		<small class="dateline">Posted: <time class="published dt-published" itemprop="datePublished" datetime="2017-01-15">2017-01-15</time></small>
+		| <small class="commentline"><a href="/posts/openpreppad.html#isso-thread">Comments</a></small>
+	</article>
+	</article>
+	
 	<article class="h-entry post-text" itemscope itemtype="http://schema.org/Blog">
 		<header>
 			<h1 class="p-name entry-title" itemprop="headline">
@@ -210,26 +358,6 @@ twanager bag default <span style="color:#0ff;font-weight:bold">&lt;&lt;EOF
 	</article>
 	</article>
 	
-	<article class="h-entry post-text" itemscope itemtype="http://schema.org/Blog">
-		<header>
-			<h1 class="p-name entry-title" itemprop="headline">
-				<a href="/posts/removing-pldtmydslbiz-from-the-zyxel-p-2612hnu.html" class="u-url">Removing PLDTMyDSLBiz from the ZyXEL P-2612HNU</a>
-			</h1>
-		</header>
-		<div class="e-content entry-content">
-			<p>I&rsquo;ve always thought that people were just too lazy to change their SSIDs when I see &ldquo;PLDTMyDSLBizCafeJapan&rdquo;. It became apparent when we got our own PLDT line that it was because the bundled router/modem <em>does not</em> allow you to remove the prefix.</p>
-<p>This is not the kind of thing you expect as a business customer. Even for home customers, I feel it&rsquo;s still a bit dishonest. I&rsquo;d be fine if it was just the default SSID, but forcing people to have it as part of their SSID is like advertising that your company (I mean PLDT) is a douche.</p>
-<p>Of course, we couldn&rsquo;t just leave the SSID prefix there, so we tried a number of things to get rid of it. There are articles for removing it from the <a href="http://www.phandroidinternet.com/2013/06/how-to-remove-on-wifi-name-or-ssid-on.html">Prolink H5004N</a> or the <a href="http://www.symbianize.com/showthread.php?t=730091">ZyXEL P-660HN-T1A</a> but not for the one we got which was the ZyXEL P-2612HNU-F1F.</p>
-<p>We did still try the firebug/inspector tricks, but it seems that there is a server-side check that adds in the &ldquo;PLDTMyDSLBiz&rdquo;. We tried a number of things, but the one that ultimately worked (and we had a good laugh about) was to backup the configuration, edit the dumped file and restore it.</p>
-<p>The backup is actually just an XML file. You can search for SSID and change the parameter there. It&rsquo;s a bit annoying because the router has to restart after restoring the configuration, but it works!</p>
-<p>A minor note, the router doesn&rsquo;t seem to support SSIDs with a comma (,) well. It just gets everything before the comma as the SSID for some reason.</p>
-
-		</div>
-		<small class="dateline">Posted: <time class="published dt-published" itemprop="datePublished" datetime="2013-11-27">2013-11-27</time></small>
-		| <small class="commentline"><a href="/posts/removing-pldtmydslbiz-from-the-zyxel-p-2612hnu.html#isso-thread">Comments</a></small>
-	</article>
-	</article>
-	
 </div>
 <nav class="postindexpager">
 	<ul class="pager clearfix">

+ 20 - 21
output/page/3.html

@@ -36,6 +36,26 @@
 <main id="content" role="main">
 <div class="postindex">
 	
+	<article class="h-entry post-text" itemscope itemtype="http://schema.org/Blog">
+		<header>
+			<h1 class="p-name entry-title" itemprop="headline">
+				<a href="/posts/removing-pldtmydslbiz-from-the-zyxel-p-2612hnu.html" class="u-url">Removing PLDTMyDSLBiz from the ZyXEL P-2612HNU</a>
+			</h1>
+		</header>
+		<div class="e-content entry-content">
+			<p>I&rsquo;ve always thought that people were just too lazy to change their SSIDs when I see &ldquo;PLDTMyDSLBizCafeJapan&rdquo;. It became apparent when we got our own PLDT line that it was because the bundled router/modem <em>does not</em> allow you to remove the prefix.</p>
+<p>This is not the kind of thing you expect as a business customer. Even for home customers, I feel it&rsquo;s still a bit dishonest. I&rsquo;d be fine if it was just the default SSID, but forcing people to have it as part of their SSID is like advertising that your company (I mean PLDT) is a douche.</p>
+<p>Of course, we couldn&rsquo;t just leave the SSID prefix there, so we tried a number of things to get rid of it. There are articles for removing it from the <a href="http://www.phandroidinternet.com/2013/06/how-to-remove-on-wifi-name-or-ssid-on.html">Prolink H5004N</a> or the <a href="http://www.symbianize.com/showthread.php?t=730091">ZyXEL P-660HN-T1A</a> but not for the one we got which was the ZyXEL P-2612HNU-F1F.</p>
+<p>We did still try the firebug/inspector tricks, but it seems that there is a server-side check that adds in the &ldquo;PLDTMyDSLBiz&rdquo;. We tried a number of things, but the one that ultimately worked (and we had a good laugh about) was to backup the configuration, edit the dumped file and restore it.</p>
+<p>The backup is actually just an XML file. You can search for SSID and change the parameter there. It&rsquo;s a bit annoying because the router has to restart after restoring the configuration, but it works!</p>
+<p>A minor note, the router doesn&rsquo;t seem to support SSIDs with a comma (,) well. It just gets everything before the comma as the SSID for some reason.</p>
+
+		</div>
+		<small class="dateline">Posted: <time class="published dt-published" itemprop="datePublished" datetime="2013-11-27">2013-11-27</time></small>
+		| <small class="commentline"><a href="/posts/removing-pldtmydslbiz-from-the-zyxel-p-2612hnu.html#isso-thread">Comments</a></small>
+	</article>
+	</article>
+	
 	<article class="h-entry post-text" itemscope itemtype="http://schema.org/Blog">
 		<header>
 			<h1 class="p-name entry-title" itemprop="headline">
@@ -152,27 +172,6 @@ sed -i .bak <span style="color:#0ff;font-weight:bold">&#39;/^72/ s/,600/,60/&#39
 	</article>
 	</article>
 	
-	<article class="h-entry post-text" itemscope itemtype="http://schema.org/Blog">
-		<header>
-			<h1 class="p-name entry-title" itemprop="headline">
-				<a href="/posts/elevation-data-in-otp.html" class="u-url">Elevation Data in OTP</a>
-			</h1>
-		</header>
-		<div class="e-content entry-content">
-			<p><img src="../galleries/transit/otpelevation.png" alt="OpenTripPlanner showing elevation data"></p>
-<p>One thing I hadn&rsquo;t tested out last time was OTP&rsquo;s support for elevation data. It makes use of this by showing the elevation you have to traverse while walking along the suggested route. It can also take it into account when suggesting bike routes.</p>
-<p>The <a href="https://github.com/openplans/OpenTripPlanner/wiki/FiveMinutes">5 minute tutorial</a> actually discusses the elevation data briefly, but a more in-depth thing you can look at is the <a href="https://github.com/openplans/OpenTripPlanner/wiki/GraphBuilder#elevation-data">GraphBuilder documentation</a>. It suggests using the ASTER dataset which is free but requires registration. I just opted to use the SRTM data available from the <a href="http://www.philgis.org/freegisdata.htm">PhilGIS website</a>.</p>
-<p>I don&rsquo;t know about the ASTER dataset, but the PhilGIS data was in the ERDAS img format. OTP only supports GeoTIFF so there was a need to convert it beforehand. You can use <a href="http://www.gdal.org/">GDAL</a> for this. You&rsquo;d just then run,</p>
-<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">gdal_translate srtm41_90m_phl.img phil.tiff
-</code></pre></div><p>Afterwards, it&rsquo;s just a matter of following the OTP instructions on using a local elevation dataset. The process actually doubled the size of the generated Graph.obj so it might not be ideal if you&rsquo;re running on limited RAM.</p>
-<p>I&rsquo;ve actually hosted a <a href="http://maps.pleasantprogrammer.com">working example</a>. It&rsquo;s pretty much at the limits of the RAM so it might be slow and unreliable, but you can test it out just for fun. Please don&rsquo;t abuse it though.</p>
-
-		</div>
-		<small class="dateline">Posted: <time class="published dt-published" itemprop="datePublished" datetime="2013-07-23">2013-07-23</time></small>
-		| <small class="commentline"><a href="/posts/elevation-data-in-otp.html#isso-thread">Comments</a></small>
-	</article>
-	</article>
-	
 </div>
 <nav class="postindexpager">
 	<ul class="pager clearfix">

+ 21 - 26
output/page/4.html

@@ -36,6 +36,27 @@
 <main id="content" role="main">
 <div class="postindex">
 	
+	<article class="h-entry post-text" itemscope itemtype="http://schema.org/Blog">
+		<header>
+			<h1 class="p-name entry-title" itemprop="headline">
+				<a href="/posts/elevation-data-in-otp.html" class="u-url">Elevation Data in OTP</a>
+			</h1>
+		</header>
+		<div class="e-content entry-content">
+			<p><img src="../galleries/transit/otpelevation.png" alt="OpenTripPlanner showing elevation data"></p>
+<p>One thing I hadn&rsquo;t tested out last time was OTP&rsquo;s support for elevation data. It makes use of this by showing the elevation you have to traverse while walking along the suggested route. It can also take it into account when suggesting bike routes.</p>
+<p>The <a href="https://github.com/openplans/OpenTripPlanner/wiki/FiveMinutes">5 minute tutorial</a> actually discusses the elevation data briefly, but a more in-depth thing you can look at is the <a href="https://github.com/openplans/OpenTripPlanner/wiki/GraphBuilder#elevation-data">GraphBuilder documentation</a>. It suggests using the ASTER dataset which is free but requires registration. I just opted to use the SRTM data available from the <a href="http://www.philgis.org/freegisdata.htm">PhilGIS website</a>.</p>
+<p>I don&rsquo;t know about the ASTER dataset, but the PhilGIS data was in the ERDAS img format. OTP only supports GeoTIFF so there was a need to convert it beforehand. You can use <a href="http://www.gdal.org/">GDAL</a> for this. You&rsquo;d just then run,</p>
+<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">gdal_translate srtm41_90m_phl.img phil.tiff
+</code></pre></div><p>Afterwards, it&rsquo;s just a matter of following the OTP instructions on using a local elevation dataset. The process actually doubled the size of the generated Graph.obj so it might not be ideal if you&rsquo;re running on limited RAM.</p>
+<p>I&rsquo;ve actually hosted a <a href="http://maps.pleasantprogrammer.com">working example</a>. It&rsquo;s pretty much at the limits of the RAM so it might be slow and unreliable, but you can test it out just for fun. Please don&rsquo;t abuse it though.</p>
+
+		</div>
+		<small class="dateline">Posted: <time class="published dt-published" itemprop="datePublished" datetime="2013-07-23">2013-07-23</time></small>
+		| <small class="commentline"><a href="/posts/elevation-data-in-otp.html#isso-thread">Comments</a></small>
+	</article>
+	</article>
+	
 	<article class="h-entry post-text" itemscope itemtype="http://schema.org/Blog">
 		<header>
 			<h1 class="p-name entry-title" itemprop="headline">
@@ -288,32 +309,6 @@ Caused by: java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
 	</article>
 	</article>
 	
-	<article class="h-entry post-text" itemscope itemtype="http://schema.org/Blog">
-		<header>
-			<h1 class="p-name entry-title" itemprop="headline">
-				<a href="/posts/one-bus-or-maybe-jeep-away.html" class="u-url">One Bus (or maybe Jeep) Away</a>
-			</h1>
-		</header>
-		<div class="e-content entry-content">
-			<p>Link: <a href="http://onebusaway.org/">http://onebusaway.org/</a></p>
-<p><strong>TL;DR</strong> no routing; useless in Philippines</p>
-<p>OneBusAway is a transit information app. It provides data on what bus stops are near you, which buses pass by. You can also get schedules and the route of a particular bus given the number. It can also provide realtime updates like how many minutes until the next bus arrives. It does not, however, provide routing. There is no support for providing directions to get from point A to point B.</p>
-<p>It&rsquo;s comparable to what you get in some bus stops abroad. You&rsquo;d get a vicinity map and a list of buses passing through the stop. You might also get the times when the next buses will pass. It&rsquo;s useful for locals who already know how to get around, and want to avoid waiting for the bus. But it&rsquo;s not particularly good for people who want to know how to get around the city.</p>
-<p>OneBusAway is quite comprehensive in its platform support though. There is a webapp, apps for iOS, Android and Windows Phone, as well as SMS and Voice support. This would all be nice but we don&rsquo;t have the necessary infrastructure yet in the Philippines. We don&rsquo;t have bus or jeepney stops. We also wouldn&rsquo;t have realtime data to make the app particularly useful.</p>
-<p>You can try it out for yourself by following their <a href="https://github.com/OneBusAway/onebusaway-application-modules/wiki/OneBusAway-Quickstart-Guide">Quickstart Guide</a>. One caveat is you will have to add <code>-P tripEntriesFactory.throwExceptionOnInvalidStopToShapeMappingException=false</code> when building the bundle. This has to do with the OneBusAway having difficulty matching the <a href="https://github.com/OneBusAway/onebusaway-application-modules/wiki/Stop-to-Shape-Matching">stops to the shape data</a>.</p>
-<p>Here&rsquo;s some screenshots of the app with the Philippine data. Notice how you only see the stops but there isn&rsquo;t a line for the route. This is a problem with our GTFS data. Also, at some points it&rsquo;s hard to tell where the jeep is going to pass since there isn&rsquo;t any indication of order either. This is more of a OneBusAway problem. It usually expects there to be shape data available.</p>
-<p><a href="../galleries/transit/onebusaway1.png"><img src="../galleries/transit/onebusaway1.png" alt="OneBusAway" title="All the stops along Katipunan Avenue are named Katipunan Avenue."></a></p>
-<p>It doesn&rsquo;t really handle too many routes passing through a stop. The list just overflows past the bubble. You can still actually read it by panning the map. It&rsquo;s just a bit weird though.</p>
-<p>If you also noticed, there are usually 2 of each route. This is how the jeepney data was modeled as jeep routes might be different going one way and going back. This isn&rsquo;t the case for all jeeps though, so it might also be an implementation issue with the GTFS editor.</p>
-<p><a href="../galleries/transit/onebusaway2.png"><img src="../galleries/transit/onebusaway2.png" alt="OneBusAway"></a></p>
-<p><a href="../galleries/transit/onebusaway3.png"><img src="../galleries/transit/onebusaway3.png" alt="OneBusAway"></a></p>
-
-		</div>
-		<small class="dateline">Posted: <time class="published dt-published" itemprop="datePublished" datetime="2013-07-09">2013-07-09</time></small>
-		| <small class="commentline"><a href="/posts/one-bus-or-maybe-jeep-away.html#isso-thread">Comments</a></small>
-	</article>
-	</article>
-	
 </div>
 <nav class="postindexpager">
 	<ul class="pager clearfix">

+ 26 - 0
output/page/5.html

@@ -36,6 +36,32 @@
 <main id="content" role="main">
 <div class="postindex">
 	
+	<article class="h-entry post-text" itemscope itemtype="http://schema.org/Blog">
+		<header>
+			<h1 class="p-name entry-title" itemprop="headline">
+				<a href="/posts/one-bus-or-maybe-jeep-away.html" class="u-url">One Bus (or maybe Jeep) Away</a>
+			</h1>
+		</header>
+		<div class="e-content entry-content">
+			<p>Link: <a href="http://onebusaway.org/">http://onebusaway.org/</a></p>
+<p><strong>TL;DR</strong> no routing; useless in Philippines</p>
+<p>OneBusAway is a transit information app. It provides data on what bus stops are near you, which buses pass by. You can also get schedules and the route of a particular bus given the number. It can also provide realtime updates like how many minutes until the next bus arrives. It does not, however, provide routing. There is no support for providing directions to get from point A to point B.</p>
+<p>It&rsquo;s comparable to what you get in some bus stops abroad. You&rsquo;d get a vicinity map and a list of buses passing through the stop. You might also get the times when the next buses will pass. It&rsquo;s useful for locals who already know how to get around, and want to avoid waiting for the bus. But it&rsquo;s not particularly good for people who want to know how to get around the city.</p>
+<p>OneBusAway is quite comprehensive in its platform support though. There is a webapp, apps for iOS, Android and Windows Phone, as well as SMS and Voice support. This would all be nice but we don&rsquo;t have the necessary infrastructure yet in the Philippines. We don&rsquo;t have bus or jeepney stops. We also wouldn&rsquo;t have realtime data to make the app particularly useful.</p>
+<p>You can try it out for yourself by following their <a href="https://github.com/OneBusAway/onebusaway-application-modules/wiki/OneBusAway-Quickstart-Guide">Quickstart Guide</a>. One caveat is you will have to add <code>-P tripEntriesFactory.throwExceptionOnInvalidStopToShapeMappingException=false</code> when building the bundle. This has to do with the OneBusAway having difficulty matching the <a href="https://github.com/OneBusAway/onebusaway-application-modules/wiki/Stop-to-Shape-Matching">stops to the shape data</a>.</p>
+<p>Here&rsquo;s some screenshots of the app with the Philippine data. Notice how you only see the stops but there isn&rsquo;t a line for the route. This is a problem with our GTFS data. Also, at some points it&rsquo;s hard to tell where the jeep is going to pass since there isn&rsquo;t any indication of order either. This is more of a OneBusAway problem. It usually expects there to be shape data available.</p>
+<p><a href="../galleries/transit/onebusaway1.png"><img src="../galleries/transit/onebusaway1.png" alt="OneBusAway" title="All the stops along Katipunan Avenue are named Katipunan Avenue."></a></p>
+<p>It doesn&rsquo;t really handle too many routes passing through a stop. The list just overflows past the bubble. You can still actually read it by panning the map. It&rsquo;s just a bit weird though.</p>
+<p>If you also noticed, there are usually 2 of each route. This is how the jeepney data was modeled as jeep routes might be different going one way and going back. This isn&rsquo;t the case for all jeeps though, so it might also be an implementation issue with the GTFS editor.</p>
+<p><a href="../galleries/transit/onebusaway2.png"><img src="../galleries/transit/onebusaway2.png" alt="OneBusAway"></a></p>
+<p><a href="../galleries/transit/onebusaway3.png"><img src="../galleries/transit/onebusaway3.png" alt="OneBusAway"></a></p>
+
+		</div>
+		<small class="dateline">Posted: <time class="published dt-published" itemprop="datePublished" datetime="2013-07-09">2013-07-09</time></small>
+		| <small class="commentline"><a href="/posts/one-bus-or-maybe-jeep-away.html#isso-thread">Comments</a></small>
+	</article>
+	</article>
+	
 	<article class="h-entry post-text" itemscope itemtype="http://schema.org/Blog">
 		<header>
 			<h1 class="p-name entry-title" itemprop="headline">

+ 5 - 0
output/posts.html

@@ -40,6 +40,11 @@
 	</header>
 	<ul class="postlist">
 		
+		<li>
+			<a class="listtitle" href="/posts/ssh-git-repo-docker.html">SSH Access to Git Repository in Docker</a>
+			<span class="entry-meta"><time itemprop="datePublished" datetime="2022-01-23">2022-01-23</time></span>
+		</li>
+		
 		<li>
 			<a class="listtitle" href="/posts/android-multisim.html">Android Multisim Pre-5.1</a>
 			<span class="entry-meta"><time itemprop="datePublished" datetime="2021-09-12">2021-09-12</time></span>

+ 4 - 0
output/posts/android-multisim.html

@@ -81,6 +81,10 @@
 				</li>
 				
 				
+				<li class="next">
+					<a href="/posts/ssh-git-repo-docker.html" rel="next" title="SSH Access to Git Repository in Docker">Next post &rarr;</a>
+				</li>
+				
 			</ul>
 		</nav>
 	</aside>

+ 93 - 1
output/posts/rss.xml

@@ -6,11 +6,103 @@
     <description>Recent content in Posts on Pleasant Programmer</description>
     <generator>Hugo -- gohugo.io</generator>
     <language>en-us</language>
-    <lastBuildDate>Sun, 12 Sep 2021 19:37:22 +0900</lastBuildDate>
+    <lastBuildDate>Sun, 23 Jan 2022 15:06:47 +0900</lastBuildDate>
     
 	<atom:link href="https://pleasantprogrammer.com/posts/rss.xml" rel="self" type="application/rss+xml" />
     
     
+    <item>
+      <title>SSH Access to Git Repository in Docker</title>
+      <link>https://pleasantprogrammer.com/posts/ssh-git-repo-docker.html</link>
+      <pubDate>Sun, 23 Jan 2022 15:06:47 +0900</pubDate>
+      
+      <guid>https://pleasantprogrammer.com/posts/ssh-git-repo-docker.html</guid>
+      <description>With the likes of Gogs and Gitea, self-hosting a personal git service has become quite common. It&amp;rsquo;s also not unlikely that the software is run via docker but this brings a problem with regards to SSH access.
+Ideally, the git service container just exposes port 22 but this would conflict with the host&amp;rsquo;s own SSH service. One solution would be to just use different ports for the git SSH and host SSH and that&amp;rsquo;s perfectly fine.</description>
+      <content:encoded>&lt;p&gt;With the likes of &lt;a href=&#34;https://gogs.io/&#34;&gt;Gogs&lt;/a&gt; and &lt;a href=&#34;https://gitea.io&#34;&gt;Gitea&lt;/a&gt;,
+self-hosting a personal git service has become quite common. It&amp;rsquo;s also not
+unlikely that the software is run via docker but this brings a problem with
+regards to SSH access.&lt;/p&gt;
+&lt;p&gt;Ideally, the git service container just exposes port 22 but this would conflict
+with the host&amp;rsquo;s own SSH service. One solution would be to just use different
+ports for the git SSH and host SSH and that&amp;rsquo;s perfectly fine. But we can also
+just have the host SSH service forward the request to the git service itself
+using the &lt;code&gt;command&lt;/code&gt; option in &lt;code&gt;authorized_keys&lt;/code&gt;. And as we&amp;rsquo;ll find out later,
+the git service itself is using this functionality.&lt;/p&gt;
+&lt;h2 id=&#34;how-git-over-ssh-works&#34;&gt;How git over SSH works&lt;/h2&gt;
+&lt;p&gt;When you do a &lt;code&gt;git push&lt;/code&gt;, what actually happens is it runs &lt;code&gt;git-send-pack&lt;/code&gt; which
+runs &lt;code&gt;git-receive-pack &amp;lt;directory&amp;gt;&lt;/code&gt; on the remote using SSH. The actual
+communication then just simply happens via stdin/stdout. Conversely, doing a
+&lt;code&gt;git pull&lt;/code&gt; just runs &lt;code&gt;git-fetch-pack&lt;/code&gt; and the somewhat confusingly named
+&lt;code&gt;git-upload-pack&lt;/code&gt; on the remote.&lt;/p&gt;
+&lt;p&gt;So far so good, but did you notice that when cloning via SSH, the remote is
+typically &lt;code&gt;git@github.com:org/repo&lt;/code&gt;? If everyone SSHs in as the &lt;code&gt;git&lt;/code&gt; user, how
+does the git service know which user is which? And how does it prevent users
+from accessing each other&amp;rsquo;s repositories?&lt;/p&gt;
+&lt;p&gt;One typically thinks of &lt;code&gt;authorized_keys&lt;/code&gt; as just a list of allowed SSH keys but
+it can do &lt;a href=&#34;https://linux.die.net/man/8/sshd&#34;&gt;much more than that&lt;/a&gt;. Of particular
+interest is the &lt;code&gt;command&lt;/code&gt; directive which gets run instead of the user supplied
+command. The original command is passed in as an environment variable
+&lt;code&gt;SSH_ORIGINAL_COMMAND&lt;/code&gt; which can be used to check if we allow it to be run or
+not.&lt;/p&gt;
+&lt;p&gt;So with an &lt;code&gt;authorized_keys&lt;/code&gt; file like so:&lt;/p&gt;
+&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;command=&amp;#34;verify-user user-1&amp;#34; ssh-rsa ...
+command=&amp;#34;verify-user user-1&amp;#34; ssh-ed25519 ...
+command=&amp;#34;verify-user user-2&amp;#34; ssh-rsa ...
+command=&amp;#34;verify-user user-3&amp;#34; ssh-rsa ...
+&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each key is tied to a particular user by virtue of &lt;code&gt;command&lt;/code&gt;. And &lt;code&gt;verify-user&lt;/code&gt;
+can check the &lt;code&gt;SSH_ORIGINAL_COMMAND&lt;/code&gt; if the particular user is allowed access to
+the particular repository. You can also implement additional restrictions like
+pull-only or push-only permissions with this setup. This is how both Gogs and
+Gitea work.&lt;/p&gt;
+&lt;h2 id=&#34;forwarding-git-ssh-to-docker&#34;&gt;Forwarding git SSH to Docker&lt;/h2&gt;
+&lt;p&gt;When running Gogs on the host, it&amp;rsquo;s typically run as the &lt;code&gt;git&lt;/code&gt; user and when an
+SSH key is added or removed, it simply rewrites &lt;code&gt;~git/.ssh/authorized_keys&lt;/code&gt;.
+Thus it just works with the host SSH service without problems. When running
+inside docker, one thing we can do is bind mount the host&amp;rsquo;s &lt;code&gt;~git/.ssh&lt;/code&gt; folder
+into the docker container so that the host can authorize the SSH connections.&lt;/p&gt;
+&lt;p&gt;The problem lies with the &lt;code&gt;command&lt;/code&gt; which only exists inside the docker
+container itself. So any user trying to connect can authenticate successfully
+but will get a &lt;code&gt;command not found&lt;/code&gt; error. For the Gogs docker image, the command
+looks like &lt;code&gt;/app/gogs/gogs serv key-1&lt;/code&gt;. So we can just make &lt;code&gt;/app/gogs/gogs&lt;/code&gt;
+available on the host and forward the command to the docker container.&lt;/p&gt;
+&lt;p&gt;&lt;a href=&#34;https://docs.gitea.io/en-us/install-with-docker/#ssh-container-passthrough&#34;&gt;Most&lt;/a&gt;
+&lt;a href=&#34;http://www.ateijelo.com/blog/2016/07/09/share-port-22-between-docker-gogs-ssh-and-local-system&#34;&gt;instructions&lt;/a&gt;
+I&amp;rsquo;ve seen with regards to this involves using &lt;code&gt;ssh&lt;/code&gt; to connect to the internal
+docker SSH service but this just seems overly complicated to me. If you
+remember, at it&amp;rsquo;s core git really only communicates over stdin/stdout and SSH is
+just a means to get that.&lt;/p&gt;
+&lt;p&gt;If all we need is for our shim &lt;code&gt;/app/gogs/gogs&lt;/code&gt; to be able to run a command
+inside the docker container with stdin/stdout attached, then we can actually
+just do that with &lt;code&gt;docker exec&lt;/code&gt;. So it can be something like this:&lt;/p&gt;
+&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;color:#0f0;font-weight:bold&#34;&gt;#!/usr/bin/env bash
+&lt;/span&gt;&lt;span style=&#34;color:#0f0;font-weight:bold&#34;&gt;&lt;/span&gt;
+&lt;span style=&#34;color:#007f7f&#34;&gt;# Requires the following in sudoers&lt;/span&gt;
+&lt;span style=&#34;color:#007f7f&#34;&gt;# git ALL=(ALL) NOPASSWD: /app/gogs/gogs&lt;/span&gt;
+&lt;span style=&#34;color:#007f7f&#34;&gt;# Defaults:git env_keep=SSH_ORIGINAL_COMMAND&lt;/span&gt;
+
+GOGS_CONTAINER=git-gogs-1
+
+&lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;if&lt;/span&gt; [[ $EUID -ne &lt;span style=&#34;color:#ff0;font-weight:bold&#34;&gt;0&lt;/span&gt; ]]; &lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;then&lt;/span&gt;
+  &lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;exec&lt;/span&gt; sudo &lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt;$0&lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt;$@&lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt;
+&lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;fi&lt;/span&gt;
+
+&lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;if&lt;/span&gt; [ &lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt;$1&lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt; != &lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;serv&amp;#34;&lt;/span&gt; ]; &lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;then&lt;/span&gt;
+  &lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;exit&lt;/span&gt; &lt;span style=&#34;color:#ff0;font-weight:bold&#34;&gt;1&lt;/span&gt;
+&lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;fi&lt;/span&gt;
+
+&lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;exec&lt;/span&gt; docker &lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;exec&lt;/span&gt; -i -u git -e &lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;SSH_ORIGINAL_COMMAND=&lt;/span&gt;$SSH_ORIGINAL_COMMAND&lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt;$GOGS_CONTAINER&lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt; /app/gogs/gogs &lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt;$@&lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt;
+&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So for git SSH access to Gogs running in docker, the necessary steps here are:&lt;/p&gt;
+&lt;ol&gt;
+&lt;li&gt;Have a &lt;code&gt;git&lt;/code&gt; user on the host&lt;/li&gt;
+&lt;li&gt;Bind mount &lt;code&gt;~git/.ssh&lt;/code&gt; to &lt;code&gt;/data/git/.ssh&lt;/code&gt; in the Gogs container&lt;/li&gt;
+&lt;li&gt;Add the shim script to &lt;code&gt;/app/gogs/gogs&lt;/code&gt; (make sure it&amp;rsquo;s owned by root and
+is chmod-ed &lt;code&gt;0755&lt;/code&gt;)&lt;/li&gt;
+&lt;li&gt;Add the listed sudoers rules&lt;/li&gt;
+&lt;/ol&gt;
+</content:encoded>
+    </item>
+    
     <item>
       <title>Android Multisim Pre-5.1</title>
       <link>https://pleasantprogrammer.com/posts/android-multisim.html</link>

+ 190 - 0
output/posts/ssh-git-repo-docker.html

@@ -0,0 +1,190 @@
+<!DOCTYPE html>
+<html lang="en-us">
+<head>
+	<meta charset="utf-8">
+	<meta name="generator" content="Hugo 0.92.0" />
+	<meta name="viewport" content="width=device-width, initial-scale=1">
+	<link rel="stylesheet" href="/assets/css/theme.css">
+	<link rel="alternate" href="/rss.xml" type="application/rss+xml" title="Pleasant Programmer">
+	<script type="text/javascript" src="//use.typekit.net/iwm5axp.js"></script>
+	<script type="text/javascript">try{Typekit.load();}catch(e){}</script>
+	<title>SSH Access to Git Repository in Docker - Pleasant Programmer</title>
+</head>
+
+<body>
+	<header id="header" role="banner">
+		<div id="thomas">
+			<img src="/assets/img/thomas.gif" alt="DJ THOMAS IN DA HAUS">
+			<img src="/assets/img/thomas.png" alt="Pleasant Programmer">
+		</div>
+		<h1 class="site-title"><a href="/">Pleasant Programmer</a></h1>
+		<nav id="menu" role="navigation">
+			<ul>
+				<li class="twitter">
+					<a href="http://twitter.com/pleasantprog">@pleasantprog</a>
+				</li>
+				<li><a href="/pages/projects.html">projects</a></li>
+				<li><a href="/posts.html">archives</a></li>
+				<li><a href="/tags.html">tags</a></li>
+				<li><a href="/rss.xml">rss</a></li>
+			</ul>
+		</nav>
+	</header>
+	<div id="container">
+
+
+<main id="content" role="main">
+<article itemscope itemtype="http://schema.org/BlogPosting">
+	<h1 class="p-name entry-title" itemprop="headline name">
+		<a href="/posts/ssh-git-repo-docker.html">SSH Access to Git Repository in Docker</a></h1>
+	<small>
+		<span class="dateline">Posted: <time itemprop="datePublished" datetime="2022-01-23">2022-01-23</time></span>
+		| More posts about
+		
+		<a class="tag p-category" href="/tags/sysadmin.html" rel="tag">
+			sysadmin
+		</a>
+		
+	</small>
+	<div class="e-content entry-content" itemprop="entry-text">
+		<p>With the likes of <a href="https://gogs.io/">Gogs</a> and <a href="https://gitea.io">Gitea</a>,
+self-hosting a personal git service has become quite common. It&rsquo;s also not
+unlikely that the software is run via docker but this brings a problem with
+regards to SSH access.</p>
+<p>Ideally, the git service container just exposes port 22 but this would conflict
+with the host&rsquo;s own SSH service. One solution would be to just use different
+ports for the git SSH and host SSH and that&rsquo;s perfectly fine. But we can also
+just have the host SSH service forward the request to the git service itself
+using the <code>command</code> option in <code>authorized_keys</code>. And as we&rsquo;ll find out later,
+the git service itself is using this functionality.</p>
+<h2 id="how-git-over-ssh-works">How git over SSH works</h2>
+<p>When you do a <code>git push</code>, what actually happens is it runs <code>git-send-pack</code> which
+runs <code>git-receive-pack &lt;directory&gt;</code> on the remote using SSH. The actual
+communication then just simply happens via stdin/stdout. Conversely, doing a
+<code>git pull</code> just runs <code>git-fetch-pack</code> and the somewhat confusingly named
+<code>git-upload-pack</code> on the remote.</p>
+<p>So far so good, but did you notice that when cloning via SSH, the remote is
+typically <code>git@github.com:org/repo</code>? If everyone SSHs in as the <code>git</code> user, how
+does the git service know which user is which? And how does it prevent users
+from accessing each other&rsquo;s repositories?</p>
+<p>One typically thinks of <code>authorized_keys</code> as just a list of allowed SSH keys but
+it can do <a href="https://linux.die.net/man/8/sshd">much more than that</a>. Of particular
+interest is the <code>command</code> directive which gets run instead of the user supplied
+command. The original command is passed in as an environment variable
+<code>SSH_ORIGINAL_COMMAND</code> which can be used to check if we allow it to be run or
+not.</p>
+<p>So with an <code>authorized_keys</code> file like so:</p>
+<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 ...
+command=&#34;verify-user user-1&#34; ssh-ed25519 ...
+command=&#34;verify-user user-2&#34; ssh-rsa ...
+command=&#34;verify-user user-3&#34; ssh-rsa ...
+</code></pre></div><p>Each key is tied to a particular user by virtue of <code>command</code>. And <code>verify-user</code>
+can check the <code>SSH_ORIGINAL_COMMAND</code> if the particular user is allowed access to
+the particular repository. You can also implement additional restrictions like
+pull-only or push-only permissions with this setup. This is how both Gogs and
+Gitea work.</p>
+<h2 id="forwarding-git-ssh-to-docker">Forwarding git SSH to Docker</h2>
+<p>When running Gogs on the host, it&rsquo;s typically run as the <code>git</code> user and when an
+SSH key is added or removed, it simply rewrites <code>~git/.ssh/authorized_keys</code>.
+Thus it just works with the host SSH service without problems. When running
+inside docker, one thing we can do is bind mount the host&rsquo;s <code>~git/.ssh</code> folder
+into the docker container so that the host can authorize the SSH connections.</p>
+<p>The problem lies with the <code>command</code> which only exists inside the docker
+container itself. So any user trying to connect can authenticate successfully
+but will get a <code>command not found</code> error. For the Gogs docker image, the command
+looks like <code>/app/gogs/gogs serv key-1</code>. So we can just make <code>/app/gogs/gogs</code>
+available on the host and forward the command to the docker container.</p>
+<p><a href="https://docs.gitea.io/en-us/install-with-docker/#ssh-container-passthrough">Most</a>
+<a href="http://www.ateijelo.com/blog/2016/07/09/share-port-22-between-docker-gogs-ssh-and-local-system">instructions</a>
+I&rsquo;ve seen with regards to this involves using <code>ssh</code> to connect to the internal
+docker SSH service but this just seems overly complicated to me. If you
+remember, at it&rsquo;s core git really only communicates over stdin/stdout and SSH is
+just a means to get that.</p>
+<p>If all we need is for our shim <code>/app/gogs/gogs</code> to be able to run a command
+inside the docker container with stdin/stdout attached, then we can actually
+just do that with <code>docker exec</code>. So it can be something like this:</p>
+<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
+</span><span style="color:#0f0;font-weight:bold"></span>
+<span style="color:#007f7f"># Requires the following in sudoers</span>
+<span style="color:#007f7f"># git ALL=(ALL) NOPASSWD: /app/gogs/gogs</span>
+<span style="color:#007f7f"># Defaults:git env_keep=SSH_ORIGINAL_COMMAND</span>
+
+GOGS_CONTAINER=git-gogs-1
+
+<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>
+  <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>
+<span style="color:#fff;font-weight:bold">fi</span>
+
+<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>
+  <span style="color:#fff;font-weight:bold">exit</span> <span style="color:#ff0;font-weight:bold">1</span>
+<span style="color:#fff;font-weight:bold">fi</span>
+
+<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>
+</code></pre></div><p>So for git SSH access to Gogs running in docker, the necessary steps here are:</p>
+<ol>
+<li>Have a <code>git</code> user on the host</li>
+<li>Bind mount <code>~git/.ssh</code> to <code>/data/git/.ssh</code> in the Gogs container</li>
+<li>Add the shim script to <code>/app/gogs/gogs</code> (make sure it&rsquo;s owned by root and
+is chmod-ed <code>0755</code>)</li>
+<li>Add the listed sudoers rules</li>
+</ol>
+
+	</div>
+	<aside class="postpromonav">
+		<nav>
+			<ul class="pager clearfix">
+				
+				<li class="previous">
+					<a href="/posts/android-multisim.html" rel="prev" title="Android Multisim Pre-5.1">&larr; Previous post</a>
+				</li>
+				
+				
+			</ul>
+		</nav>
+	</aside>
+	<section class="comments">
+		<script
+	data-isso="https://isso.pleasantprogrammer.com/"
+	data-isso-require-author="true"
+	data-isso-vote="false"
+	src="https://isso.pleasantprogrammer.com/js/embed.min.js">
+</script>
+<section id="isso-thread"></section>
+
+	</section>
+</article>
+</main>
+
+
+	<footer id="footer" role="contentinfo">
+		<p>
+		<a rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/deed.en_US">
+			<img alt="CC-BY-SA" style="border-width:0" src="https://licensebuttons.net/l/by-sa/3.0/80x15.png">
+		</a> &copy; 2020 Thomas Dy - Powered by <a href="http://gohugo.io">Hugo</a></p>
+	</footer>
+</div>
+
+<script src="/assets/js/konami.js"></script>
+<script>
+var easter_egg = new Konami();
+easter_egg.code = function() {
+	var el = document.getElementById('thomas');
+	if(el.className == "whoa") {
+		el.className = "";
+	}
+	else {
+		el.className = "whoa";
+	}
+	document.body.scrollTop = document.documentElement.scrollTop = 0;
+}
+easter_egg.load();
+
+
+
+</script>
+
+
+
+</body>
+</html>
+

+ 93 - 1
output/rss.xml

@@ -6,11 +6,103 @@
     <description>Recent content on Pleasant Programmer</description>
     <generator>Hugo -- gohugo.io</generator>
     <language>en-us</language>
-    <lastBuildDate>Sun, 12 Sep 2021 19:37:22 +0900</lastBuildDate>
+    <lastBuildDate>Sun, 23 Jan 2022 15:06:47 +0900</lastBuildDate>
     
 	<atom:link href="https://pleasantprogrammer.com/rss.xml" rel="self" type="application/rss+xml" />
     
     
+    <item>
+      <title>SSH Access to Git Repository in Docker</title>
+      <link>https://pleasantprogrammer.com/posts/ssh-git-repo-docker.html</link>
+      <pubDate>Sun, 23 Jan 2022 15:06:47 +0900</pubDate>
+      
+      <guid>https://pleasantprogrammer.com/posts/ssh-git-repo-docker.html</guid>
+      <description>With the likes of Gogs and Gitea, self-hosting a personal git service has become quite common. It&amp;rsquo;s also not unlikely that the software is run via docker but this brings a problem with regards to SSH access.
+Ideally, the git service container just exposes port 22 but this would conflict with the host&amp;rsquo;s own SSH service. One solution would be to just use different ports for the git SSH and host SSH and that&amp;rsquo;s perfectly fine.</description>
+      <content:encoded>&lt;p&gt;With the likes of &lt;a href=&#34;https://gogs.io/&#34;&gt;Gogs&lt;/a&gt; and &lt;a href=&#34;https://gitea.io&#34;&gt;Gitea&lt;/a&gt;,
+self-hosting a personal git service has become quite common. It&amp;rsquo;s also not
+unlikely that the software is run via docker but this brings a problem with
+regards to SSH access.&lt;/p&gt;
+&lt;p&gt;Ideally, the git service container just exposes port 22 but this would conflict
+with the host&amp;rsquo;s own SSH service. One solution would be to just use different
+ports for the git SSH and host SSH and that&amp;rsquo;s perfectly fine. But we can also
+just have the host SSH service forward the request to the git service itself
+using the &lt;code&gt;command&lt;/code&gt; option in &lt;code&gt;authorized_keys&lt;/code&gt;. And as we&amp;rsquo;ll find out later,
+the git service itself is using this functionality.&lt;/p&gt;
+&lt;h2 id=&#34;how-git-over-ssh-works&#34;&gt;How git over SSH works&lt;/h2&gt;
+&lt;p&gt;When you do a &lt;code&gt;git push&lt;/code&gt;, what actually happens is it runs &lt;code&gt;git-send-pack&lt;/code&gt; which
+runs &lt;code&gt;git-receive-pack &amp;lt;directory&amp;gt;&lt;/code&gt; on the remote using SSH. The actual
+communication then just simply happens via stdin/stdout. Conversely, doing a
+&lt;code&gt;git pull&lt;/code&gt; just runs &lt;code&gt;git-fetch-pack&lt;/code&gt; and the somewhat confusingly named
+&lt;code&gt;git-upload-pack&lt;/code&gt; on the remote.&lt;/p&gt;
+&lt;p&gt;So far so good, but did you notice that when cloning via SSH, the remote is
+typically &lt;code&gt;git@github.com:org/repo&lt;/code&gt;? If everyone SSHs in as the &lt;code&gt;git&lt;/code&gt; user, how
+does the git service know which user is which? And how does it prevent users
+from accessing each other&amp;rsquo;s repositories?&lt;/p&gt;
+&lt;p&gt;One typically thinks of &lt;code&gt;authorized_keys&lt;/code&gt; as just a list of allowed SSH keys but
+it can do &lt;a href=&#34;https://linux.die.net/man/8/sshd&#34;&gt;much more than that&lt;/a&gt;. Of particular
+interest is the &lt;code&gt;command&lt;/code&gt; directive which gets run instead of the user supplied
+command. The original command is passed in as an environment variable
+&lt;code&gt;SSH_ORIGINAL_COMMAND&lt;/code&gt; which can be used to check if we allow it to be run or
+not.&lt;/p&gt;
+&lt;p&gt;So with an &lt;code&gt;authorized_keys&lt;/code&gt; file like so:&lt;/p&gt;
+&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;command=&amp;#34;verify-user user-1&amp;#34; ssh-rsa ...
+command=&amp;#34;verify-user user-1&amp;#34; ssh-ed25519 ...
+command=&amp;#34;verify-user user-2&amp;#34; ssh-rsa ...
+command=&amp;#34;verify-user user-3&amp;#34; ssh-rsa ...
+&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each key is tied to a particular user by virtue of &lt;code&gt;command&lt;/code&gt;. And &lt;code&gt;verify-user&lt;/code&gt;
+can check the &lt;code&gt;SSH_ORIGINAL_COMMAND&lt;/code&gt; if the particular user is allowed access to
+the particular repository. You can also implement additional restrictions like
+pull-only or push-only permissions with this setup. This is how both Gogs and
+Gitea work.&lt;/p&gt;
+&lt;h2 id=&#34;forwarding-git-ssh-to-docker&#34;&gt;Forwarding git SSH to Docker&lt;/h2&gt;
+&lt;p&gt;When running Gogs on the host, it&amp;rsquo;s typically run as the &lt;code&gt;git&lt;/code&gt; user and when an
+SSH key is added or removed, it simply rewrites &lt;code&gt;~git/.ssh/authorized_keys&lt;/code&gt;.
+Thus it just works with the host SSH service without problems. When running
+inside docker, one thing we can do is bind mount the host&amp;rsquo;s &lt;code&gt;~git/.ssh&lt;/code&gt; folder
+into the docker container so that the host can authorize the SSH connections.&lt;/p&gt;
+&lt;p&gt;The problem lies with the &lt;code&gt;command&lt;/code&gt; which only exists inside the docker
+container itself. So any user trying to connect can authenticate successfully
+but will get a &lt;code&gt;command not found&lt;/code&gt; error. For the Gogs docker image, the command
+looks like &lt;code&gt;/app/gogs/gogs serv key-1&lt;/code&gt;. So we can just make &lt;code&gt;/app/gogs/gogs&lt;/code&gt;
+available on the host and forward the command to the docker container.&lt;/p&gt;
+&lt;p&gt;&lt;a href=&#34;https://docs.gitea.io/en-us/install-with-docker/#ssh-container-passthrough&#34;&gt;Most&lt;/a&gt;
+&lt;a href=&#34;http://www.ateijelo.com/blog/2016/07/09/share-port-22-between-docker-gogs-ssh-and-local-system&#34;&gt;instructions&lt;/a&gt;
+I&amp;rsquo;ve seen with regards to this involves using &lt;code&gt;ssh&lt;/code&gt; to connect to the internal
+docker SSH service but this just seems overly complicated to me. If you
+remember, at it&amp;rsquo;s core git really only communicates over stdin/stdout and SSH is
+just a means to get that.&lt;/p&gt;
+&lt;p&gt;If all we need is for our shim &lt;code&gt;/app/gogs/gogs&lt;/code&gt; to be able to run a command
+inside the docker container with stdin/stdout attached, then we can actually
+just do that with &lt;code&gt;docker exec&lt;/code&gt;. So it can be something like this:&lt;/p&gt;
+&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;color:#0f0;font-weight:bold&#34;&gt;#!/usr/bin/env bash
+&lt;/span&gt;&lt;span style=&#34;color:#0f0;font-weight:bold&#34;&gt;&lt;/span&gt;
+&lt;span style=&#34;color:#007f7f&#34;&gt;# Requires the following in sudoers&lt;/span&gt;
+&lt;span style=&#34;color:#007f7f&#34;&gt;# git ALL=(ALL) NOPASSWD: /app/gogs/gogs&lt;/span&gt;
+&lt;span style=&#34;color:#007f7f&#34;&gt;# Defaults:git env_keep=SSH_ORIGINAL_COMMAND&lt;/span&gt;
+
+GOGS_CONTAINER=git-gogs-1
+
+&lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;if&lt;/span&gt; [[ $EUID -ne &lt;span style=&#34;color:#ff0;font-weight:bold&#34;&gt;0&lt;/span&gt; ]]; &lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;then&lt;/span&gt;
+  &lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;exec&lt;/span&gt; sudo &lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt;$0&lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt;$@&lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt;
+&lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;fi&lt;/span&gt;
+
+&lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;if&lt;/span&gt; [ &lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt;$1&lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt; != &lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;serv&amp;#34;&lt;/span&gt; ]; &lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;then&lt;/span&gt;
+  &lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;exit&lt;/span&gt; &lt;span style=&#34;color:#ff0;font-weight:bold&#34;&gt;1&lt;/span&gt;
+&lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;fi&lt;/span&gt;
+
+&lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;exec&lt;/span&gt; docker &lt;span style=&#34;color:#fff;font-weight:bold&#34;&gt;exec&lt;/span&gt; -i -u git -e &lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;SSH_ORIGINAL_COMMAND=&lt;/span&gt;$SSH_ORIGINAL_COMMAND&lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt;$GOGS_CONTAINER&lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt; /app/gogs/gogs &lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt;$@&lt;span style=&#34;color:#0ff;font-weight:bold&#34;&gt;&amp;#34;&lt;/span&gt;
+&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So for git SSH access to Gogs running in docker, the necessary steps here are:&lt;/p&gt;
+&lt;ol&gt;
+&lt;li&gt;Have a &lt;code&gt;git&lt;/code&gt; user on the host&lt;/li&gt;
+&lt;li&gt;Bind mount &lt;code&gt;~git/.ssh&lt;/code&gt; to &lt;code&gt;/data/git/.ssh&lt;/code&gt; in the Gogs container&lt;/li&gt;
+&lt;li&gt;Add the shim script to &lt;code&gt;/app/gogs/gogs&lt;/code&gt; (make sure it&amp;rsquo;s owned by root and
+is chmod-ed &lt;code&gt;0755&lt;/code&gt;)&lt;/li&gt;
+&lt;li&gt;Add the listed sudoers rules&lt;/li&gt;
+&lt;/ol&gt;
+</content:encoded>
+    </item>
+    
     <item>
       <title>Android Multisim Pre-5.1</title>
       <link>https://pleasantprogrammer.com/posts/android-multisim.html</link>

+ 15 - 12
output/sitemap.xml

@@ -2,29 +2,32 @@
 <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
   xmlns:xhtml="http://www.w3.org/1999/xhtml">
   <url>
-    <loc>https://pleasantprogrammer.com/tags/android.html</loc>
-    <lastmod>2021-09-12T19:37:22+09:00</lastmod>
-  </url><url>
-    <loc>https://pleasantprogrammer.com/posts/android-multisim.html</loc>
-    <lastmod>2021-09-12T19:37:22+09:00</lastmod>
-  </url><url>
     <loc>https://pleasantprogrammer.com/</loc>
-    <lastmod>2021-09-12T19:37:22+09:00</lastmod>
+    <lastmod>2022-01-23T15:06:47+09:00</lastmod>
   </url><url>
     <loc>https://pleasantprogrammer.com/posts.html</loc>
+    <lastmod>2022-01-23T15:06:47+09:00</lastmod>
+  </url><url>
+    <loc>https://pleasantprogrammer.com/posts/ssh-git-repo-docker.html</loc>
+    <lastmod>2022-01-23T15:06:47+09:00</lastmod>
+  </url><url>
+    <loc>https://pleasantprogrammer.com/tags/sysadmin.html</loc>
+    <lastmod>2022-01-23T15:06:47+09:00</lastmod>
+  </url><url>
+    <loc>https://pleasantprogrammer.com/tags.html</loc>
+    <lastmod>2022-01-23T15:06:47+09:00</lastmod>
+  </url><url>
+    <loc>https://pleasantprogrammer.com/tags/android.html</loc>
     <lastmod>2021-09-12T19:37:22+09:00</lastmod>
   </url><url>
-    <loc>https://pleasantprogrammer.com/tags/programming.html</loc>
+    <loc>https://pleasantprogrammer.com/posts/android-multisim.html</loc>
     <lastmod>2021-09-12T19:37:22+09:00</lastmod>
   </url><url>
-    <loc>https://pleasantprogrammer.com/tags.html</loc>
+    <loc>https://pleasantprogrammer.com/tags/programming.html</loc>
     <lastmod>2021-09-12T19:37:22+09:00</lastmod>
   </url><url>
     <loc>https://pleasantprogrammer.com/posts/isp-issues.html</loc>
     <lastmod>2018-08-16T23:03:05+09:00</lastmod>
-  </url><url>
-    <loc>https://pleasantprogrammer.com/tags/sysadmin.html</loc>
-    <lastmod>2018-08-16T23:03:05+09:00</lastmod>
   </url><url>
     <loc>https://pleasantprogrammer.com/pages.html</loc>
     <lastmod>2018-02-10T16:47:55+09:00</lastmod>

+ 1 - 1
output/tags.html

@@ -60,7 +60,7 @@
 		
 		<li><a class="reference listtitle" href="/tags/programming.html">programming</a> (15)</li>
 		
-		<li><a class="reference listtitle" href="/tags/sysadmin.html">sysadmin</a> (6)</li>
+		<li><a class="reference listtitle" href="/tags/sysadmin.html">sysadmin</a> (7)</li>
 		
 		<li><a class="reference listtitle" href="/tags/systemd.html">systemd</a> (1)</li>