1
0
mirror of https://github.com/gryf/mkinitramfs.git synced 2026-04-27 06:21:25 +02:00

Compare commits

...

4 Commits

Author SHA1 Message Date
gryf 4633e0bc66 Added ability for providing username for ssh session. 2025-06-27 21:47:09 +02:00
gryf e1aef8338b Make config as a separate attribute for initramfs objects 2025-06-27 12:13:57 +02:00
gryf 7ecdc90baf Added dropbear support 2025-06-27 10:56:23 +02:00
gryf 7efac1607f Moved configuration to toml 2025-06-27 10:54:16 +02:00
2 changed files with 331 additions and 86 deletions
+84 -28
View File
@@ -7,46 +7,40 @@ Usage
-----
- Create encrypted disk or partition using `cryptsetup`_
- Create ``~/.config/mkinitramfs/disks.json`` file with similar content to:
- Create ``~/.config/mkinitramfs.toml`` file with similar content to:
.. code:: json
.. code:: toml
{
"name": {
"uuid": "disk-uuid",
"key": "key-filename"
},
...
}
[name]
uuid = "disk-uuid"
key = "key-filename"
where every entry have disk name (**name** in this case), which have two
attributes - disk/partition UUID and key filename.
...
where every entry have disk name (**name** in this case), which have at least
two attributes - disk/partition UUID and key filename.
- Provide a key file for the disk/partition. Assumption is, that it is an
encrypted file using `ccrypt`_ instead of plain file or password protected
luks. Keys will be looked using provided path, i.e.
.. code:: json
.. code:: toml
{
"laptop": {
"uuid": "88b99002-028f-4744-94e7-45e4580e2ddd",
"key": "/full/path/to/the/laptop.key"
},
"desktop": {
"uuid": "23e31327-1411-491c-ab00-c36f74c441f1",
"key": "desktop.key"
},
"pendrive": {
"uuid": "1453a45e-ca3f-4d39-8fd7-a6a96873c25c",
"key": "../pendrive.key"
}
}
[laptop]
uuid = "88b99002-028f-4744-94e7-45e4580e2ddd"
key = "/full/path/to/the/laptop.key"
[desktop]
uuid = "23e31327-1411-491c-ab00-c36f74c441f1"
key = "desktop.key"
[pendrive]
uuid = "1453a45e-ca3f-4d39-8fd7-a6a96873c25c"
key = "../pendrive.key"
so yes - it is possible to use key file in absolute or relative paths. If no
key will be found, it's been looking for in path specified by
``--key-path | -k`` parameter, which by default is in
``$XDG_CONFIG_HOME/mkinitramfs/keys`` (usually in
``~/.config/mkinitramfs/keys``.
``$XDG_DATA_HOME/keys`` (usually it will be ``~/.local/share/keys``).
- Move ``mkinitramfs.py`` script to some location in your ``$PATH`` (like
``~/bin``)
- Invoke ``mkinitramfs.py`` script:
@@ -62,6 +56,41 @@ Usage
on ``/boot`` with appropriate links. Note, that old images (they have
``.old`` suffix in the filename) will be removed in that case.
Configuration
-------------
Other than key path and device UUID, configuration can hold additional options
similar to those passed via commandline. Consider following example:
.. code:: toml
[laptop]
uuid = "88b99002-028f-4744-94e7-45e4580e2ddd"
key_path = "/full/path/to/the/keys/dir"
key = "laptop.key"
yubikey = true
dropbear = true
ip = '192.168.0.1'
gateway = '192.168.0.254'
netmask = '24'
user = 'gryf'
authorized_keys = "/full/path/to/the/.ssh/authorized_keys"
This will inform mkinitramfs script, that dropbear and yubikey features are
enabled. Also for network related configuration, there are last three options.
The complete list of supported options is listed below:
- ``copy_modules``
- ``no_key``
- ``key_path``
- ``key``
- ``disk_label``
- ``sdcard``
- ``yubikey``
- ``dropbear``
- ``user``
Using key devices
-----------------
@@ -77,7 +106,34 @@ There is possibility for using key which is encrypted using response from
challenge response using `ykchalresp`_ command. The challenge here could be
any string, so the name of the device from config is used.
Dropbear
--------
To unlock LUKS root filesystem remotely `dropbear`_ is used. There are expected
configuration options in ``mkinitramfs.toml`` file:
- ``dropbear`` - true or false, false by default
- ``iface`` interface name - ``eth0`` by default
- ``ip`` - static IP address
- ``netmask`` - netmask for the network
- ``gateway`` - gateway for the network
- ``user`` - username used for logging in, ``root`` by default. Note, whatever
username will be placed here, it will be ``root`` effectively anyway
- ``authorized_keys`` - path to ssh ``authorized_keys`` file. If there is no
user set - which mens root username is used, by default it will look for the
``/root/.ssh/authorized_keys``
You'll need to put at least ``ip``, ``netmask``, ``gateway`` to make this work
with defaults, with assumption that interface is ``eth0`` and ``root`` user
have needed ``authorized_keys`` file.
Then execute script with flag ``-b`` which include dropbear part.:
.. code:: shell-session
# mkinitramfs.py -b laptop
.. _ccrypt: https://sourceforge.net/projects/ccrypt/
.. _cryptsetup: https://gitlab.com/cryptsetup/cryptsetup/blob/master/README.md
.. _ykchalresp: https://github.com/Yubico/yubikey-personalization
.. _dropbear: https://matt.ucc.asn.au/dropbear/dropbear.html
+247 -58
View File
@@ -1,21 +1,22 @@
#!/usr/bin/env python
"""
Python2/3 compatible initrd generating script
Python initrd generating script
"""
import argparse
import json
import os
import shutil
import subprocess
import sys
import tempfile
import tomllib
XDG_CONFIG_HOME = os.getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
XDG_DATA_HOME = os.getenv('XDG_DATA_HOME',
os.path.expanduser('~/.local/share'))
CONF_PATH = os.path.join(XDG_CONFIG_HOME, 'mkinitramfs.json')
CONF_PATH = os.path.join(XDG_CONFIG_HOME, 'mkinitramfs.toml')
KEYS_PATH = os.path.join(XDG_DATA_HOME, 'keys')
ROOT_AK = '/root/.ssh/authorized_keys'
SHEBANG = "#!/bin/bash\n"
SHEBANG_ASH = "#!/bin/sh\n"
DEPS = """
@@ -25,6 +26,7 @@ DEPS=(
/sbin/cryptsetup
%(lvm)s
%(yubikey)s
%(dropbear)s
)
"""
# /usr/sbin/dropbear
@@ -45,6 +47,15 @@ for path in $(find /usr/lib/gcc|grep libgcc_s.so.1); do
[ "$(basename $(dirname $path))" = '32' ] && continue
cp $path lib/
done
if %s; then
if [ ! -f ~/.cache/askpass ]; then
wget "https://bitbucket.org/piotrkarbowski/better-initramfs/downloads/askpass.c"
gcc -Os -static askpass.c -o ~/.cache/askpass
rm askpass.c
fi
cp ~/.cache/askpass bin/
fi
"""
COPY_MODULES = """
KERNEL=$(readlink /usr/src/linux)
@@ -68,14 +79,11 @@ $CLEAR
export PATH=/bin
umask 0077
[ ! -d /proc ] && mkdir /proc
[ ! -d /tmp ] && mkdir /tmp
[ ! -d /mnt ] && mkdir /mnt
[ ! -d /new-root ] && mkdir /new-root
mount -t devtmpfs -o nosuid,relatime,size=10240k,mode=755 devtmpfs /dev
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
# clean i/o
exec >/dev/console </dev/console 2>&1
@@ -147,6 +155,49 @@ for counter in $(seq 3); do
done
"""
# optional: dropbear script for mounting device. It will use key if present
# and interactively prompt for password
DROPBEAR_SCRIPT = """
for counter in $(seq 3); do
sleep 1
$CLEAR
for dev in /dev/sd* /dev/nvme*; do
if cryptsetup isLuks ${dev}; then
if [ $(cryptsetup luksUUID ${dev}) = "${UUID}" ]; then
DEVICE=$dev
break
fi
fi
done
[ -n "${DEVICE}" ] && break
done
if [ -z "${DEVICE}" ]; then
echo "No LUKS device found to boot from! Giving up."
exit 1
fi
if [ ! -b /dev/mapper/root ]; then
for i in 0 1 2 ; do
askpass 'Enter decryption key: ' |ccrypt -c -k - $KEY | \
cryptsetup open --allow-discards $DEVICE root
ret=$?
[ ${ret} -eq 0 ] && break
done
fi
if [ ! -b /dev/mapper/root ]; then
echo "Failed to open encrypted device $DEVICE"
exit 2
else
echo "Successfully opened root device, continue booting."
fi
# Kill the process for interactively providing password
if [ ${ret} -eq 0 ]; then
killall ccrypt
fi
"""
# Open encrypted fs
INIT_OPEN = """
for counter in $(seq 3); do
@@ -201,14 +252,29 @@ done
"""
DROPBEAR = """\
mkdir /dev/pts
mount devpts /dev/pts -t devpts
ifconfig eth0 %(ip)s netmask %(netmask)s up
route add default gw %(gateway)s eth0
dropbear -s -g -p 22
"""
DECRYPT_PASSWORD = """
if [ ! -b /dev/mapper/root ]; then
for i in 0 1 2 ; do
ccrypt -c $KEY | cryptsetup open --allow-discards $DEVICE root
ret=$?
[ ${ret} -eq 0 ] && break
if [ -b /dev/mapper/root ]; then
break
fi
done
fi
if [ ! -b /dev/mapper/root ]; then
echo "Failed to open encrypted device. Rebooting in 5 seconds."
reboot -f -d 5
fi
"""
SWROOT = """
@@ -227,22 +293,67 @@ exec switch_root /new-root /sbin/init
"""
class Initramfs(object):
def __init__(self, args, disks):
self.lvm = args.lvm
self.yk = args.yubikey
self.name = args.disk
self.modules = args.copy_modules
self.key_path = args.key_path
self.disk_label = args.disk_label
self.sdcard = args.sdcard
self.install = args.install
self.no_key = args.no_key
class Config:
defaults = {'copy_modules': False,
'disk_label': None,
'dropbear': False,
'install': False,
'key_path': None,
'lvm': False,
'no_key': False,
'sdcard': None,
'yubikey': False}
def __init__(self, args, toml_conf):
self.drive = args.get('drive')
toml_ = toml_conf[self.drive]
for k, v in self.defaults.items():
setattr(self, k, toml_.get(k, v))
if getattr(self, k) is not args.get(k) and args.get(k) is not None:
setattr(self, k, args[k])
key = None
if not self.key_path and toml_.get('key'):
key = toml_.get('key')
if not os.path.exists(key):
key = os.path.join(KEYS_PATH, key)
if not os.path.exists(key):
sys.stderr.write(f'Cannot find key file for '
f'{toml_.get("key")}.\n')
sys.exit(5)
self.key_path = key
if not (self.key_path or self.no_key):
sys.stderr.write(f'key file for {self.drive} is not provided, '
f'while no-key option is not set.\n')
sys.exit(6)
# UUID is only available via config file
self.uuid = toml_.get('uuid')
# dropbear conf available only via config file
self.ip = toml_.get('ip')
self.gateway = toml_.get('gateway')
self.netmask = toml_.get('netmask')
self.iface = toml_.get('iface', 'eth0')
self.user = toml_.get('user', 'root')
if self.user != 'root' and not toml_.get('authorized_keys'):
sys.stderr.write(f'User {self.user} is not authorized for '
f'utilizing root .ssh/authorized_keys file. '
f'Set authorized_keys file path in'
f' configuration.')
sys.exit(7)
self.authorized_keys = toml_.get('authorized_keys', ROOT_AK)
class Initramfs(object):
def __init__(self, conf):
self.conf = conf
self.key = None
self.dirname = None
self.kernel_ver = os.readlink('/usr/src/linux').replace('linux-', '')
self._make_tmp()
self._disks = disks
def _make_tmp(self):
self.dirname = tempfile.mkdtemp(prefix='init_')
@@ -250,7 +361,7 @@ class Initramfs(object):
def _make_dirs(self):
os.chdir(self.dirname)
for dir_ in ('bin', 'dev', 'etc', 'keys', 'lib64', 'proc',
for dir_ in ('bin', 'dev', 'etc', 'keys', 'lib64', 'proc', 'root',
'run/cryptsetup', 'run/lock', 'sys', 'tmp'):
os.makedirs(os.path.join(self.dirname, dir_))
@@ -265,11 +376,13 @@ class Initramfs(object):
_fd, fname = tempfile.mkstemp(dir=self.dirname, suffix='.sh')
os.close(_fd)
with open(fname, 'w') as fobj:
lvm = '/sbin/lvscan\n/sbin/vgchange' if self.lvm else ''
yubikey = '/usr/bin/ykchalresp' if self.yk else ''
lvm = '/sbin/lvscan\n/sbin/vgchange' if self.conf.lvm else ''
yubikey = '/usr/bin/ykchalresp' if self.conf.yubikey else ''
dropbear = '/usr/sbin/dropbear' if self.conf.dropbear else ''
fobj.write(SHEBANG)
fobj.write(DEPS % {'lvm': lvm, 'yubikey': yubikey})
fobj.write(COPY_DEPS)
fobj.write(DEPS % {'lvm': lvm, 'yubikey': yubikey,
'dropbear': dropbear})
fobj.write(COPY_DEPS % 'true' if self.conf.dropbear else 'false')
# extra crap, which seems to be needed, but is not direct dependency
for root, _, fnames in os.walk('/usr/lib'):
@@ -280,14 +393,62 @@ class Initramfs(object):
if f.split('.')[0] in additional_libs:
shutil.copy(os.path.join(root, f), 'lib64',
follow_symlinks=False)
self._copy_dropbear_deps()
os.chmod(fname, 0b111101101)
subprocess.call([fname])
os.unlink(fname)
os.chdir(self.curdir)
def _copy_dropbear_deps(self):
if not self.conf.dropbear:
return
for dir_ in ('root/.ssh', 'etc/dropbear'):
os.makedirs(os.path.join(self.dirname, dir_))
additional_libs = ['libnss_compat', 'libnss_files']
for root, _, fnames in os.walk('/lib64'):
for f in fnames:
if f.split('.')[0] in additional_libs:
shutil.copy(os.path.join(root, f), 'lib64',
follow_symlinks=False)
shutil.copy('/etc/localtime', 'etc')
# Copy the authorized keys for your regular user you administrate with
if (self.conf.authorized_keys and
os.path.exists(self.conf.authorized_keys)):
shutil.copy(self.conf.authorized_keys, 'root/.ssh')
else:
sys.stderr.write(f'Warning {self.conf.authorized_keys} not found!')
# Copy OpenSSH's host keys to keep both initramfs' and regular ssh
# signed the same otherwise openssh clients will see different host
# keys and chicken out. Here we only copy the ecdsa host key, because
# ecdsa is default with OpenSSH. For RSA and others, copy adequate
# keyfile.
subprocess.run(['dropbearconvert', 'openssh', 'dropbear',
'/etc/ssh/ssh_host_ecdsa_key',
'etc/dropbear/dropbear_ecdsa_host_key'])
# Basic system defaults
with open('etc/passwd', 'w') as fobj:
fobj.write(f"{self.conf.user}:x:0:0:root:/root:/bin/sh\n")
with open('etc/shadow', 'w') as fobj:
fobj.write(f"{self.conf.user}:*:::::::\n")
with open('etc/group', 'w') as fobj:
fobj.write(f"{self.conf.user}:x:0:{self.conf.user}\n")
with open('etc/shells', 'w') as fobj:
fobj.write("/bin/sh\n")
os.chmod('etc/shadow', 0b110100000)
with open('etc/nsswitch.conf', 'w') as fobj:
fobj.write("passwd: files\n"
"shadow: files\n"
"group: files\n")
def _copy_modules(self):
if not self.modules:
if not self.conf.copy_modules:
return
os.chdir(self.dirname)
os.mkdir(os.path.join('lib', 'modules'))
@@ -318,42 +479,63 @@ class Initramfs(object):
os.symlink('busybox', command)
def _copy_key(self, suffix=''):
key_path = self._disks[self.name]['key'] + suffix
if not os.path.exists(key_path):
key_path = os.path.join(self.key_path,
self._disks[self.name]['key'] + suffix)
key_path = self.conf.key_path + suffix
if not os.path.exists(key_path):
self._cleanup()
sys.stderr.write(f'Cannot find key(s) file for {self.name}.\n')
sys.stderr.write(f'Cannot find key(s) file for '
f'{self.conf.drive}.\n')
sys.exit(2)
key_path = os.path.abspath(key_path)
os.chdir(self.dirname)
shutil.copy2(key_path, 'keys')
os.chdir(self.curdir)
if not (suffix or self.key):
# set self.key only when:
# - there is no key set to self
# - suffix is empty
# so that we could get the key name calculated for the yk
self.key = os.path.basename(key_path)
def _generate_init(self):
os.chdir(self.dirname)
with open('init', 'w') as fobj:
fobj.write(SHEBANG_ASH)
fobj.write(f"UUID='{self._disks[self.name]['uuid']}'\n")
fobj.write(f"KEY='/keys/{self._disks[self.name]['key']}'\n")
fobj.write(f"UUID='{self.conf.uuid}'\n")
if self.key:
fobj.write(f"KEY='/keys/{self.key}'\n")
fobj.write(INIT)
fobj.write(INIT_CMD)
if self.disk_label:
fobj.write(INIT_LABELED % {'label': self.disk_label})
if self.sdcard:
if self.conf.disk_label:
fobj.write(INIT_LABELED % {'label': self.conf.disk_label})
if self.conf.sdcard:
fobj.write(INIT_SD)
fobj.write(INIT_OPEN)
if self.disk_label or self.sdcard:
if self.conf.disk_label or self.conf.sdcard:
fobj.write(DECRYPT_KEYDEV)
if self.yk:
fobj.write(DECRYPT_YUBICP % {'disk': self.name})
if self.conf.yubikey:
fobj.write(DECRYPT_YUBICP % {'disk': self.conf.drive})
if self.conf.dropbear:
fobj.write(DROPBEAR % {'ip': self.conf.ip,
'gateway': self.conf.gateway,
'netmask': self.conf.netmask})
fobj.write(DECRYPT_PASSWORD)
if self.conf.dropbear:
fobj.write("killall dropbear\n")
fobj.write(SWROOT)
os.chmod('init', 0b111101101)
if self.conf.dropbear:
with open('root/decrypt.sh', 'w') as fobj:
fobj.write(SHEBANG_ASH)
fobj.write(f"UUID='{self.conf.uuid}'\n")
if self.key:
fobj.write(f"KEY='/keys/{self.key}'\n")
fobj.write(DROPBEAR_SCRIPT)
os.chmod('root/decrypt.sh', 0b111101101)
os.chdir(self.curdir)
def _mkcpio_arch(self):
@@ -372,7 +554,7 @@ class Initramfs(object):
os.chmod(self.cpio_arch, 0b110100100)
if self.install:
if self.conf.install:
self._make_boot_links()
else:
shutil.move(self.cpio_arch, 'initramfs.cpio')
@@ -412,31 +594,31 @@ class Initramfs(object):
self._copy_modules()
# self._copy_wlan_modules()
self._populate_busybox()
if not self.no_key:
if not self.conf.no_key:
self._copy_key()
if self.yk:
if self.conf.yubikey:
self._copy_key('.yk')
self._generate_init()
self._mkcpio_arch()
self._cleanup()
def _disks_msg():
sys.stdout.write('You need to create %s json file with the '
'contents:\n\n'
'{\n'
' "name": {\n'
' "uuid": "disk-uuid",\n'
' "key": "key-filename"\n'
' },\n'
' ...\n'
'}\n' % CONF_PATH)
def _disks_msg(msg=None):
if not msg:
sys.stdout.write('You need to create %s toml file with the '
'contents:\n\n'
'[name]\n'
'uuid = "disk-uuid"\n'
'key = "key-filename"\n'
'...\n' % CONF_PATH)
else:
sys.stdout.write(msg + '\n')
def _load_disks():
try:
with open(CONF_PATH) as fobj:
return json.load(fobj)
with open(CONF_PATH, 'rb') as fobj:
return tomllib.load(fobj)
except IOError:
_disks_msg()
sys.exit(1)
@@ -465,7 +647,7 @@ def main():
'assuming SD card/usb stick is the only way to open '
'encrypted root.')
parser.add_argument('-k', '--key-path', help='path to the location where '
'keys are stored', default=KEYS_PATH)
'keys are stored')
parser.add_argument('-d', '--disk-label', help='Provide disk label '
'to be read decription key from.')
parser.add_argument('-s', '--sdcard', help='Use built in sdcard reader to '
@@ -474,10 +656,17 @@ def main():
help='Enable LVM in init.')
parser.add_argument('-y', '--yubikey', action='store_true',
help='Enable Yubikey challenge-response in init.')
parser.add_argument('disk', choices=disks.keys(), help='Disk name')
parser.add_argument('-b', '--dropbear', action='store_true',
help='Enable dropbear ssh server for remotely connect '
'to initrd.')
parser.add_argument('drive', choices=disks.keys(), help='Drive name')
args = parser.parse_args()
init = Initramfs(args, disks)
if args.drive not in disks:
_disks_msg(f'Drive {args.drive} not found in configuration')
sys.exit(4)
conf = Config(args.__dict__, disks)
init = Initramfs(conf)
init.build()