diff options
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | 404.html | 6 | ||||
-rw-r--r-- | README.md | 22 | ||||
-rw-r--r-- | TODO.md | 59 | ||||
-rw-r--r-- | _drafts/dotfiles-under-revision-control.md | 83 | ||||
-rw-r--r-- | _drafts/my-preferred-tools.md | 34 | ||||
-rw-r--r-- | _drafts/ssh-proxy.md | 296 | ||||
-rw-r--r-- | _drafts/temporary-postgresql-server.md | 100 | ||||
-rw-r--r-- | about.md | 16 | ||||
-rw-r--r-- | contact.md | 20 | ||||
-rw-r--r-- | css/normalize.css | 349 | ||||
-rw-r--r-- | css/site.css | 303 | ||||
-rwxr-xr-x | deploy | 41 | ||||
-rw-r--r-- | files/pgp.asc | 30 | ||||
-rw-r--r-- | index.html | 8 | ||||
-rw-r--r-- | posts/unix-domain-socket-forwarding-with-openssh.md | 173 | ||||
-rw-r--r-- | privacy.md | 7 | ||||
-rw-r--r-- | robots.txt | 4 | ||||
-rw-r--r-- | site.hs | 208 | ||||
-rw-r--r-- | skreutz-dot-com.cabal | 18 | ||||
-rw-r--r-- | stack.yaml | 5 | ||||
-rw-r--r-- | stack.yaml.lock | 19 | ||||
-rw-r--r-- | templates/default.html | 43 | ||||
-rw-r--r-- | templates/direct.html | 2 | ||||
-rw-r--r-- | templates/post.html | 8 | ||||
-rw-r--r-- | templates/posts.html | 10 | ||||
-rw-r--r-- | templates/sitemap.xml | 23 |
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/). @@ -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; + } +} @@ -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 @@ -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> |