openssh forced commands keybased config setter und getter

reinhard@finalmedia.de Sun 14 Apr 2024 07:38:56 PM CEST

Worum gehts hier?

Wir realisieren mittels /etc/accounts eine netzwerktaugliche keyvalue storage flatfile database mit openssh bordmitteln und kurzen shellscripten, analog zu einem ldap oder active directory, nur wesentlich minimalistischer. Unsere administrativ ausführbare Scripte liegen dabei unter /etc/accounts als dotfile und sind mit der bash auto completion bequem listbar und jederzeit erweiterbar.

Wir binden openssh mit zwei universellen set-config und get-config systemusern ein, um werte aus dem config Unterverzeichnis zu lesen und zu erfragen.

Jedem virtuellen User-Account geben wir dabei eine eindeutige ID, im Beispiel "u000023". Accounts sind bequem deaktivierbar, indem man ein u Verzeichnis von 1 nach 0 verschiebt. In 2 kann man neue u-Verzeichnisse vorbereiten und dann atomar nach 1 bewegen, um sie zu aktivieren.

Innerhalb eines u-Verzeichnisse kann man beliebige Unterordner und Dateien definieren, um Wertepaare und Hierarchie abzubilden. Dazu gleich mehr im Punkt populate /etc/accounts.

Vorbereitungen

Wir arbeiten als root auf dem System:

## zwei neue systemuser mit zufallspasswort erstellen
useradd -m -p"$(tr -dc "0-9a-zA-Z" < /dev/urandom | head -c 32 | openssl passwd -5 -stdin)" "get-config"
useradd -m -p"$(tr -dc "0-9a-zA-Z" < /dev/urandom | head -c 32 | openssl passwd -5 -stdin)" "set-config"

## unsere flatfile database in /etc/accounts beginnen
mkdir -p /etc/accounts/0
mkdir -p /etc/accounts/1
mkdir -p /etc/accounts/2
chown -R root:root /etc/accounts/
chmod +x /etc/accounts/.show.ssh.active.authorizedkeys
chmod +x /etc/accounts/.get.config
touch /etc/accounts/.show.ssh.active.authorizedkeys
touch /etc/accounts/.get.config
chown root:root /etc/accounts/.show.ssh.active.authorizedkeys
chown root:root /etc/accounts/.get.config

populate /etc/accounts

Testdaten in /etc/accounts ablegen

Wir bauen uns eine Flatfile Datenbank, die alle unserer Accountdaten enthält. Dabei brauchen wir nur Dateien und Verzeichnisse, um sehr umfangreiche und universelle Strukturen zu realisieren:

Wir leiten auch den fingerprint vom angegebenen publickey ab. Verwende daher bitte in der nächsten Variable deinen eigenen publickey, wenn du das Ergebnis später auch Testen können willst!

Alles andere kannst du so lassen.

publickey="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOID6tldu9PbX6ksmDH0SVm6bkv6VQv39WVvg2hw8MuF"

state="1"
accountdir="/etc/accounts"
uid="u000023"

mkdir -p ${accountdir}/${state}/${uid}

cd ${accountdir}/${state}/${uid}

## +++ Demouser +++

mkdir -p name.user/maxmustermann
mkdir -p name.short/MaMu
mkdir -p name.title/Mr.
mkdir -p name.vorname/Max
mkdir -p name.nachname/Mustermann
mkdir -p private.birthday/19700101

mkdir -p ssh.pubkeys

# filename deines pubkeys wird dein md5 fingerprint mit dem prefix md5.
# Im Beispiel also md5.b62391a58d612bcc2c01e1f74af8144a
# content der datei ist dein publickey
echo "${publickey}" | tee ssh.pubkeys/$(echo "${publickey}" | ssh-keygen -E md5 -l -f- | cut -d " " -f2 | cut -d: -f2- | tr -dc "0-9a-f" | sed "s/^/md5./g")

## +++ Democonfig des Users +++

mkdir -p config/alpha.allow/
mkdir -p config/beta.allow/
mkdir -p config/gamma.allow/
cp ssh.pubkeys/md5.* config/alpha.allow/
cp ssh.pubkeys/md5.* config/beta.allow/
echo "1237487569" > config/alpha.uid
echo "C:\Programme\Gibts\Nicht" > config/alpha.path
echo "bulgur" > config/beta.username
echo "#009933" > config/beta.color01
echo "#C13399" > config/beta.color02
echo "#A9362E" > config/beta.color03
echo "erbsen" > config/gamma.gem
echo "127.0.0.1" > config/gamma.ip

