summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--404.html6
-rw-r--r--README.md22
-rw-r--r--TODO.md59
-rw-r--r--_drafts/dotfiles-under-revision-control.md83
-rw-r--r--_drafts/my-preferred-tools.md34
-rw-r--r--_drafts/ssh-proxy.md296
-rw-r--r--_drafts/temporary-postgresql-server.md100
-rw-r--r--about.md16
-rw-r--r--contact.md20
-rw-r--r--css/normalize.css349
-rw-r--r--css/site.css303
-rwxr-xr-xdeploy41
-rw-r--r--files/pgp.asc30
-rw-r--r--index.html8
-rw-r--r--posts/unix-domain-socket-forwarding-with-openssh.md173
-rw-r--r--privacy.md7
-rw-r--r--robots.txt4
-rw-r--r--site.hs208
-rw-r--r--skreutz-dot-com.cabal18
-rw-r--r--stack.yaml5
-rw-r--r--stack.yaml.lock19
-rw-r--r--templates/default.html43
-rw-r--r--templates/direct.html2
-rw-r--r--templates/post.html8
-rw-r--r--templates/posts.html10
-rw-r--r--templates/sitemap.xml23
27 files changed, 1890 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ed68b4a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+.stack-work
+_cache
+_site
diff --git a/404.html b/404.html
new file mode 100644
index 0000000..a5eee1c
--- /dev/null
+++ b/404.html
@@ -0,0 +1,6 @@
+---
+title: Resource not found
+---
+
+<h1>Resource not found</h1>
+<p>The server has not found the requested resource. Please <a href="/contact/">contact me</a> if you've found a dead link on this website. Otherwise you might wish to go to the <a href="/">home page</a> instead.</p>
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2481d1a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,22 @@
+# skreutz-dot-com
+
+This is the source code of my personal website: https://www.skreutz.com/.
+
+## Usage
+
+Print the help text of the custom static website generator:
+
+ $ stack run -- -h
+
+Serve a preview of the website at http://localhost:8080/:
+
+ $ stack run -- clean
+ $ stack run -- watch
+
+Deploy the website:
+
+ $ stack run -- deploy
+
+## Credits
+
+Built with [Hakyll](https://jaspervdj.be/hakyll/) and [Pandoc](https://pandoc.org/).
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000..a28e446
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,59 @@
+# TODO
+
+## Hosting
+
+* Regularly check HTTPS security using Mozilla's [Observatory](https://observatory.mozilla.org/) (includes the services below)
+ * https://hstspreload.org/?domain=skreutz.com
+ * https://securityheaders.com/?q=www.skreutz.com&hide=on&followRedirects=on
+ * https://www.ssllabs.com/ssltest/analyze.html?d=www.skreutz.com&hideResults=on&latest
+* Submit sitemap to [Google](https://support.google.com/webmasters/answer/183668?hl=en) and [Bing](https://www.bing.com/webmaster/help/how-to-submit-sitemaps-82a15bd4)
+* Configure custom 404 page
+* Consider to disable logging (and advertise this on the about page)
+
+## Content
+
+* Test and advertise readability in lynx and friends
+* Advertize feeds in blog post footer
+* Publish the website's source code
+* Recommended readings page and feed
+
+## Styling
+
+* Consider to highlight :target footnote
+* Add :target or :checked menu on mobile
+* Style `<pre title="foo">`, e.g., `pre[title]::before { content: attr(title); display: block; text-align: right; }`
+* Test iOS scrolling as described [here](https://css-tricks.com/snippets/css/momentum-scrolling-on-ios-overflow-elements/)
+
+## Implementation
+
+* Validate HTML, CSS, RSS, Atom, Sitemap using [W3C tools](https://w3c.github.io/developers/tools/)
+* Cabal build (without stack)
+* Format source code
+* Consider to disable pandoc extension `auto_identifiers`
+* Consider to filter draft posts as described [here](https://odone.io/posts/2020-05-18-published-posts-hakyll.html)
+* Consider to use select pandoc extensions as described [here](https://github.com/rpearce/robertwpearce.com/blob/master/site.hs)
+* Consider to re-implement deployment shell script using [shelly](https://hackage.haskell.org/package/shelly) as described [here](https://gist.github.com/ethagnawl/0ada86cb6eb996d95d5b65bff014188c)
+* Consider to add keywords to blog posts
+* Consider to tag blog posts
+* Consider to create an HTML sitemap in addition to the XML sitemap
+* Consider to record source code revision in generated website
+
+## Blog
+
+* Dotfiles under revision control: How I manage my personal configuration files with Git
+* My preferred tools: A whirlwind tour of my preferred command-line tools
+* SSH proxy: How to establish a secure tunnel between two firewalled machines
+* Temporary PostgreSQL server: A shell script to run the PostgreSQL server off a temporary directory
+* Your favorite Newsletter may track you
+* Redirect and kill non-interactive subshells (in a POSIX shell script)
+* HTTP health check using cURL
+* Migrate PostgreSQL schema without downtime (with zero downtime) using triggers
+* Filter Nix Packages by platform (e.g. Chromium on macOS)
+* Backup with zfs, mbuffer, lz4, and cron
+* Inline HTML images with this simple script
+* Arch Linux on MacBook Pro
+* Noscript photo gallery with pure CSS carousel, responsive images and deep links
+* SQLite hexastore vs. Neo4j
+* Setup a FreeBSD sftp server with basic email notifications from the command-line using bash, ssh, and the DigitalOcean command-line application
+* Cloud/VPS provider comparison
+* Auto-install OpenBSD to QEMU guest machine
diff --git a/_drafts/dotfiles-under-revision-control.md b/_drafts/dotfiles-under-revision-control.md
new file mode 100644
index 0000000..4cccea3
--- /dev/null
+++ b/_drafts/dotfiles-under-revision-control.md
@@ -0,0 +1,83 @@
+---
+title: "Dotfiles under revision control"
+description: "How I manage my configuration files with Git."
+published: 2019-09-02
+---
+
+<!-- TODO: Test commands -->
+
+I regularly spend some time to fit my preferred tools to my personal need and taste.
+Luckily, most command-line tools and a growing number of graphical tools, accept configuration files -- commonly called _dotfiles_ because of the typical dot at the beginnig of the file name, e.g., `.tmux.conf`.
+This post describes my not-so-special way to put these dotfiles under revision control using Git and Bash.
+In fact, you'll find a myriad of public dotfiles repositories on the web, for example on [GitHub](https://github.com/search?q=dotfiles).
+
+Add the following snippet to your `~/.bashrc`.
+The first line defines a `dotfiles` alias for `git` to distinguish your dotfiles repository from any other Git repository in and below your home directory.
+The remaining lines reuse -- you might say hack -- Git's Bash completion for the alias.
+
+```sh
+alias dotfiles="git --git-dir=\${HOME}/.dotfiles/ --work-tree=\${HOME}"
+if [ -f /usr/share/git/completion/git-completion.bash ]; then
+ source /usr/share/git/completion/git-completion.bash
+ __git_complete dotfiles __git_main
+fi
+```
+
+Initialize a bare Git repository for your dotfiles, and tell Git to ignore untracked files.
+
+```sh
+mkdir ~/.dotfiles
+git -C ~/.dotfiles init --bare
+dotfiles config status.showUntrackedFiles no
+```
+
+Now you can `add`, `commit`, and `push` your dotfiles as usual.
+
+You can even add other repositories as submodules.
+The following snippet, for example, adds Vim and Tmux plug-ins for the acclaimed [Solarized](https://ethanschoonover.com/solarized/) color scheme.
+
+```sh
+mkdir -p ~/.tmux/plugins
+cd ~/.tmux/plugins
+dotfiles submodule add https://github.com/seebi/tmux-colors-solarized.git
+
+mkdir -p ~/.vim/pack/stefan/{start,opt}
+cd ~/.vim/pack/stefan/start
+dotfiles submodule add https://github.com/altercation/vim-colors-solarized.git
+
+dotfiles add ~/gitmodules
+```
+
+Update the submodules as always.
+
+```sh
+dotfiles submodule update --remote --merge
+```
+
+Generate help tags.
+
+```sh
+for d in ~/.vim/pack/stefan/*/*/doc; do
+ vim -u NONE -c "helptags $d" -c q
+done
+```
+
+Finally, clone your dotfiles to another machine.
+Be careful to clone into a temporary directory, though.
+Otherwise you might screw up your home directory.
+
+```sh
+git clone \
+ --recurse-submodules \
+ --separate-git-dir=$HOME/.dotfiles \
+ example.com:~/git/dotfiles ~/dotfiles-tmp
+rm ~/dotfiles-tmp/.git
+cp -ai ~/dotfiles-tmp/.* ~
+rm -r ~/dotfiles-tmp
+dotfiles config status.showUntrackedFiles no
+```
+
+That's it. Happy tracking!
+
+P.S. Did you know that Unix' hidden files were a mistake?
+See [this archived post](https://web.archive.org/web/20190318012059/https://plus.google.com/101960720994009339267/posts/R58WgWwN9jp) by Rob Pike.
diff --git a/_drafts/my-preferred-tools.md b/_drafts/my-preferred-tools.md
new file mode 100644
index 0000000..b0a1d65
--- /dev/null
+++ b/_drafts/my-preferred-tools.md
@@ -0,0 +1,34 @@
+---
+title: "My preferred tools"
+description: "A whirlwind tour of my preferred command-line tools."
+date: 2019-06-29
+---
+
+Tools shape our perception of problems.
+That is, one's proficiency with a certain set of tools, and personal preference for some tools over others, suggests and sometimes limits, not only how we tackle a given problem, but also _what_ exactly we attempt to solve.
+Hence, let me introduce you to my preferred tools so you may get a feeling for my view on software.
+
+_Disclaimer:_
+Follow the hyperlinks at your own risk.
+You might end up in a rabbit hole.
+
+I prefer to use and _compose_ small, sharp tools as coined by Eric Steve Raymond in his book [The Art of Unix Programming](http://www.catb.org/esr/writings/taoup/).
+Think [sed](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sed.html), [rsync](https://rsync.samba.org/), [cURL](https://curl.haxx.se/), and [friends](https://en.wikipedia.org/w/index.php?title=List_of_Unix_commands&oldid=892119460).
+As such, I feel most comfortable on the command-line of Unix-like, do-it-yourself operating systems like [Arch Linux](https://www.archlinux.org/), [FreeBSD](https://www.freebsd.org/), and [OpenBSD](https://www.openbsd.org/).
+Most of the time, you'll see my terminal [Solarized](https://ethanschoonover.com/solarized/) and tiled by [tmux](https://github.com/tmux/tmux).
+
+When it comes to editing source code and markup, I rely on _the ubiquitous text editor_ [Vim](https://www.vim.org/) with a few, handpicked plug-ins like [Fugitive](https://github.com/tpope/vim-fugitive).
+Needless to say, I prefer the keyboard over a mouse or touch interface --- a split keyboard with a Dvorak layout and a compose key to draw German umlauts.
+
+I love to search my files at the speed of light using [fzf](https://github.com/junegunn/fzf) and [ripgrep](https://github.com/BurntSushi/ripgrep), both inside and outside of Vim.
+I enjoy to version control my files --- not just code but also configuration, notes, and this very blog --- with [Git](https://git-scm.com/).
+As a matter of fact, even my favorite password manager, [pass](https://www.passwordstore.org/), uses Git under the hood to track changes.
+
+Writing of source code, I favor two, arguably polar opposite programming languages.
+On the one hand, I love [Go](https://golang.org/) for its straightforwardness, exhaustive standard library and excellent tooling.
+On the other hand, I am deeply attracted by the expressiveness and safety of [Haskell](https://www.haskell.org/).
+Considering documentation, I generally forechoose [Asciidoctor](https://asciidoctor.org/)'s extension of [Asciidoc](http://asciidoc.org/) over anybody's flavor of [Markdown](https://daringfireball.net/projects/markdown/).
+Finally, I prefer to store and evaluate large amounts of structured information using [SQLite](https://sqlite.org/index.html) and [PostgreSQL](https://www.postgresql.org/).
+
+When you read this far without loosing yourself in the numerous hyperlinks above, you deserve yourself a cookie.
+Otherwise, remember that a tool without a purpose is a toy --- and that's fine.
diff --git a/_drafts/ssh-proxy.md b/_drafts/ssh-proxy.md
new file mode 100644
index 0000000..26a68c0
--- /dev/null
+++ b/_drafts/ssh-proxy.md
@@ -0,0 +1,296 @@
+---
+title: "SSH Proxy"
+description: "How to establish a secure tunnel between two firewalled machines."
+published: 2020-06-22
+---
+
+Consumer grade internet connections typically prevent inbound connections by means of network address translation (NAT) or firewalls.
+While this protects consumers to some degree from the evil outside, it also hinders them from providing network services.
+Luckily, people came up with several [NAT traversal techniques](https://en.wikipedia.org/w/index.php?title=NAT_traversal&oldid=950406393#Techniques) including the widespread [SOCKS](https://en.wikipedia.org/w/index.php?title=SOCKS&oldid=963014782) protocol and the martial [UDP hole punching](https://en.wikipedia.org/w/index.php?title=UDP_hole_punching&oldid=957144154).
+However, I found that [OpenSSH](https://www.openssh.com/)'s little-known remote port forwarding feature enables a simple and yet secure alternative that I would like to share in this post.
+
+We'll use the [OpenSSH](https://www.openssh.com/) client and server here because its a free, battle-tested, and portable implementation of the SSH protocol.
+Chances are, that your operating system ships with OpenSSH built-in or packaged.
+In fact, even [macOS](https://support.apple.com/guide/remote-desktop/about-systemsetup-apd95406b8d/mac) and [Windows 10](https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_overview) include OpenSSH, activatable from the command-line.
+
+Contents:
+
+* TCP port forwarding. Anyone on the proxy may access the exposed service.
+* End-to-end encrypted tunnel. Servers needs to run local SSH server.
+* Unix domain socket forwarding. Protected socket instead of TCP port.
+
+## TL;DR
+
+Basically, you can ...
+
+You've got three different options to share a local network service by means of a publically reachable OpenSSH proxy:
+
+1. Create a remote port forwarding from the proxy to the server and
+
+Authentication and authorization aside, you can share a local network service in three steps:
+
+1. Create a remote port forwarding from a publically reachable proxy to the firewalled server:
+
+ $ ssh -nNTR 8080:localhost:80 proxy.example.com
+
+1. Create a local port forwarding from the client to the proxy:
+
+ $ ssh -nNTL 8080:localhost:8080 proxy.example.com
+
+1. Access the exposed network service from the client:
+
+ $ curl http://localhost:8080/
+
+So far so good, but the real work is of course to setup the public key authentication and to restrict the keys' permissions.
+Thus, the remainder of this blog post describes a typical use case in detail.
+
+## Screen sharing example
+
+Let's say, Alex wants to share his screen with Tyler.
+Sure enough, Alex can run a local [RFB](https://www.iana.org/assignments/rfb/rfb.xml) server such as [TigerVNC](https://tigervnc.org/) or [Apple Remote Desktop](https://support.apple.com/remote-desktop) on port 5900.
+But thanks to their internet provider's NAT, neither of the two may connect to the other directly.
+Fortunately, Tyler has access to a publicly reachable OpenSSH server that they can use as a forward proxy as follows.
+
+Note:
+OpenSSH is developed as a part of [OpenBSD](https://www.openbsd.org/).
+Thus, it comes with excellent [man pages](https://www.openssh.com/manual.html).
+So please _read the fine manual_ to understand the various options that we'll use.
+I cannot describe them better.
+
+First, Alex starts an OpenSSH server on his machine.
+By default, the SSH server should listen on port 22 of all local addresses and permit both, password and public key authentication, see [`sshd_config(5)`](https://man.openbsd.org/sshd_config).
+This is sufficient for our setup.
+However, the truly paranoid may safely bind the SSH server to the loopback interface only and forbid password authentication:
+
+ ListenAddress 127.0.0.1
+ ListenAddress ::1
+ Port 22
+ PasswordAuthentication no
+ PubkeyAuthentication yes
+
+Second, Alex prepares a [`known_hosts`](https://man.openbsd.org/sshd#SSH_KNOWN_HOSTS_FILE_FORMAT) file for Tyler:
+
+ $ cat /etc/ssh/ssh_host_*_key.pub \
+ | sed -e 's/^/alex_workstation /' \
+ | tee -a alex_public_host_keys
+ alex_workstation ssh-dss AAAAB3N... root@alex.localdomain
+ alex_workstation ssh-ed25519 AAAAC3N... root@alex.localdomain
+ alex_workstation ssh-rsa AAAAB3N... root@alex.localdomain
+
+Third, Alex generates an SSH key to authenticate at the proxy.
+We'll generate a dedicated key here, so you can follow this guide without using your normal SSH key:
+
+ $ ssh-keygen -f ~/.ssh/id_proxy
+
+Fourth, Tyler imports Alex' public host keys on his machine:
+
+ $ cat alex_public_host_keys >> ~/.ssh/known_hosts
+
+Fifth, Tyler authorizes Alex to connect to the proxy.
+That is, Tyler adds Alex' public user key to his personal [`authorized_keys`](https://man.openbsd.org/sshd#AUTHORIZED_KEYS_FILE_FORMAT) file on the proxy, applying the following restrictions to prevent Alex from misusing Tyler's account:
+
+ restrict,command="echo 'ssh command restricted by authorized_keys'",port-forwarding,permitlisten="2222" ssh-rsa AAAAB3N...
+
+The truly paranoid may wish to create a dedicated [`nologin(8)`](https://man.openbsd.org/nologin) user account on the proxy instead and apply the above restrictions by means of the [`sshd_config(5)`](https://man.openbsd.org/sshd_config):
+
+ Match User port-forward-only
+ DisableForwarding yes
+ ForceCommand echo 'ssh command forced by sshd_config'
+ PermitTTY no
+ AllowTcpForwarding remote
+ PermitListen 2222
+
+Sixth, Tyler prepares a [`known_hosts`](https://man.openbsd.org/sshd#SSH_KNOWN_HOSTS_FILE_FORMAT) file for Alex:
+
+ $ cat /etc/ssh/ssh_host_*_key.pub \
+ | sed -e 's/^/[proxy.example.com]:22 /' \
+ | tee -a proxy_public_host_keys
+ [proxy.example.com]:22 ssh-dss AAAAB3N... root@proxy.example.com
+ [proxy.example.com]:22 ssh-ed25519 AAAAC3N... root@proxy.example.com
+ [proxy.example.com]:22 ssh-rsa AAAAB3N... root@proxy.example.com
+
+Seventh, Alex imports the public host keys of the proxy on his machine:
+
+ $ cat proxy_public_host_keys >> ~/.ssh/known_hosts
+
+Eighth, Alex authorizes Tyler to connect to his machine.
+That is, Alex adds Tyler's public user key to his personal [`authorized_keys`](https://man.openbsd.org/sshd#AUTHORIZED_KEYS_FILE_FORMAT) file on his machine, applying the following restrictions to prevent Tyler from misusing Alex' account:
+
+ restrict,command="echo 'ssh command restricted by authorized_keys'",port-forwarding,permitopen="5900" ssh-rsa AAAAB3N...
+
+Again, the truly paranoid may wish to create a dedicated user account as described above.
+
+Ninth, Alex connects to the proxy and forwards port 2222 to port 22 on his machine:
+
+ $ ssh \
+ -nNT \
+ -R 2222:localhost:22 \
+ -i ~/.ssh/id_proxy \
+ -o "IdentitiesOnly yes" \
+ -o "StrictHostKeyChecking yes" \
+ -o "ExitOnForwardFailure yes" \
+ -l tyler \
+ proxy.example.com
+
+Alternatively, Alex may add the following settings to his personal [`ssh_config(5)`](https://man.openbsd.org/ssh_config) and run `ssh -nN proxy`:
+
+ Host proxy
+ Hostname proxy.example.com
+ Port 22
+ User alex
+ StrictHostKeyChecking yes
+ IdentitiesFile ~/.ssh/id_proxy
+ IdentitiesOnly yes
+ RemoteForward 2222 localhost:22
+ ExitOnForwardFailure yes
+ RequestTTY no
+
+ Host *
+ Protocol 2
+ AddKeysToAgent yes
+ IgnoreUnknown UseKeychain
+ UseKeychain yes
+
+
+Tenth, Tyler connects to Alex' machine and forwards port 5900 from his machine to Alex' machine:
+
+ $ ssh \
+ -J tyler@proxy.example.com \
+ -L 5900:localhost:5900 \
+ -o "ExitOnForwardFailure yes" \
+ -o "StrictHostKeyChecking yes" \
+ -l alex \
+ -p 2222 \
+ -o "HostKeyAlias alex_workstation" \
+ localhost
+
+Alternatively, Tyler may add the following settings to his personal [`ssh_config(5)`](https://man.openbsd.org/ssh_config) and run `ssh alex_workstation`:
+
+ Host alex_workstation
+ ProxyJump tyler@proxy.example.com
+ User alex
+ Hostname localhost
+ Port 2222
+ HostKeyAlias alex_workstation
+ StrictHostKeyChecking yes
+ ExitOnForwardFailure yes
+ LocalForward 5900 localhost:5900
+
+ Host *
+ AddKeysToAgent yes
+ IgnoreUnknown UseKeychain
+ UseKeychain yes
+
+Finally, Tyler accesses the tunneled RFD server from his machine.
+For example, using TigerVNC's [`vncviewer(1)`](https://tigervnc.org/doc/vncviewer.html)[^vncviewer]
+
+ $ vncviewer localhost
+
+[^vncviewer]:
+Apparently, the [TigerVNC](https://tigervnc.org/)'s client, [`vncviewer(1)`](https://tigervnc.org/doc/vncviewer.html), may not connect to an [Apple Remote Desktop](https://support.apple.com/remote-desktop) agent, even if you enable the legacy VNC option of the latter.
+However, you can use [FreeRDP](https://www.freerdp.com/) instead, or one of its graphical front-ends like [Gnome Boxes](https://wiki.gnome.org/Apps/Boxes).
+
+TODO: permitlisten="none",permitopen="none"
+
+## Conclusion
+
+We've established a secure, end-to-end encrypted tunnel between two otherwise disconnected machines through an OpenSSH forward proxy.
+Moreover, the two parties need minimal trust in each other because we've restricted the keys' permissions.
+
+OpenSSH enables even more fine grained control if need be.
+For example:
+
+* Apply an `expiry-time` to your `authorized_keys` to grant temporary access.
+
+* Add a trusted `cert-authority` to your `authorized_keys` instead of individual keys.
+
+* Enable `VerifyHostKeyDNS` to automatically trust host keys with a corresponding SSHFP resource record in the DNS.
+
+* Use [Unix domain sockets](https://en.wikipedia.org/wiki/Unix_domain_socket) instead of network ports to further restrict access to the tunnel from the proxy to the actual server.[^socket]
+
+[^socket]:
+Simply replace the port number with an absolute path like `/home/tyler/proxy.sock`.
+Do not use the `~` to represent the user's home directory.
+
+# Rewrite
+
+## Goal
+
+Forward a (protected) Unix domain socket on the proxy to a local port.
+Such that nobody else on the proxy may use the forwarding, as with a forwarded TCP port.
+
+Serve the current directory at http://127.0.0.1:8080/:
+
+ python3 -m http.server --bind 127.0.0.1 8080
+
+Using explicit loopback address 127.0.0.1 (or ::1) instead of localhost lest socat or ssh shoud bind to a non-loopback address.
+
+Want either end-to-end encryption (using SSH server on the server) or protected address (Unix domain socket instead of TCP port) on the proxy.
+
+## OpenSSH RemoteForward
+
+Works as advertised.
+Missing `PermitListen` (and `permitlisten`) equivalent to restrict the name of the socket.
+
+## OpenBSD netcat
+
+Idea: manually bind to remote socket using netcat.
+Problem: Doesn't even work locally. Neither on Arch Linux nor on OpenBSD.
+
+Forward Unix domain socket test.sock to TCP port 8080 using OpenBSD's [`nc`](https://man.openbsd.org/nc):
+
+ #! /bin/sh
+ rm -f backpipe
+ mkfifo backpipe
+ nc -lkU test.sock 0<backpipe \
+ | nc 127.0.0.1 8080 1>backpipe
+
+Retrieve the home page http://127.0.0.1:8080/ via the socket:
+
+ printf "GET / HTTP/1.0\r\n\r\n" \
+ | nc -UN test.sock
+
+Unfortunately, this doesn't work reliably.
+The retrieval command prints the response at most once, ofter on the second invocation and NOT on the first invocation.
+
+I found neither the cause of this behavior nor a workaround.
+I suspect, the problem is a combination of the following:
+
+* Netcat closes connection to early because of an early EOF
+
+* Shell pipe errors
+
+* Shell buffers standard input/output
+
+Moreover, each Linux appears to implement a slightly different version of OpenBSD's netcat.
+
+## socat
+
+Forward remote socket test.sock to local port 8080:
+
+ socat EXEC:'ssh -T engine.skreutz.com socat "UNIX-LISTEN:test.sock,fork,unlink-early" STDIO' TCP4:127.0.0.1:8080,fork
+
+TODO: Test local nc and remote socat, because it might be easier to find a nc implementation for Windows than socat.
+
+Forward local port 8081 to remote socket test.sock:
+
+ ssh -vnNT -L 127.0.0.1:8081:/home/stefan/test.sock -o "ExitOnForwardFailure yes" engine.skreutz.com
+
+Note: Specify the local bind address 127.0.0.1 and option `ExitOnForwardFailure` to make `ssh` fail if `GatewayPorts` is set to `yes`.
+
+Noto: Specify the absolute path of the socket. Do not rely on the ~.
+
+Retrieve home page:
+
+ printf "GET / HTTP/1.0\r\n\r\n" \
+ | nc -N 127.0.0.1 8081
+
+Alternatively:
+
+ curl http://127.0.0.1:8081/
+
+TODO: Restrict authorized_keys
+TODO: Inspect ports with netstat.
+TODO: permitlisten="none",permitopen="none".
+TODO: Request OpenSSH feature PermitStreamLocalListen/Open analoguous to PermitListen/Open.
+TODO: Test tcpserver instead of socat: https://cr.yp.to/ucspi-tcp.html
diff --git a/_drafts/temporary-postgresql-server.md b/_drafts/temporary-postgresql-server.md
new file mode 100644
index 0000000..a46cd86
--- /dev/null
+++ b/_drafts/temporary-postgresql-server.md
@@ -0,0 +1,100 @@
+---
+title: "Temporary PostgreSQL server"
+description: "A simple shell script to run the PostgreSQL server off a temporary directory."
+published: 2020-06-02
+---
+
+Sometimes I need to spin up a local PostgreSQL server for one-off purposes.
+In these cases, I don't particularly like to either configure PostgreSQL manually or use a pre-configured Docker image because I experience this as overkill and inaccessible respectively.
+Instead, I use the simple shell script below to run the PostgreSQL server off a temporary directory until I decide to `kill` it.
+The script is inspired by a [blog post](https://www.johbo.com/2017/on-demand-postgresql-for-your-development-environment.html) by Johannes Bornhold that reminded me of Unix' simplicity.
+
+The script essentially performs seven steps:
+
+* Create a temporary directory using `mktemp`
+* Initialize the directory using `initdb`
+* Serve the directory using `postgres`
+* Ensure the server is up using `pg_isready`
+* Create a database using `createdb`
+* Wait for a `SIGINT`
+* Remove the temporary directory
+
+Obviously, you still need to install PostgreSQL to use the script.
+However, you may use the [Nix package manager](https://nixos.org/nix/) to install PostgreSQL _on-the-fly_ and have it removed too, if you are into this.
+Simply put the following shebang in front of the script.
+
+```sh
+#! /usr/bin/env nix-shell
+#! nix-shell --pure --packages postgresql -i bash
+```
+
+Here is the full script with minimal error handling.
+
+```sh
+#! /bin/sh
+
+# temp_postgres runs a PostgreSQL server with a temporary data directory until
+# it receives a SIGINT.
+
+set -o nounset
+
+# Remove the temporary directory before exiting
+trap 'quit' INT
+quit() {
+ code="${1:-0}"
+ trap '' INT TERM
+ kill -TERM 0
+ wait
+ rm -rf "${tmpdir-}" || {
+ >&2 printf "temp_postgres: failed to remove temporary directory \"%s\"\\n" "${tmpdir}"
+ [ "${code}" -ne 0 ] || code=1
+ }
+ exit "${code}"
+}
+
+# Parse arguments
+[ $# -eq 2 ] || {
+ >&2 printf "temp_postgres: invalid arguments\\n"
+ printf "Usage: temp_postgres <dbname> <username>\\n"
+ quit 1
+}
+dbname="$1"
+username="$2"
+
+# Create a temporary directory
+tmpdir="$( mktemp --directory )" || {
+ >&2 printf "temp_postgres: failed to create temporary directory\\n"
+ quit 1
+}
+
+# Initialize the directory
+initdb --pgdata="${tmpdir}" --username="${username}" || {
+ >&2 printf "temp_postgres: failed to initialize database\\n"
+ quit 1
+}
+
+# Serve the directory
+( postgres -k "${tmpdir}" -D "${tmpdir}" </dev/null ) &
+
+# Test the connection
+sleep 1
+pg_isready --host="${tmpdir}" --dbname="postgres" --username="${username}" --timeout=10 || {
+ >&2 printf "temp_postgres: failed to connect to server\\n"
+ quit 1
+}
+
+# Create a database
+createdb --host="${tmpdir}" --username="${username}" --no-password "${dbname}" || {
+ >&2 printf "temp_postgres: failed to create database\\n"
+ quit 1
+}
+
+printf '
+Connect with the following command:
+
+\tpsql --host=localhost "%s" "%s"
+
+' "${dbname}" "${username}"
+
+wait
+```
diff --git a/about.md b/about.md
new file mode 100644
index 0000000..fbda3ad
--- /dev/null
+++ b/about.md
@@ -0,0 +1,16 @@
+---
+title: About
+---
+
+My name is Stefan Kreutz.
+Among other things, I am a passionate software engineer living in Bad Honnef, Germany.
+On my day job, I maintain a well-known self-service logistics solution for a small company near Bonn.
+Besides, I hack on various side projects.
+
+## Credits
+
+* Written with [Vim](https://www.vim.org/).
+
+* Built with [Hakyll](https://jaspervdj.be/hakyll/) and [Pandoc](https://pandoc.org/).
+
+* Hosted with [OpenBSD](https://www.openbsd.org/) from a personal, economical server.
diff --git a/contact.md b/contact.md
new file mode 100644
index 0000000..27ff67f
--- /dev/null
+++ b/contact.md
@@ -0,0 +1,20 @@
+---
+title: Contact
+---
+
+You may contact me at the following address:
+
+<address>
+ Stefan Kreutz<br>
+ Frankenweg 2<br>
+ 53604 Bad Honnef<br>
+ Germany<br>
+ Phone: Germany 157 Five Seven One Seven 0987<br>
+ Email: mail (at) skreutz (dot) com
+</address>
+
+I prefer emails to letters and phone calls.
+No SMS and no advertising please.
+
+Feel free to encrypt your message with [my public PGP key](/files/pgp.asc).
+Remember to include your own public key in this case.
diff --git a/css/normalize.css b/css/normalize.css
new file mode 100644
index 0000000..192eb9c
--- /dev/null
+++ b/css/normalize.css
@@ -0,0 +1,349 @@
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
+
+/* Document
+ ========================================================================== */
+
+/**
+ * 1. Correct the line height in all browsers.
+ * 2. Prevent adjustments of font size after orientation changes in iOS.
+ */
+
+html {
+ line-height: 1.15; /* 1 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/* Sections
+ ========================================================================== */
+
+/**
+ * Remove the margin in all browsers.
+ */
+
+body {
+ margin: 0;
+}
+
+/**
+ * Render the `main` element consistently in IE.
+ */
+
+main {
+ display: block;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+hr {
+ box-sizing: content-box; /* 1 */
+ height: 0; /* 1 */
+ overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Remove the gray background on active links in IE 10.
+ */
+
+a {
+ background-color: transparent;
+}
+
+/**
+ * 1. Remove the bottom border in Chrome 57-
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+ border-bottom: none; /* 1 */
+ text-decoration: underline; /* 2 */
+ text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+ font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove the border on images inside links in IE 10.
+ */
+
+img {
+ border-style: none;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers.
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family: inherit; /* 1 */
+ font-size: 100%; /* 1 */
+ line-height: 1.15; /* 1 */
+ margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input { /* 1 */
+ overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select { /* 1 */
+ text-transform: none;
+}
+
+/**
+ * Correct the inability to style clickable types in iOS and Safari.
+ */
+
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+ -webkit-appearance: button;
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+ border-style: none;
+ padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring {
+ outline: 1px dotted ButtonText;
+}
+
+/**
+ * Correct the padding in Firefox.
+ */
+
+fieldset {
+ padding: 0.35em 0.75em 0.625em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ * `fieldset` elements in all browsers.
+ */
+
+legend {
+ box-sizing: border-box; /* 1 */
+ color: inherit; /* 2 */
+ display: table; /* 1 */
+ max-width: 100%; /* 1 */
+ padding: 0; /* 3 */
+ white-space: normal; /* 1 */
+}
+
+/**
+ * Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+ vertical-align: baseline;
+}
+
+/**
+ * Remove the default vertical scrollbar in IE 10+.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10.
+ * 2. Remove the padding in IE 10.
+ */
+
+[type="checkbox"],
+[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding in Chrome and Safari on macOS.
+ */
+
+[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+ -webkit-appearance: button; /* 1 */
+ font: inherit; /* 2 */
+}
+
+/* Interactive
+ ========================================================================== */
+
+/*
+ * Add the correct display in Edge, IE 10+, and Firefox.
+ */
+
+details {
+ display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+ display: list-item;
+}
+
+/* Misc
+ ========================================================================== */
+
+/**
+ * Add the correct display in IE 10+.
+ */
+
+template {
+ display: none;
+}
+
+/**
+ * Add the correct display in IE 10.
+ */
+
+[hidden] {
+ display: none;
+}
diff --git a/css/site.css b/css/site.css
new file mode 100644
index 0000000..05a3afa
--- /dev/null
+++ b/css/site.css
@@ -0,0 +1,303 @@
+:root {
+ --foreground-color: rgba(0, 0, 0, .8);
+ --background-color: rgb(255, 255, 255);
+ --link-color: rgb(33, 86, 165);
+ --accent-color: rgb(186, 57, 37);
+
+ --light-gray: rgba(0, 0, 0, .03);
+ --dark-gray: rgba(0, 0, 0, .3);
+}
+
+html {
+ height: 100%;
+}
+
+body {
+ min-height: 100%;
+
+ display: grid;
+ grid-template-rows: auto 1fr auto;
+ grid-template-columns: 100%;
+
+ font-family: "Dejavu Serif", serif;
+ line-height: 1.5;
+ text-rendering: optimizeLegibility;
+
+ color: var(--foreground-color);
+ background-color: var(--background-color);
+}
+
+body > header {
+ grid-row: 1 / 2;
+ grid-column: 1 / 2;
+}
+
+body > main {
+ grid-row: 2 / 3;
+ grid-column: 1 / 2;
+}
+
+body > footer {
+ grid-row: 3 / 4;
+ grid-column: 1 / 2;
+}
+
+body > header,
+body > footer {
+ display: inherit;
+ overflow-x: auto;
+
+ font-family: "Dejavu Sans", sans-serif;
+ color: var(--foreground-color);
+ background-color: var(--light-gray);
+}
+
+body > header nav ul,
+body > footer nav ul {
+ margin: 0 auto;
+ padding: 0;
+ width: 100%;
+ max-width: 42em;
+ box-sizing: border-box;
+
+ display: flex;
+ justify-content: flex-end;
+ list-style: none;
+}
+
+body > header nav ul li,
+body > footer nav ul li {
+ margin: 0;
+ padding: 0;
+}
+
+body > header nav a,
+body > footer nav a {
+ padding: 1em;
+
+ display: block;
+
+ color: inherit;
+ text-decoration: none;
+ font-variant-caps: all-small-caps;
+ letter-spacing: 0.1em;
+
+ white-space: nowrap;
+}
+
+body > header nav a:hover,
+body > footer nav a:hover {
+ background: linear-gradient(
+ rgba(0, 0, 0, 0.2),
+ rgba(0, 0, 0, 0.2)
+ ), var(--light-gray);
+}
+
+body > header nav li:first-of-type {
+ margin-right: auto;
+}
+
+body > footer nav {
+ margin: 0 auto;
+}
+
+body > main {
+ margin: 0 auto;
+ padding: 0 1em 0 1em;
+ width: 100%;
+ max-width: 42em;
+ box-sizing: border-box;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: "Dejavu Sans", sans-serif;
+ /* color: var(--accent-color); */
+}
+
+h1 > a,
+h2 > a,
+h3 > a,
+h4 > a,
+h5 > a,
+h6 > a {
+ color: inherit;
+ text-decoration: inherit;
+}
+
+main > nav {
+ text-align: center;
+}
+
+main > nav > a + a {
+ margin-left: 2em;
+}
+
+nav a {
+ text-decoration: none;
+}
+
+article > h1,
+article > header {
+ text-align: center;
+}
+
+article header h1 {
+ margin-bottom: 0;
+}
+
+article header h1 + * {
+ margin-top: 0.5em;
+}
+
+:not(pre) > code {
+ background: var(--light-gray);
+ white-space: nowrap;
+}
+
+pre {
+ background: var(--light-gray) !important;
+ padding: 1em;
+ hyphens: none;
+ overflow-x: auto;
+ width: 100%;
+ box-sizing: border-box;
+ border-left: 2px var(--accent-color) solid;
+}
+
+code {
+ font-family: "DejaVu Sans Mono", monospace;
+ hyphens: none;
+}
+
+pre.numberLines > code {
+ counter-reset: line;
+}
+
+pre.numberLines > code > span {
+ counter-increment: line;
+}
+
+pre.numberLines > code > span::before {
+ display: inline-block;
+ /* FIXME: Align line numbers without fixed width. */
+ width: 2ch;
+ padding-right: 1em;
+
+ content: counter(line);
+ user-select: none;
+ text-align: right;
+}
+
+a {
+ color: var(--link-color);
+ hyphens: none;
+}
+
+p {
+ text-align: justify;
+ hyphens: auto;
+}
+
+table {
+ border-collapse: collapse;
+}
+
+th, td {
+ padding: 0.5em;
+ /* border-bottom: 1px solid var(--foreground-color); */
+}
+
+tr:nth-child(even) {
+ background-color: var(--light-gray);
+}
+
+caption {
+ caption-side: bottom;
+}
+
+li + li {
+ margin-top: 0.5em;
+}
+
+blockquote {
+ border-left: 2px var(--dark-gray) solid;
+ margin: 0 2em 0 2em;
+ padding: 0 0 0 1em;
+}
+
+svg.icon {
+ pointer-events: none;
+ width: 100%;
+ max-width: 1em;
+ height: 100%;
+ max-height: 1em;
+ fill: var(--foreground-color);
+}
+
+.home-page {
+ /* margin: auto; */
+ /* max-width: 32em; */
+ height: 100%;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-content: center;
+}
+
+.home-page > h1,
+.home-page > p {
+ text-align: center;
+}
+
+.blog-index {
+ list-style: none;
+ padding: 0;
+}
+.blog-index > li > a {
+ display: block;
+}
+.blog-index > li > time {
+ font-style: italic;
+ padding-right: 1em;
+}
+.blog-index > li > time::after {
+ content: ".";
+}
+.blog-index > li > p {
+ display: inline;
+}
+.blog-index > li + li {
+ padding-top: 1em;
+}
+@media (min-width: 42em) {
+ .blog-index {
+ display: grid;
+ grid-template-columns: max-content minmax(0, 1fr);
+ grid-gap: 0 1em;
+ }
+ .blog-index > li {
+ display: contents;
+ }
+ .blog-index > li > a {
+ grid-row: span 2;
+ grid-column: 2 / 3;
+ }
+ .blog-index > li > time {
+ grid-column: 1 / 2;
+ text-align: right;
+ font-style: initial;
+ }
+ .blog-index > li > time::after {
+ content: "";
+ }
+ .blog-index > li > p {
+ grid-column: 2 / 3;
+ display: block;
+ }
+}
diff --git a/deploy b/deploy
new file mode 100755
index 0000000..7c51dbe
--- /dev/null
+++ b/deploy
@@ -0,0 +1,41 @@
+#! /bin/sh
+
+# Deploy the static website.
+#
+# Re-generates the static website from source and asserts a clean working tree
+# before uploading the website to the server.
+
+set -o errexit
+set -o nounset
+
+# Git root directory
+root="$( git rev-parse --show-toplevel )"
+
+# Source directory with a trailing slash for rsync
+src="${root}/_site/"
+
+# Git revision
+rev="$( git rev-parse --verify HEAD )"
+
+# ISO 8601 timestamp
+now="$( date -u "+%Y-%m-%dT%H:%M:%SZ" )"
+
+# Archive file name
+archive="./${now}_${rev}.tar.gz"
+
+printf "Re-generating static website from source ...\\n"
+( cd "${root}" \
+ && stack run -- clean >/dev/null 2>&1 \
+ && stack run -- build >/dev/null 2>&1 )
+
+[ -z "$( git status --porcelain )" ] || {
+ ( >&2 printf "error: dirty working tree\\n" )
+ printf "Aborting deployment due to unstaged changes or untracked files.\\n"
+ exit 1
+}
+
+tar -czf "${archive}" "${src}"
+scp "${archive}" engine.skreutz.com:www-archive/
+
+rsync --rsync-path=openrsync --archive --delete --verbose \
+ "${src}" "engine.skreutz.com:/var/www/htdocs/www.skreutz.com"
diff --git a/files/pgp.asc b/files/pgp.asc
new file mode 100644
index 0000000..3e829c6
--- /dev/null
+++ b/files/pgp.asc
@@ -0,0 +1,30 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBF4Pu30BCADFB2CUXWLr7Y9ZLvo/hYm+LQq9osrj05wusnrXQ9egpHgJAd0S
+Z90D5oHiwyDhHEO/TOtiaGv/GKQbLWmvz1LWh0VMGYI1clzn2J0IBYej5GzX77Hk
+AgKu6E/YhhFQoz9cedPbZekdnSxU6RpV+dQNivVJTde5wJSEE1zglfs3GCMaZsKL
+2qIQl9JbXA9j9bQJLaizMMf/GZlv6YNyiNuYh0eOZHwiO3AXvveyrK7mJ62IZKR7
+VlN+6ShYfjQInOU4YwdU7hmg2//k0c9OX8xJoZU2Jmr8FsMRE56Iw5ZOOBqPeTAC
+p5yjZZ6bjzvrm6thowOT+FHEKCZ2SkdIu3qhABEBAAG0IFN0ZWZhbiBLcmV1dHog
+PG1haWxAc2tyZXV0ei5jb20+iQFUBBMBCAA+FiEECRao+HQZX7AimDEWWPRe6BN5
+rMoFAl4Pu30CGwMFCQHhM4AFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQWPRe
+6BN5rMrrlgf7B27FDn0Jbk5+FMpEOEAC+VJLzLRBGyWB62UvQ/YNRkN52RN5/SSr
+R145zuNixU+zlm7zAe85QOpdjR1h+GnRrBFIu9wVGJDKuEq/ZGmqvqvPbbuiL9r2
+qEiykC4v5EldQDg/84lroX83ORAojfu+JfMamZQeTG3O690nPSWx58jzs4TaHF+C
+avoiOjVZFFMYp1jSAYwLwBKV+8WTGXOk6bPWJVAlLwt7cMFpg28EBNp2nqoAcAbZ
+2h+BQow8ab6exWvvWrTzbvPwUTrv+WHhA3tW4AZg8alYlW3vPoLcUcOwVIJ1ghLj
+4bRnGnt8ChLxsJAOjloz/9h1m5aUoH6hNrkBDQReD7t9AQgAuYPtejQNZaMXPt8m
+YIr+F3Pzjnc0Mm0ypZBgRhRvMoD3eAa6zsaRA4BJSZ2E313ZFqaNyMUsD3bDBJXD
+nqNSKxKkqRUL2crQKkHKT7uAoi5QhuxJnqvQWNkzUnUnojh6DM5kw1zixsbqOA7b
+vvprvxM3/PHZw5HPbwavv7xkCTi0djkuSOiKMOzuLmL15b7EgB1Bwfdbsm/2GML3
+drJqO3TQmWH703ibFfyUKfDFfq6lyI6I9eRc6kS9TJfWI+xIbzMVmzgGVU9hO348
+ezV1jZx+8UY4A09+9Tpduyt2YC2CNkh4U/Ot1ksoBdD1pkFKAl8qbdArBn6QFSN0
+b3OIYwARAQABiQE8BBgBCAAmFiEECRao+HQZX7AimDEWWPRe6BN5rMoFAl4Pu30C
+GwwFCQHhM4AACgkQWPRe6BN5rMpsLwgAv2rNM1Zc5ibsP9jF1tjfVrPqnUc7j3sp
+QftfVduaokKU6qN9NpAI0pdelqBeDMDAYlgTt/XmVY/t2OjsO/LySUCWUame+Yi+
+LoZBYbAHgPDR1/S1Il2Od6bHTg3PvFhynDXU8g/vyZFJ7aFvsg8oUiUTZya+Xk44
+tkpSQKAnne/AoWpWXJHKrDOdVnz45NZ/TGay7cthS+noNbH9PVN8mQ7TC24r+o++
+lmIQAPKDc3jX2gYYPONPDzIzUTuHwsjtZepPXGwNyzny9VkCUMR4DnIczcD9sQ2U
+AJJZT7bKUhwI7LzU8BqjZmvWMBrd+KlYtHvILVUA22/sGKNNbR4VYw==
+=8v/R
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..5391cb1
--- /dev/null
+++ b/index.html
@@ -0,0 +1,8 @@
+---
+title: Home
+---
+
+<div class="home-page">
+ <h1>Welcome!</h1>
+ <p>My name is Stefan Kreutz. This is my personal website.</p>
+</div>
diff --git a/posts/unix-domain-socket-forwarding-with-openssh.md b/posts/unix-domain-socket-forwarding-with-openssh.md
new file mode 100644
index 0000000..295b0c2
--- /dev/null
+++ b/posts/unix-domain-socket-forwarding-with-openssh.md
@@ -0,0 +1,173 @@
+---
+title: Unix Domain Socket Forwarding with OpenSSH
+description: How to control access to OpenSSH forwardings using Unix domain sockets instead of TCP ports.
+published: 2020-07-06
+---
+
+[OpenSSH](https://www.openssh.com/) is well-known for its ability to forward TCP ports from a local host to a remote host and vice versa.
+Typical use cases include:
+
+* Access an otherwise unreachable server via a bastion host.
+
+* Access the loopback interface of a remote host.
+
+* Expose a local network service to a remote host.
+
+Recently, I had an interesting variant of the third use case:
+I wanted to expose a local network service --- in this case, an inherently insecure [RFB](https://www.iana.org/assignments/rfb/rfb.xml) server --- via a remote host on condition that only a select user could connect to the exposed service.
+That is, I wanted to control access to the remote forwarding.
+It turned out that Unix domain socket forwardings are better suited than TCP port forwardings in this case.
+
+In the remainder of this post, I explain why it is difficult to control access to a TCP port forwarding, and how you can control access using a Unix domain socket forwarding instead.
+Moreover, I describe how to restrict forwardings to select TCP ports respectively socket files.
+
+## TCP port forwarding
+
+The following command forwards port 8080 on a remote host to port 80 on the local host:[^localhost]
+
+ $ ssh -nNT \
+ -R 8080:localhost:80 \
+ -o "ExitOnForwardFailure yes" \
+ remote_host
+
+Of course, you can restrict port forwardings by means of the [`sshd_config(5)`](https://man.openbsd.org/OpenBSD-6.6/sshd_config) or an [`authorized_keys`](https://man.openbsd.org/OpenBSD-6.6/sshd#AUTHORIZED_KEYS_FILE_FORMAT) file.[^manuals]
+For example, the following settings restrict the user Alex to listen to --- and thus forward --- remote port 8080 only:
+
+ Match User alex
+ AllowTcpForwarding remote
+ PermitOpen none
+ PermitListen 8080
+ PermitTTY no
+ ForceCommand /bin/echo 'ssh command forced by sshd_config(5)'
+
+However, as the [man page clearly states](https://man.openbsd.org/OpenBSD-6.6/sshd_config#AllowTcpForwarding), users with access to a shell can generally bypass this restriction:
+
+> **AllowTcpForwarding**
+>
+> Specifies whether TCP forwarding is permitted.
+> The available options are **yes** (the default) or **all** to allow TCP forwarding, **no** to prevent all TCP forwarding, **local** to allow local (from the perspective of ssh(1)) forwarding only or **remote** to allow remote forwarding only.
+> Note that disabling TCP forwarding does not improve security unless users are also denied shell access, as they can always install their own forwarders.
+
+For example, the following command uses the infamous [netcat](https://nc110.sourceforge.io/) utility --- in this case OpenBSD's widespread reimplementation, [`nc(1)`](https://man.openbsd.org/OpenBSD-6.6/nc) --- to query the exposed HTTP service at port 8080:
+
+ $ printf "GET / HTTP/1.0\r\n\r\n" \
+ | ssh -T remote_host nc localhost 8080
+
+As far as I know, you have two practical options to control access to forwardings:
+
+1. Add user-specific rules to your firewall of choice, if supported.
+For example, the [owner module](http://ipset.netfilter.org/iptables-extensions.man.html#lbBP) of [`iptables(8)`](http://ipset.netfilter.org/iptables.man.html) enables you to match the user ID and the group ID of a local packet creator.
+
+1. Use [Unix domain sockets](https://en.wikipedia.org/w/index.php?title=Unix_domain_socket&oldid=949050080) instead of TCP ports, and protect the special socket files just like regular files --- i.e., set the file owner, group, and mode using [`chown(8)`](https://man.openbsd.org/OpenBSD-6.6/chown) and [`chmod(1)`](https://man.openbsd.org/OpenBSD-6.6/chmod).
+
+## Unix domain socket forwarding
+
+OpenSSH supports Unix domain socket forwarding out of the box.[^windows]
+Simply specify a file name instead of a TCP port.
+For example, the following command creates, binds to, and forwards a socket on a remote host to port 80 on the local host:
+
+ $ ssh -nNT \
+ -R /var/run/http.sock:localhost:80 \
+ -o "ExitOnForwardFailure yes" \
+ remote_host
+
+With this, you can query the exposed HTTP service again using [`nc(1)`](https://man.openbsd.org/OpenBSD-6.6/nc):
+
+ $ printf "GET / HTTP/1.0\r\n\r\n" \
+ | ssh -T remote_host nc -U /var/run/http.sock
+
+There are two caveats, though:
+
+1. The file name of the socket must contain a forward slash. Otherwise, [`ssh(1)`](https://man.openbsd.org/OpenBSD-6.6/ssh) misinterprets the socket name as a TCP port.
+
+1. The file name of the socket should not rely on [tilde expansion](https://man.openbsd.org/OpenBSD-6.6/ksh#Tilde_expansion).
+That is, use `/home/alex/http.sock` instead of `~/http.sock` or `~alex/http.sock`.
+
+1. You must not re-bind an existing socket by default.[^unlink] Set `StreamLocalBindUnlink yes` in the [`sshd_config(5)`](https://man.openbsd.org/OpenBSD-6.6/sshd_config) to allow this.
+
+1. There are no [`sshd_config(5)`](https://man.openbsd.org/OpenBSD-6.6/sshd_config) options equivalent to `PermitOpen` and `PermitListen` for Unix domain sockets.
+That is, you cannot restrict the file name of the socket.
+
+However, if you really want to restrict the socket's file name, then we can build upon the netcat trick from above.
+
+## Restricting the socket's file name
+
+This time we'll use [`socat(1)`](http://www.dest-unreach.org/socat/doc/socat.html), another successor of netcat.[^backpipe]
+
+The following command effectively establishes the same forwarding as the previous one: it creates, binds to, and forwards a socket on a remote host to port 80 on the local host.
+
+ $ socat \
+ EXEC:'ssh -T remote_host socat "UNIX-LISTEN:/var/run/http.sock,fork,unlink-early" STDIO' \
+ TCP4:127.0.0.1:80,fork
+
+Essentially, the command builds the following bidirectional stream:
+
+1. [`socat(1)`](http://www.dest-unreach.org/socat/doc/socat.html) pipes the remote socket to standard input/output on the remote host.
+
+1. [`ssh(1)`](https://man.openbsd.org/OpenBSD-6.6/ssh) pipes this input/output from the remote host to the local host.
+
+1. [`socat(1)`](http://www.dest-unreach.org/socat/doc/socat.html) pipes the input/output from [`ssh(1)`](https://man.openbsd.org/OpenBSD-6.6/ssh) to port 80 on the local host.
+
+With this, we can restrict the socket's name by means of the [`sshd_config`](https://man.openbsd.org/OpenBSD-6.6/sshd_config) or an [`authorized_keys`](https://man.openbsd.org/OpenBSD-6.6/sshd#AUTHORIZED_KEYS_FILE_FORMAT) file as follows:
+
+ Match User alex
+ DisableForwarding yes
+ PermitTTY no
+ ForceCommand /usr/local/bin/socat UNIX-LISTEN:/var/run/http.sock,fork,unlink-early STDIO
+
+With these settings, the previous commands comes down to the following, where `forced-command` is an optional no-op reminder:
+
+ $ socat \
+ EXEC:'ssh -T remote_host forced-command' \
+ TCP4:127.0.0.1:80,fork
+
+Finally, a client may connect to this socket as follows --- regardless of how we created the socket:
+
+ $ ssh -nNT \
+ -L 3000:/var/run/http.sock \
+ -o "ExitOnForwardFailure yes" \
+ remote_host
+ $ curl http://localhost:3000/
+
+Unfortunately, [`ssh(1)`](https://man.openbsd.org/OpenBSD-6.6/ssh)'s `ExitOnForwardFailure` option does not catch missing permissions to access the socket file.
+Thus, if the final [`curl(1)`](https://curl.haxx.se/docs/manpage.html) command fails and you cannot actually use the forwarding, please check the group and the mode of the socket file created by [`socat(1)`](http://www.dest-unreach.org/socat/doc/socat.html) on the remote host.
+You can set the group and mode using the corresponding `UNIX-LISTEN` options.
+
+## Conclusion
+
+OpenSSH is able to forward TCP ports and Unix domain sockets.
+The server can be configured to restrict who may open respectively listen to which port.
+However, users with access to a shell can generally bypass this restriction.
+In particular, any user with access to a shell can open any forwarded port.
+Thus, you might want to use Unix domain sockets instead of TCP ports for remote forwardings.
+This way, you can control access to the special socket file, and thus to the forwarded network service.
+You can restrict the socket's file name by forcing a special command instead of using the built-in forwarding.
+
+[^localhost]:
+ I use the descriptive `localhost` hostname instead of actual addresses on the loopback interface such as `127.0.0.1` or `::1` throughout this blog post.
+ However, the truly paranoid hacker might want to use the latter to save the hostname resolution.
+
+[^manuals]:
+ OpenSSH is developed as a part of [OpenBSD](https://www.openbsd.org/).
+ That's why I'm referring to OpenBSD's man pages here.
+ Besides, I'm referring to the man pages at the time of writing.
+
+[^windows]:
+ Notably, the OpenSSH version shipped with Windows 10 does not support Unix domain socket forwarding, see [here](https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_server_configuration).
+
+[^unlink]:
+ There is no system call to re-bind an existing Unix domain socket.
+ Instead, you have to [`unlink(2)`](https://man.openbsd.org/OpenBSD-6.6/unlink) --- and thereby remove --- the socket file before you can [`bind(2)`](https://man.openbsd.org/OpenBSD-6.6/bind) and [`listen(2)`](https://man.openbsd.org/OpenBSD-6.6/listen) again.
+
+[^backpipe]:
+ In my opinion, the following two commands should be equivalent:
+
+ $ socat UNIX-LISTEN:foo.sock,fork TCP4:127.0.0.1:8080,fork
+
+ $ mkfifo backpipe
+ $ nc -lkU foo.sock 0<backpipe \
+ | nc 127.0.0.1 8080 1>backpipe
+
+ However, in practice, the second command did not work reliably on [OpenBSD 6.6](https://www.openbsd.org/66.html) and [Arch Linux](https://www.archlinux.org/) in June 2020.
+ I suspect, it's got something to do with an early EOF or standard output buffering.
+ Please drop me a mail if you can help me out.
diff --git a/privacy.md b/privacy.md
new file mode 100644
index 0000000..71f9c94
--- /dev/null
+++ b/privacy.md
@@ -0,0 +1,7 @@
+---
+title: Privacy
+---
+
+This website does not collect any personal data and proudly eschews third-party content including advertisements and like buttons.
+
+Feel free to [contact me](/contact/) if you have any questions on this.
diff --git a/robots.txt b/robots.txt
new file mode 100644
index 0000000..a8ad87f
--- /dev/null
+++ b/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Disallow: /contact/
+Allow: /
+Sitemap: https://www.skreutz.com/sitemap.xml
diff --git a/site.hs b/site.hs
new file mode 100644
index 0000000..fcf617d
--- /dev/null
+++ b/site.hs
@@ -0,0 +1,208 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+import Control.Monad (msum)
+import Data.Time (TimeLocale, formatTime, parseTimeM, defaultTimeLocale, UTCTime)
+import Hakyll
+import System.FilePath.Posix ((</>), (<.>), splitExtension, splitFileName, takeDirectory)
+
+main :: IO ()
+main = hakyllWith hakyllConfig $ do
+ match ("images/*" .||. "files/*" .||. "robots.txt") $ do
+ route idRoute
+ compile copyFileCompiler
+
+ match "css/*" $ do
+ route idRoute
+ -- WORKAROUND: compressCssCompiler removes copyright notices
+ compile copyFileCompiler
+
+ match (fromList ["about.md", "contact.md", "privacy.md"]) $ do
+ route $ setExtension "html" `composeRoutes` appendIndex
+ let context = dropIndexHtml "url" <> defaultContext
+ compile $ pandocCompiler
+ >>= loadAndApplyTemplate "templates/direct.html" context
+ >>= loadAndApplyTemplate "templates/default.html" context
+ >>= relativizeUrls
+
+ match "posts/*" $ do
+ route $ setExtension "html" `composeRoutes` appendIndex
+ compile $ pandocCompiler
+ >>= loadAndApplyTemplate "templates/post.html" postContext
+ >>= saveSnapshot "content"
+ >>= loadAndApplyTemplate "templates/default.html" postContext
+ >>= relativizeUrls
+
+ create ["posts.html"] $ do
+ route appendIndex
+ compile $ do
+ posts <- recentFirst =<< loadAll "posts/*"
+ let archiveContext =
+ listField "posts" postContext (return posts) <>
+ constField "title" "Blog" <>
+ dropIndexHtml "url" <>
+ defaultContext
+
+ makeItem ""
+ >>= loadAndApplyTemplate "templates/posts.html" archiveContext
+ >>= loadAndApplyTemplate "templates/default.html" archiveContext
+ >>= relativizeUrls
+
+ create ["feeds/posts.rss"] $ do
+ route idRoute
+ compileFeed renderRss rfc822DateTimeFormat
+
+ create ["feeds/posts.atom"] $ do
+ route idRoute
+ compileFeed renderAtom rfc3339DateTimeFormat
+
+ create ["sitemap.xml"] $ do
+ route idRoute
+ compile $ do
+ posts <- recentFirst =<< loadAll "posts/*"
+ singles <- loadAll (fromList ["about.md", "contact.md", "privacy.md", "posts.html"])
+ let
+ pages = posts <> singles
+ sitemapContext =
+ constField "root" root <>
+ listField "pages" postContext (return pages)
+ makeItem ""
+ >>= loadAndApplyTemplate "templates/sitemap.xml" sitemapContext
+
+ match "index.html" $ do
+ route idRoute
+ compile $ do
+ posts <- recentFirst =<< loadAll "posts/*"
+ let indexContext =
+ listField "posts" postContext (return posts) <>
+ defaultContext
+
+ getResourceBody
+ >>= applyAsTemplate indexContext
+ >>= loadAndApplyTemplate "templates/default.html" indexContext
+ -- >>= relativizeUrls
+
+ match "404.html" $ do
+ route idRoute
+ compile $ pandocCompiler
+ >>= loadAndApplyTemplate "templates/default.html" defaultContext
+ >>= relativizeUrls
+
+ match "templates/*" $ compile templateBodyCompiler
+
+hakyllConfig :: Configuration
+hakyllConfig = defaultConfiguration
+ { previewPort = 8080
+ , deployCommand = "./deploy"
+ }
+
+postContext :: Context String
+postContext =
+ constField "root" root <>
+ dateField "date" "%-e %B %Y" <>
+ dateField "formalDate" "%Y-%m-%d" <>
+ dropIndexHtml "url" <>
+ defaultContext
+
+appendIndex :: Routes
+appendIndex =
+ customRoute $ append . splitExtension . toFilePath
+ where append (path, extension) = path </> "index" <.> extension
+
+dropIndexHtml :: String -> Context a
+dropIndexHtml key = mapContext transform (urlField key)
+ where
+ transform url = case splitFileName url of
+ (p, "index.html") -> takeDirectory p <> "/"
+ _ -> url
+
+root :: String
+root = "https://www.skreutz.com"
+
+feedConfiguration :: FeedConfiguration
+feedConfiguration = FeedConfiguration
+ { feedTitle = "Stefan Kreutz' Blog"
+ , feedDescription = "Random bits from a passionate software engineer."
+ , feedAuthorName = "Stefan Kreutz"
+ , feedAuthorEmail = "mail@skreutz.com"
+ , feedRoot = root
+ }
+
+type FeedRenderer = FeedConfiguration -> Context String -> [Item String] -> Compiler (Item String)
+
+-- | Compile a feed of posts with a given time format.
+compileFeed :: FeedRenderer -> [Char] -> Rules ()
+compileFeed render format = compile $ do
+ let feedContext =
+ -- Format built-in fields @published@ and @updated@.
+ -- TODO: Consider to vendor hakyll.
+ updatedField format <>
+ dateField "published" format <>
+ bodyField "description" <>
+ postContext
+ posts <- fmap (take 10) . recentFirst =<<
+ loadAllSnapshots "posts/*" "content"
+ render feedConfiguration feedContext posts
+
+-- | RFC 822 compliant date and time format.
+--
+-- This format string differs in two ways from
+-- 'Data.Time.Format.rfc822DateFormat' from package time-1.10. First, it padds
+-- days with zeros instead of spaces. Second, it uses the offset of the time
+-- zone instead of its name because RFC 822 does not define common time zone
+-- names like UTC. See
+-- https://validator.w3.org/feed/docs/warning/ProblematicalRFC822Date.html
+rfc822DateTimeFormat :: String
+rfc822DateTimeFormat = "%a, %d %b %Y %H:%M:%S %z"
+
+-- | RFC 3339 compliant date and time format.
+rfc3339DateTimeFormat :: String
+rfc3339DateTimeFormat = "%Y-%m-%dT%H:%M:%S%Ez"
+
+-- | Format field @updated@.
+--
+-- Copied from Hakyll's 'dateFieldWith'. Falls back to field @published@.
+updatedFieldWith
+ :: TimeLocale -- ^ Output time locale
+ -> String -- ^ Format to use on the date
+ -> Context a -- ^ Resulting context
+updatedFieldWith locale format = field "updated" $ \i -> do
+ time <- getUpdatedUTC locale $ itemIdentifier i
+ return $ formatTime locale format time
+
+-- | Format field @updated@.
+--
+-- Copied from Hakyll's 'dateField'. Falls back to field @published@.
+updatedField
+ :: String -- ^ Format to use on the date
+ -> Context a -- ^ Resulting context
+updatedField = updatedFieldWith defaultTimeLocale
+
+-- | Parse field @updated@.
+--
+-- Copied from Hakyll's 'getItemUTC'. Falls back to field @published@.
+getUpdatedUTC
+ :: (MonadMetadata m, MonadFail m)
+ => TimeLocale -- ^ Output time locale
+ -> Identifier -- ^ Input page
+ -> m UTCTime -- ^ Parsed UTCTime
+getUpdatedUTC locale id' = do
+ metadata <- getMetadata id'
+ let tryField k fmt = lookupString k metadata >>= parseTime' fmt
+ maybe empty' return $ msum $
+ [ tryField "updated" fmt | fmt <- formats ] ++
+ [ tryField "published" fmt | fmt <- formats ]
+ where
+ empty' = fail $ "getUpdatedUTC: could not parse time for " ++ show id'
+ parseTime' = parseTimeM True locale
+ formats =
+ [ "%a, %d %b %Y %H:%M:%S %Z"
+ , "%a, %d %b %Y %H:%M:%S"
+ , "%Y-%m-%dT%H:%M:%S%Z"
+ , "%Y-%m-%dT%H:%M:%S"
+ , "%Y-%m-%d %H:%M:%S%Z"
+ , "%Y-%m-%d %H:%M:%S"
+ , "%Y-%m-%d"
+ , "%B %e, %Y %l:%M %p"
+ , "%B %e, %Y"
+ , "%b %d, %Y"
+ ]
diff --git a/skreutz-dot-com.cabal b/skreutz-dot-com.cabal
new file mode 100644
index 0000000..053b0b4
--- /dev/null
+++ b/skreutz-dot-com.cabal
@@ -0,0 +1,18 @@
+name: skreutz-dot-com
+version: 0.1.0.0
+synopsis: Stefan Kreutz' personal website
+author: Stefan Kreutz
+maintainer: mail@skreutz.com
+bug-reports: mailto:mail@skreutz.com
+homepage: https://www.skreutz.com/
+build-type: Simple
+cabal-version: >= 1.10
+
+executable site
+ main-is: site.hs
+ build-depends: base == 4.*
+ , hakyll == 4.13.*
+ , filepath
+ , time
+ ghc-options: -threaded
+ default-language: Haskell2010
diff --git a/stack.yaml b/stack.yaml
new file mode 100644
index 0000000..f11ecdc
--- /dev/null
+++ b/stack.yaml
@@ -0,0 +1,5 @@
+resolver: lts-15.6
+packages:
+ - .
+extra-deps:
+ - hakyll-4.13.3.0
diff --git a/stack.yaml.lock b/stack.yaml.lock
new file mode 100644
index 0000000..5db215a
--- /dev/null
+++ b/stack.yaml.lock
@@ -0,0 +1,19 @@
+# This file was autogenerated by Stack.
+# You should not edit this file by hand.
+# For more information, please see the documentation at:
+# https://docs.haskellstack.org/en/stable/lock_files
+
+packages:
+- completed:
+ hackage: hakyll-4.13.3.0@sha256:7d8903f03974691aec049a6188ace8d93457563bad9f078c6faf4c9156e27a33,8867
+ pantry-tree:
+ size: 7841
+ sha256: bdbacc241f680dd0c8d7666c9ec4e7d230dad85172067e106cc35f7c125cd7ab
+ original:
+ hackage: hakyll-4.13.3.0
+snapshots:
+- completed:
+ size: 491387
+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/15/6.yaml
+ sha256: 8d81505a6de861e167a58534ab62330afb75bfa108735c7db1204f7ef2a39d79
+ original: lts-15.6
diff --git a/templates/default.html b/templates/default.html
new file mode 100644
index 0000000..739995b
--- /dev/null
+++ b/templates/default.html
@@ -0,0 +1,43 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="x-ua-compatible" content="ie=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta name="author" content="Stefan Kreutz">
+ $if(description)$
+ <meta name="description" content="$description$">
+ $else$
+ <meta name="description" content="Stefan Kreutz' personal website.">
+ $endif$
+ <title>skreutz.com - $title$</title>
+ <link rel="stylesheet" href="/css/normalize.css">
+ <link rel="stylesheet" href="/css/site.css">
+ <link rel="alternate" type="application/rss+xml" href="/feeds/posts.rss" title="Stefan Kreutz' Blog">
+ <link rel="alternate" type="application/atom+xml" href="/feeds/posts.atom" title="Stefan Kreutz' Blog">
+ </head>
+ <body>
+ <header>
+ <nav>
+ <ul>
+ <li><a href="/" title="Home page">skreutz.com</a></li>
+ <li><a href="/about/">About</a></li>
+ <li><a href="/posts/">Blog</a></li>
+ <li><a href="/contact/">Contact</a></li>
+ </ul>
+ </nav>
+ </header>
+ <main>
+ $body$
+ </main>
+ <footer>
+ <nav>
+ <ul>
+ <li><a href="/privacy/">Privacy</a></li>
+ <li><a href="/feeds/posts.rss">RSS feed</a></li>
+ <li><a href="/feeds/posts.atom">Atom feed</a></li>
+ </ul>
+ </nav>
+ </footer>
+ </body>
+</html>
diff --git a/templates/direct.html b/templates/direct.html
new file mode 100644
index 0000000..a16a6f8
--- /dev/null
+++ b/templates/direct.html
@@ -0,0 +1,2 @@
+<h1>$title$</h1>
+$body$
diff --git a/templates/post.html b/templates/post.html
new file mode 100644
index 0000000..726fa77
--- /dev/null
+++ b/templates/post.html
@@ -0,0 +1,8 @@
+<article>
+ <h1>$title$</h1>
+ <header>
+ Posted on <time datetime="$formalDate$">$date$</time>
+ </header>
+ $body$
+ <footer></footer>
+</article>
diff --git a/templates/posts.html b/templates/posts.html
new file mode 100644
index 0000000..b35e0e7
--- /dev/null
+++ b/templates/posts.html
@@ -0,0 +1,10 @@
+<h1>Blog</h1>
+<ol class="blog-index">
+ $for(posts)$
+ <li>
+ <a href="$url$">$title$</a>
+ <time datetime="$formalDate$">$date$</time>
+ <p>$description$</p>
+ </li>
+ $endfor$
+</ol>
diff --git a/templates/sitemap.xml b/templates/sitemap.xml
new file mode 100644
index 0000000..c95874b
--- /dev/null
+++ b/templates/sitemap.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<urlset
+ xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
+ xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"
+ xmlns:xhtml="http://www.w3.org/1999/xhtml"
+ xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0"
+ xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
+ xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"
+>
+ <url>
+ <loc>$root$</loc>
+ <changefreq>daily</changefreq>
+ <priority>1.0</priority>
+ </url>
+ $for(pages)$
+ <url>
+ <loc>$root$$url$</loc>
+ <lastmod>$if(updated)$$updated$$else$$if(date)$$date$$endif$$endif$</lastmod>
+ <changefreq>weekly</changefreq>
+ <priority>0.8</priority>
+ </url>
+ $endfor$
+</urlset>
Generated by cgit. See skreutz.com for my tech blog and contact information.