Skip to content

Commit

Permalink
add secret contract and use it in ldap block
Browse files Browse the repository at this point in the history
  • Loading branch information
ibizaman authored and ibizaman committed Sep 22, 2024
1 parent d7136b5 commit 7610097
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 0 deletions.
5 changes: 5 additions & 0 deletions docs/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Provided contracts are:
Two implementations are provided: self-signed and Let's Encrypt.
- [Backup contract](contracts-backup.html) to backup directories.
This contract allows to backup multiple times the same directories for extra protection.
- [Secret contract](contracts-secret.html) to provide secrets that are deployed outside of the Nix store.

```{=include=} chapters html:into-file=//contracts-ssl.html
modules/contracts/ssl/docs/default.md
Expand All @@ -32,6 +33,10 @@ modules/contracts/ssl/docs/default.md
modules/contracts/backup/docs/default.md
```

```{=include=} chapters html:into-file=//contracts-secret.html
modules/contracts/secret/docs/default.md
```

## Why do we need this new concept? {#contracts-why}

Currently in nixpkgs, every module needing access to a shared resource must implement the logic
Expand Down
5 changes: 5 additions & 0 deletions docs/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ in stdenv.mkDerivation {
'@OPTIONS_JSON@' \
${individualModuleOptionsDocs [ ../modules/contracts/backup/dummyModule.nix ]}/share/doc/nixos/options.json
substituteInPlace ./modules/contracts/secret/docs/default.md \
--replace \
'@OPTIONS_JSON@' \
${individualModuleOptionsDocs [ ../modules/contracts/secret/dummyModule.nix ]}/share/doc/nixos/options.json
substituteInPlace ./modules/contracts/ssl/docs/default.md \
--replace \
'@OPTIONS_JSON@' \
Expand Down
28 changes: 28 additions & 0 deletions modules/blocks/ldap.nix
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,34 @@ in
description = "File containing the JWT secret.";
};

secret = {
ldapUserPasswordFile = lib.mkOption {
type = contracts.secret;
description = ''
Secret configuration for the file containing the LDAP admin user password.
'';
default = {
mode = "0440";
owner = "lldap";
group = "lldap";
restartUnits = [ "lldap.service" ];
};
};

jwtSecretFile = lib.mkOption {
type = contracts.secret;
description = ''
Secret configuration for the file containing the JWT secret.
'';
default = {
mode = "0440";
owner = "lldap";
group = "lldap";
restartUnits = [ "lldap.service" ];
};
};
};

restrictAccessIPRange = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Set a local network range to restrict access to the UI to only those IPs.";
Expand Down
1 change: 1 addition & 0 deletions modules/contracts/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
{
backup = import ./backup.nix { inherit lib; };
mount = import ./mount.nix { inherit lib; };
secret = import ./secret.nix { inherit lib; };
ssl = import ./ssl.nix { inherit lib; };
}
38 changes: 38 additions & 0 deletions modules/contracts/secret.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{ lib, ... }:
lib.types.submodule {
freeformType = lib.types.anything;

options = {
mode = lib.mkOption {
description = ''
Mode of the secret file.
'';
type = lib.types.str;
default = "0400";
};

owner = lib.mkOption {
description = ''
Linux user owning the secret file.
'';
type = lib.types.str;
default = "root";
};

group = lib.mkOption {
description = ''
Linux group owning the secret file.
'';
type = lib.types.str;
default = "root";
};

restartUnits = lib.mkOption {
description = ''
Systemd units to restart after the secret is updated.
'';
type = lib.types.listOf lib.types.str;
default = [];
};
};
}
150 changes: 150 additions & 0 deletions modules/contracts/secret/docs/default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Secret Contract {#secret-contract}

This NixOS contract represents a secret file
that must be created out of band, from outside the nix store,
and that must be placed in an expected location with expected permission.

It is a contract between a service that needs a secret
and a service that will provide the secret.
All options in this contract should be set by the former.
The latter will then use the values of those options to know where to produce the file.

## Contract Reference {#secret-contract-options}

These are all the options that are expected to exist for this contract to be respected.

```{=include=} options
id-prefix: contracts-secret-options-
list-id: selfhostblocks-options
source: @OPTIONS_JSON@
```

## Usage {#secret-contract-usage}

A service that needs access to a secret will provide one or more `secret` option.

Here is an example module defining two `secret` options:

```nix
{
options = {
myservice.secret = lib.mkOption {
type = lib.types.submodule {
options = {
adminPassword = lib.mkOption {
type = contracts.secret;
readOnly = true;
default = {
owner = "myservice";
group = "myservice";
mode = "0440";
restartUnits = [ "myservice.service" ];
};
};
databasePassword = lib.mkOption {
type = contracts.secret;
readOnly = true;
default = {
owner = "myservice";
restartUnits = [ "myservice.service" "mysql.service" ];
};
};
};
};
};
};
};
```

As you can see, NixOS modules are a bit abused to make contracts work.
Default values are set as well as the `readOnly` attribute to ensure those values stay as defined.

Now, on the other side we have a service that uses these `secret` options and provides the secrets
Let's assume such a module is available under the `secretservice` option
and that one can create multiple instances under `secretservice.instances`.
Then, to actually provide the secrets defined above, one would write:

```nix
secretservice.instances.adminPassword = myservice.secret.adminPassword // {
enable = true;
secretFile = ./secret.yaml;
# ... Other options specific to secretservice.
};
secretservice.instances.databasePassword = myservice.secret.databasePassword // {
enable = true;
secretFile = ./secret.yaml;
# ... Other options specific to secretservice.
};
```

Assuming the `secretservice` module accepts default options,
the above snippet could be reduced to:

```nix
secretservice.default.secretFile = ./secret.yaml;
secretservice.instances.adminPassword = myservice.secret.adminPassword;
secretservice.instances.databasePassword = myservice.secret.databasePassword;
```

### With sops-nix {#secret-contract-usage-sopsnix}

For a concrete example, let's provide the [ldap SHB module][ldap-module] option `ldapUserPasswordFile`
with a secret managed by [sops-nix][].

[ldap-module]: TODO
[sops-nix]: TODO

Without the secret contract, configuring the option would look like so:

```nix
sops.secrets."ldap/user_password" = {
sopsFile = ./secrets.yaml;
mode = "0440";
owner = "lldap";
group = "lldap";
restartUnits = [ "lldap.service" ];
};
shb.ldap.ldapUserPasswordFile = config.sops.secrets."ldap/user_password".path;
```

We can already see the problem here.
How does the end user know what values to give to the
`mode`, `owner`, `group` and `restartUnits` options?
If lucky, the documentation of the option would tell them
or more likely, they will need to figure it out by looking
at the module source code. Not a great user experience.

Now, with this contract, the configuration becomes:

```nix
sops.secrets."ldap/user_password" = config.shb.ldap.secret.ldapUserPasswordFile // {
sopsFile = ./secrets.yaml;
};
shb.ldap.ldapUserPasswordFile = config.sops.secrets."ldap/user_password".path;
```

The issue is now gone.
The module maintainer is now in charge of describing
how the module expects the secret to be provided.

If taking advantage of the `sops.defaultSopsFile` option like so:

```nix
sops.defaultSopsFile = ./secrets.yaml;
```

Then the snippet above is even more simplified:

```nix
sops.secrets."ldap/user_password" = config.shb.ldap.secret.ldapUserPasswordFile;
shb.ldap.ldapUserPasswordFile = config.sops.secrets."ldap/user_password".path;
```
10 changes: 10 additions & 0 deletions modules/contracts/secret/dummyModule.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{ pkgs, lib, ... }:
let
contracts = pkgs.callPackage ../. {};
in
{
options.shb.contracts.secret = lib.mkOption {
description = "Contract for secrets.";
type = contracts.secret;
};
}

0 comments on commit 7610097

Please sign in to comment.