summaryrefslogtreecommitdiffstats
path: root/nixos
diff options
context:
space:
mode:
authorMichele Guerini Rocco <rnhmjoj@users.noreply.github.com>2020-07-01 09:04:30 +0200
committerGitHub <noreply@github.com>2020-07-01 09:04:30 +0200
commitdab676b2d7bccee76d005ff1f8a9b01c21718c70 (patch)
treea1eb928902426078a1a0e2f28ebf2251702bcd01 /nixos
parent8ba675379ab1af0e1b2165fb4cbaffeea2468925 (diff)
parent33c4a4bdd52a750205046cc85775e6caea342868 (diff)
Merge pull request #65231 from buckley310/grub-password
grub: add support for passwords
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/development/writing-nixos-tests.xml12
-rw-r--r--nixos/lib/test-driver/test-driver.py23
-rw-r--r--nixos/modules/system/boot/loader/grub/grub.nix62
-rw-r--r--nixos/modules/system/boot/loader/grub/install-grub.pl45
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/grub.nix60
6 files changed, 200 insertions, 3 deletions
diff --git a/nixos/doc/manual/development/writing-nixos-tests.xml b/nixos/doc/manual/development/writing-nixos-tests.xml
index 150bea8c2d86..ff37b3b2f6f3 100644
--- a/nixos/doc/manual/development/writing-nixos-tests.xml
+++ b/nixos/doc/manual/development/writing-nixos-tests.xml
@@ -362,6 +362,18 @@ start_all()
</varlistentry>
<varlistentry>
<term>
+ <methodname>wait_for_console_text</methodname>
+ </term>
+ <listitem>
+ <para>
+ Wait until the supplied regular expressions match a line of the serial
+ console output. This method is useful when OCR is not possibile or
+ accurate enough.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
<methodname>wait_for_window</methodname>
</term>
<listitem>
diff --git a/nixos/lib/test-driver/test-driver.py b/nixos/lib/test-driver/test-driver.py
index f454b052dc31..4cb928538e7c 100644
--- a/nixos/lib/test-driver/test-driver.py
+++ b/nixos/lib/test-driver/test-driver.py
@@ -3,6 +3,8 @@ from contextlib import contextmanager, _GeneratorContextManager
from queue import Queue, Empty
from typing import Tuple, Any, Callable, Dict, Iterator, Optional, List
from xml.sax.saxutils import XMLGenerator
+import queue
+import io
import _thread
import argparse
import atexit
@@ -672,6 +674,22 @@ class Machine:
with self.nested("waiting for {} to appear on screen".format(regex)):
retry(screen_matches)
+ def wait_for_console_text(self, regex: str) -> None:
+ self.log("waiting for {} to appear on console".format(regex))
+ # Buffer the console output, this is needed
+ # to match multiline regexes.
+ console = io.StringIO()
+ while True:
+ try:
+ console.write(self.last_lines.get())
+ except queue.Empty:
+ self.sleep(1)
+ continue
+ console.seek(0)
+ matches = re.search(regex, console.read())
+ if matches is not None:
+ return
+
def send_key(self, key: str) -> None:
key = CHAR_TO_KEY.get(key, key)
self.send_monitor_command("sendkey {}".format(key))
@@ -735,11 +753,16 @@ class Machine:
self.monitor, _ = self.monitor_socket.accept()
self.shell, _ = self.shell_socket.accept()
+ # Store last serial console lines for use
+ # of wait_for_console_text
+ self.last_lines: Queue = Queue()
+
def process_serial_output() -> None:
assert self.process.stdout is not None
for _line in self.process.stdout:
# Ignore undecodable bytes that may occur in boot menus
line = _line.decode(errors="ignore").replace("\r", "").rstrip()
+ self.last_lines.put(line)
eprint("{} # {}".format(self.name, line))
self.logger.enqueue({"msg": line, "machine": self.name})
diff --git a/nixos/modules/system/boot/loader/grub/grub.nix b/nixos/modules/system/boot/loader/grub/grub.nix
index 3975372e15ec..b760c3f96ddf 100644
--- a/nixos/modules/system/boot/loader/grub/grub.nix
+++ b/nixos/modules/system/boot/loader/grub/grub.nix
@@ -55,6 +55,7 @@ let
storePath = config.boot.loader.grub.storePath;
bootloaderId = if args.efiBootloaderId == null then "NixOS${efiSysMountPoint'}" else args.efiBootloaderId;
timeout = if config.boot.loader.timeout == null then -1 else config.boot.loader.timeout;
+ users = if cfg.users == {} || cfg.version != 1 then cfg.users else throw "GRUB version 1 does not support user accounts.";
inherit efiSysMountPoint;
inherit (args) devices;
inherit (efi) canTouchEfiVariables;
@@ -137,6 +138,67 @@ in
'';
};
+ users = mkOption {
+ default = {};
+ example = {
+ root = { hashedPasswordFile = "/path/to/file"; };
+ };
+ description = ''
+ User accounts for GRUB. When specified, the GRUB command line and
+ all boot options except the default are password-protected.
+ All passwords and hashes provided will be stored in /boot/grub/grub.cfg,
+ and will be visible to any local user who can read this file. Additionally,
+ any passwords and hashes provided directly in a Nix configuration
+ (as opposed to external files) will be copied into the Nix store, and
+ will be visible to all local users.
+ '';
+ type = with types; attrsOf (submodule {
+ options = {
+ hashedPasswordFile = mkOption {
+ example = "/path/to/file";
+ default = null;
+ type = with types; uniq (nullOr str);
+ description = ''
+ Specifies the path to a file containing the password hash
+ for the account, generated with grub-mkpasswd-pbkdf2.
+ This hash will be stored in /boot/grub/grub.cfg, and will
+ be visible to any local user who can read this file.
+ '';
+ };
+ hashedPassword = mkOption {
+ example = "grub.pbkdf2.sha512.10000.674DFFDEF76E13EA...2CC972B102CF4355";
+ default = null;
+ type = with types; uniq (nullOr str);
+ description = ''
+ Specifies the password hash for the account,
+ generated with grub-mkpasswd-pbkdf2.
+ This hash will be copied to the Nix store, and will be visible to all local users.
+ '';
+ };
+ passwordFile = mkOption {
+ example = "/path/to/file";
+ default = null;
+ type = with types; uniq (nullOr str);
+ description = ''
+ Specifies the path to a file containing the
+ clear text password for the account.
+ This password will be stored in /boot/grub/grub.cfg, and will
+ be visible to any local user who can read this file.
+ '';
+ };
+ password = mkOption {
+ example = "Pa$$w0rd!";
+ default = null;
+ type = with types; uniq (nullOr str);
+ description = ''
+ Specifies the clear text password for the account.
+ This password will be copied to the Nix store, and will be visible to all local users.
+ '';
+ };
+ };
+ });
+ };
+
mirroredBoots = mkOption {
default = [ ];
example = [
diff --git a/nixos/modules/system/boot/loader/grub/install-grub.pl b/nixos/modules/system/boot/loader/grub/install-grub.pl
index e469b18abd09..918a66866e96 100644
--- a/nixos/modules/system/boot/loader/grub/install-grub.pl
+++ b/nixos/modules/system/boot/loader/grub/install-grub.pl
@@ -247,6 +247,45 @@ if ($grubVersion == 1) {
}
else {
+ my @users = ();
+ foreach my $user ($dom->findnodes('/expr/attrs/attr[@name = "users"]/attrs/attr')) {
+ my $name = $user->findvalue('@name') or die;
+ my $hashedPassword = $user->findvalue('./attrs/attr[@name = "hashedPassword"]/string/@value');
+ my $hashedPasswordFile = $user->findvalue('./attrs/attr[@name = "hashedPasswordFile"]/string/@value');
+ my $password = $user->findvalue('./attrs/attr[@name = "password"]/string/@value');
+ my $passwordFile = $user->findvalue('./attrs/attr[@name = "passwordFile"]/string/@value');
+
+ if ($hashedPasswordFile) {
+ open(my $f, '<', $hashedPasswordFile) or die "Can't read file '$hashedPasswordFile'!";
+ $hashedPassword = <$f>;
+ chomp $hashedPassword;
+ }
+ if ($passwordFile) {
+ open(my $f, '<', $passwordFile) or die "Can't read file '$passwordFile'!";
+ $password = <$f>;
+ chomp $password;
+ }
+
+ if ($hashedPassword) {
+ if (index($hashedPassword, "grub.pbkdf2.") == 0) {
+ $conf .= "\npassword_pbkdf2 $name $hashedPassword";
+ }
+ else {
+ die "Password hash for GRUB user '$name' is not valid!";
+ }
+ }
+ elsif ($password) {
+ $conf .= "\npassword $name $password";
+ }
+ else {
+ die "GRUB user '$name' has no password!";
+ }
+ push(@users, $name);
+ }
+ if (@users) {
+ $conf .= "\nset superusers=\"" . join(' ',@users) . "\"\n";
+ }
+
if ($copyKernels == 0) {
$conf .= "
" . $grubStore->search;
@@ -350,7 +389,7 @@ sub copyToKernelsDir {
}
sub addEntry {
- my ($name, $path) = @_;
+ my ($name, $path, $options) = @_;
return unless -e "$path/kernel" && -e "$path/initrd";
my $kernel = copyToKernelsDir(Cwd::abs_path("$path/kernel"));
@@ -396,7 +435,7 @@ sub addEntry {
$conf .= " " . ($xen ? "module" : "kernel") . " $kernel $kernelParams\n";
$conf .= " " . ($xen ? "module" : "initrd") . " $initrd\n\n";
} else {
- $conf .= "menuentry \"$name\" {\n";
+ $conf .= "menuentry \"$name\" " . ($options||"") . " {\n";
$conf .= $grubBoot->search . "\n";
if ($copyKernels == 0) {
$conf .= $grubStore->search . "\n";
@@ -413,7 +452,7 @@ sub addEntry {
# Add default entries.
$conf .= "$extraEntries\n" if $extraEntriesBeforeNixOS;
-addEntry("NixOS - Default", $defaultConfig);
+addEntry("NixOS - Default", $defaultConfig, "--unrestricted");
$conf .= "$extraEntries\n" unless $extraEntriesBeforeNixOS;
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index debc60a21d01..8f6e76b51c89 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -126,6 +126,7 @@ in
grafana = handleTest ./grafana.nix {};
graphite = handleTest ./graphite.nix {};
graylog = handleTest ./graylog.nix {};
+ grub = handleTest ./grub.nix {};
gvisor = handleTest ./gvisor.nix {};
hadoop.hdfs = handleTestOn [ "x86_64-linux" ] ./hadoop/hdfs.nix {};
hadoop.yarn = handleTestOn [ "x86_64-linux" ] ./hadoop/yarn.nix {};
diff --git a/nixos/tests/grub.nix b/nixos/tests/grub.nix
new file mode 100644
index 000000000000..84bfc90955b5
--- /dev/null
+++ b/nixos/tests/grub.nix
@@ -0,0 +1,60 @@
+import ./make-test-python.nix ({ lib, ... }: {
+ name = "grub";
+
+ meta = with lib.maintainers; {
+ maintainers = [ rnhmjoj ];
+ };
+
+ machine = { ... }: {
+ virtualisation.useBootLoader = true;
+
+ boot.loader.timeout = null;
+ boot.loader.grub = {
+ enable = true;
+ users.alice.password = "supersecret";
+
+ # OCR is not accurate enough
+ extraConfig = "serial; terminal_output serial";
+ };
+ };
+
+ testScript = ''
+ def grub_login_as(user, password):
+ """
+ Enters user and password to log into GRUB
+ """
+ machine.wait_for_console_text("Enter username:")
+ machine.send_chars(user + "\n")
+ machine.wait_for_console_text("Enter password:")
+ machine.send_chars(password + "\n")
+
+
+ def grub_select_all_configurations():
+ """
+ Selects "All configurations" from the GRUB menu
+ to trigger a login request.
+ """
+ machine.send_monitor_command("sendkey down")
+ machine.send_monitor_command("sendkey ret")
+
+
+ machine.start()
+
+ # wait for grub screen
+ machine.wait_for_console_text("GNU GRUB")
+
+ grub_select_all_configurations()
+ with subtest("Invalid credentials are rejected"):
+ grub_login_as("wronguser", "wrongsecret")
+ machine.wait_for_console_text("error: access denied.")
+
+ grub_select_all_configurations()
+ with subtest("Valid credentials are accepted"):
+ grub_login_as("alice", "supersecret")
+ machine.send_chars("\n") # press enter to boot
+ machine.wait_for_console_text("Linux version")
+
+ with subtest("Machine boots correctly"):
+ machine.wait_for_unit("multi-user.target")
+ '';
+})