oddlama / agenix-rekey

An agenix extension adding secret generation and automatic rekeying using a YubiKey or master-identity
MIT License
197 stars 16 forks source link

Question: Best way to refer to sibling secrets (not generators)? #22

Closed LoganBarnett closed 3 months ago

LoganBarnett commented 3 months ago

I'm open to the idea that I'm going about this the wrong way.

Some context: I have an ambitious goal of generating TLS certificates for all of my hosts, all chained to a CA - and the best part is I want agenix-rekey conducting all of this for me. My #21 PR is part of this work.

I know TLS can get pretty complicated and distributed. I'm just working with the assumption of a single root + leaf certificates - nothing in between and no crazy features forwarded. That is presently beyond my simian mind.

I want my CA generated on the fly. Granted this happens once, but when I have to reissue it (for when it expires), I want the CA and the leaf certs to be regenerated as well. It also makes the configuration really easy to share with others as a reusable generator. Most of this agenix-rekey can already do as I understand it.

Here's the caveat:

I know I can use deps to declare dependencies between generators. However I want to have dependencies between secrets. My reasoning: Let's say I have hosts alice and bob. They both will need leaf certificates tied to the same CA. I need a way of indicating that they are tied to this CA, and not just any CA. Under the current system of deps, each host would generate a leaf on its own CA, unless I'm misunderstanding something. That, or I wouldn't be able to indicate an arbitrary CA (only one CA because there's only one CA generator).

Okay that's a lot of words. Let's show some code that pushes this idea. Please, correct me if I'm just on the wrong path here :) Note that this does assume my settings addition in #21 went in.

I have these generators:

The root:

  age.generators.tls-ca-root = { name, pkgs, secret, ... }: let
      inherit (lib) isAttrs isString;
      inherit (lib.trivial) throwIfNot;
      inherit (secret) settings;
    in
      throwIfNot (isAttrs settings) "Secret '${name}' must have a `settings` attrset."
      validate-tls-settings name settings.tls
      '' \
      ${pkgs.openssl}/bin/openssl req \
         -new \
         -newkey rsa:4096 \
         -keyout root.key \
         -x509 \
         -nodes \
         -out root.crt \
         -subj "/CN=${settings.tls.domain}${subject-string settings.tls.subject}" \
         -days "${toString settings.tls.validity}"
    '';

The leaf:

    age.generators.tls-signed-certificate = {
      deps,
      file,
      name,
      pkgs,
      secret,
      ...
    }: let
      inherit (lib) isAttrs isString;
      inherit (lib.trivial) throwIfNot;
      inherit (secret) settings;
    in
      throwIfNot (isAttrs settings) "Secret '${name}' must have a `settings` attrset."
      throwIfNot (isString settings.fqdn) "Secret '${name}' is missing a `fqdn` string."
      # throwIfNot (isAttrs settings.root-certificate) "Secret '${name}' is missing a `root-certificate` value."
      ''
      ${pkgs.openssl}/bin/openssl req \
         -new \
         -newkey rsa:4096 \
         -sha256 \
         -nodes \
         -keyout signing.key \
         -out signing.crt \
         -subj "/CN=${settings.fqdn}${subject-string settings.root-certificate.settings.tls.subject}" \
         -addext "subjectAltName = DNS:${settings.fqdn}"
      ls -al signing.* 1>&2
      ls -al ${settings.root-certificate.path}* 1>&2

      ${pkgs.openssl}/bin/openssl req \
         -new \
         -newkey rsa:4096 \
         -sha256 \
         -nodes \
         -keyout signing.key \
         -out signing.crt \
         -subj "/CN=${settings.fqdn}${subject-string settings.root-certificate.settings.tls.subject}" \
         -addext "subjectAltName = DNS:${settings.fqdn}" \
      echo "subjectAltName = DNS:${settings.fqdn}" > san.cnf
      ${pkgs.openssl}/bin/openssl x509 \
         -req \
         -in signing.key \
         -CA ${settings.root-certificate.path}.crt \
         -CAkey ${settings.root-certificate.path}.key \
         -CAcreateserial \
         -out ${file}.crt \
         -days 356 \
         -extfile san.cnf \
      ''
    ;

You'll notice the leaf generator has some ls statements in it so I can see why I can't get the settings.root-certificate.path expression to give me some actual files. This is its output:

-rw-r--r-- 1 logan staff 1785 May 23 20:30 signing.crt
-rw------- 1 logan staff 3272 May 23 20:30 signing.key
ls: cannot access '/run/agenix/internal-ca': No such file or directory

The first ls showing me the signing key that was generating during this run, and the second showing me the root certificate decrypted values (I would hope). The second ls is failing.

This got me thinking: This error is likely presenting itself because root-certificate hasn't been emitted yet. That then leads me to the question I've had, which is: Is there a way we can depend upon and reference secrets from other secrets? Or is that the wrong approach entirely?

For additional context, here is the secret definitions:

  age.secrets.internal-ca = {
    rekeyFile = ../secrets/internal-ca.age;
    settings = {
      tls = {
        domain = "proton";
        subject = {
          country = "US";
          state = "Oregon";
          location = "Portland";
          organization = "Barnett family";
          organizational-unit = "IT Department";
        };
        validity = 365 * 5;
      };
    };
    generator.script = "tls-ca-root";
  };

      age.secrets."tls-${host-id}" = {
        # dependencies = [
        #   config.age.secrets.internal-ca
        # ];
        generator.script = "tls-signed-certificate";
        settings = {
          root-certificate = config.age.secrets.internal-ca;
          fqdn = "lithium.proton";
        };
        rekeyFile = ../secrets/tls-${host-id}.age;
      };

Thanks again for your work on this and all of your help!

oddlama commented 3 months ago

I know I can use deps to declare dependencies between generators. However I want to have dependencies between secrets.

No reasoning needed, dependencies are already between secrets - not generators :D I've built this precisely for the purpose of regenerating dependencies when the parent changes, because I wanted to auto-generate nginx htpasswd files (you can see this example in the readme).

The generator gets passed deps as an argument so it has access to the specific dependencies of the secret that is generated. And it will automatically be regenerated if any of these deps changes. It might be a bit misleading that the definition of dependencies is nested in the generator scope, but this is becaues it's only relevant to the generator. What we usually refer to as the generator is really just the .script setting.

Does this make sense or have I missed something specific in your example about certificates?

LoganBarnett commented 3 months ago

I think between your explanation here and a closer look at the htpasswd example you mentioned, I get it now. The generator "script" is provided deps but the generator.dependencies are the script's dependencies given to that generator. I'll take another pass at it but I suspect this will do the trick. Thanks again!

I think we can close this. I can refine my question further if I run into something anew :)

LoganBarnett commented 3 months ago

It occurs to me that I mentioned #21 for an added settings but that doesn't have it (nor is the branch I was thinking of actually there!). I'll get that filed, and update the references for posterity. I still believe a settings is needed.