touch config/beta.pass.hex.clientspecified
chown "set-config" config/beta.pass.hex.clientspecified

Das Ergebnis kannst du dir anzeigen lassen mittels

cd 
find /etc/accounts

im obigen Beispiel wäre es also diese Ausgabe/Struktur zu sehen:

/etc/accounts/1/u000023
/etc/accounts/1/u000023/name.nachname
/etc/accounts/1/u000023/name.nachname/Mustermann
/etc/accounts/1/u000023/name.vorname
/etc/accounts/1/u000023/name.vorname/Max
/etc/accounts/1/u000023/name.title
/etc/accounts/1/u000023/name.title/Mr.
/etc/accounts/1/u000023/private.birthday
/etc/accounts/1/u000023/private.birthday/19700101
/etc/accounts/1/u000023/name.user
/etc/accounts/1/u000023/name.user/maxmustermann
/etc/accounts/1/u000023/ssh.pubkeys
/etc/accounts/1/u000023/ssh.pubkeys/md5.b62391a58d612bcc2c01e1f74af8144a
/etc/accounts/1/u000023/config
/etc/accounts/1/u000023/config/beta.color03
/etc/accounts/1/u000023/config/beta.pass.hex.clientspecified
/etc/accounts/1/u000023/config/gamma.allow
/etc/accounts/1/u000023/config/gamma.ip
/etc/accounts/1/u000023/config/beta.username
/etc/accounts/1/u000023/config/alpha.path
/etc/accounts/1/u000023/config/gamma.gem
/etc/accounts/1/u000023/config/alpha.uid
/etc/accounts/1/u000023/config/beta.allow
/etc/accounts/1/u000023/config/beta.allow/md5.b62391a58d612bcc2c01e1f74af8144a
/etc/accounts/1/u000023/config/alpha.allow
/etc/accounts/1/u000023/config/alpha.allow/md5.b62391a58d612bcc2c01e1f74af8144a
/etc/accounts/1/u000023/config/beta.color02
/etc/accounts/1/u000023/config/beta.color01
/etc/accounts/1/u000023/name.short
/etc/accounts/1/u000023/name.short/MaMu

Du kannst nach diesem Schema beliebig viele weitere accounts mit anderer uid erstellen. inaktive accounts erhalten den State "0", aktive erhalten den State "1" und Accounts in Vorbereitung kannst du in "2" ablegen.

Wie du siehst, kodieren wir einige Dinge direkt im Dateinamen, weil dies bei der direkten Verzeichnis-Durchsicht bei der schnell Zuordnung hilft.

Andere Dinge wiederum werden als payload im Content einer Datei abgelegt, vorallem wenn dieser Inhalt aufgrund des Zeichenumfangs auch gar nicht im Dateinamen selbst abbildbar wäre.

