
Anonymous OpenSSH Account Creation
At this year’s MRMCD, I had the pleasure of giving a workshop on self-restricting software. In particular, I started with a primer on software privileges, followed by software self-hardening methods without the need for extended permissions. Regular readers of this weblog (just me, I guess) might know what to expect.
Since it was a workshop and not a presentation, there was an interactive part to it.
To ease participation, I have decided to prepare everything on a remote machine and just let the participants ssh(1)
into it.
This brought me to the problem of how to ease account creation as much as possible.
To make things more spicy, there was not just one machine, but two, one running Debian GNU/Linux and the other OpenBSD.
After procrastinating on this for a while, I came up with the following solution, only using the OpenSSH daemon sshd(8)
and a short shell script.
Feeling quite clever about this and even being asked about the “how” after the workshop, I came up with this post.
Skip the how, but actually what?
My aim was to provide an unauthorized and anonymous SSH access with user account creation on the fly.
So any workshop participant would ssh(1)
into the machine and end up with their own user.
Additionally, participants should be able to reuse their user and not always end up with a new one.
In the end, my setup had only one requirement on the participant’s side: an SSH public key.
Stating the obvious, giving strangers an account on a machine might have some implications. However, I put my trust in strangers, gave them a relatively unrestricted user, and was not swatted (at the time of writing).
Demo Time
After this brief description, having a small demonstration might ease understanding.
The first login creates a new user account.
$ ssh mrmcd@mrmcd-test
** Only public key authentication is possible. If you are being asked for a
** password or receive an error such as
** Permission denied (publickey,keyboard-interactive),
** please configure ssh(1) to send an identity file.
**
** If you have no idea what I am talking about, please execute
** $ ssh-keygen
** once, use the suggested location and do NOT set a passphrase, and retry.
** Congratulations. You can ignore the banner above!
Welcome, stranger! Take a seat. You are now mute.
For future logins, you may use either your new name or this login service.
Treat each other with kindness and respect, including this machine. Do not cause
harm to others.
_
_____ ___ _____ ___ _| |
| | _| | _| . |
|_|_|_|_| |_|_|_|___|___|
Self-Restricting Software
Workshop 2025-09-13
OpenBSD Machine.
mute@mrmmcd-test:~>
Any subsequent login will result in the same account.
$ ssh mrmcd@mrmcd-test
** Only public key authentication is possible. If you are being asked for a
** password or receive an error such as
** Permission denied (publickey,keyboard-interactive),
** please configure ssh(1) to send an identity file.
**
** If you have no idea what I am talking about, please execute
** $ ssh-keygen
** once, use the suggested location and do NOT set a passphrase, and retry.
** Congratulations. You can ignore the banner above!
Welcome back, mute.
_
_____ ___ _____ ___ _| |
| | _| | _| . |
|_|_|_|_| |_|_|_|___|___|
Self-Restricting Software
Workshop 2025-09-13
OpenBSD Machine.
mute@mrmmcd-test:~>
Of course, the account can also be used directly by its name, without going through this logic.
$ ssh mute@mrmcd-test
_
_____ ___ _____ ___ _| |
| | _| | _| . |
|_|_|_|_| |_|_|_|___|___|
Self-Restricting Software
Workshop 2025-09-13
OpenBSD Machine.
mute@mrmmcd-test:~>
Back to the how
While the complete workshop - including slides, demo code, and setup - is available on codeberg.org/oxzi/self-restricting-software, this section will briefly walk you through the relevant parts for anonymous account creation.
In a nutshell, sshd(8)
is instructed to allow any public key for a certain user login, but does not provide a shell, and executes a certain script instead.
This script checks if the current public key is stored in any user’s passwd(5)
gecos field.
If there is such a system user, a shell as this user will be invoked.
Otherwise, a new user will be created, and this user will be used.
sshd_config
Relying on sshd(8)
, I found most tricks by simply reading sshd_config(5)
.
Reading man pages is cool, trust me - give it a try.
This one started with a Match
rule, allowing conditional configuration.
While there is an option to match for an Invalid-User
, this would not have allowed to actually create a session, and, on broader questions, many workshop participants would be creative enough to pick the same username.
Thus, everything that follows goes into a Match User mrmcd
rule, where mrmcd
is the username.
Public key authentication usually works by placing the public key in some ~/.ssh/authorized_keys
file.
However, setting AuthorizedKeysCommand
together with AuthorizedKeysCommandUser
allows invoking a command which tells which keys are acceptable.
This program is expected to return valid public keys in the same authorized_keys
format as known from the ~/.ssh/authorized_keys
file.
To sweeten things up, sshd(8)
allows command arguments from tokens, such as %t
for “[t]he key or certificate type” or %k
for “[t]he base64-encoded key or certificate for authentication”.
These two tokens combined already result in the desired format.
Now, only a command is missing to echo its argument.. such as echo(1)
.
Just to be sure, the normal file-based authentication was disabled by setting AuthorizedKeysFile
to none
.
To ensure that only the mentioned script is executed, a ForceCommand
was set.
This command cannot be overwritten by the user.
As the Match User
block deals with a normal system user and the script needs certain privileges to create and switch users, a doas(1)
wrapper call was added.
The doas.conf(5)
should contain a line like this:
permit nopass keepenv mrmcd cmd /usr/local/sbin/demoauth
For Debian, the doas
part should be replaced by sudo
.
Setting ExposeAuthInfo
to yes
creates a temporary file with the currently used public keys, referenced in the SSH_USER_AUTH
environment variable.
And this was all to do in the sshd_config(5)
, setting like five options.
The result looks like this in the git or as follows.
Match User mrmcd
AuthorizedKeysCommand /bin/echo %t %k # /usr/bin/echo on Debian
AuthorizedKeysCommandUser mrmcd
AuthorizedKeysFile none
ForceCommand doas /usr/local/sbin/demoauth # sudo instead of doas on Debian
ExposeAuthInfo yes
ForceCommand
After all the explanations above, the /usr/local/sbin/demoauth
script itself is quite boring.
I will paste it first and explain some details below.
#!/bin/sh
set -eu
# _sha256 creates a SHA256 hash for the 1st parameter.
_sha256() {
if command -v sha256 > /dev/null 2>&1; then
h="sha256"
elif command -v sha256sum > /dev/null 2>&1; then
h="sha256sum"
else
logger -s "Neither sha256 nor sha256sum is available"
exit 2
fi
echo "$1" | "$h" | awk '{ print $1 }'
}
# _usergen creates a new user and either returns the username or exits.
# Expects the Public Key hash as the 1st parameter to be set as the comment.
_usergen() {
# Try to find a not already used name..
for _ in $(seq 1 10); do
user="$(sort -R /usr/local/share/demoauth-dict | head -n 1)"
if [ -z "$(getent passwd | awk -F : -v user="$user" '$1 == user { print }')" ]; then
break
fi
user=""
done
# ..or fall back to a random string.
if [ -z "$user" ]; then
user="$(openssl rand -hex 12)"
fi
# There might be a race, but then it just fails :3
if [ "$(uname -s)" = "OpenBSD" ]; then
useradd -c "$1" -g =uid -m -L demouser "$user"
else # Debian it is
useradd -c "$1" -m -s /usr/bin/bash -U "$user"
fi
echo "$user"
}
if [ ! -s "$SSH_USER_AUTH" ]; then
logger -s "SSH_USER_AUTH - $SSH_USER_AUTH - does either not exist or is empty"
exit 2
fi
PUBKEY="$(awk '/^publickey ssh.*/ { print $2, $3 }' "$SSH_USER_AUTH")"
if [ -z "$PUBKEY" ]; then
logger -s "SSH_USER_AUTH misses publickey entry"
exit 2
fi
PUBKEY_ID="demoauth-$(_sha256 "$PUBKEY")"
logger "New connection from '$PUBKEY' with hash '$PUBKEY_ID'"
echo
echo "** Congratulations. You can ignore the banner above!"
echo
USER="$(getent passwd | awk -F : -v comment="$PUBKEY_ID" '$5 == comment { print $1 }')"
if [ -n "$USER" ]; then
logger "Known Public Key resolves to $USER"
echo "Welcome back, $USER."
else
USER="$(_usergen "$PUBKEY_ID")"
logger "Unknown Public Key, created $USER"
mkdir -p "/home/${USER}/.ssh"
chmod 0700 "/home/${USER}/.ssh"
echo "$PUBKEY" > "/home/${USER}/.ssh/authorized_keys"
chmod 0600 "/home/${USER}/.ssh/authorized_keys"
chown -R "$USER:$USER" "/home/${USER}/.ssh"
echo "Welcome, stranger! Take a seat. You are now $USER."
echo
echo "For future logins, you may use either your new name or this login service."
echo
echo "Treat each other with kindness and respect, including this machine. Do not cause"
echo "harm to others."
fi
cat /etc/motd
su -l "$USER"
Being a POSIX shell script, it should be quite portable. Unless I am missing something, it should only require coreutils and does not use any nonstandard arguments outside of the OS-switching cases. In other words, all this works on a base OpenBSD setup and a normal Debian installation after adding OpenSSH.
The _sha256
function was needed to abstract the different SHA256 utilities from OpenBSD and Debian away into one simple function to call.
Actually, a hash function became required, as I decided not to blindly paste user-provided public keys (hoping only public keys are possible) into the gecos field, but to hash them first and add a known prefix. Doing so somehow felt safer.
The _usergen
function receives such a prefixed hash as an argument and returns a user name.
As a side effect, the user was created.
A username is picked from the /usr/local/share/demoauth-dict
wordlist, containing one potential username per line.
If the script fails to pick an unused username from the list, openssl(1)
is used to create a random string.
After a preflight check to ensure that a public key can be extracted from the SSH_USER_AUTH
file, passwd
is queried for a user with this hash using getent(1)
and awk(1)
.
Unless such a user was found, a new one will be created using the _usergen
function.
This new user gets its ~/.ssh/authorized_keys
file populated, allowing SSH logins outside of this hack.
In the end, su(1)
invokes a login for the user account, letting the SSH session result in a shell as this very user.
That’s all, folks!