Compare commits

...

5 commits

Author SHA1 Message Date
033755b4bb
feat(nixos): add user password secrets to sops
This commit adds the user password secrets to sops.
It leverages the `userListToAttrs` lib function.
2025-07-07 17:26:57 +08:00
f53a6c6c99
feat(lib): add userListToAttrs function 2025-07-07 17:23:36 +08:00
d33aabf9d8
feat(nixos): add sops module for secrets
Adds .sops.yaml file and sops module to nixos to manage secrets.
2025-07-07 16:55:37 +08:00
986f99715b
feat(nix): add sops-nix to flake inputs 2025-07-07 16:15:20 +08:00
d3ee4cc169
feat(lib): add admin user using custom lib function 2025-07-07 16:14:03 +08:00
9 changed files with 211 additions and 3 deletions

7
.sops.yaml Normal file
View file

@ -0,0 +1,7 @@
keys:
- &rafiq age12l33pas8eptwjc7ewux3d8snyzfzwz0tn9qg5kw8le79fswmjgjqdjgyy6
creation_rules:
- path_regex: \.(yaml)$
key_groups:
- age:
- *rafiq

21
flake.lock generated
View file

@ -215,10 +215,31 @@
"import-tree": "import-tree",
"make-shell": "make-shell",
"nixpkgs": "nixpkgs",
"sops-nix": "sops-nix",
"systems": "systems",
"text": "text"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1751606940,
"narHash": "sha256-KrDPXobG7DFKTOteqdSVeL1bMVitDcy7otpVZWDE6MA=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "3633fc4acf03f43b260244d94c71e9e14a2f6e0d",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,

View file

@ -21,6 +21,11 @@
};
# impermanence provides a nice abstraction over linking files from /persist
impermanence.url = "github:nix-community/impermanence";
# sops-nix lets us version control secrets like passwords and api keys
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
# import-tree imports all nix files in a given directory.
import-tree.url = "github:vic/import-tree";
# files lets us write text files and automatically add checks for them

View file

@ -1,8 +1,16 @@
{ lib, inputs, ... }:
{
lib,
config,
inputs,
...
}:
let
inherit (lib.options) mkOption;
inherit (lib.types) path lazyAttrsOf raw;
inherit (inputs.flake-parts.lib) mkSubmoduleOptions;
inherit (cfg.lib.attrsets) firstAttrNameMatching;
cfg = config.flake;
username = firstAttrNameMatching (_: v: v.primary or false) cfg.manifest.users;
in
{
options.flake = mkSubmoduleOptions {
@ -14,5 +22,13 @@ in
type = path;
default = "";
};
admin = mkOption {
type = lazyAttrsOf raw;
default = { };
};
};
config.flake.admin = cfg.manifest.users.${username} // {
inherit username;
};
}

54
nix/lib/attrsets.nix Normal file
View file

@ -0,0 +1,54 @@
{ lib, ... }:
let
inherit (builtins) attrNames head;
inherit (lib.trivial) pipe;
inherit (lib.attrsets) filterAttrs;
in
{
flake.lib.attrsets = {
/**
`firstAttrNameMatching pred set` filters an attribute set `set` based on a predicate `pred`
and returns the *first* attribute name that satisfies the predicate.
# Example
```nix
let
mySet = {
a = { value = 1; };
b = { value = 2; };
c = { value = 3; };
};
isGreaterThanOne = name: value: value.value > 1;
result = firstAttrNameMatching isGreaterThanOne mySet;
in
result
# Output: "b"
```
# Type
```
firstAttrNameMatching :: (String -> Any -> Bool) -> AttrSet -> String
```
# Arguments
pred
: A function that takes an attribute name and its value and returns a boolean.
set
: The attribute set to filter.
*/
firstAttrNameMatching =
pred: set:
pipe set [
(filterAttrs pred)
attrNames
head
];
};
}

View file

@ -1,10 +1,41 @@
{ lib, config, ... }:
let
cfg = config.flake;
inherit (builtins) foldl' attrNames;
inherit (lib.attrsets) mapAttrs;
in
{
flake.lib.modules = {
/**
Fold over the users list and create an attribute set.
# Inputs
`f`
: A function that takes the name of a user and returns an attribute set.
# Type
```
userListToAttrs :: (String -> AttrSet) -> AttrSet
```
# Examples
:::{.example}
## `userListToAttrs` usage example
```nix
flake.manifest.users.rafiq = { ... };
flake.modules.homeManager.users = userListToAttrs (name: {
${name}.home.username = name;
});
=> flake.modules.homeManager.default.users.rafiq.home.username = "rafiq";
```
:::
*/
userListToAttrs = f: foldl' (acc: elem: acc // (f elem)) { } (attrNames cfg.manifest.users);
/**
Return an attribute set for use with a option that needs to be used for all users.

52
nix/modules/secrets.nix Normal file
View file

@ -0,0 +1,52 @@
{
config,
inputs,
lib,
...
}:
let
cfg = config.flake;
inherit (builtins) readFile;
inherit (lib.meta) getExe;
inherit (lib.strings) trim;
inherit (cfg.admin) username pubkey;
in
{
flake.modules.nixos.default =
{ config, ... }:
{
imports = [ inputs.sops-nix.nixosModules.sops ];
config.sops = {
defaultSopsFile = "${cfg.root}/secrets/secrets.yaml";
age.sshKeyPaths = [
"/persist${config.users.defaultUserHome}/${username}/.ssh/id_ed25519"
];
};
};
perSystem =
{ pkgs, ... }:
{
files.files = [
{
path_ = ".sops.yaml";
drv =
pkgs.writeText ".sops.yaml" # yaml
''
keys:
- &${username} ${trim (
readFile "${
pkgs.runCommand "" { } ''
mkdir $out; echo ${pubkey} | ${getExe pkgs.ssh-to-age} > $out/agepubkey
''
}/agepubkey"
)}
creation_rules:
- path_regex: \.(yaml)$
key_groups:
- age:
- *${username}
'';
}
];
};
}

View file

@ -1,7 +1,7 @@
{ config, lib, ... }:
let
cfg = config.flake;
inherit (cfg.lib.modules) forAllUsers';
inherit (cfg.lib.modules) userListToAttrs forAllUsers';
inherit (lib.lists) optional;
in
{
@ -19,13 +19,17 @@ in
mutableUsers = false;
groups.users.gid = 100;
users = forAllUsers' (
_: value: {
name: value: {
isNormalUser = true;
hashedPasswordFile = config.sops.secrets."${name}/hashedPassword".path;
extraGroups = optional (value.primary or false) "wheel";
openssh.authorizedKeys.keys = [ value.pubkey ];
}
);
};
sops.secrets = userListToAttrs (name: {
"${name}/hashedPassword".neededForUsers = true;
});
home-manager.users = forAllUsers' (
name: _: {
home.username = name;

18
secrets/secrets.yaml Normal file
View file

@ -0,0 +1,18 @@
rafiq:
password: ENC[AES256_GCM,data:8KAfatz+YSaNozd5VGo=,iv:LNRxt47iBKSWzMZuBHSxv/qDZ2h6JiTIPps7OK/o7uU=,tag:oiSfLyRVswb/wxSTE69QMA==,type:str]
hashedPassword: ENC[AES256_GCM,data:NogYQ3kR1TseC79HIXARrXhIncCnvxzf9zMF2QrUyTmojTffPXRGtMdjNpfMEFj5dkKfZujBL/QTIpPFFTm1py7Dreg5/9VSKQ==,iv:IwfZsrsJbLYG1ELte6aBHUtff6hIQu9rHT5tSvILIGQ=,tag:oav3paDcUY+cl4FJlZa90A==,type:str]
sops:
age:
- recipient: age12l33pas8eptwjc7ewux3d8snyzfzwz0tn9qg5kw8le79fswmjgjqdjgyy6
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVd09tYkhKUkVjNTBRdld6
a1RkUnZqdnRqMlFTSGgwUFVCZlRhL0tLTnpVCjNXVjZldzNUOE9DQ0ZGejhWakY2
TmRIZnpobE0ydDhNSDdJQUp2U3pSTzgKLS0tIDkxU3Fxa2lMUkhZY0g1Wm02T2ZE
UkQwOWZtVXVPSGJiRk1qRHVHYkN2cDgKLiYiA0q5se/oHfGRqvHLn3gRRDfmefEZ
z2U2N1Tjt0QgCfYOOXVfPV9F36a7PpabFva5ElSazawHgvI+Bot6og==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2025-07-07T08:56:26Z"
mac: ENC[AES256_GCM,data:2uGjIMxRgk7uWToQC4MrHpHFAt4bI7sEhaHvPU6Ae3bvRVH/TdJxZtikSPe95LEwReOuBmPajbcM580/d3Jt6VbA7nZzj1JduVscrRkSAFCzZp9Ti/mbOGITPJa6xWSGwVF1wSN3BnHXYIHDcKeSGtUdP7L7nBZr1KXPkok4NCo=,iv:+ELIes7lzb8M6CvOemAcyoq7Rx7L6NkNmHwntJN/RSc=,tag:ubyxO6VllH9cQK3VbvxiGg==,type:str]
unencrypted_suffix: _unencrypted
version: 3.10.2