Merge pull request #121750 from m1cr0man/master

nixos/acme: Ensure certs are always protected

Changed files
+27 -13
nixos
modules
security
tests
+10 -6
nixos/modules/security/acme.nix
···
serviceConfig = commonServiceConfig // {
StateDirectory = "acme/.minica";
BindPaths = "/var/lib/acme/.minica:/tmp/ca";
};
# Working directory will be /tmp
···
--ca-key ca/key.pem \
--ca-cert ca/cert.pem \
--domains selfsigned.local
-
-
chmod 600 ca/*
'';
};
···
serviceConfig = commonServiceConfig // {
Group = data.group;
StateDirectory = "acme/${cert}";
···
cat cert.pem chain.pem > fullchain.pem
cat key.pem fullchain.pem > full.pem
-
chmod 640 *
-
# Group might change between runs, re-apply it
chown 'acme:${data.group}' *
'';
};
···
fi
mv domainhash.txt certificates/
-
chmod 640 certificates/*
-
chmod -R u=rwX,g=,o= accounts/*
# Group might change between runs, re-apply it
chown 'acme:${data.group}' certificates/*
···
ln -sf fullchain.pem out/cert.pem
cat out/key.pem out/fullchain.pem > out/full.pem
fi
'';
};
};
···
serviceConfig = commonServiceConfig // {
StateDirectory = "acme/.minica";
BindPaths = "/var/lib/acme/.minica:/tmp/ca";
+
UMask = 0077;
};
# Working directory will be /tmp
···
--ca-key ca/key.pem \
--ca-cert ca/cert.pem \
--domains selfsigned.local
'';
};
···
serviceConfig = commonServiceConfig // {
Group = data.group;
+
UMask = 0027;
StateDirectory = "acme/${cert}";
···
cat cert.pem chain.pem > fullchain.pem
cat key.pem fullchain.pem > full.pem
# Group might change between runs, re-apply it
chown 'acme:${data.group}' *
+
+
# Default permissions make the files unreadable by group + anon
+
# Need to be readable by group
+
chmod 640 *
'';
};
···
fi
mv domainhash.txt certificates/
# Group might change between runs, re-apply it
chown 'acme:${data.group}' certificates/*
···
ln -sf fullchain.pem out/cert.pem
cat out/key.pem out/fullchain.pem > out/full.pem
fi
+
+
# By default group will have no access to the cert files.
+
# This chmod will fix that.
+
chmod 640 out/*
'';
};
};
+17 -7
nixos/tests/acme.nix
···
with subtest("Can request certificate with HTTPS-01 challenge"):
webserver.wait_for_unit("acme-finished-a.example.test.target")
-
check_fullchain(webserver, "a.example.test")
-
check_issuer(webserver, "a.example.test", "pebble")
-
check_connection(client, "a.example.test")
with subtest("Certificates and accounts have safe + valid permissions"):
group = "${nodes.webserver.config.security.acme.certs."a.example.test".group}"
webserver.succeed(
-
f"test $(stat -L -c \"%a %U %G\" /var/lib/acme/a.example.test/* | tee /dev/stderr | grep '640 acme {group}' | wc -l) -eq 5"
)
webserver.succeed(
-
f"test $(stat -L -c \"%a %U %G\" /var/lib/acme/.lego/a.example.test/**/* | tee /dev/stderr | grep '640 acme {group}' | wc -l) -eq 5"
)
webserver.succeed(
-
f"test $(stat -L -c \"%a %U %G\" /var/lib/acme/a.example.test | tee /dev/stderr | grep '750 acme {group}' | wc -l) -eq 1"
)
webserver.succeed(
-
f"test $(find /var/lib/acme/accounts -type f -exec stat -L -c \"%a %U %G\" {{}} \\; | tee /dev/stderr | grep -v '600 acme {group}' | wc -l) -eq 0"
)
with subtest("Can generate valid selfsigned certs"):
webserver.succeed("systemctl clean acme-a.example.test.service --what=state")
webserver.succeed("systemctl start acme-selfsigned-a.example.test.service")
check_fullchain(webserver, "a.example.test")
check_issuer(webserver, "a.example.test", "minica")
# Will succeed if nginx can load the certs
webserver.succeed("systemctl start nginx-config-reload.service")
···
webserver.wait_for_unit("acme-finished-a.example.test.target")
check_connection_key_bits(client, "a.example.test", "384")
webserver.succeed("grep testing /var/lib/acme/a.example.test/test")
with subtest("Correctly implements OCSP stapling"):
switch_to(webserver, "ocsp-stapling")
···
with subtest("Can request certificate with HTTPS-01 challenge"):
webserver.wait_for_unit("acme-finished-a.example.test.target")
with subtest("Certificates and accounts have safe + valid permissions"):
group = "${nodes.webserver.config.security.acme.certs."a.example.test".group}"
webserver.succeed(
+
f"test $(stat -L -c '%a %U %G' /var/lib/acme/a.example.test/*.pem | tee /dev/stderr | grep '640 acme {group}' | wc -l) -eq 5"
)
webserver.succeed(
+
f"test $(stat -L -c '%a %U %G' /var/lib/acme/.lego/a.example.test/**/a.example.test* | tee /dev/stderr | grep '600 acme {group}' | wc -l) -eq 4"
)
webserver.succeed(
+
f"test $(stat -L -c '%a %U %G' /var/lib/acme/a.example.test | tee /dev/stderr | grep '750 acme {group}' | wc -l) -eq 1"
)
webserver.succeed(
+
f"test $(find /var/lib/acme/accounts -type f -exec stat -L -c '%a %U %G' {{}} \\; | tee /dev/stderr | grep -v '600 acme {group}' | wc -l) -eq 0"
)
+
with subtest("Certs are accepted by web server"):
+
webserver.succeed("systemctl start nginx.service")
+
check_fullchain(webserver, "a.example.test")
+
check_issuer(webserver, "a.example.test", "pebble")
+
check_connection(client, "a.example.test")
+
+
# Selfsigned certs tests happen late so we aren't fighting the system init triggering cert renewal
with subtest("Can generate valid selfsigned certs"):
webserver.succeed("systemctl clean acme-a.example.test.service --what=state")
webserver.succeed("systemctl start acme-selfsigned-a.example.test.service")
check_fullchain(webserver, "a.example.test")
check_issuer(webserver, "a.example.test", "minica")
+
# Check selfsigned permissions
+
webserver.succeed(
+
f"test $(stat -L -c '%a %U %G' /var/lib/acme/a.example.test/*.pem | tee /dev/stderr | grep '640 acme {group}' | wc -l) -eq 5"
+
)
# Will succeed if nginx can load the certs
webserver.succeed("systemctl start nginx-config-reload.service")
···
webserver.wait_for_unit("acme-finished-a.example.test.target")
check_connection_key_bits(client, "a.example.test", "384")
webserver.succeed("grep testing /var/lib/acme/a.example.test/test")
+
# Clean to remove the testing file (and anything else messy we did)
+
webserver.succeed("systemctl clean acme-a.example.test.service --what=state")
with subtest("Correctly implements OCSP stapling"):
switch_to(webserver, "ocsp-stapling")