Skip to content

[FEAT]: Add a feature to use a certificate path for the certificate and key #464

@Lyamc

Description

@Lyamc

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

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions