/* Because I'm way too lazy I'm automatically generating the secret files config. Secrets can be found below hosts/${hostname}/secrets/*.age Pubkeys can be found for the specific host below hosts/${hostname}/ssh.pub The users have their keys below users/${username}/ssh.pub Secrets get encrypted for the host they are in and the users specified. Every host with a secrets directory has an entry for a secret called "new". This exist to overcome the chicken and egg problem. Create a secret with them name new in the specific secrets directory and rename it afterwards with the suffix .age. */ let /* Returns an attrset for a given directory, having the name of a subdirectory as its attribute names and the contents of the containing ssh.pub file as their value { clerie = "ssh-ed25519 AAAA..."; } */ pubkeysFor = directory: let instances = builtins.attrNames (builtins.readDir directory); instancesWithPubkey = builtins.filter (i: builtins.pathExists (directory + "/${i}/ssh.pub")) instances; in builtins.listToAttrs (map (i: { name = i; value = builtins.readFile (directory + "/${i}/ssh.pub"); }) instancesWithPubkey); users = pubkeysFor ./users; hosts = pubkeysFor ./hosts; /* Returns secret configuration for a given hostname */ secretsForHost = hostname: let /* Returns a list of all file names in the secrets directory of the specified host */ secretsFiles = builtins.attrNames (builtins.readDir (./hosts + "/${hostname}/secrets")); /* Returns all file names that end with .age */ listOfSecrets = builtins.filter (i: # Make sure the file name is longer than the file extension (builtins.stringLength i) > 4 # Take the last four letters of the file name and check if it is .age && builtins.substring ((builtins.stringLength i) - 4) (builtins.stringLength i) i == ".age" ) secretsFiles; in if # Make sure the host has a secrets directory builtins.pathExists (./hosts + "/${hostname}/secrets") # Make sure the host has a public ssh key provided && builtins.pathExists (./hosts + "/${hostname}/ssh.pub") then /* This map specifies all public keys for which a given secret file should be encrypted It returns a list of name value pairs The name is the path to the secret file The value is an attribute set containing a list of public keys as a string */ map (secret: { name = "hosts/${hostname}/secrets/${secret}"; value = { publicKeys = [ # Hardcode clerie's public key here users.clerie # No other user should have access to any secrets # A host should only have access to their own secrets hosts."${hostname}" ]; }; }) # All file names of already existing secrets plus the magic "new" secret (listOfSecrets ++ [ "new" ]) else # Answer with an empty list, if no secrets are provided for a host []; in # We just have a list of name value pairs that need to get transformed into an attribute set builtins.listToAttrs ( builtins.concatMap # Provide a list of secrets for a given hostname (hostname: secretsForHost hostname) # Names of all hosts (builtins.attrNames (builtins.readDir ./hosts)) )