-
-
Notifications
You must be signed in to change notification settings - Fork 181
Open
Description
Feature Description
Long story, but due to some complicated DNS setup I have, the LetsEncypt doesn't work for me in Cosmos Server, but it works in other things. I'm on NixOS so I use Acme to get the certs, then I convert the key to pkcs8, then it's ready to be used.
For Cosmos Server, it doesn't allow me to specify the file paths, so I had to be a little more creative.
I just wanted to share my setup on NixOS. Hoping it'll be useful to someone.
sudo nano /etc/nixos/configuration.nix
imports =
[ # Include the results of the hardware scan.
./hardware-configuration.nix
./services/acme.nix # added
./services/cosmos-updater.nix #added
];
# Make sure to add openssl, jq, and anything else I might have missed.
sudo mkdir /etc/nixos/services
sudo nano /etc/nixos/services/acme.nix
{ config, pkgs, ... }:
let
domain = "example.com";
certDir = "/var/lib/acme/${domain}";
CFDnsApi = "<redacted>";
CFZoneApi = "<redacted>";
convertCertsScript = pkgs.writeShellScript "convert-certs-pkcs8" ''
set -e
if [ -f "${certDir}/fullchain.pem" ]; then
chown acme:acme "${certDir}/fullchain.pem"
chmod 640 "${certDir}/fullchain.pem"
else
echo "Error: ${certDir}/fullchain.pem not found"
exit 1
fi
if [ -f "${certDir}/key.pem" ]; then
#rm -f "${certDir}/key-pkcs8.pem"
${pkgs.openssl}/bin/openssl pkcs8 -topk8 -inform PEM -outform PEM \
-in "${certDir}/key.pem" -out "${certDir}/key-pkcs8-new.pem" -nocrypt
mv -f "${certDir}/key-pkcs8-new.pem" "${certDir}/key-pkcs8.pem"
chown acme:acme "${certDir}/key-pkcs8.pem"
chmod 640 "${certDir}/key-pkcs8.pem"
else
echo "Error: ${certDir}/key.pem not found"
exit 1
fi
'';
in {
environment.systemPackages = [ pkgs.openssl ];
users.users.acme = {
isSystemUser = true;
group = "acme";
home = "/var/lib/acme";
description = "ACME certificate management user";
};
users.groups.acme = {};
environment.etc."letsencrypt/cloudflare.ini" = {
text = ''
CF_DNS_API_TOKEN=${CFDnsApi}
CF_ZONE_API_TOKEN=${CFZoneApi}
'';
mode = "0600";
};
security.acme = {
acceptTerms = true;
preliminarySelfsigned = false;
defaults.email = "admin@example.com";
certs."${domain}" = {
dnsProvider = "cloudflare";
dnsResolver = "1.1.1.1:53";
dnsPropagationCheck = true;
domain = domain;
extraDomainNames = [ "*.${domain}" ];
credentialsFile = "/etc/letsencrypt/cloudflare.ini";
reloadServices = [ "acme-cert-convert.service" ];
};
};
systemd.services.acme-cert-convert = {
description = "Convert certificates to PKCS#8 format";
wantedBy = [ "multi-user.target" ];
after = [ "acme-${domain}.service" ]; # ensure certs exist
serviceConfig = {
Type = "oneshot";
ExecStart = "${convertCertsScript}";
User = "acme";
Group = "acme";
RemainAfterExit = true;
};
};
}
sudo nano /etc/nixos/services/cosmos-updater.nix
{ config, pkgs, ... }:
let
domain = "example.com";
certDir = "/var/lib/acme/${domain}";
cosmosConfigFile = "/var/lib/cosmos/cosmos.config.json";
updateCosmosCertScript = pkgs.writeShellScriptBin "update-cosmos-cert" ''
#!${pkgs.bash}/bin/bash
set -euo pipefail
# Number of days before expiration to trigger an update.
RENEW_BEFORE_DAYS=14
echo "Starting Cosmos certificate check for ${domain}..."
RENEW_SECONDS=$((RENEW_BEFORE_DAYS * 24 * 60 * 60))
echo "Reading current certificate expiry from ${cosmosConfigFile}..."
COSMOS_EXPIRY_STRING=$(${pkgs.jq}/bin/jq -r '.HTTPConfig.TLSValidUntil' "${cosmosConfigFile}")
if [ -z "$COSMOS_EXPIRY_STRING" ] || [ "$COSMOS_EXPIRY_STRING" == "null" ]; then
echo "Warning: TLSValidUntil not found in Cosmos config. Forcing update."
EXPIRY_TIMESTAMP=0
else
echo "Found TLSValidUntil: $COSMOS_EXPIRY_STRING"
EXPIRY_TIMESTAMP=$(${pkgs.coreutils}/bin/date -d "$COSMOS_EXPIRY_STRING" +%s)
fi
CURRENT_TIMESTAMP=$(${pkgs.coreutils}/bin/date +%s)
if [ "$EXPIRY_TIMESTAMP" -lt "$((CURRENT_TIMESTAMP + RENEW_SECONDS))" ]; then
echo "Certificate in Cosmos is expiring in less than $RENEW_BEFORE_DAYS days. Updating from ACME files."
else
echo "Certificate in Cosmos is still valid for more than $RENEW_BEFORE_DAYS days. No action needed."
exit 0
fi
echo "Reading certificate and key files from ${certDir}..."
CERT_CONTENT=$(${pkgs.gawk}/bin/awk 'NF {sub(/\r/, ""); printf "%s\n",$0} END {printf "\\n"}' "${certDir}/fullchain.pem")
KEY_CONTENT=$(${pkgs.gawk}/bin/awk 'NF {sub(/\r/, ""); printf "%s\n",$0} END {printf "\\n"}' "${certDir}/key-pkcs8.pem")
if [ -z "$CERT_CONTENT" ] || [ -z "$KEY_CONTENT" ]; then
echo "Error: Certificate or key file is empty or could not be read."
exit 1
fi
echo "Calculating new expiration date from ${certDir}/fullchain.pem..."
NEW_EXPIRY_RAW=$(${pkgs.openssl}/bin/openssl x509 -enddate -noout -in "${certDir}/fullchain.pem" | cut -d= -f2)
NEW_EXPIRY_RFC3339=$(${pkgs.coreutils}/bin/date -d "$NEW_EXPIRY_RAW" --utc --rfc-3339=ns | sed 's/ /T/')
echo "Updating ${cosmosConfigFile} with new cert and expiry date: $NEW_EXPIRY_RFC3339"
${pkgs.jq}/bin/jq \
--arg cert "$CERT_CONTENT" \
--arg key "$KEY_CONTENT" \
--arg expiry "$NEW_EXPIRY_RFC3339" \
'.HTTPConfig.TLSCert = $cert | .HTTPConfig.TLSKey = $key | .HTTPConfig.TLSValidUntil = $expiry | .HTTPConfig.HTTPSCertificateMode = "PROVIDED"' \
"${cosmosConfigFile}" > "${cosmosConfigFile}.tmp"
if [ -s "${cosmosConfigFile}.tmp" ]; then
${pkgs.coreutils}/bin/mv "${cosmosConfigFile}.tmp" "${cosmosConfigFile}"
echo "Cosmos config updated successfully."
else
echo "Error: Failed to create temporary config file. Aborting."
${pkgs.coreutils}/bin/rm -f "${cosmosConfigFile}.tmp"
exit 1
fi
echo "Restarting the cosmos-server container..."
if ${pkgs.docker}/bin/docker restart "cosmos-server"; then
echo "Container restarted successfully."
else
echo "Error: Failed to restart container cosmos-server."
exit 1
fi
echo "Cosmos certificate update process complete."
'';
in {
systemd.services.cosmos-cert-updater = {
description = "Update Cosmos Cloud certificate from ACME files";
serviceConfig = {
Type = "oneshot";
User = "root";
ExecStart = "${updateCosmosCertScript}/bin/update-cosmos-cert";
};
};
systemd.timers.cosmos-cert-updater = {
description = "Daily check to update Cosmos Cloud certificate";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "daily";
Persistent = true;
RandomizedDelaySec = "6h";
};
};
}
Metadata
Metadata
Assignees
Labels
No labels