mirror of
https://github.com/gryf/mkinitramfs.git
synced 2026-03-25 19:13:32 +01:00
Compare commits
9 Commits
9ea18fa68b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 156722a62f | |||
| 578af86baa | |||
| 610cfdaca7 | |||
| 21dcfae6b1 | |||
| 4633e0bc66 | |||
| e1aef8338b | |||
| 7ecdc90baf | |||
| 7efac1607f | |||
| 84f93c519f |
115
README.rst
115
README.rst
@@ -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,37 @@ 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. There is also ``askpass.c`` which origins
|
||||
from `better-initramfs`_ project, included in this repository just for
|
||||
preservation purposes.
|
||||
|
||||
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
|
||||
.. _better-initramfs: https://bitbucket.org/piotrkarbowski/better-initramfs
|
||||
|
||||
418
askpass.c
Normal file
418
askpass.c
Normal file
@@ -0,0 +1,418 @@
|
||||
/*
|
||||
* askpass.c - prompts a user for a passphrase using any suitable method
|
||||
* and prints the result to stdout.
|
||||
*
|
||||
* Copyright (C) 2008 David Härdeman <david@hardeman.nu>
|
||||
*
|
||||
* This package is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This package is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this package; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*
|
||||
* 2012 - Piotr Karbowski <piotr.karbowski@gmail.com>
|
||||
* * Dropped splasy code.
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#define _BSD_SOURCE
|
||||
#define _POSIX_C_SOURCE 1
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <termios.h>
|
||||
#include <sys/klog.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <signal.h>
|
||||
#include <dirent.h>
|
||||
#include <linux/vt.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/uio.h>
|
||||
|
||||
#define DEBUG 0
|
||||
|
||||
#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
|
||||
|
||||
static bool disable_method(const char *method);
|
||||
|
||||
/*****************************************************************************
|
||||
* Utility functions *
|
||||
*****************************************************************************/
|
||||
static void
|
||||
debug(const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
static bool first = true;
|
||||
static FILE *dbgfile;
|
||||
|
||||
if (!DEBUG)
|
||||
return;
|
||||
|
||||
if (first) {
|
||||
first = false;
|
||||
dbgfile = fopen("/tmp/askpass.debug", "a");
|
||||
}
|
||||
|
||||
if (!dbgfile)
|
||||
return;
|
||||
|
||||
va_start(ap, fmt);
|
||||
vfprintf(dbgfile, fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
static void
|
||||
usage(const char *arg0, const char *errmsg)
|
||||
{
|
||||
if (errmsg)
|
||||
fprintf(stderr, "Error: %s\nUsage: %s PROMPT\n", errmsg, arg0);
|
||||
else
|
||||
fprintf(stderr, "Usage: %s PROMPT\n", arg0);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
static void
|
||||
fifo_common_finish(int fd, char **buf, size_t *used, size_t *size)
|
||||
{
|
||||
if (fd >= 0)
|
||||
close(fd);
|
||||
|
||||
if (!*buf)
|
||||
return;
|
||||
|
||||
memset(*buf, '\0', *size);
|
||||
free(*buf);
|
||||
*buf = NULL;
|
||||
*used = 0;
|
||||
*size = 0;
|
||||
}
|
||||
|
||||
static bool
|
||||
fifo_common_read(int fd, char **buf, size_t *used, size_t *size)
|
||||
{
|
||||
ssize_t result;
|
||||
|
||||
again:
|
||||
if ((*size - *used) == 0) {
|
||||
*size += 4096;
|
||||
*buf = realloc(*buf, *size);
|
||||
if (!*buf) {
|
||||
*size = 0;
|
||||
*used = 0;
|
||||
debug("Failed to allocate memory for passphrase\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
reread:
|
||||
result = read(fd, *buf + *used, *size - *used);
|
||||
|
||||
if (result < 0) {
|
||||
if (errno == EAGAIN)
|
||||
return false;
|
||||
if (errno == EINTR)
|
||||
goto reread;
|
||||
debug("Error when reading from fifo\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
debug("Read %i bytes from fifo\n", (int)result);
|
||||
*used += result;
|
||||
|
||||
if (result == 0)
|
||||
return true;
|
||||
|
||||
goto again;
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************************************
|
||||
* fifo functions *
|
||||
*****************************************************************************/
|
||||
#define FIFO_PATH "/luks_passfifo"
|
||||
static size_t fifoused = 0;
|
||||
static size_t fifosize = 0;
|
||||
static char *fifobuf = NULL;
|
||||
|
||||
static void
|
||||
fifo_finish(int fd)
|
||||
{
|
||||
fifo_common_finish(fd, &fifobuf, &fifoused, &fifosize);
|
||||
}
|
||||
|
||||
static bool
|
||||
fifo_read(int fd, char **buf, size_t *size)
|
||||
{
|
||||
debug("In fifo_read\n");
|
||||
if (fifo_common_read(fd, &fifobuf, &fifoused, &fifosize)) {
|
||||
*buf = fifobuf;
|
||||
*size = fifoused;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static int
|
||||
fifo_prepare(const char *prompt)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = mkfifo(FIFO_PATH, 0600);
|
||||
if (ret && errno != EEXIST)
|
||||
return -1;
|
||||
|
||||
return open(FIFO_PATH, O_RDONLY | O_NONBLOCK);
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* console functions *
|
||||
*****************************************************************************/
|
||||
#define CONSOLE_PATH "/dev/console"
|
||||
static struct termios term_old;
|
||||
static bool term_set = false;
|
||||
static char *consolebuf = NULL;
|
||||
static size_t consolebuflen = 0;
|
||||
|
||||
static void
|
||||
console_finish(int fd)
|
||||
{
|
||||
if (consolebuf) {
|
||||
memset(consolebuf, '\0', consolebuflen);
|
||||
free(consolebuf);
|
||||
consolebuf = NULL;
|
||||
consolebuflen = 0;
|
||||
}
|
||||
|
||||
if (!term_set || fd < 0)
|
||||
return;
|
||||
|
||||
term_set = false;
|
||||
tcsetattr(fd, TCSAFLUSH, &term_old);
|
||||
fprintf(stderr, "\n");
|
||||
klogctl(7, NULL, 0);
|
||||
}
|
||||
|
||||
bool
|
||||
console_read(int fd, char **buf, size_t *size)
|
||||
{
|
||||
ssize_t nread;
|
||||
|
||||
/* Console is in ICANON mode so we'll get entire lines */
|
||||
nread = getline(&consolebuf, &consolebuflen, stdin);
|
||||
|
||||
if (nread < 0)
|
||||
return NULL;
|
||||
|
||||
/* Strip trailing newline, if any */
|
||||
if (nread > 0 && consolebuf[nread - 1] == '\n') {
|
||||
nread--;
|
||||
consolebuf[nread] = '\0';
|
||||
}
|
||||
|
||||
*size = nread;
|
||||
*buf = consolebuf;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int
|
||||
console_prepare(const char *prompt)
|
||||
{
|
||||
struct termios term_new;
|
||||
char *prompt_ptr = prompt;
|
||||
char *newline = NULL;
|
||||
|
||||
if (!isatty(STDIN_FILENO)) {
|
||||
if (access(CONSOLE_PATH, R_OK | W_OK)) {
|
||||
debug("No access to console device " CONSOLE_PATH "\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!freopen(CONSOLE_PATH, "r", stdin) ||
|
||||
!freopen(CONSOLE_PATH, "a", stdout) ||
|
||||
!freopen(CONSOLE_PATH, "a", stderr) ||
|
||||
!isatty(STDIN_FILENO)) {
|
||||
debug("Failed to open console\n");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (tcgetattr(STDIN_FILENO, &term_old)) {
|
||||
debug("Failed to get terminal settings\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
term_new = term_old;
|
||||
term_new.c_lflag &= ~ECHO;
|
||||
term_new.c_lflag |= ICANON;
|
||||
|
||||
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &term_new)) {
|
||||
debug("Failed to disable echoing\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* handle any non-literal embedded newlines in prompt */
|
||||
while ( (newline = strstr(prompt_ptr,"\\n")) != NULL ) {
|
||||
/* Calculate length of string leading up to newline. */
|
||||
int line_len = newline - prompt_ptr;
|
||||
|
||||
/* Force trimming of prompt to location of newline. */
|
||||
if (fwrite(prompt_ptr, line_len, 1, stderr) < 1 ||
|
||||
fwrite("\n", 1, 1, stderr) < 1) {
|
||||
debug("Failed to print prompt\n");
|
||||
tcsetattr(STDIN_FILENO, TCSAFLUSH, &term_old);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Skip over newline. */
|
||||
prompt_ptr = newline + 2;
|
||||
}
|
||||
if (fputs(prompt_ptr, stderr) < 0) {
|
||||
debug("Failed to print prompt\n");
|
||||
tcsetattr(STDIN_FILENO, TCSAFLUSH, &term_old);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Disable printk to console */
|
||||
klogctl(6, NULL, 0);
|
||||
term_set = true;
|
||||
return STDIN_FILENO;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* main functions *
|
||||
*****************************************************************************/
|
||||
|
||||
struct method {
|
||||
const char *name;
|
||||
int (*prepare)(const char *prompt);
|
||||
bool (*read)(int fd, char **buf, size_t *size);
|
||||
void (*finish)(int fd);
|
||||
bool active;
|
||||
bool enabled;
|
||||
int fd;
|
||||
};
|
||||
|
||||
static struct method methods[] = {
|
||||
{ "fifo", fifo_prepare, fifo_read, fifo_finish, false, true, -1 },
|
||||
{ "console", console_prepare, console_read, console_finish, false, true, -1 }
|
||||
};
|
||||
|
||||
static bool
|
||||
disable_method(const char *method)
|
||||
{
|
||||
int i;
|
||||
bool result = false;
|
||||
|
||||
debug("Disabling method %s\n", method ? method : "ALL");
|
||||
|
||||
for (i = 0; i < ARRAY_SIZE(methods); i++) {
|
||||
/* A NULL method means all methods should be disabled */
|
||||
if (method && strcmp(methods[i].name, method))
|
||||
continue;
|
||||
if (!methods[i].enabled)
|
||||
continue;
|
||||
if (methods[i].active)
|
||||
methods[i].finish(methods[i].fd);
|
||||
|
||||
methods[i].active = false;
|
||||
methods[i].fd = -1;
|
||||
methods[i].enabled = false;
|
||||
result = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv, char **envp)
|
||||
{
|
||||
char *pass = NULL;
|
||||
size_t passlen = 0;
|
||||
int i;
|
||||
int nfds;
|
||||
fd_set fds;
|
||||
int ret;
|
||||
bool done = false;
|
||||
sigset_t sigset;
|
||||
|
||||
if (argc != 2)
|
||||
usage(argv[0], "incorrect number of arguments");
|
||||
|
||||
sigfillset(&sigset);
|
||||
sigprocmask(SIG_BLOCK, &sigset, NULL);
|
||||
|
||||
for (i = 0; i < ARRAY_SIZE(methods); i++) {
|
||||
if (!methods[i].enabled)
|
||||
continue;
|
||||
debug("Enabling method %s\n", methods[i].name);
|
||||
methods[i].fd = methods[i].prepare(argv[1]);
|
||||
if (methods[i].fd < 0)
|
||||
methods[i].active = false;
|
||||
else
|
||||
methods[i].active = true;
|
||||
}
|
||||
|
||||
while (!done) {
|
||||
nfds = 0;
|
||||
FD_ZERO(&fds);
|
||||
for (i = 0; i < ARRAY_SIZE(methods); i++) {
|
||||
if (!methods[i].enabled || methods[i].fd < 0)
|
||||
continue;
|
||||
debug("method %i has fd %i and name %s\n", i, methods[i].fd, methods[i].name);
|
||||
FD_SET(methods[i].fd, &fds);
|
||||
if (methods[i].fd + 1 > nfds)
|
||||
nfds = methods[i].fd + 1;
|
||||
}
|
||||
|
||||
if (nfds == 0) {
|
||||
debug("All methods disabled\n");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
debug("Starting select with nfds %i\n", nfds);
|
||||
ret = select(nfds, &fds, NULL, NULL, NULL);
|
||||
|
||||
if (ret <= 0) {
|
||||
if (ret == 0 || errno == EINTR)
|
||||
continue;
|
||||
debug("Select failed\n");
|
||||
disable_method(NULL);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
for (i = 0; i < ARRAY_SIZE(methods); i++) {
|
||||
if (!methods[i].enabled || methods[i].fd < 0)
|
||||
continue;
|
||||
if (!FD_ISSET(methods[i].fd, &fds))
|
||||
continue;
|
||||
if (methods[i].read(methods[i].fd, &pass, &passlen) && pass) {
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug("Writing %i bytes to stdout\n", (int)passlen);
|
||||
write(STDOUT_FILENO, pass, passlen);
|
||||
disable_method(NULL);
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
7
askpass.license
Normal file
7
askpass.license
Normal file
@@ -0,0 +1,7 @@
|
||||
Copyright (c) 2010-2018, Piotr Karbowski <piotr.karbowski@gmail.com>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer i…LL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
341
mkinitramfs.py
341
mkinitramfs.py
@@ -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
|
||||
@@ -37,7 +39,7 @@ for bin in ${DEPS[*]}; do
|
||||
for lib in $(ldd $bin | sed -nre 's,.* (/.*lib.*/.*.so.*) .*,\\1,p' \\
|
||||
-e 's,.*(/lib.*/ld.*.so.*) .*,\\1,p')
|
||||
do
|
||||
mkdir -p .${lib%/*} && cp {,.}$lib
|
||||
cp $lib lib64/
|
||||
done
|
||||
done
|
||||
# extra lib for new version of cryptsetup, which need to do locks
|
||||
@@ -45,6 +47,20 @@ 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
|
||||
if ! wget "https://bitbucket.org/piotrkarbowski/better-initramfs/downloads/askpass.c"; then
|
||||
if ! wget "https://raw.githubusercontent.com/gryf/mkinitramfs/refs/heads/master/askpass.c"; then
|
||||
echo "Error: Unable to fetch the 'askpass.c'. Aborting" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
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,16 +84,18 @@ $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
|
||||
mount -t configfs none /sys/kernel/config
|
||||
mount -t debugfs none /sys/kernel/debug
|
||||
|
||||
# clean i/o
|
||||
# do not let kernel spill its messages to the console
|
||||
echo 0 > /proc/sys/kernel/printk
|
||||
|
||||
# clean console i/o
|
||||
exec >/dev/console </dev/console 2>&1
|
||||
|
||||
# tty fix
|
||||
@@ -116,7 +134,7 @@ done
|
||||
# be carefull, which disk you select to write.
|
||||
INIT_SD = """
|
||||
for counter in $(seq 5); do
|
||||
clear
|
||||
$CLEAR
|
||||
if [ -b /dev/mmcblk0p1 ]; then
|
||||
KEYDEV=/dev/mmcblk0p1
|
||||
break
|
||||
@@ -135,8 +153,7 @@ done
|
||||
# off.
|
||||
INIT_LABELED = """
|
||||
for counter in $(seq 3); do
|
||||
sleep 1
|
||||
clear
|
||||
$CLEAR
|
||||
for dev in /dev/sd* /dev/mmcblk*; do
|
||||
if blkid "${dev}" | grep -w LABEL | grep -iqw "%(label)s"; then
|
||||
KEYDEV="${dev}"
|
||||
@@ -144,13 +161,14 @@ for counter in $(seq 3); do
|
||||
fi
|
||||
done
|
||||
[ -n "${KEYDEV}" ] && break
|
||||
sleep 1
|
||||
done
|
||||
"""
|
||||
|
||||
# Open encrypted fs
|
||||
INIT_OPEN = """
|
||||
# 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
|
||||
@@ -161,12 +179,54 @@ for counter in $(seq 3); do
|
||||
fi
|
||||
done
|
||||
[ -n "${DEVICE}" ] && break
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [ -z "${DEVICE}" ]; then
|
||||
echo "No LUKS device found to boot from! Giving up."
|
||||
sleep 3
|
||||
exec reboot -f
|
||||
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
|
||||
$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
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [ -z "${DEVICE}" ]; then
|
||||
echo "No LUKS device found to boot from! Giving up."
|
||||
exec reboot -f -d 3
|
||||
fi
|
||||
"""
|
||||
|
||||
@@ -183,8 +243,7 @@ fi
|
||||
|
||||
if [[ ${ret} -ne 0 && ! -f ${KEY} ]]; then
|
||||
echo "Failed to open boot system fs. Giving up."
|
||||
sleep 3
|
||||
reboot -f
|
||||
reboot -f -d 3
|
||||
fi
|
||||
"""
|
||||
|
||||
@@ -192,7 +251,7 @@ DECRYPT_YUBICP = """
|
||||
for i in 1 2 3 4 5 6; do
|
||||
pass=$(ykchalresp %(disk)s 2>/dev/null)
|
||||
if [ -n "$pass" ]; then
|
||||
ccrypt -K $pass -c "$KEY.yk" | \
|
||||
echo "$pass" | ccrypt -c -k - "$KEY.yk" | \
|
||||
cryptsetup open --allow-discards $DEVICE root
|
||||
break
|
||||
fi
|
||||
@@ -201,14 +260,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 +301,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:
|
||||
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,8 +369,8 @@ class Initramfs(object):
|
||||
|
||||
def _make_dirs(self):
|
||||
os.chdir(self.dirname)
|
||||
for dir_ in ('bin', 'dev', 'etc', 'keys', 'lib64', 'proc',
|
||||
'run/cryptsetup', 'run/lock', 'sys', 'tmp', 'usr'):
|
||||
for dir_ in ('bin', 'dev', 'etc', 'keys', 'lib64', 'proc', 'root',
|
||||
'run/cryptsetup', 'run/lock', 'sys', 'tmp'):
|
||||
os.makedirs(os.path.join(self.dirname, dir_))
|
||||
|
||||
for link, target in (('lib', 'lib64'), ('sbin', 'bin'),
|
||||
@@ -265,11 +384,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 +401,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 +487,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 +562,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 +602,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 +655,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 +664,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()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user