Wir verwenden zudem geschickt auch Verzeichnisnamen als Informationsträger. Generell ist das config/*.allow also immer ein Verzeichnis, in dem die erlaubten ssh publickeys liegen, die auf diesen configdatensatz zugreifen und ihn ggf. partiell modifizieren können. Das prefix for dem .allow ist dabei identisch mit dem configname/keyname prefix. die allow pubkeys müssen keine Dateien sein, sondern könnten auch als symlinks zu den keys in ssh.pubkeys des Nutzers realisiert sein. Es müssen also keine Kopien sein. Allerdings können es auch vollständig andere keys sein, die nichts mit dem useraccount zu tun haben und nur spezielle zum config lesen/setzen definiert wurden.

/etc/ssh/sshd_config

In der sshd config stellen auf die Fingerprint-Hash Anzeige md5 um, statt sha256. Zudem definieren wir für die beiden neuen user get-config und set-config ein AuthorizedKeysCommand. Wir rufen dabei /etc/accounts/.show.ssh.active.authorizedkeys mit drei Parametern auf, sodass die dynamisch erzeugte Liste mit "restrict,command=" beginnen wird - und dabei übergeben wir auch das als forced command "/etc/accounts/.get.config" unser script, sowie die wichtigen Parameter %k und %f. openssh ersetzt diese automatisch mit den verwendeten publickkey %k des sich gerade einloggenden SSH Nutzers und dem fingerprint %f des verwendeten publickeys.


FingerprintHash md5

Match User get-config
        AuthorizedKeysCommand /etc/accounts/.show.ssh.active.authorizedkeys /etc/accounts/.get.config %k %f
        AuthorizedKeysFile none
        AuthorizedKeysCommandUser root

Match User set-config
        AuthorizedKeysCommand /etc/accounts/.show.ssh.active.authorizedkeys /etc/accounts/.set.config %k %f
        AuthorizedKeysFile none
        AuthorizedKeysCommandUser root

Denke daran, ggf. die bei dir vorhandene globale Config Zeile "AllowUsers" um die neuen systemuser get-config und set-config zu erweitern! Dort sind alle user space separated aufgeführt, denen du allgemein ssh Zugriffs getattest.

Vergiss nicht, "service ssh reload" auszuführen, wenn du die Config geändert hast.

/etc/accounts/.show.ssh.active.authorizedkeys

In diesem Script füllen wir die prepend variable, sobald mehr als ein parameter übergeben wurde. (was ja bei unserem Command-Aufruf durch ssh dann immer der Fall sein wird). Ansonsten werden alleverfügbaren Publickeys ohne forced command aufgelistet.

Wir sind damit sehr universell aufgestellt und können beim Listengenerierungs-Aufruf also beliebige Forced-Commands als ersten parameter mit angeben, oder das gleiche Script auch zur Allgemeinen Freigabe verwenden.

Alle erlaubten ssh pubkeys erzeugen wir dabei dynamisch aus den Publickeys Dateien, die in /etc/accounts/1/... beim jeweiligen aktiven user liegen. diese geben wir dann auch auf stdout aus

#!/bin/sh
test -n "$1" && prepend="restrict,command=\"$@\" " || prepend=""
export accountdir="/etc/accounts/1"
find "${accountdir}" -maxdepth 3 -type f -wholename "${accountdir}/*/ssh.pubkeys/md5.*" -exec head -n 1 "{}" \; | sed "s|^|${prepend}|g"

/etc/accounts/.get.config

#!/bin/sh
# reinhard@finalmedia.de
# Fr 12. Apr 20:44:28 CEST 2024

test "$#" -ge 2 || exit 1

# Variablen MUESSEN exportiert werden
export confname="$(timeout 6 head -n 1 | head -c 32 | tr -dc "0-9a-z")"
export fingerprint="$(echo "$2" | grep -i "^md5:" | cut -d: -f2- | tr -dc "0-9a-f\n")"
export pubkey="$(echo "$1" | tr -dc "0-9a-zA-Z/+=")"
export accountdir="/etc/accounts/1"

test $DEBUG && echo "# systemuser: $(whoami) (soll: get-config)" >&2
test $DEBUG && echo "# publickey: $1" >&2
test $DEBUG && echo "# fingerprint: $2" >&2

echo "[${confname}]"
find "${accountdir}" -not -type d -wholename "${accountdir}/*/config/${confname}.allow/md5.${fingerprint}" \
-execdir sh -c 'grep -q "$pubkey" md5.${fingerprint} && find ../ -not -type d -maxdepth 1 -mindepth 1 -name "${confname}.*" -printf "$(pwd -P)/../%f\n"' \; | while read p
do
	# key/value paare ausgeben
	basename "$p" | tr "\n" "="
	head -n1 "$p" | tr -d "\n"
	echo
done

/etc/accounts/.set.config

#!/bin/sh
# reinhard@finalmedia.de
# Sa 13. Apr 11:49:22 CEST 2024

# keyvaluefile muss bereits existieren, damit es änderbar ist.
# set setzt also nur, legt aber nicht neu an. (wichtiges sicherheitsfeature)
# zudem muss die datei dem user set-config gehören oder
# die datei betreffende datei muss chmod 666 aufweisen.

test "$#" -ge 2 || exit 1

# Variablen MUESSEN exportiert werden
export line="$(timeout 10 head -n 1 | head -c 1024 | tr -dc "=0-9a-zA-Z@ :;.+[]()!#/_-")"
export confname="$(echo "$line" | cut -d= -f1 | cut -d. -f 1 | tr -dc "0-9a-z")"
export conffullkeyname="$(echo "$line" | cut -d= -f1 | tr -dc "0-9a-z.")"
export confvalue="$(echo "$line" | cut -d= -f2-)"
export fingerprint="$(echo "$2" | grep -i "^md5:" | cut -d: -f2- | tr -dc "0-9a-f\n")"
export pubkey="$(echo "$1" | tr -dc "0-9a-zA-Z/+=")"
export accountdir="/etc/accounts/1"

# Abbruch, wenn der conffullkeyname nicht auf .clientspecified endet
echo "${conffullkeyname}" | grep -q "\.clientspecified$" || exit 1

test $DEBUG && echo "# systemuser: $(whoami) (soll: set-config)" >&2
test $DEBUG && echo "# publickey: $1" >&2
test $DEBUG && echo "# fingerprint: $2" >&2

# alle conffullkeyname einträge finden und neuen wert setzen, wenn mit aktuellem fingerprint und pubkey erlaubt
find "${accountdir}" -not -type d -wholename "${accountdir}/*/config/${confname}.allow/md5.${fingerprint}" \
-execdir sh -c 'grep -q "$pubkey" md5.${fingerprint} && find ../ -not -type d -maxdepth 1 -mindepth 1 -name "${conffullkeyname}" -printf "$(pwd -P)/../%f\n"' \; | while read p
do
echo "${confvalue}" | tee "$p" | tr -d "\n"
echo
head -n 1 "$p" | tr -d "\n"
echo
done

Testaufruf via ssh loopback connect

Wenn du die gesamte config für das "beta" tool abrufen willst, übergibst du via ssh also den string "beta" auf stdin und verbindest dich als user "get-config"

 ssh get-config@localhost -T <<< beta

Um über den ssh client eine Config zu schreiben (nur für die .clientspecified values möglich), nutzt du:

 ssh set-config@localhost -T <<< beta.pass.hex.clientspecified=a3b763baeb72abb29e728cb25

Du kannst auch mit alternativen Keys und Confignames testen, die wir ja testweise im Verzeichnis angelegt haben.

 ssh -i irgendeinkeyfile get-config@localhost -T <<< gamma

Sicherheithinweise

Dieses Howto ist als "root" user angegeben und du konfigurierst ja auch sshd und die /etc/accounts struktur als root. Nur das Script .show.ssh.active.authorizedkeys läuft dabei auch als root. die dort aufgerufenen eigentlichen Scripte zur User-Interaktion (also die forced commands) .set.config und .get.config laufen jeweils unter dem neuen systemuser "set-config" und "get-config", dafür sorgt open-sshd.

Daher müssen auch die Dateien, die der set-config verändern können soll, dem user set-config gehören also 644 (empfholen) oder aber world writeable sein, also 666.

Debugging

Tipp: Wenn du in der /etc/ssh/sshd_config SetEnv DEBUG=1 setzt, erhält der sich einloggende User auf stderr die zusätzlichen debug Informationen, da die Scripte .get.config und .set.config diese Varible dann ebenso erhalten. Dein Auszug aus der /etc/ssh/sshd_config sähe dann so aus:


FingerprintHash md5

Match User get-config
        AuthorizedKeysCommand /etc/accounts/.show.ssh.active.authorizedkeys /etc/accounts/.get.config %k %f
        AuthorizedKeysFile none
        AuthorizedKeysCommandUser root
	SetEnv DEBUG=1

Match User set-config
        AuthorizedKeysCommand /etc/accounts/.show.ssh.active.authorizedkeys /etc/accounts/.set.config %k %f
        AuthorizedKeysFile none
        AuthorizedKeysCommandUser root
	SetEnv DEBUG=1

Vergiss nicht, "service ssh reload" auszuführen, wenn du die Config geändert hast.

/etc/accounts/.show.all.config

Es empfhielt sich auch noch dieses Script anzulegen, um eine bequeme Config Übersicht abrufen zu können.

#!/bin/sh
find "/etc/accounts/" -type f -wholename "/etc/accounts/1/*/config/*"

Denke daran auch ein chmod +x darauf zu setzen. Dann kannst du den Befehl "/etc/accounts/.show.all.config" absetzen.