Compare commits

..

1 commit

Author SHA1 Message Date
936e38be15
WIP: erofs: zstd compression
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
FIXME: needs zstd support in the kernel, so probablt not a great idea
for now
2024-11-15 19:50:39 +01:00
20 changed files with 211 additions and 292 deletions

1
.gitignore vendored
View file

@ -6,4 +6,3 @@
/target
.*.swp
.*.swo
.nixos-test-history

View file

@ -1,60 +0,0 @@
:showtitle:
:toc: left
:icons: font
= PatOS - Patagia OS
[link=https://ci.patagia.dev/repos/2,window=_blank]
image::https://ci.patagia.dev/api/badges/2/status.svg[Build Status]
PatOS is a minimal, immutable Linux distribution specialized for the Patagia Platform.
== Features
* Immutable read-only system
* Trust chain verification using secure boot and dm-verity
* Automatic updates and unattended rollbacks
* Host/TPM bound data encryption by default
== Roadmap and status
[cols="3,1",options="header"]
|===
|Feature |Status
| A/B root partitions with discoverable partitions
| ✅
| Verity protection of root partitions
| ✅
| Encrypted state partition with tpm2 host binding
| ✅
| Factory reset / clearing of state partition
| ✅
| Automatic updates
| ⚠️
| Installer
| ❌
| Machine registration and provisioning
| ❌
| Boot assessment w/automatic rollback
| ❌
|===
== Community
* Source code: https://patagia.dev/Patagia/patos
== License
Copyright (C) 2024 Patagia AB
Unless otherwise noted, all components are licenced under the Mozilla Public License Version 2.0.

5
README.md Normal file
View file

@ -0,0 +1,5 @@
# PatOS - Patagia OS
[![status-badge](https://ci.patagia.dev/api/badges/42/status.svg)](https://ci.patagia.dev/repos/42)
PatOS is a minimal, immutable Linux distribution specialized for the Patagia Platform.

View file

@ -14,10 +14,6 @@
pkgs = import nixpkgs { inherit system; };
in
{
nixosModules.devel.imports = [
./modules/profiles/devel.nix
];
nixosModules.server.imports = [
./modules/profiles/server.nix
];
@ -25,31 +21,10 @@
nixosModules.image.imports = [
./modules
./modules/profiles/base.nix
./modules/image
./modules/image/disk
];
packages.${system} = {
devel =
(nixpkgs.lib.nixosSystem {
modules = [
(
{ lib, ... }:
{
nixpkgs.hostPlatform = system;
system.stateVersion = "24.05";
}
)
{
system.image.updates.url = "${updateUrl}";
system.image.id = "patos";
system.image.version = releaseVersion;
image.compress = false;
}
self.nixosModules.image
self.nixosModules.devel
];
}).config.system.build.updatePackage;
patos =
(nixpkgs.lib.nixosSystem {
modules = [
@ -61,6 +36,10 @@
}
)
{
boot.kernelParams = [
"console=ttyS0"
"systemd.journald.forward_to_console"
];
system.image.updates.url = "${updateUrl}";
system.image.id = "patos";
system.image.version = releaseVersion;
@ -74,6 +53,7 @@
};
checks.${system} = {
ssh-preseed = import ./tests/ssh-preseed.nix { inherit pkgs self; };
podman = import ./tests/podman.nix { inherit pkgs self; };
system-update = import ./tests/system-update.nix { inherit pkgs self; };
};

View file

@ -15,13 +15,5 @@ build: build-image
build-image:
nix build .#patos
# Build PatOS image (developer mode)
build-devel-image:
nix build .#devel
run: build-image
qemu-uefi-tpm ./result/*.img
run-devel: build-devel-image
qemu-uefi-tpm ./result/*.img

View file

@ -5,6 +5,7 @@
(final: prev: {
composefs = final.callPackage ../../pkgs/composefs.nix { inherit prev; };
erofs-utils = final.callPackage ../../pkgs/erofs-utils.nix { inherit prev; };
qemu_tiny = final.callPackage ../../pkgs/qemu.nix { inherit prev; };
systemdUkify = final.callPackage ../../pkgs/systemd-ukify.nix { inherit prev; };

View file

@ -1,7 +1,6 @@
{
config,
lib,
options,
pkgs,
...
}:
@ -47,8 +46,10 @@ let
split = true;
mkfsOptions = lib.mkIf config.image.compress {
erofs = [
"-zlz4hc,level=12"
"-Efragments,dedupe,ztailpacking"
"-zzstd,6" # Zstd compression
"-T0" # Fixed timestamp for all files
"-C1048576" # 1 MiB cluster size
"-Efragments,dedupe,ztailpacking" # Extra features
];
};
partitions = initialPartitions;
@ -76,22 +77,9 @@ let
contents = {
"/EFI/BOOT/BOOT${lib.toUpper efiArch}.EFI".source = "${pkgs.systemdUkify}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi";
"/EFI/Linux/${config.system.boot.loader.ukiFile}".source = "${config.system.build.uki}/${config.system.boot.loader.ukiFile}";
"/EFI/memtest86/memtest86.efi".source = "${pkgs.memtest86plus}/memtest.efi";
"/loader/entries/patos-factory-reset.conf".source = pkgs.writeText "patos-factory-reset.conf" ''
title Patos Factory Reset
efi /EFI/Linux/${config.system.boot.loader.ukiFile}
options ${toString config.boot.kernelParams} systemd.factory_reset=yes
sort-key z_factory_reset
'';
"/loader/entries/memtest86.conf".source = pkgs.writeText "memtest86.conf" ''
title Memtest86+
efi /EFI/memtest86/memtest86.efi
options console=ttyS0
sort-key z_memtest
'';
"/loader/loader.conf".source = pkgs.writeText "loader.conf" ''
timeout 2
'';
"/default-ssh-authorized-keys.txt" = lib.mkIf config.system.image.sshKeys.enable {
source = pkgs.writeText "ssh-keys" (lib.concatStringsSep "\n" config.system.image.sshKeys.keys);
};
};
repartConfig = {
Type = "esp";
@ -146,21 +134,6 @@ let
in
{
# This fields is immutable by default, but can be overridden.
options.system.nixos.codeName = lib.mkOption { readOnly = false; };
options.system.nixos.release = lib.mkOption { readOnly = false; };
# FIXME: Should be configured somehow
config.system.nixos = {
codeName = "Finn";
distroId = "patos";
distroName = "PatOS";
release = "2024-11";
variant_id = "server";
variantName = "Server";
vendorName = "PatOS";
};
options.image.compress = lib.mkEnableOption "image compression" // {
default = true;
};

View file

@ -8,6 +8,7 @@
imports = [
./updater.nix
./ssh.nix
./builder.nix
./veritysetup.nix
];
@ -19,6 +20,9 @@
${pkgs.coreutils}/bin/sha256sum * > SHA256SUMS
'';
boot.initrd.systemd.enable = true;
boot.initrd.systemd.repart.enable = true;
systemd.repart.partitions = {
"10-esp" = {
Type = "esp";
@ -50,74 +54,46 @@
Label = "_empty";
ReadOnly = 1;
};
"40-var" = {
Type = "var";
UUID = "4d21b016-b534-45c2-a9fb-5c16e091fd2d"; # Well known
"40-home" = {
Type = "home";
Format = "btrfs";
Label = "patos-state";
Minimize = "off";
FactoryReset = "yes";
SizeMinBytes = "512M";
Encrypt = "tpm2";
SizeMinBytes = "2G";
SplitName = "-";
};
};
boot.initrd.compressor = "zstd";
boot.initrd.compressorArgs = [ "-8" ];
boot.loader.grub.enable = false;
boot.loader.efi.canTouchEfiVariables = true;
boot.loader.systemd-boot.enable = true;
boot.uki.name = "patos";
boot.initrd = {
compressor = "zstd";
compressorArgs = [ "-8" ];
boot.initrd.luks.forceLuksSupportInInitrd = true;
boot.initrd.kernelModules = [
"dm_mod"
"dm_crypt"
] ++ config.boot.initrd.luks.cryptoModules;
luks.forceLuksSupportInInitrd = true;
kernelModules = [
"dm_mod"
"dm_crypt"
] ++ config.boot.initrd.luks.cryptoModules;
supportedFilesystems = {
btrfs = true;
erofs = true;
};
systemd.enable = true;
systemd.repart.enable = true;
systemd.services.systemd-repart = {
after = lib.mkForce [ "sysroot.mount" ];
requires = [ "sysroot.mount" ];
serviceConfig.Environment = [
"SYSTEMD_REPART_MKFS_OPTIONS_BTRFS=--nodiscard"
];
};
boot.initrd.supportedFilesystems = {
btrfs = true;
erofs = true;
};
system.etc.overlay.mutable = false;
users.mutableUsers = false;
boot.initrd.systemd.services.systemd-repart.after = lib.mkForce [ "sysroot.mount" ];
boot.initrd.systemd.services.systemd-repart.requires = [ "sysroot.mount" ];
boot.kernelParams = [
"rootfstype=erofs"
"rootflags=ro"
"roothash=${config.system.build.verityRootHash}"
];
fileSystems =
let
parts = config.systemd.repart.partitions;
in
{
"/var" = {
fsType = parts."40-var".Format;
device = "/dev/mapper/var";
encrypted = {
enable = true;
blkDev = "/dev/disk/by-partuuid/${parts."40-var".UUID}";
label = "var";
};
};
};
fileSystems."/var" = {
fsType = "tmpfs";
options = [ "mode=0755" ];
};
# Required to mount the efi partition
boot.kernelModules = [
@ -126,11 +102,27 @@
"nls_iso8859-1"
];
# Store SSH host keys on /home since /etc is read-only
services.openssh.hostKeys = [
{
path = "/home/.ssh/ssh_host_ed25519_key";
type = "ed25519";
}
];
environment.etc."machine-id" = {
text = "";
mode = "0755";
};
boot.initrd.systemd.services.systemd-repart.serviceConfig.Environment = [
"SYSTEMD_REPART_MKFS_OPTIONS_BTRFS=--nodiscard"
];
# Refuse to boot on mount failure
systemd.targets."sysinit".requires = [ "local-fs.target" ];
# Make sure home gets mounted
systemd.targets."local-fs".requires = [ "home.mount" ];
}

View file

@ -0,0 +1,40 @@
{ config, lib, ... }:
{
options.system.image.sshKeys = {
enable = lib.mkEnableOption "provisioning of default SSH keys from ESP";
keys = lib.mkOption {
type = lib.types.listOf lib.types.singleLineStr;
default = [ ];
};
};
config = lib.mkIf config.system.image.sshKeys.enable {
assertions = [
{
assertion = config.services.openssh.enable;
message = "OpenSSH must be enabled to preseed authorized keys";
}
];
systemd.services."default-ssh-keys" = {
script = ''
mkdir -p /home/admin/.ssh/
cat /efi/default-ssh-authorized-keys.txt >> /home/admin/.ssh/authorized_keys
'';
wantedBy = [
"sshd.service"
"sshd.socket"
];
unitConfig = {
ConditionPathExists = [
"/home/admin"
"!/home/admin/.ssh/authorized_keys"
"/efi/default-ssh-authorized-keys.txt"
];
};
};
};
}

View file

@ -1,5 +1,4 @@
{ config, lib, ... }:
{
{ config, lib, ... }: {
options.system.image.updates = {
enable = lib.mkEnableOption "system updates via systemd-sysupdate" // {

View file

@ -16,7 +16,8 @@
nixpkgs.flake.setNixPath = false;
nixpkgs.flake.setFlakeRegistry = false;
boot.enableContainers = false;
networking.hostName = "patos";
boot.kernelModules = [
"zram"
@ -34,6 +35,8 @@
];
system.etc.overlay.mutable = lib.mkDefault false;
users.mutableUsers = lib.mkDefault false;
systemd.watchdog = lib.mkDefault {
runtimeTime = "10s";
@ -42,17 +45,14 @@
zramSwap.enable = true;
# FIXME: fstrim should only be enabled for virtual machine images?
services.fstrim.enable = true;
services.openssh.settings.PasswordAuthentication = lib.mkDefault false;
users.allowNoPasswordLogin = true;
users.users.root.home = lib.mkForce "/";
security.sudo.enable = lib.mkDefault false;
security.polkit = {
enable = true;
extraConfig = ''
extraConfig =''
polkit.addRule(function(action, subject) {
if (subject.isInGroup("wheel")) {
return polkit.Result.YES;
@ -63,17 +63,17 @@
i18n.supportedLocales = [ "en_US.UTF-8/UTF-8" ];
# Console
systemd.enableEmergencyMode = false;
console.enable = false;
systemd.services."getty@tty1".enable = lib.mkDefault false;
systemd.services."autovt@".enable = lib.mkDefault false;
boot.tmp.useTmpfs = true;
boot.consoleLogLevel = lib.mkDefault 1;
boot.kernelParams = [
"panic=1"
"boot.panic_on_fail"
# "nomodeset"
"console=ttyS0,115200n8"
"earlyprintk=ttyS0,115200n8"
"systemd.mask=systemd-vconsole-setup.service" # FIXME: Figure out why vconsole-setup fails when loading keymap
"nomodeset"
];
# This is vi country
@ -81,15 +81,6 @@
programs.vim.enable = true;
programs.vim.defaultEditor = lib.mkDefault true;
# Temporary file
boot.tmp.useTmpfs = true;
# Logging
services.journald = {
storage = "volatile";
extraConfig = ''
SystemMaxUse=10M
'';
};
services.journald.storage = "volatile";
}

View file

@ -1,39 +0,0 @@
{
modulesPath,
...
}:
{
imports = [ ./server.nix ];
boot.kernel.sysctl = {
"net.ipv4.ip_unprivileged_port_start" = 0;
};
boot.kernelParams = [
"systemd.log_level=info"
"systemd.log_target=console"
"systemd.journald.forward_to_console"
];
users.users."admin" = {
isNormalUser = true;
linger = true;
extraGroups = [ "wheel" ];
home = "/var/home/admin";
};
environment.etc = {
subuid = {
text = "admin:100000:65536";
mode = "0644";
};
subgid = {
text = "admin:100000:65536";
mode = "0644";
};
};
services.getty.autologinUser = "admin";
}

View file

@ -1,32 +1,23 @@
{ lib, ... }:
{
# Use networkd
networking.useNetworkd = true;
systemd.network.wait-online.enable = true;
# Firewall
networking.firewall.enable = false;
networking.nftables.enable = lib.mkDefault true;
# DNS
services.resolved = {
fallbackDns = [ ]; # Disable fallback DNS. DNS will fail if resolvers are unconfigured
extraConfig = ''
DNSStubListener=no
'';
};
# Configuration
networking.hostName = "";
# Kernel
# Use TCP BBR
boot.kernel.sysctl = {
"net.core.default_qdisc" = "fq"; # FIXME: manage these with networkd?
"net.core.default_qdisc" = "fq";
"net.ipv4.tcp_congestion_control" = "bbr";
};
# Modules
services.resolved.extraConfig = ''
DNSStubListener=no
'';
networking.firewall.enable = false;
networking.nftables.enable = lib.mkDefault true;
networking.useNetworkd = true;
systemd.network.wait-online.enable = true;
# Explicitly load networking modules
boot.kernelModules = [
"ip_tables"
"x_tables"

View file

@ -7,11 +7,36 @@
imports = [
(modulesPath + "/profiles/minimal.nix")
./network.nix
./sysext.nix
];
boot.kernelParams = [
"quiet"
boot.kernel.sysctl = {
"net.ipv4.ip_unprivileged_port_start" = 0;
};
users.users."admin" = {
isNormalUser = true;
linger = true;
extraGroups = [ "wheel" ];
};
environment.etc = {
subuid = {
text = "admin:100000:65536";
mode = "0644";
};
subgid = {
text = "admin:100000:65536";
mode = "0644";
};
};
services.openssh.enable = true;
system.image.sshKeys.enable = true;
system.image.sshKeys.keys = [
"sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIIHMAEZx02kbHrEygyPQYStiXlrIe6EIqBCv7anIkL0pAAAABHNzaDo= dln1"
"sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIJNOBFoU7Cdsgi4KpYRcv7EhR/8kD4DYjEZnwk6urRx7AAAABHNzaDo= dln2"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKDx+7ZEJi7lUCAtoHRRIduJzH3hrpx4YS1f0ZxrJ+uW dln3"
];
virtualisation.podman.enable = true;

View file

@ -1,23 +0,0 @@
{ ... }:
{
system.activationScripts.sysext = ''
mkdir -p /var/lib/confexts
mkdir -p /var/lib/extensions
mkdir -p /etc/systemd/extensions
'';
systemd.additionalUpstreamSystemUnits = [
"systemd-confext.service"
"systemd-sysext.service"
];
# systemd.services."systemd-confext" = {
# enable = true;
# wantedBy = [ "multi-user.target" ];
# };
# systemd.services."systemd-sysext.service" = {
# enable = true;
# wantedBy = [ "multi-user.target" ];
# };
}

10
pkgs/erofs-utils.nix Normal file
View file

@ -0,0 +1,10 @@
{ prev, ... }:
# Build mkfs.erofs with zstd support
prev.erofs-utils.overrideAttrs (final: p: {
buildInputs = p.buildInputs ++ [ prev.zstd ];
configureFlags = p.configureFlags ++ [
"--enable-multithreading"
"--enable-zstd"
];
})

7
pkgs/openssh.nix Normal file
View file

@ -0,0 +1,7 @@
{ prev, ... }:
prev.openssh.overrideAttrs (final: prev: {
doCheck = false;
doInstallCheck = false;
dontCheck = true;
})

View file

@ -38,12 +38,11 @@ rec {
"systemd.journald.forward_to_console=1"
];
image.compress = false;
boot.uki.name = lib.mkForce "test";
boot.initrd.compressor = lib.mkForce "zstd";
boot.initrd.compressorArgs = lib.mkForce [ "-8" ];
}
(pkgs.path + "/nixos/modules/testing/test-instrumentation.nix")
self.nixosModules.devel
self.nixosModules.server
self.nixosModules.image
extraConfig
];

37
tests/ssh-preseed.nix Normal file
View file

@ -0,0 +1,37 @@
{ pkgs, self }:
let
lib = pkgs.lib;
test-common = import ./common.nix { inherit self lib pkgs; };
sshKeys = import (pkgs.path + "/nixos/tests/ssh-keys.nix") pkgs;
image = test-common.makeImage {
system.image.sshKeys.keys = [ sshKeys.snakeOilPublicKey ];
system.extraDependencies = [ sshKeys.snakeOilPrivateKey ];
};
in
test-common.makeImageTest {
name = "ssh-preseed";
inherit image;
script = ''
start_tpm()
machine.start()
machine.wait_for_unit("multi-user.target")
machine.succeed("[ -e /efi/default-ssh-authorized-keys.txt ]")
machine.succeed("[ -e /home/admin/.ssh/authorized_keys ]")
machine.wait_for_open_port(22)
machine.succeed(
"cat ${sshKeys.snakeOilPrivateKey} > privkey.snakeoil"
)
machine.succeed("chmod 600 privkey.snakeoil")
machine.succeed(
"ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil admin@127.0.0.1 true",
timeout=30
)
'';
}