Merge pull request #65231 from buckley310/grub-password
grub: add support for passwords
This commit is contained in:
commit
dab676b2d7
|
@ -360,6 +360,18 @@ start_all()
|
||||||
</note>
|
</note>
|
||||||
</listitem>
|
</listitem>
|
||||||
</varlistentry>
|
</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>
|
<varlistentry>
|
||||||
<term>
|
<term>
|
||||||
<methodname>wait_for_window</methodname>
|
<methodname>wait_for_window</methodname>
|
||||||
|
|
|
@ -3,6 +3,8 @@ from contextlib import contextmanager, _GeneratorContextManager
|
||||||
from queue import Queue, Empty
|
from queue import Queue, Empty
|
||||||
from typing import Tuple, Any, Callable, Dict, Iterator, Optional, List
|
from typing import Tuple, Any, Callable, Dict, Iterator, Optional, List
|
||||||
from xml.sax.saxutils import XMLGenerator
|
from xml.sax.saxutils import XMLGenerator
|
||||||
|
import queue
|
||||||
|
import io
|
||||||
import _thread
|
import _thread
|
||||||
import argparse
|
import argparse
|
||||||
import atexit
|
import atexit
|
||||||
|
@ -672,6 +674,22 @@ class Machine:
|
||||||
with self.nested("waiting for {} to appear on screen".format(regex)):
|
with self.nested("waiting for {} to appear on screen".format(regex)):
|
||||||
retry(screen_matches)
|
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:
|
def send_key(self, key: str) -> None:
|
||||||
key = CHAR_TO_KEY.get(key, key)
|
key = CHAR_TO_KEY.get(key, key)
|
||||||
self.send_monitor_command("sendkey {}".format(key))
|
self.send_monitor_command("sendkey {}".format(key))
|
||||||
|
@ -735,11 +753,16 @@ class Machine:
|
||||||
self.monitor, _ = self.monitor_socket.accept()
|
self.monitor, _ = self.monitor_socket.accept()
|
||||||
self.shell, _ = self.shell_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:
|
def process_serial_output() -> None:
|
||||||
assert self.process.stdout is not None
|
assert self.process.stdout is not None
|
||||||
for _line in self.process.stdout:
|
for _line in self.process.stdout:
|
||||||
# Ignore undecodable bytes that may occur in boot menus
|
# Ignore undecodable bytes that may occur in boot menus
|
||||||
line = _line.decode(errors="ignore").replace("\r", "").rstrip()
|
line = _line.decode(errors="ignore").replace("\r", "").rstrip()
|
||||||
|
self.last_lines.put(line)
|
||||||
eprint("{} # {}".format(self.name, line))
|
eprint("{} # {}".format(self.name, line))
|
||||||
self.logger.enqueue({"msg": line, "machine": self.name})
|
self.logger.enqueue({"msg": line, "machine": self.name})
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@ let
|
||||||
storePath = config.boot.loader.grub.storePath;
|
storePath = config.boot.loader.grub.storePath;
|
||||||
bootloaderId = if args.efiBootloaderId == null then "NixOS${efiSysMountPoint'}" else args.efiBootloaderId;
|
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;
|
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 efiSysMountPoint;
|
||||||
inherit (args) devices;
|
inherit (args) devices;
|
||||||
inherit (efi) canTouchEfiVariables;
|
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 {
|
mirroredBoots = mkOption {
|
||||||
default = [ ];
|
default = [ ];
|
||||||
example = [
|
example = [
|
||||||
|
|
|
@ -247,6 +247,45 @@ if ($grubVersion == 1) {
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
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) {
|
if ($copyKernels == 0) {
|
||||||
$conf .= "
|
$conf .= "
|
||||||
" . $grubStore->search;
|
" . $grubStore->search;
|
||||||
|
@ -350,7 +389,7 @@ sub copyToKernelsDir {
|
||||||
}
|
}
|
||||||
|
|
||||||
sub addEntry {
|
sub addEntry {
|
||||||
my ($name, $path) = @_;
|
my ($name, $path, $options) = @_;
|
||||||
return unless -e "$path/kernel" && -e "$path/initrd";
|
return unless -e "$path/kernel" && -e "$path/initrd";
|
||||||
|
|
||||||
my $kernel = copyToKernelsDir(Cwd::abs_path("$path/kernel"));
|
my $kernel = copyToKernelsDir(Cwd::abs_path("$path/kernel"));
|
||||||
|
@ -396,7 +435,7 @@ sub addEntry {
|
||||||
$conf .= " " . ($xen ? "module" : "kernel") . " $kernel $kernelParams\n";
|
$conf .= " " . ($xen ? "module" : "kernel") . " $kernel $kernelParams\n";
|
||||||
$conf .= " " . ($xen ? "module" : "initrd") . " $initrd\n\n";
|
$conf .= " " . ($xen ? "module" : "initrd") . " $initrd\n\n";
|
||||||
} else {
|
} else {
|
||||||
$conf .= "menuentry \"$name\" {\n";
|
$conf .= "menuentry \"$name\" " . ($options||"") . " {\n";
|
||||||
$conf .= $grubBoot->search . "\n";
|
$conf .= $grubBoot->search . "\n";
|
||||||
if ($copyKernels == 0) {
|
if ($copyKernels == 0) {
|
||||||
$conf .= $grubStore->search . "\n";
|
$conf .= $grubStore->search . "\n";
|
||||||
|
@ -413,7 +452,7 @@ sub addEntry {
|
||||||
# Add default entries.
|
# Add default entries.
|
||||||
$conf .= "$extraEntries\n" if $extraEntriesBeforeNixOS;
|
$conf .= "$extraEntries\n" if $extraEntriesBeforeNixOS;
|
||||||
|
|
||||||
addEntry("NixOS - Default", $defaultConfig);
|
addEntry("NixOS - Default", $defaultConfig, "--unrestricted");
|
||||||
|
|
||||||
$conf .= "$extraEntries\n" unless $extraEntriesBeforeNixOS;
|
$conf .= "$extraEntries\n" unless $extraEntriesBeforeNixOS;
|
||||||
|
|
||||||
|
|
|
@ -126,6 +126,7 @@ in
|
||||||
grafana = handleTest ./grafana.nix {};
|
grafana = handleTest ./grafana.nix {};
|
||||||
graphite = handleTest ./graphite.nix {};
|
graphite = handleTest ./graphite.nix {};
|
||||||
graylog = handleTest ./graylog.nix {};
|
graylog = handleTest ./graylog.nix {};
|
||||||
|
grub = handleTest ./grub.nix {};
|
||||||
gvisor = handleTest ./gvisor.nix {};
|
gvisor = handleTest ./gvisor.nix {};
|
||||||
hadoop.hdfs = handleTestOn [ "x86_64-linux" ] ./hadoop/hdfs.nix {};
|
hadoop.hdfs = handleTestOn [ "x86_64-linux" ] ./hadoop/hdfs.nix {};
|
||||||
hadoop.yarn = handleTestOn [ "x86_64-linux" ] ./hadoop/yarn.nix {};
|
hadoop.yarn = handleTestOn [ "x86_64-linux" ] ./hadoop/yarn.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")
|
||||||
|
'';
|
||||||
|
})
|
Loading…
Reference in New Issue