Manage secrets using agenix

This commit is contained in:
teutat3s 2023-07-15 16:32:18 +02:00
parent 7095204d06
commit aff559a20e
Signed by: teutat3s
GPG key ID: 4FA1D3FA524F22C1
10 changed files with 306 additions and 27 deletions

View file

@ -1,4 +1,4 @@
# Erpnext on NixOS
# ERPNext on NixOS
### Cachix
Using the https://pub-solar.cachix.org binary cache:
@ -22,15 +22,40 @@ nix build --json .#run-erpnext \
### NixOS VM
```
# Build the test VM
nix build '.#nixosConfigurations.test-vm.config.system.build.vm'
# Start the VM, to generate SSH host key files required for secrets
./result/bin/run-nixos-vm
# In the VM, use root & empty password to login
# Get the ed25519 SSH hostkey
ssh-keyscan -p 2222 127.0.0.1
# Edit secrets.nix and add the SSH hostkey to machine 'test-vm'
vim secrets/secrets.nix
# Rekey the secrets with agenix
cd secrets
agenix --rekey
cd ..
# Stop with CTRL-c & build the test VM again
nix build '.#nixosConfigurations.test-vm.config.system.build.vm'
# Start the VM
./result/bin/run-nixos-vm
# Access the VM via SSH, use an empty password (Enter) to login
ssh root@localhost -p 2222
# Watch erpnext startup:
# journalctl -fu erpnext.service
# The initial startup can take a few minutes
journalctl -fu erpnext-web.service
# Open http://localhost:8081 in your browser
# User: Administrator
# Password: admin
# To reset all data for the VM
rm nixos.qcow2
```
### Docker

117
docs/secrets.md Normal file
View file

@ -0,0 +1,117 @@
# Secrets
Secrets are managed using [agenix][agenix]
so you can keep your flake in a public repository like GitHub without
exposing your password or other sensitive data.
## Agenix
Currently, there is [no mechanism][secrets-issue] in nix itself to deploy secrets
within the nix store because it is world-readable.
Most NixOS modules have the ability to set options to files in the system, outside
the nix store, that contain sensitive information. You can use [agenix][agenix]
to easily setup those secret files declaratively.
[agenix][agenix] encrypts secrets and stores them as .age files in your repository.
Age files are encrypted with multiple ssh public keys, so any host or user with a
matching ssh private key can read the data. The [age module][age module] will add those
encrypted files to the nix store and decrypt them on activation to `/run/agenix`.
### Setup
All hosts must have openssh enabled, this is done by default in the core profile.
You need to populate your `secrets/secrets.nix` with the proper ssh public keys.
Be extra careful to make sure you only add public keys, you should never share a
private key!!
secrets/secrets.nix:
```nix
let
system = "<system ssh key>";
user = "<user ssh key>";
allKeys = [ system user ];
in
```
On most systems, you can get your systems ssh public key from `/etc/ssh/ssh_host_ed25519_key.pub`. If
this file doesn't exist you likely need to enable openssh and rebuild your system.
Your users ssh public key is probably stored in `~/.ssh/id_ed25519.pub` or
`~/.ssh/id_rsa.pub`. If you haven't generated a ssh key yet, be sure do so:
```sh
ssh-keygen -t ed25519
```
> ##### _Note:_
>
> The underlying tool used by agenix, rage, doesn't work well with password protected
> ssh keys. So if you have lots of secrets you might have to type in your password many
> times.
### Secrets
You will need the `agenix` command to create secrets. DevOS conveniently provides that
in the devShell, so just run `nix develop` whenever you want to edit secrets. Make sure
to always run `agenix` while in the `secrets/` folder, so it can pick up your `secrets.nix`.
To create secrets, simply add lines to your `secrets/secrets.nix`:
```
let
...
allKeys = [ system user ];
in
{
"secret.age".publicKeys = allKeys;
}
```
That would tell agenix to create a `secret.age` file that is encrypted with the `system`
and `user` ssh public key.
Then go into the `secrets` folder and run:
```sh
agenix -e secret.age
```
This will create the `secret.age`, if it doesn't already exist, and allow you to edit it.
If you ever change the `publicKeys` entry of any secret make sure to rekey the secrets:
```sh
agenix --rekey
```
### Usage
Once you have your secret file encrypted and ready to use, you can utilize the [age module][age module]
to ensure that your secrets end up in `/run/secrets`.
In any profile that uses a NixOS module that requires a secret you can enable a particular secret like so:
```nix
{ self, ... }:
{
age.secrets.mysecret.file = "${self}/secrets/mysecret.age";
}
```
Then you can just pass the path `/run/agenix/mysecret` to the module.
You can make use of the many options provided by the age module to customize where and how
secrets get decrypted. You can learn about them by looking at the
[age module][age module].
> ##### _Note:_
>
> You can take a look at the [agenix repository][agenix] for more information
> about the tool.
[agenix]: https://github.com/ryantm/agenix
[age module]: https://github.com/ryantm/agenix/blob/master/modules/age.nix
[secrets-issue]: https://github.com/NixOS/nix/issues/8

