From 07c1d9f590e8de064e9b527c3d425eb898f7e59e Mon Sep 17 00:00:00 2001 From: Stefan Kreutz Date: Mon, 6 Jul 2020 22:40:21 +0200 Subject: Add initial version This commit adds the first published version of the website including the first blog post, Unix Domain Socket Forwarding with OpenSSH. --- .gitignore | 3 + 404.html | 6 + README.md | 22 ++ TODO.md | 59 ++++ _drafts/dotfiles-under-revision-control.md | 83 +++++ _drafts/my-preferred-tools.md | 34 ++ _drafts/ssh-proxy.md | 296 +++++++++++++++++ _drafts/temporary-postgresql-server.md | 100 ++++++ about.md | 16 + contact.md | 20 ++ css/normalize.css | 349 +++++++++++++++++++++ css/site.css | 303 ++++++++++++++++++ deploy | 41 +++ files/pgp.asc | 30 ++ index.html | 8 + .../unix-domain-socket-forwarding-with-openssh.md | 173 ++++++++++ privacy.md | 7 + robots.txt | 4 + site.hs | 208 ++++++++++++ skreutz-dot-com.cabal | 18 ++ stack.yaml | 5 + stack.yaml.lock | 19 ++ templates/default.html | 43 +++ templates/direct.html | 2 + templates/post.html | 8 + templates/posts.html | 10 + templates/sitemap.xml | 23 ++ 27 files changed, 1890 insertions(+) create mode 100644 .gitignore create mode 100644 404.html create mode 100644 README.md create mode 100644 TODO.md create mode 100644 _drafts/dotfiles-under-revision-control.md create mode 100644 _drafts/my-preferred-tools.md create mode 100644 _drafts/ssh-proxy.md create mode 100644 _drafts/temporary-postgresql-server.md create mode 100644 about.md create mode 100644 contact.md create mode 100644 css/normalize.css create mode 100644 css/site.css create mode 100755 deploy create mode 100644 files/pgp.asc create mode 100644 index.html create mode 100644 posts/unix-domain-socket-forwarding-with-openssh.md create mode 100644 privacy.md create mode 100644 robots.txt create mode 100644 site.hs create mode 100644 skreutz-dot-com.cabal create mode 100644 stack.yaml create mode 100644 stack.yaml.lock create mode 100644 templates/default.html create mode 100644 templates/direct.html create mode 100644 templates/post.html create mode 100644 templates/posts.html create mode 100644 templates/sitemap.xml 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 +--- + +

Resource not found

+

The server has not found the requested resource. Please contact me if you've found a dead link on this website. Otherwise you might wish to go to the home page instead.

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 `
`, 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
+---
+
+
+
+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 0backpipe
+
+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  \\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}" &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:
+
+
+ Stefan Kreutz
+ Frankenweg 2
+ 53604 Bad Honnef
+ Germany
+ Phone: Germany 157 Five Seven One Seven 0987
+ Email: mail (at) skreutz (dot) com +
+ +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 +--- + +
+

Welcome!

+

My name is Stefan Kreutz. This is my personal website.

+
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 0backpipe + + 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 @@ + + + + + + + + $if(description)$ + + $else$ + + $endif$ + skreutz.com - $title$ + + + + + + +
+ +
+
+ $body$ +
+ + + 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 @@ +

$title$

+$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 @@ +
+

$title$

+
+ Posted on +
+ $body$ +
+
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 @@ +

Blog

+
    + $for(posts)$ +
  1. + $title$ + +

    $description$

    +
  2. + $endfor$ +
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 @@ + + + + $root$ + daily + 1.0 + + $for(pages)$ + + $root$$url$ + $if(updated)$$updated$$else$$if(date)$$date$$endif$$endif$ + weekly + 0.8 + + $endfor$ + -- cgit v1.2.3