Co-authored-by: Drew DeVault <sir@cmpwn.com>
---
I rebased Drew's patch onto current master. I had to change two things
to make the script work correctly.
1. install_mounted_root: Move adding cryptsetup to initfs_features
before generating mkinitfs.conf.
2. init_progs: Add cryptsetup package if using encryption.
I have tested the script with syslinux (no EFI). Now there is a question
about making it work with GRUB (EFI). This would require:
1. changing luksFormat type to luks1 as GRUB2 does not support luks2;
2. adding a workaround in order for grub-install to succeed (it is not
called from a chroot so it complains about GRUB_ENABLE_CRYPTODISK=y
not being present in /etc/default/grub, we can temporarily replace
/etc/default/grub with "$mnt"/etc/default/grub);
I would appreciate feedback about those two issues before I start
working on this again.
setup-disk.in | 120 ++++++++++++++++++++++++++++++++++++++++----------
1 file changed, 96 insertions(+), 24 deletions(-)
diff --git a/setup-disk.in b/setup-disk.in
index 656b5bc..e617101 100644
--- a/setup-disk.in+++ b/setup-disk.in
@@ -82,6 +82,26 @@ enumerate_fstab() {
done
}
+# given an fstab on stdin, determine if any of the mountpoints are encrypted+crypt_required() {+ while read devname mountpoint fstype mntops freq passno; do+ if [ -z "$devname" ] || [ "${devname###}" != "$devname" ]; then+ continue+ fi+ uuid="${devname##UUID=}"+ if [ "$uuid" != "$devname" ]; then+ devname="$(blkid --uuid "$uuid")"+ fi+ mapname="${devname##/dev/mapper/}"+ if [ "$mapname" != "$devname" ]; then+ if cryptsetup status "$mapname" >&1 >/dev/null; then+ return 0+ fi+ fi+ done+ return 1+}+is_vmware() {
grep -q VMware /proc/scsi/scsi 2>/dev/null \
|| grep -q VMware /proc/ide/hd*/model 2>/dev/null
@@ -427,7 +447,7 @@ setup_raspberrypi_bootloader() {
install_mounted_root() {
local mnt="$1"
shift 1
- local disks="${@}" mnt_boot= boot_fs= root_fs=+ local disks="${@}" mnt_boot= boot_fs= root_fs= use_crypt= local initfs_features="ata base ide scsi usb virtio"
local pvs= dev= rootdev= bootdev= extlinux_raidopt= root= modules=
local kernel_opts="$KERNELOPTS"
@@ -490,7 +510,6 @@ install_mounted_root() {
esac
done
- if [ -n "$VERBOSE" ]; then
echo "Root device: $rootdev"
echo "Root filesystem: $root_fs"
@@ -513,6 +532,28 @@ install_mounted_root() {
# we should not try start modloop on sys install
rm -f "$mnt"/etc/runlevels/*/modloop
+ # generate the fstab+ if [ -f "$mnt"/etc/fstab ]; then+ mv "$mnt"/etc/fstab "$mnt"/etc/fstab.old+ fi+ enumerate_fstab "$mnt" >> "$mnt"/etc/fstab+ if [ -n "$SWAP_DEVICES" ]; then+ local swap_dev+ for swap_dev in $SWAP_DEVICES; do+ echo -e "$(uuid_or_device ${swap_dev})\tswap\tswap\tdefaults\t0 0" \+ >> "$mnt"/etc/fstab+ done+ fi+ cat >>"$mnt"/etc/fstab <<-__EOF__+ /dev/cdrom /media/cdrom iso9660 noauto,ro 0 0+ /dev/usbdisk /media/usb vfat noauto 0 0+ __EOF__++ if crypt_required <"$mnt"/etc/fstab; then+ use_crypt=1+ initfs_features="${initfs_features% cryptsetup} cryptsetup"+ fi+ # generate mkinitfs.conf
mkdir -p "$mnt"/etc/mkinitfs/features.d
echo "features=\"$initfs_features\"" > "$mnt"/etc/mkinitfs/mkinitfs.conf
@@ -530,24 +571,14 @@ install_mounted_root() {
if [ -n "$(get_bootopt nomodeset)" ]; then
kernel_opts="nomodeset $kernel_opts"
fi
+ if [ "$use_crypt" ] && cryptsetup status "$rootdev" 2>&1 >/dev/null; then+ # Boot to encrypted root+ root=$(cryptsetup status "$rootdev" | awk '/device:/ { print $2 }')+ kernel_opts="cryptroot=$root cryptdm=root"+ root=/dev/mapper/root+ fi modules="sd-mod,usb-storage,${root_fs}${raidmod}"
- # generate the fstab- if [ -f "$mnt"/etc/fstab ]; then- mv "$mnt"/etc/fstab "$mnt"/etc/fstab.old- fi- enumerate_fstab "$mnt" >> "$mnt"/etc/fstab- if [ -n "$SWAP_DEVICES" ]; then- local swap_dev- for swap_dev in $SWAP_DEVICES; do- echo -e "$(uuid_or_device ${swap_dev})\tswap\tswap\tdefaults\t0 0" \- >> "$mnt"/etc/fstab- done- fi- cat >>"$mnt"/etc/fstab <<-__EOF__- /dev/cdrom /media/cdrom iso9660 noauto,ro 0 0- /dev/usbdisk /media/usb vfat noauto 0 0- __EOF__ # remove the installed db in case its there so we force re-install
rm -f "$mnt"/var/lib/apk/installed "$mnt"/lib/apk/db/installed
echo "Installing system on $rootdev:"
@@ -595,6 +626,10 @@ unmount_partitions() {
# unmount the partitions
umount $(awk '{print $2}' /proc/mounts | egrep "^$mnt(/|\$)" | sort -r)
++ if [ "$USE_CRYPT" ]; then+ cryptsetup close /dev/mapper/root+ fi}
# figure out decent default swap size in mega bytes
@@ -697,9 +732,10 @@ select_bootloader_pkg() {
# install needed programs
init_progs() {
- local raidpkg= lvmpkg= fs= fstools= grub=+ local raidpkg= lvmpkg= cryptpkg= fs= fstools= grub= [ -n "$USE_RAID" ] && raidpkg="mdadm"
[ -n "$USE_LVM" ] && lvmpkg="lvm2"
+ [ -n "$USE_CRYPT" ] && cryptpkg="cryptsetup" for fs in $BOOTFS $ROOTFS $VARFS; do
# we need load btrfs module early to avoid the error message:
# 'failed to open /dev/btrfs-control'
@@ -714,7 +750,7 @@ init_progs() {
vfat) fstools="$fstools dosfstools";;
esac
done
- apk add --quiet sfdisk $lvmpkg $raidpkg $fstools $@+ apk add --quiet sfdisk $cryptpkg $lvmpkg $raidpkg $fstools $@}
show_disk_info() {
@@ -1082,6 +1118,18 @@ native_disk_install_lvm() {
setup_root $root_dev $BOOT_DEV
}
+setup_crypt() {+ local dev="$1" local dmname="$2"+ mkdir -p /run/cryptsetup+ echo "Preparing root partition for encryption." >&2+ echo "You will be prompted for your password at boot." >&2+ echo "If you forget your password, your data will be lost." >&2+ cryptsetup luksFormat --type luks2 "$dev" >&2+ echo "Enter password again to unlock disk for installation." >&2+ cryptsetup open "$dev" "$dmname" >&2+ echo "/dev/mapper/$dmname"+}+native_disk_install() {
local prep_part_type=$(partition_id prep)
local root_part_type=$(partition_id linux)
@@ -1156,6 +1204,10 @@ native_disk_install() {
root_dev=$(find_nth_non_boot_parts $index "$root_part_type" $@)
fi
+ if [ "$USE_CRYPT" ]; then+ root_dev=$(setup_crypt $root_dev root)+ fi+ [ $SWAP_SIZE -gt 0 ] && setup_swap_dev $swap_dev
setup_root $root_dev $BOOT_DEV $@
}
@@ -1176,7 +1228,7 @@ diskselect_help() {
diskmode_help() {
cat <<-__EOF__
- You can select between 'sys', 'data', 'lvm', 'lvmsys' or 'lvmdata'.+ You can select between 'sys', 'cryptsys', 'data', 'lvm', 'lvmsys' or 'lvmdata'. sys:
This mode is a traditional disk install. The following partitions will be
@@ -1184,6 +1236,12 @@ diskmode_help() {
This mode may be used for development boxes, desktops, virtual servers, etc.
+ cryptsys:+ This mode is equivalent to sys, except that the root filesystem will be+ encrypted with cryptsetup. You will be prompted to enter a decryption+ password, and will need to use this password to boot up the operating+ system after installation.+ data:
This mode uses your disk(s) for data storage, not for the operating system.
The system itself will run from tmpfs (RAM).
@@ -1233,7 +1291,7 @@ ask_disk() {
usage() {
cat <<-__EOF__
- usage: setup-disk [-hLqrv] [-k kernelflavor] [-m MODE] [-o apkovl] [-s SWAPSIZE]+ usage: setup-disk [-hLqrve] [-k kernelflavor] [-m MODE] [-o apkovl] [-s SWAPSIZE] [MOUNTPOINT | DISKDEV...]
Install alpine on harddisk.
@@ -1247,6 +1305,7 @@ usage() {
options:
-h Show this help
+ -e Encrypt disk -m Use disk for MODE without asking, where MODE is either 'data' or 'sys'
-o Restore system from given apkovl file
-k Use kernelflavor instead of $KERNEL_FLAVOR
@@ -1286,11 +1345,13 @@ case $kver in
*) KERNEL_FLAVOR=lts;;
esac
+USE_CRYPT=DISK_MODE=
USE_LVM=
# Parse args
-while getopts "hk:Lm:o:qrs:v" opt; do+while getopts "hek:Lm:o:qrs:v" opt; do case $opt in
+ e) USE_CRYPT=1;; m) DISK_MODE="$OPTARG";;
k) KERNEL_FLAVOR="$OPTARG";;
L) USE_LVM="_lvm";;
@@ -1319,6 +1380,12 @@ fi
reset_var
swapoff -a
+if [ $USE_CRYPT -eq 1 ] && [ -n "$USE_LVM" ]; then+ echo "cryptsys and lvm are currently mutually incompatible."+ echo "Please run $0 again with only one of these options selected."+ exit 1+fi+# stop all volume groups in use
vgchange --ignorelockingfailure -a n >/dev/null 2>&1
@@ -1381,9 +1448,14 @@ if [ -n "$diskdevs" ] && [ -z "$DISK_MODE" ]; then
echo "The following $disk_is_or_disks_are selected${USE_LVM:+ (with LVM)}:"
show_disk_info $diskdevs
_lvm=${USE_LVM:-", 'lvm'"}
- ask "How would you like to use $it_them? ('sys', 'data'${_lvm#_lvm} or '?' for help)" "?"+ ask "How would you like to use $it_them? ('sys', 'cryptsys', 'data'${_lvm#_lvm} or '?' for help)" "?" case "$resp" in
'?') diskmode_help;;
+ cryptsys)+ resp=sys+ USE_CRYPT=1+ break+ ;; sys|data) break;;
lvm) USE_LVM="_lvm" ;;
nolvm) USE_LVM="";;
--
2.32.0
Hello,
Thanks for the patch and contribution. I've added my feedback to everything below.
> I have tested the script with syslinux (no EFI). Now there is a question> about making it work with GRUB (EFI). This would require:> > 1. changing luksFormat type to luks1 as GRUB2 does not support luks2;
GRUB has recently added support for luks2 but the mkimage wrapper doesn't include the required modules so the support is not as automated as it should be.
I would instead go for a encrypted root with a unencrypted /boot to avoid using luks1 for everything and it would simplify the setup. This is not "full-disk" encryption as one would want but it doesn't have the limitation that luks1 has.
"LUKS provides a generic key store on the dedicated area on a disk, with the ability to use multiple passphrases to unlock a stored key. LUKS2 extends this concept for more flexible ways of storing metadata, redundant information to provide recovery in the case of corruption in a metadata area, and an interface to store externally managed metadata for integration with other tools."
https://gitlab.com/cryptsetup/cryptsetup/blob/master/docs/on-disk-format-luks2.pdf> 2. adding a workaround in order for grub-install to succeed (it is not> called from a chroot so it complains about GRUB_ENABLE_CRYPTODISK=y> not being present in /etc/default/grub, we can temporarily replace> /etc/default/grub with "$mnt"/etc/default/grub);
A temporary symlink could work for this instead of making a full copy. Alternatively, a change of root would work as well but that requires setting up the chroot so that grub-install has the required paths.
I have also added some feedback to the patch:
> setup-disk.in | 120 ++++++++++++++++++++++++++++++++++++++++----------> 1 file changed, 96 insertions(+), 24 deletions(-)> > diff --git a/setup-disk.in b/setup-disk.in> index 656b5bc..e617101 100644> --- a/setup-disk.in> +++ b/setup-disk.in> @@ -82,6 +82,26 @@ enumerate_fstab() {> done> }> > +# given an fstab on stdin, determine if any of the mountpoints are encrypted> +crypt_required() {> + while read devname mountpoint fstype mntops freq passno; do
I would use the '-r' flag for read as fstab has no escapes.
> + if [ -z "$devname" ] || [ "${devname###}" != "$devname" ]; then> + continue> + fi> + uuid="${devname##UUID=}"> + if [ "$uuid" != "$devname" ]; then> + devname="$(blkid --uuid "$uuid")"> + fi> + mapname="${devname##/dev/mapper/}"> + if [ "$mapname" != "$devname" ]; then> + if cryptsetup status "$mapname" >&1 >/dev/null; then> + return 0> + fi> + fi> + done> + return 1> +}> +> is_vmware() {> grep -q VMware /proc/scsi/scsi 2>/dev/null \> || grep -q VMware /proc/ide/hd*/model 2>/dev/null> @@ -427,7 +447,7 @@ setup_raspberrypi_bootloader() {> install_mounted_root() {> local mnt="$1"> shift 1> - local disks="${@}" mnt_boot= boot_fs= root_fs=> + local disks="${@}" mnt_boot= boot_fs= root_fs= use_crypt=> local initfs_features="ata base ide scsi usb virtio"> local pvs= dev= rootdev= bootdev= extlinux_raidopt= root= modules=> local kernel_opts="$KERNELOPTS"> @@ -490,7 +510,6 @@ install_mounted_root() {> esac> done> > -> if [ -n "$VERBOSE" ]; then> echo "Root device: $rootdev"> echo "Root filesystem: $root_fs"> @@ -513,6 +532,28 @@ install_mounted_root() {> # we should not try start modloop on sys install> rm -f "$mnt"/etc/runlevels/*/modloop> > + # generate the fstab> + if [ -f "$mnt"/etc/fstab ]; then> + mv "$mnt"/etc/fstab "$mnt"/etc/fstab.old> + fi> + enumerate_fstab "$mnt" >> "$mnt"/etc/fstab> + if [ -n "$SWAP_DEVICES" ]; then> + local swap_dev> + for swap_dev in $SWAP_DEVICES; do> + echo -e "$(uuid_or_device ${swap_dev})\tswap\tswap\tdefaults\t0 0" \> + >> "$mnt"/etc/fstab> + done> + fi> + cat >>"$mnt"/etc/fstab <<-__EOF__> + /dev/cdrom /media/cdrom iso9660 noauto,ro 0 0> + /dev/usbdisk /media/usb vfat noauto 0 0> + __EOF__> +> + if crypt_required <"$mnt"/etc/fstab; then> + use_crypt=1> + initfs_features="${initfs_features% cryptsetup} cryptsetup"
Isn't this the only place where cryptsetup can be added to the initfs_features? Wouldn't this duplicate the feature if it wasn't the last appended?
> + fi> +> # generate mkinitfs.conf> mkdir -p "$mnt"/etc/mkinitfs/features.d> echo "features=\"$initfs_features\"" > "$mnt"/etc/mkinitfs/mkinitfs.conf> @@ -530,24 +571,14 @@ install_mounted_root() {> if [ -n "$(get_bootopt nomodeset)" ]; then> kernel_opts="nomodeset $kernel_opts"> fi> + if [ "$use_crypt" ] && cryptsetup status "$rootdev" 2>&1 >/dev/null; then> + # Boot to encrypted root> + root=$(cryptsetup status "$rootdev" | awk '/device:/ { print $2 }')> + kernel_opts="cryptroot=$root cryptdm=root"
cryptsetup returns devices without unique identifiers which means this might fail when the order of devices found by the kernel changes.
> + root=/dev/mapper/root> + fi> modules="sd-mod,usb-storage,${root_fs}${raidmod}"> > - # generate the fstab> - if [ -f "$mnt"/etc/fstab ]; then> - mv "$mnt"/etc/fstab "$mnt"/etc/fstab.old> - fi> - enumerate_fstab "$mnt" >> "$mnt"/etc/fstab> - if [ -n "$SWAP_DEVICES" ]; then> - local swap_dev> - for swap_dev in $SWAP_DEVICES; do> - echo -e "$(uuid_or_device ${swap_dev})\tswap\tswap\tdefaults\t0 0" \> - >> "$mnt"/etc/fstab> - done> - fi> - cat >>"$mnt"/etc/fstab <<-__EOF__> - /dev/cdrom /media/cdrom iso9660 noauto,ro 0 0> - /dev/usbdisk /media/usb vfat noauto 0 0> - __EOF__> # remove the installed db in case its there so we force re-install> rm -f "$mnt"/var/lib/apk/installed "$mnt"/lib/apk/db/installed> echo "Installing system on $rootdev:"> @@ -595,6 +626,10 @@ unmount_partitions() {> > # unmount the partitions> umount $(awk '{print $2}' /proc/mounts | egrep "^$mnt(/|\$)" | sort -r)> +> + if [ "$USE_CRYPT" ]; then> + cryptsetup close /dev/mapper/root> + fi> }> > # figure out decent default swap size in mega bytes> @@ -697,9 +732,10 @@ select_bootloader_pkg() {> > # install needed programs> init_progs() {> - local raidpkg= lvmpkg= fs= fstools= grub=> + local raidpkg= lvmpkg= cryptpkg= fs= fstools= grub=> [ -n "$USE_RAID" ] && raidpkg="mdadm"> [ -n "$USE_LVM" ] && lvmpkg="lvm2"> + [ -n "$USE_CRYPT" ] && cryptpkg="cryptsetup"> for fs in $BOOTFS $ROOTFS $VARFS; do> # we need load btrfs module early to avoid the error message:> # 'failed to open /dev/btrfs-control'> @@ -714,7 +750,7 @@ init_progs() {> vfat) fstools="$fstools dosfstools";;> esac> done> - apk add --quiet sfdisk $lvmpkg $raidpkg $fstools $@> + apk add --quiet sfdisk $cryptpkg $lvmpkg $raidpkg $fstools $@> }> > show_disk_info() {> @@ -1082,6 +1118,18 @@ native_disk_install_lvm() {> setup_root $root_dev $BOOT_DEV> }> > +setup_crypt() {> + local dev="$1" local dmname="$2"> + mkdir -p /run/cryptsetup> + echo "Preparing root partition for encryption." >&2> + echo "You will be prompted for your password at boot." >&2> + echo "If you forget your password, your data will be lost." >&2> + cryptsetup luksFormat --type luks2 "$dev" >&2> + echo "Enter password again to unlock disk for installation." >&2> + cryptsetup open "$dev" "$dmname" >&2> + echo "/dev/mapper/$dmname"
I would check that the device is unlocked properly here and if not, prompt for a retry.
> +}> +> native_disk_install() {> local prep_part_type=$(partition_id prep)> local root_part_type=$(partition_id linux)> @@ -1156,6 +1204,10 @@ native_disk_install() {> root_dev=$(find_nth_non_boot_parts $index "$root_part_type" $@)> fi> > + if [ "$USE_CRYPT" ]; then> + root_dev=$(setup_crypt $root_dev root)> + fi> +> [ $SWAP_SIZE -gt 0 ] && setup_swap_dev $swap_dev> setup_root $root_dev $BOOT_DEV $@> }> @@ -1176,7 +1228,7 @@ diskselect_help() {> diskmode_help() {> cat <<-__EOF__> > - You can select between 'sys', 'data', 'lvm', 'lvmsys' or 'lvmdata'.> + You can select between 'sys', 'cryptsys', 'data', 'lvm', 'lvmsys' or 'lvmdata'.> > sys:> This mode is a traditional disk install. The following partitions will be> @@ -1184,6 +1236,12 @@ diskmode_help() {> > This mode may be used for development boxes, desktops, virtual servers, etc.> > + cryptsys:> + This mode is equivalent to sys, except that the root filesystem will be> + encrypted with cryptsetup. You will be prompted to enter a decryption> + password, and will need to use this password to boot up the operating> + system after installation.> +> data:> This mode uses your disk(s) for data storage, not for the operating system.> The system itself will run from tmpfs (RAM).> @@ -1233,7 +1291,7 @@ ask_disk() {> > usage() {> cat <<-__EOF__> - usage: setup-disk [-hLqrv] [-k kernelflavor] [-m MODE] [-o apkovl] [-s SWAPSIZE]> + usage: setup-disk [-hLqrve] [-k kernelflavor] [-m MODE] [-o apkovl] [-s SWAPSIZE]> [MOUNTPOINT | DISKDEV...]> > Install alpine on harddisk.> @@ -1247,6 +1305,7 @@ usage() {> > options:> -h Show this help> + -e Encrypt disk> -m Use disk for MODE without asking, where MODE is either 'data' or 'sys'> -o Restore system from given apkovl file> -k Use kernelflavor instead of $KERNEL_FLAVOR> @@ -1286,11 +1345,13 @@ case $kver in> *) KERNEL_FLAVOR=lts;;> esac> > +USE_CRYPT=> DISK_MODE=> USE_LVM=> # Parse args> -while getopts "hk:Lm:o:qrs:v" opt; do> +while getopts "hek:Lm:o:qrs:v" opt; do> case $opt in> + e) USE_CRYPT=1;;> m) DISK_MODE="$OPTARG";;> k) KERNEL_FLAVOR="$OPTARG";;> L) USE_LVM="_lvm";;> @@ -1319,6 +1380,12 @@ fi> reset_var> swapoff -a> > +if [ $USE_CRYPT -eq 1 ] && [ -n "$USE_LVM" ]; then> + echo "cryptsys and lvm are currently mutually incompatible."> + echo "Please run $0 again with only one of these options selected."> + exit 1> +fi> +> # stop all volume groups in use> vgchange --ignorelockingfailure -a n >/dev/null 2>&1> > @@ -1381,9 +1448,14 @@ if [ -n "$diskdevs" ] && [ -z "$DISK_MODE" ]; then> echo "The following $disk_is_or_disks_are selected${USE_LVM:+ (with LVM)}:"> show_disk_info $diskdevs> _lvm=${USE_LVM:-", 'lvm'"}> - ask "How would you like to use $it_them? ('sys', 'data'${_lvm#_lvm} or '?' for help)" "?"> + ask "How would you like to use $it_them? ('sys', 'cryptsys', 'data'${_lvm#_lvm} or '?' for help)" "?"> case "$resp" in> '?') diskmode_help;;> + cryptsys)> + resp=sys> + USE_CRYPT=1> + break> + ;;> sys|data) break;;> lvm) USE_LVM="_lvm" ;;> nolvm) USE_LVM="";;
--
Alex D.
RedXen System & Infrastructure Administration
https://redxen.eu/
Thanks for the review.
On Mon Aug 9, 2021 at 1:07 PM CEST, Alex Denes wrote:
> GRUB has recently added support for luks2 but the mkimage wrapper> doesn't include the required modules so the support is not as automated> as it should be.> I would instead go for a encrypted root with a unencrypted /boot to> avoid using luks1 for everything and it would simplify the setup. This> is not "full-disk" encryption as one would want but it doesn't have the> limitation that luks1 has.
Ok, this seems reasonable to me. I'll modify the mount point so that in the
case of GRUB with EFI we have unencrypted /boot.
It will be similar to current behavior when choosing syslinux.
Maybe in the meantime we'll get some more opinions on this issue.
> A temporary symlink could work for this instead of making a full copy.> Alternatively, a change of root would work as well but that requires> setting up the chroot so that grub-install has the required paths.
This workaround is not needed if /boot is unencrypted.
> > + cryptsetup open "$dev" "$dmname" >&2> > + echo "/dev/mapper/$dmname"> I would check that the device is unlocked properly here and if not,> prompt for a retry.
Is this really needed? cryptsetup open prompts for a retry when it fails.
> > > + cryptsetup open "$dev" "$dmname" >&2> > > + echo "/dev/mapper/$dmname"> > I would check that the device is unlocked properly here and if not,> > prompt for a retry.> > Is this really needed? cryptsetup open prompts for a retry when it fails.
Oh right, i have forgot about that. In that case the format should use the -y argument so that the entire setup doesn't have to be restarted due to a mistyped password during the format.
--
Alex D.
RedXen System & Infrastructure Administration
https://redxen.eu/