View file

@ -1,5 +1,48 @@
{
"nodes": {
"agenix": {
"inputs": {
"darwin": "darwin",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1682101079,
"narHash": "sha256-MdAhtjrLKnk2uiqun1FWABbKpLH090oeqCSiWemtuck=",
"owner": "ryantm",
"repo": "agenix",
"rev": "2994d002dcff5353ca1ac48ec584c7f6589fe447",
"type": "github"
},
"original": {
"owner": "ryantm",
"repo": "agenix",
"type": "github"
}
},
"darwin": {
"inputs": {
"nixpkgs": [
"agenix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1689163348,
"narHash": "sha256-qL3FnBauu3PmYf0OZzv2cKtVyRg4ELVXPQl0AAYqlqs=",
"owner": "lnl7",
"repo": "nix-darwin",
"rev": "43ce086813c83184b88f67fc544af2050a3035ba",
"type": "github"
},
"original": {
"owner": "lnl7",
"ref": "master",
"repo": "nix-darwin",
"type": "github"
}
},
"devshell": {
"inputs": {
"nixpkgs": [
@ -41,6 +84,7 @@
},
"root": {
"inputs": {
"agenix": "agenix",
"devshell": "devshell",
"nixpkgs": "nixpkgs",
"systems": "systems"

View file

@ -1,15 +1,20 @@
{
description = "Dev Setup";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
inputs.systems.url = "github:nix-systems/default";
systems.url = "github:nix-systems/default";
inputs.devshell.url = "github:numtide/devshell";
inputs.devshell.inputs.nixpkgs.follows = "nixpkgs";
inputs.devshell.inputs.systems.follows = "systems";
devshell.url = "github:numtide/devshell";
devshell.inputs.nixpkgs.follows = "nixpkgs";
devshell.inputs.systems.follows = "systems";
outputs = {self, nixpkgs, systems, devshell }:
agenix.url = "github:ryantm/agenix";
agenix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = {agenix, devshell, nixpkgs, self, systems}:
let
eachSystem = nixpkgs.lib.genAttrs (import systems);
# Nixpkgs instantiated for system types in nix-systems
@ -19,6 +24,7 @@
overlays = [
self.overlays.default
self.overlays.pythonOverlay
agenix.overlays.default
devshell.overlays.default
];
}
@ -45,6 +51,11 @@
name = pkgs.cachix.pname;
package = pkgs.cachix;
}
{
help = pkgs.agenix.meta.description;
name = pkgs.agenix.pname;
package = pkgs.agenix;
}
];
bash.extra = ''
'';
@ -67,7 +78,10 @@
{
test-vm = nixpkgs.lib.nixosSystem {
inherit system pkgs;
modules = [./test-vm/configuration.nix];
modules = [
agenix.nixosModules.age
./test-vm/configuration.nix
];
};
};
};

View file

@ -42,7 +42,7 @@ in
adminPasswordFile = mkOption {
type = types.nullOr types.path;
default = null;
example = "/run/secrets/erpnext-password";
example = "/run/secrets/erpnext-admin-password";
description = lib.mdDoc ''
A file containing the Administrator user password.
'';
@ -69,10 +69,10 @@ in
default = "erpnext";
description = lib.mdDoc "Database username.";
};
passwordFile = mkOption {
userPasswordFile = mkOption {
type = types.nullOr types.path;
default = null;
example = "/run/secrets/erpnext-db-password";
example = "/run/secrets/erpnext-db-user-password";
description = lib.mdDoc ''
A file containing the MariaDB erpnext user password.
'';
@ -80,7 +80,7 @@ in
rootPasswordFile = mkOption {
type = types.nullOr types.path;
default = null;
example = "/run/secrets/erpnext-root-db-password";
example = "/run/secrets/erpnext-db-root-password";
description = lib.mdDoc ''
A file containing the MariaDB root user password.
'';
@ -144,7 +144,7 @@ in
example = lib.literalExpression ''
{
serverAliases = [
"dolibarr.''${config.networking.domain}"
"erpnext.''${config.networking.domain}"
"erp.''${config.networking.domain}"
];
enableACME = false;
@ -195,10 +195,10 @@ in
db_host = "${cfg.database.host}";
db_port = "${toString cfg.database.port}";
db_name = "${cfg.database.name}";
db_password = "#NIXOS_ERPNEXT_DB_PASSWORD#";
redis_cache = "redis://${cfg.redis.host}:${toString cfg.redis.port}";
redis_queue = "redis://${cfg.redis.host}:${toString cfg.redis.port}";
redis_socketio = "redis://${cfg.redis.host}:${toString cfg.redis.port}";
db_password = "#NIXOS_ERPNEXT_DB_USER_PASSWORD#";
redis_cache = "redis://${cfg.redis.host}:${toString cfg.redis.port}?db=1";
redis_queue = "redis://${cfg.redis.host}:${toString cfg.redis.port}?db=2";
redis_socketio = "redis://${cfg.redis.host}:${toString cfg.redis.port}?db=0";
socketio_port = "${toString cfg.socketIoPort}";
};
commonSiteConfigFile = pkgs.writeText "erpnext-common_site_config.json" (builtins.toJSON commonSiteConfig);
@ -209,6 +209,7 @@ in
Type = "simple";
BindReadOnlyPaths = [
"/etc/hosts:/etc/hosts"
"/run/agenix:/run/agenix"
"${pkgs.frappe-app}:${pkgs.frappe-app}"
"${pkgs.frappe-app}/share/apps/frappe:${cfg.benchDir}/apps/frappe"
"${pkgs.erpnext-app}:${pkgs.erpnext-app}"
@ -264,7 +265,7 @@ in
wantedBy = [ "erpnext-web.service" ];
partOf = [ "erpnext-web.service" ];
script = ''
${pkgs.mariadb-client}/bin/mysql -e "SET PASSWORD FOR 'root'@'localhost' = PASSWORD('${builtins.readFile cfg.database.rootPasswordFile}')";
${pkgs.mariadb-client}/bin/mysql -e "SET PASSWORD FOR 'root'@'localhost' = PASSWORD('$(cat "${cfg.database.rootPasswordFile}")')";
'';
serviceConfig = {
RemainAfterExit = true;
@ -342,18 +343,21 @@ in
ExecStartPre = assert cfg.adminPasswordFile != null && cfg.database.rootPasswordFile != null; pkgs.writeScript "erpnext-web-init" ''
#!/bin/sh
if ! test -e ${escapeShellArg "${cfg.workDir}/.db-created"}; then
# Fail on error
set -e
install -m0600 ${commonSiteConfigFile} ${cfg.benchDir}/sites/common_site_config.json
${pkgs.replace-secret}/bin/replace-secret \
'#NIXOS_ERPNEXT_DB_PASSWORD#' \
${cfg.database.passwordFile} \
'#NIXOS_ERPNEXT_DB_USER_PASSWORD#' \
${cfg.database.userPasswordFile} \
${cfg.benchDir}/sites/common_site_config.json
ADMIN_PASSWORD="$(cat "${cfg.adminPasswordFile}")"
ROOT_DB_PASSWORD="$(cat "${cfg.database.rootPasswordFile}")"
DB_ROOT_PASSWORD="$(cat "${cfg.database.rootPasswordFile}")"
# Upstream initializes the database with this command
${penv}/bin/bench new-site localhost \
--mariadb-root-password "$ROOT_DB_PASSWORD" \
--mariadb-root-password "$DB_ROOT_PASSWORD" \
--admin-password "$ADMIN_PASSWORD" \
--install-app erpnext

View file

@ -0,0 +1,15 @@
age-encryption.org/v1
-> ssh-ed25519 Wp/X/Q CkX9o53yvwG6TWGy86wjz8K08iEKcDWlCFGD/rOqjG8
I1JY9R+wMRDD0NYJQ5xYGYXeEMyBzg7J6E+BAVzlTmY
-> ssh-ed25519 FnuwSg TIWzdHqIIpGG6GqTspw6tFSWgIfdNnUPiRcWCJyARwA
xYMIfXDTl0KcaEa+cQPF0Ld5py4cFAgQUNha4Lrxqa0
-> ssh-ed25519 BVsyTA G0DnHKpVXXJPOMsJz9GGUX+RNEQ9FHv5AzUQl0AkkzA
OUsicHWAoiDuOQGHQDcgnBX58GdFKP0CpQiB+fF7KPs
-> ssh-ed25519 BVsyTA SQ7tgv5R8XNEv9nZaq27Y1/wDhgTzG628EuJh0yHOGk
cyBVqL5j0oIsVoIHMz9If9sJvRq7zNrB8aSqlFyeyYw
-> nr#`|-grease ORM\VtI 0#l nsQ`v=O 3[zMr%O(
Zs1oB4DhG5oE2QYXl/8yVUcdafTSJlK5LtB3gWYhi1pgyox63Pi5oZ0XpzzqN855
/egFwpJ9QIMpU7etG94XLjEJq/k0T7P3Nu66Z4pGuRDTybqLCAKlbkcwWGxMxEkH
Yg
--- aRI3fjSGDjF34d8JK+Ow6dfmncI8qvl3Betzlc1hsVc
©¢²¼{õHå/6kSÀÓšclµ e_0ÀÀ_œ]< h(²

View file

@ -0,0 +1,14 @@
age-encryption.org/v1
-> ssh-ed25519 Wp/X/Q uMcrhm3fS+M/P2uaEJvdRab5G4Nk6zZNAlHyRISlGg0
9QNitHuuKYzn99Hr76+yOa51/4qfTlhUwf5cD/Px/FI
-> ssh-ed25519 FnuwSg ukuZCjWTcyC4LiHWmrunskAk3WbNZsdAXzHi3fydJwI
k3N3F4hxw55E6C68jLoaIQfuDUaOILxbw8BYwIcI2e8
-> ssh-ed25519 BVsyTA q+2w2O+4c8NahzHuo2KLDxa8RHB79+L+h9KOY9GdGkA
eWRM4yyQtefZ6tnozqg37eLLMz7rQYl3eem2jxGThwo
-> ssh-ed25519 BVsyTA 6qkLU9OPuBv6KlgaILTkeFsyNgm8uW2PgZvsIQ8pGwM
kXBSETY11ZCJs5HZ2jH9LAw0ntrsSW2J5ES97X7FkpI
-> }6jwb-grease ,2 {su
Yypdzw8GT6HmlqgBxHAuEiKDWsH+wSYK2rMxketTP6OlWJHfmV9jc/YBMXwWNkhh
jcQfjmwx2Wjcn0mjiv6RUYi3tulpbkB+jctRW+vstPHTIptEMhOfs5FM
--- WPd5tG8HvNuhMZfqvzaCeDz2tavijgWewAuLV+5wFuE
9öêÔ¯“áY÷%"|V`WV%l»''oKÀ$W`Ÿ!è)ŸBؘºN¥p

View file

@ -0,0 +1,14 @@
age-encryption.org/v1
-> ssh-ed25519 Wp/X/Q 3hW/8PTjCcRN4cj1OJlmKgGrupW/At/XcZP9mX5relM
DrYHHxhA6DDj5tkUwMEP1Diedo5ZRtdaDaytodWcRlU
-> ssh-ed25519 FnuwSg 7szTuXVOxA1gEKvXFDDpPBak5Eh2CwUGnL52wGLmogw
J/no4LHtQ5FR43r6evCk4n6V/6gQ5navfvVGOc6Mnl0
-> ssh-ed25519 BVsyTA cGS0ASOgef9VUxNQF0h70h9aJnctBZRc6FBQgahLn0c
b35sRRi+yNu2arU7yipZctcie0itomjJtc3Nr4i2nss
-> ssh-ed25519 BVsyTA VG8SwHZkdvGwmbx/kOT5oabV+WpO5QGIJXtse4CtKgE
WiEuIMlsaW98kPB6pChEZhCNWo0pVbDMEIwfRtuKNnY
-> 2IOJi1-grease s^`TsZL %{ &~L*
TU4AdoaLUEYR8YQRMcUdFho/WOs9IpwM1z4t36FbM3GPFqz0IY5MCC9shj+oxhh1
aW5/FKRXoTLG5A
--- 9HuacZ7Sr0o0XKFkdT4qIQSdx8Idymsb/jc3QxLOwg0
MÄ0H±„{ÝË»—ˆ;Ÿc”ž*$½6dŒ(ÖÕ1ÏüDÍ÷<C38D>3OÂe†

16
secrets/secrets.nix Normal file
View file

@ -0,0 +1,16 @@
let
# set ssh public keys here for your system and user
machines = {
dumpyourvms = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILDATEWAgDZFfYs1ZPh33Kg4sqQ9tWMVKyk8XqFu3Koe host@dumpyourvms";
test-vm = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILD8RziyMKgRj6MLkRjdHnNwG2+VolUl8ejjeteehVnw host@test-vm";
};
users = {
teutat3s = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHcU6KPy4b1MQXd6EJhcYwbJu7E+0IrBZF/IP6T7gbMf teutat3s@dumpyourvms";
root = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHcU6KPy4b1MQXd6EJhcYwbJu7E+0IrBZF/IP6T7gbMf root@test-vm";
};
allKeys = [machines.dumpyourvms machines.test-vm users.root users.teutat3s];
in {
"admin-password.age".publicKeys = allKeys;
"database-root-password.age".publicKeys = allKeys;
"database-user-password.age".publicKeys = allKeys;
}

View file

@ -60,11 +60,27 @@
neovim
];
age.secrets.erpnext-admin-password = {
file = ../secrets/admin-password.age;
mode = "700";
owner = "erpnext";
};
age.secrets.erpnext-db-root-password = {
file = ../secrets/database-root-password.age;
mode = "700";
owner = "erpnext";
};
age.secrets.erpnext-db-user-password = {
file = ../secrets/database-user-password.age;
mode = "700";
owner = "erpnext";
};
services.erpnext = {
enable = true;
adminPasswordFile = ../adminpass.txt;
database.rootPasswordFile = ../dbrootpass.txt;
database.passwordFile = ../dbuserpass.txt;
adminPasswordFile = config.age.secrets.erpnext-admin-password.path;
database.rootPasswordFile = config.age.secrets.erpnext-db-root-password.path;
database.userPasswordFile = config.age.secrets.erpnext-db-user-password.path;
caddy = {};
};
};