Skip to content

feat: Add ZeroSSL support as alternative ACME CA #2

@ameistad

Description

@ameistad

Add support for ZeroSSL, configurable per-domain via haloy.yaml. Let's Encrypt remains the default. EAB credentials for ZeroSSL use the existing ValueSource pattern, so they can be provided inline, via env vars, or via built-in secret management

Proposed Changes:

Add three new fields to config.Domain:

  • acme_provider (string): "letsencrypt" (default) or "zerossl"
  • eab_kid (ValueSource): External Account Binding Key ID for ZeroSSL
  • eab_hmac (ValueSource): External Account Binding HMAC key for ZeroSSL
domains:
  - domain: example.com
    acme_provider: zerossl
    eab_kid:
      from:
        secret: "onepassword:zerossl_creds.eab_kid"
    eab_hmac:
      from:
        secret: "onepassword:zerossl_creds.eab_hmac"

# Using environment variables
domains:
  - domain: example.com
    acme_provider: zerossl
    eab_kid:
      from:
        env: ZEROSSL_EAB_KID
    eab_hmac:
      from:
        env: ZEROSSL_EAB_HMAC

# Inline 
domains:
  - domain: example.com
    acme_provider: zerossl
    eab_kid:
      value: "abc123"
    eab_hmac:
      value: "hmac-key-here"

Implementation Plan

Config layer (internal/config/)

  • deploy_config.go: Add ACMEProvider, EABKeyID, EABHMACKey fields to Domain struct. Update Domain.Validate() to enforce that ZeroSSL requires both EAB fields and that Let's Encrypt rejects them.
  • labels.go: Add label format strings (dev.haloy.domain.%d.acme-provider, dev.haloy.domain.%d.eab-kid, dev.haloy.domain.%d.eab-hmac). Update ToLabels() and ParseContainerLabels() to serialize/deserialize the new fields.

Secret resolution (internal/configloader/)

  • resolve_secrets.go: Add domain EAB ValueSource fields to gatherValueSources() and gatherTargetValueSources() so they go through the same resolution pipeline as env vars and API tokens.

Certificate management (internal/haloyd/)

  • certificates.go:
    • Add ZeroSSL ACME directory constant (https://acme.zerossl.com/v2/DV90)
    • Extend CertificatesDomain with ACMEProvider, EABKeyID, EABHMACKey fields
    • Refactor ACMEClientManager to support per-provider ACME clients (keyed by provider name)
    • Support EAB registration for ZeroSSL via acme.Client.Register with ExternalAccountBinding
    • Separate account storage per provider (accounts/letsencrypt/account.json, accounts/zerossl/account.json)
    • Update ObtainCertificate to accept and use provider info
    • Update hasConfigurationChanged to detect provider changes (trigger re-issuance when provider switches)
  • deployments.go: Update GetCertificateDomains() to populate new CertificatesDomain fields from parsed container labels.
  • updater.go: Extended domain info flows through naturally since GetCertificateDomains populates it.

Config validation

  • acme_provider: "zerossl" requires both eab_kid and eab_hmac
  • acme_provider: "" or "letsencrypt" rejects EAB fields
  • Unknown provider values are rejected

Files to modify

File Change
internal/config/deploy_config.go Add fields to Domain, update validation
internal/config/labels.go Add label constants, update ToLabels/ParseContainerLabels
internal/configloader/resolve_secrets.go Gather EAB ValueSources from domains
internal/haloyd/certificates.go ZeroSSL directory, EAB registration, per-provider clients
internal/haloyd/deployments.go Populate provider/EAB in GetCertificateDomains
internal/haloyd/updater.go Minor comment update

Testing

  • Unit tests for Domain validation with new fields (valid/invalid combos)
  • Unit tests for label round-trip (ToLabels -> ParseContainerLabels) with EAB fields
  • Unit tests for gatherValueSources including domain EAB fields
  • Unit tests for provider-specific ACME client creation
  • Verify existing Let's Encrypt flow is unaffected (default, no EAB)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions