Script-Sammlung
Eine Sammlung verschiedener Scripts über das Basic-Setup hinaus, um dem Admin Dinge zu erleichtern
- Backup für die Benutzerdatenbank einrichten
- Nutzerdatenbank laden / hashen / prüfen
- FabAccess State Script (Email-Warnung bei Fehlerzustand)
- DESFire CardKeys generieren
- Echtzeituhr (RTC) Modul DS3231 für Raspberry Pi
Backup für die Benutzerdatenbank einrichten
Mit folgendem Backup-Script (bash) können wir die Datenbank sichern. Diese können wir außerdem mit systemd
per Service und Timer automatisieren. Alternativ kann das Bash-Script auch als Cronjob eingebunden werden. Folgendes Script sollte je nach Bedarf angepasst werden (Pfade).
Es ist generell sehr wichtig die Benutzerdatenbank regelmäßig zu sichern. Immer dann, wenn ein Administrator bzw. Manager per Client App neue Accounts hinzufügt, Accounts löscht oder Accounts ändert (z.B. Passwortwechsel), dann werden diese Änderungen in einen transienten Zwischenspeicher gelegt. Dieser muss mit der lokalen users.toml zusammengeführt werden. Das erfolgt über den dump-users
Parameter.
Wird dieser zusammengeführte Dump nicht ausgeführt und crasht der Serverdienst, dann sind alle etwaigen Remote-Änderungen u.U. nicht wiederbringbar.
Backup Script anlegen und konfigurieren
vim /opt/fabinfra/scripts/bffh-backup.sh
#!/bin/bash
# Database dump command
DB_DUMP_CMD="/opt/fabinfra/bffh/target/release/bffhd -c /opt/fabinfra/bffh-data/config/bffh.dhall --dump-users"
# Backup directory
BACKUP_DIR="/opt/fabinfra/bffh-data/config_backup"
mkdir -p $BACKUP_DIR
# Number of backups to keep
NUM_BACKUPS_TO_KEEP=5
# Dry run flag
DRY_RUN=false
# Parse command-line options
while getopts ":n:r" opt; do
case $opt in
n)
NUM_BACKUPS_TO_KEEP="$OPTARG"
;;
r)
DRY_RUN=true
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
:)
echo "Option -$OPTARG requires an argument." >&2
exit 1
;;
esac
done
# Current date and time
CURRENT_DATE=$(date +"%Y%m%d%H%M%S")
# Create a backup file name
BACKUP_FILE="$BACKUP_DIR/db_backup_$CURRENT_DATE.toml"
# Execute the database dump command
if [ "$DRY_RUN" = true ]; then
echo "Dry run mode: Database backup command will not be executed."
else
$DB_DUMP_CMD $BACKUP_FILE
fi
# Check if the database dump was successful
if [ $? -eq 0 ]; then
echo "Database backup completed successfully."
# Sort backup files by modification time in ascending order
sorted_backup_files=($(ls -t "$BACKUP_DIR"))
# Determine number of backups to delete
num_backups_to_delete=$((${#sorted_backup_files[@]} - NUM_BACKUPS_TO_KEEP))
cd $BACKUP_DIR
# Delete oldest backups if necessary
if [ $num_backups_to_delete -gt 0 ]; then
for ((i = 0; i < $num_backups_to_delete; i++)); do
if [ "$DRY_RUN" = true ]; then
echo "Dry run mode: Would remove old backup: ${sorted_backup_files[$i]}"
else
rm "${sorted_backup_files[$i]}"
echo "Removed old backup: ${sorted_backup_files[$i]}"
fi
done
fi
else
echo "Error: Database backup failed."
fi
chmod +x /opt/fabinfra/scripts/bffh-backup.sh
Das Script kann einzeln getestet werden. Es kann mit Parametern gestartet werden:
- n = Anzahl der aufzuhebenden Backups
- r = dry run
# Trockenlauf (dry run) - nur testen und nichts löschen
/opt/fabinfra/scripts/bffh-backup.sh -r -n 2
# Backup durchführen und nur die letzten 5 aufheben, alle anderen löschen
/opt/fabinfra/scripts/bffh-backup.sh -n 5
Backup-Script mit systemd Timer
Das Script kann als timed Service eingebunden werden, um es so zu automatisieren. Unter Beachtung obiger Parameter in ExecStart
kann folgendes eingebunden werden:
vim /opt/fabinfra/scripts/bffh-backup.service
[Unit]
Description=BFFH Backup Service
[Service]
Type=oneshot
ExecStart=/opt/fabinfra/scripts/bffh-backup.sh -n 10
Außerdem als Timer. Dieser muss den gleichen Name haben wie der Service (siehe https://wiki.ubuntuusers.de/systemd/Timer_Units)
vim /opt/fabinfra/scripts/bffh-backup.timer
[Unit]
Description=BFFH Backup Timer
[Timer]
# Run every day at midnight
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
Wir aktivieren und starten das Backup schließlich einmal manuell und prüfen dessen Ausgabe.
Hinweis: Da wir einen Timer für den Service verwenden, müssen wir den Service nicht "enablen". Denn das macht der Timer selbst.
sudo ln -sf /opt/fabinfra/scripts/bffh-backup.service /etc/systemd/system/bffh-backup.service
sudo ln -sf /opt/fabinfra/scripts/bffh-backup.timer /etc/systemd/system/bffh-backup.timer
sudo systemctl daemon-reload
sudo systemctl start bffh-backup.service
journal -f -u bffh-backup.service
Backup-Script mit cron
Wer lieber auf einen klassischen Cronjob setzten möchte, kann statt dem Service folgendes machen:
sudo vim /etc/cron.d/bffh-backup
#“At 00:00.”
0 0 * * 0 bffh /opt/fabinfra/scripts/bffh-backup.sh -n 10
Der Cronjob wird um 00:00 Uhr vom Benutzer bffh
gestartet. Es gibt keinen Log Output. Dieser lässt sich jedoch leicht ergänzen.
Nutzerdatenbank laden / hashen / prüfen
Nutzerdatenbank laden / hashen
Für das Laden und ggf. Rehashen der Nutzerdatenbank (users.toml) kann folgendes Script genutzt werden. Es prüft zunächst, ob die Konfiguration von bffh valid ist. Wenn nicht, wird nichts unternommen. Im Anschluss werden die Nutzer neu geladen und gleichzeitig wieder exportiert, um etwaige unverschüsselte Passwörter automatisch zu hashen.
Hinweis: Das Script basiert auf einem auf dem System aktiven systemd Service namens bffh.service
mkdir -p /opt/fabinfra/scripts/
vim /opt/fabinfra/scripts/bffh-load-users.sh
#/!bin/bash
#check config
BASE="/opt/fabinfra/bffh/target/release"
DATA="/opt/fabinfra/bffh-data"
CFG="$DATA/config/bffh.dhall"
USERS="$DATA/config/users.toml"
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
echo "-h|--help - show help"
echo "-r|--rehash - overwrite users.toml with hashed passwords (ensure secure secrets)"
exit 1
;;
-r|--rehash)
REHASH="y"
shift
;;
-*)
;;
esac
done
echo "use -h|--help to show additional script options!"
$BASE/bffhd --check --config $CFG > /dev/null
if [ $? == 0 ]; then
#pre-check bffh.dhall
echo "Config is valid. Loading users ..."
$BASE/bffhd --verbose --config $CFG --load=$USERS
if [ $? == 0 ]; then
#then load users.toml
$BASE/bffhd --verbose --config $CFG --dump-users /tmp/users.toml --force
else
echo "Error: Newly given users.toml is invalid!"
exit 1
fi
#if this was successful and service is running, restart it, elso do nothing
if [ $? == 0 ]; then
if [[ $REHASH == "y" ]]; then #overwrite users if --rehash option is given (not null)
echo "Rehasing users.toml!"
cat /tmp/users.toml > $USERS
rm /tmp/users.toml
fi
FAS="bffh.service"
STATUS="$(systemctl is-active $FAS)"
if [ "${STATUS}" = "active" ]; then
echo "restarting $FAS"
systemctl restart $FAS
else
echo -e "\n\n$FAS not active/existing. Not restarting bffh service!"
fi
fi
else
echo "Error: Currently loaded users.toml is invalid!"
exit 1
fi
chmod +x /opt/fabinfra/scripts/bffh-load-users.sh
Nutzerdankenbank prüfen
Folgendes Python-Script erlaubt die Auswertung einer users.toml Datei. Es zählt die Nutzer und deren zugewiesene Rollen, validiert eventuell vergebene Cardkeys und gibt Hinweise bei möglichen Datenbankfehlern.
Das Script benötigt mindestens Python 3.11. Erst ab dieser Version wird tomllib
unterstützt!
vim /opt/fabinfra/scripts/show-users-toml-stats.py
'''
This script validates users.toml for several aspects
The script requires at least Python 3.11
Written by Mario Voigt (vmario89) - Stadtfabrikanten e.V. - 2024
ToDos
- enter bffh.dhall path to check roles against users.toml. If our toml contains roles, which bffh does not know, we should also warn!
'''
import argparse
import os
import sys
import tomllib
import uuid
'''
cardkeys for FabAccess use Uuid format in Version v4 (see https://docs.rs/uuid/latest/uuid/struct.Uuid.html)
allowed formattings:
- simple: a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8
- hyphenated: a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8
- urn: urn:uuid:A1A2A3A4-B1B2-C1C2-D1D2-D3D4D5D6D7D8
- braced: {a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8}
'''
def is_valid_uuid(val):
try:
_uuid = uuid.UUID(val, version=4)
return True
except ValueError:
return False
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--db", type=str, help="path of users.toml user database file")
args = parser.parse_args()
if args.db is None:
print("Error: no users.toml given. Please add with '--db </path/to/users.toml>'")
sys.exit(1)
countUsers = 0
countUsersWithoutCardkeyOrPassword = 0
uniqueRoles = []
countUserWithoutRoles = 0
countPassword = 0
countPasswordUnencrypted = 0
countPasswordEncrypted = 0
countCardkey = 0
countCardkeyInvalid = 0
countUnknownKeys = 0
countWarnings = 0
#a definition of valid keys within a user section of FabAccess
knownKeys = ['roles', 'passwd', 'cardkey']
usertoml = args.db
print("{} Checking database {}\n".format("*"*25, "*"*25))
file_stats = os.stat(usertoml)
#print(file_stats)
print("Database size: {} Bytes ({:0.5f} MB)".format(file_stats.st_size, file_stats.st_size / (1024 * 1024)))
if file_stats.st_size == 0:
print("Error: File size is zero! Database is corrupted!")
sys.exit(1)
print("\n")
with open(usertoml, "rb") as f:
try:
data = tomllib.load(f)
except Exception as e:
if "Cannot declare" in str(e) and "twice" in str(e):
print("Error: found at least one duplicate user. Cannot parse database. Please fix and try again. Message: {}".format(str(e)))
elif "Invalid value" in str(e):
print("Error: Some user contains a key without value (e.g. 'passwd = '). Cannot parse database. Please fix and try again. Message: {}".format(str(e)))
elif "Expected '=' after a key" in str(e):
print("Error: Found an incorrect key/value mapping. Cannot parse database. Please fix and try again. Message: {}".format(str(e)))
else:
print(str(e))
sys.exit(1)
for user in data:
print("--- {}".format(user))
for key in data[user].keys():
if key not in knownKeys:
print("Warning: User '{}' contains unknown key '{}' (will be ignored by BFFH server)".format(user, key))
countWarnings += 1
countUnknownKeys += 1
if "roles" in data[user]:
roles = data[user]["roles"]
for role in roles:
if role not in uniqueRoles:
uniqueRoles.append(role)
if roles is None: #if role key is defined but empty
countUserWithoutRoles += 1
else: #if role key is not existent
countUserWithoutRoles += 1
if "passwd" in data[user]:
passwd = data[user]["passwd"]
countPassword += 1
if passwd.startswith("$argon2") is False:
print("Warning: Password for user '{}' is not encrypted!".format(user))
countWarnings += 1
countPasswordUnencrypted += 1
else:
countPasswordEncrypted += 1
if "cardkey" in data[user]:
cardkey = data[user]["cardkey"]
if is_valid_uuid(cardkey) is False:
print("Warning: Cardkey for user '{}' contains invalid cardkey (no UUID v4)".format(user))
countCardkeyInvalid += 1
countWarnings += 1
countCardkey += 1
if "passwd" not in data[user] and "cardkey" not in data[user]:
countUsersWithoutCardkeyOrPassword += 1
countUsers += 1
print("\n")
print("\n")
if countUsers == 0:
print("Error: Database does not contain any users!")
sys.exit(1)
print("{} Database statistics {}\n".format("*"*25, "*"*25))
print("- Total users: {}".format(countUsers))
print("- Total unique roles: {}".format(len(uniqueRoles)))
print("- Total passwords: {} (encrypted: {}, unencrypted: {})".format(countPassword, countPasswordEncrypted, countPasswordUnencrypted))
print("- Total cardkeys: {}".format(countCardkey))
print("\n")
print("{} Important information {}\n".format("*"*25, "*"*25))
if countUnknownKeys > 0:
print("- {} unknown keys (will be ignored by BFFH server)".format(countUnknownKeys))
if countUserWithoutRoles > 0:
print("- {} users without any roles. They won't be able to do something as client!".format(countUserWithoutRoles))
if len(uniqueRoles) == 0:
print("- Globally, there are no roles assigned for any user. They won't be able to do something as client!")
if countCardkeyInvalid > 0:
print("- {} invalid cardkeys in your database. They won't be able to authenticate at BFFH server by keycard!".format(countCardkeyInvalid))
if countUsersWithoutCardkeyOrPassword > 0:
print("- {} users without both: password and cardkey. They won't be able to login anyhow!".format(countUsersWithoutCardkeyOrPassword))
if countWarnings > 0:
print("- {} warnings in total. You might need to optimize your user database!".format(countWarnings))
if __name__ == "__main__":
main()
Script benutzen:
python3 /opt/fabinfra/scripts/show-users-toml-stats.py --db /opt/fabinfra/bffh-data/config/users.toml
FabAccess State Script (Email-Warnung bei Fehlerzustand)
Ein kleines Helferscript, was an ein beliebiges Mail-Postfach eine Email sendet, wenn unser systemd Service bffh.service
nicht mehr korrekt läuft. Falls der Service nicht installiert wurde, dann wird das Script einen Fehler werfen.
Wir verwenden außerdem im Script das smarte smtp-cli Tool von mludvig: https://github.com/mludvig/smtp-cli/tags. Es kann jedoch auch jeder andere beliebige Mail-Client verwendet werden, um cli-basierte Nachrichten zu versenden. Kleiner Fix: https://github.com/mludvig/smtp-cli/issues/28
# smtp-cli installieren
sudo apt install cpanminus
sudo cpanm Net::DNS
sudo apt install libio-socket-ssl-perl libdigest-hmac-perl libterm-readkey-perl libmime-lite-perl libfile-libmagic-perl libio-socket-inet6-perl
cd /opt
sudo wget https://github.com/mludvig/smtp-cli/archive/refs/tags/v3.10.zip
sudo unzip v3.10.zip
sudo rm v3.10.zip
mkdir -p /opt/fabinfra/scripts/
vim /opt/fabinfra/scripts/bffh-state.sh
#/!bin/bash
SMTP_SERVER="smtp.fablabchemnitz.de:587"
SMTP_MAILBOX="REDACTED"
SMTP_PW='REDACTED'
FROM="fabaccess.noreply@stadtfabrikanten.org"
TO="webmaster@stadtfabrikanten.org"
SUBJECT="FabAccess BFFH Service failed"
MAILFILE="/tmp/mail.txt"
systemctl status bffh.service 2>&1 > ${MAILFILE}
if [ $? != 0 ]; then #wenn exit code von systemd unit nicht 0
cat ${MAILFILE}
/opt/smtp-cli-3.10/smtp-cli --server ${SMTP_SERVER} --user ${SMTP_MAILBOX} --password ${SMTP_PW} --from "${FROM}" --to "${TO}" --subject "${SUBJECT}" --body-plain ${MAILFILE}
fi
chmod +x /opt/fabinfra/scripts/bffh-state.sh
Und dann testen wir das Script. Es gibt den Status auf der Kommandozeile aus und sollte parallel eine Email an das Zielpostfach mit gleichem Inhalt senden.
DESFire CardKeys generieren
Ein Script zum Erzeugen von passenden DESFire EV2 CardKeys per Kommandozeile (Linux)
mkdir -p /opt/fabinfra/scripts/
vim /opt/fabinfra/scripts/generate-cardkey.sh
#/!bin/bash
hexdump -vn16 -e'4/4 "%08X" 1 "\n"' /dev/urandom
chmod +x /opt/fabinfra/scripts/generate-cardkey.sh
Der Output kann dann in jeder individuellen Nutzersektion mit dem Schlüsselwort cardkey
in die users.toml
eingebunden werden:
cardkey = "70AFE9E6B1D6352313C2D336ADC2777A"
Siehe hier.
Echtzeituhr (RTC) Modul DS3231 für Raspberry Pi
Wer FabAccess auf einem Raspberry Pi betreiben möchte und eine Echtzeituhr für genaue und unabhängige Zeitstempel wünscht, kann ein RTC Modul installieren und konfigurieren:
Diese Anleitung basiert auf https://learn.adafruit.com/adding-a-real-time-clock-to-raspberry-pi/set-rtc-time
Das von uns verbaute Uhrenmodell ist DS3231
Wir fügen in der Boot-Konfiguration folgende Device Tree Overlays (dtoverlay
) ein:
sudo vim /boot/config.txt
# Additional overlays and parameters are documented /boot/overlays/README
dtoverlay=i2c-rtc,ds3231
sudo vim /etc/modules
i2c-bcm2708
i2c_dev
Danach starten wir den Pi neu
sudo reboot
Nach dem Restart prüfen wir, ob unsere Echtzeituhr verfügbar ist.
i2cdetect -y 1 #should be visible at 0x68
Wenn ja, dann können wir die standardmäßig installierte "Fake Hardware Clock" deinstallieren:
sudo apt remove fake-hwclock
sudo update-rc.d -f fake-hwclock remove
sudo systemctl disable fake-hwclock
Noch etwas nachstellen und dann die Uhr in Betrieb nehmen:
sudo vim /lib/udev/hwclock-set
#!/bin/sh
# Reset the System Clock to UTC if the hardware clock from which it was copied by the kernel was in localtime.
dev=$1
/sbin/hwclock --rtc=$dev --hctosys
sudo hwclock -r
sudo hwclock --verbose -r
sudo hwclock -w #write the time
sudo hwclock -r #read the time