Skip to content

Conversation

@ktechmidas
Copy link
Collaborator

@ktechmidas ktechmidas commented Jan 17, 2026

Add Let's Encrypt as a new SSL certificate provider option using the goacme/lego Docker client. This enables IP-based certificate issuance with automatic renewal support for shortlived certificates.

New files:

  • LegoCertificate.js - Certificate model for parsing PEM files
  • validateLetsEncryptCertificateFactory.js - Validation logic
  • obtainLetsEncryptCertificateTaskFactory.js - Main obtain task
  • scheduleRenewLetsEncryptCertificateFactory.js - Renewal scheduler

Changes:

  • Add LETSENCRYPT to SSL_PROVIDERS constant
  • Add letsencrypt config schema and defaults
  • Add Let's Encrypt to setup wizard choices
  • Update ssl obtain command with --provider flag
  • Update helper.js with Let's Encrypt renewal scheduling

Issue being fixed or feature implemented

What was done?

How Has This Been Tested?

Breaking Changes

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

Summary by CodeRabbit

  • New Features

    • Added Let’s Encrypt as a selectable SSL provider (setup UI, config schema, and CLI provider flag).
    • Interactive obtain-and-validate flow for Let’s Encrypt certificates, including certificate parsing/validation.
    • Automatic renewal scheduler for Let’s Encrypt with restart handling, expiration monitoring and retry behavior.
    • CLI obtain command now supports choosing between SSL providers.
  • Chores / Migrations

    • Added config migration to populate a letsencrypt provider config (email: null) for existing installs.

✏️ Tip: You can customize this high-level summary in your review settings.

Add Let's Encrypt as a new SSL certificate provider option using the
goacme/lego Docker client. This enables IP-based certificate issuance
with automatic renewal support for shortlived certificates.

New files:
- LegoCertificate.js - Certificate model for parsing PEM files
- validateLetsEncryptCertificateFactory.js - Validation logic
- obtainLetsEncryptCertificateTaskFactory.js - Main obtain task
- scheduleRenewLetsEncryptCertificateFactory.js - Renewal scheduler

Changes:
- Add LETSENCRYPT to SSL_PROVIDERS constant
- Add letsencrypt config schema and defaults
- Add Let's Encrypt to setup wizard choices
- Update ssl obtain command with --provider flag
- Update helper.js with Let's Encrypt renewal scheduling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@github-actions github-actions bot added this to the v3.0.0 milestone Jan 17, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 17, 2026

📝 Walkthrough

Walkthrough

Adds Let’s Encrypt as a new SSL provider: schema, constants, DI wiring, certificate model and validators, obtain/renew Listr tasks (via Docker Lego), scheduler for automatic renewals, CLI/provider selection updates, defaults and migration to populate letsencrypt config.

Changes

Cohort / File(s) Summary
Configuration & Defaults
packages/dashmate/configs/defaults/getBaseConfigFactory.js, packages/dashmate/configs/getConfigFileMigrationsFactory.js, packages/dashmate/src/config/configJsonSchema.js, packages/dashmate/src/constants.js
Added letsencrypt provider to config defaults and schema (providerConfigs.letsencrypt.email = null), extended provider enum to include letsencrypt, exported SSL_PROVIDERS.LETSENCRYPT, and added migration to inject letsencrypt for older configs.
Dependency Injection & Wiring
packages/dashmate/src/createDIContainer.js
Registered new DI entries: obtainLetsEncryptCertificateTask, validateLetsEncryptCertificate, and scheduleRenewLetsEncryptCertificate.
Helper Script
packages/dashmate/scripts/helper.js
When SSL provider is letsencrypt, resolves and invokes scheduleRenewLetsEncryptCertificate (parallel to ZeroSSL path).
Certificate Model
packages/dashmate/src/ssl/letsencrypt/LegoCertificate.js
New LegoCertificate class to parse PEMs, expose expiry/created/CN/SANs, and provide validity/expiration helpers; includes EXPIRATION_LIMIT_DAYS constant.
Validation
packages/dashmate/src/ssl/letsencrypt/validateLetsEncryptCertificateFactory.js
New validator factory with ERRORS enum; checks presence of email/external IP, certificate/key files, parsing, IP/CN match, and expiry.
Obtainment Task
packages/dashmate/src/listr/tasks/ssl/letsencrypt/obtainLetsEncryptCertificateTaskFactory.js
New Listr workflow to validate, pull lego image, run lego container to obtain/renew certs, read outputs, enable SSL in config, persist files, and invoke save task.
Renewal Scheduler
packages/dashmate/src/helper/scheduleRenewLetsEncryptCertificateFactory.js
New scheduler factory: reads existing certs, schedules renewals relative to EXPIRATION_LIMIT_DAYS, invokes obtain task on trigger, persists outputs, and signals gateway reload via docker compose.
Setup Task Integration
packages/dashmate/src/listr/tasks/setup/regular/configureSSLCertificateTaskFactory.js
Setup UI extended with Let’s Encrypt option, prompts for email, stores it under platform.gateway.ssl.providerConfigs.letsencrypt.email, and calls obtainLetsEncryptCertificateTask.
CLI Command Updates
packages/dashmate/src/commands/ssl/obtain.js
Added provider flag and dynamic selection between ZeroSSL and Let’s Encrypt obtain tasks, provider-specific expiration handling, and explicit unsupported-provider error.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as CLI Command
    participant DI as DI Container
    participant Task as Obtain Task
    participant Docker as Docker/Lego
    participant Validator as Validator
    participant Config as Config Repository

    User->>CLI: run obtain (--provider letsencrypt)
    CLI->>DI: resolve obtainLetsEncryptCertificateTask
    DI->>Task: provide task with dependencies
    Task->>Validator: validate existing certificate (config, expirationDays)
    Validator->>Task: return validation result
    alt Certificate missing/invalid
        Task->>Docker: pull lego image & run lego container
        Docker->>Task: produce cert and key files
    end
    Task->>Validator: parse/verify obtained cert
    Validator->>Task: confirm validity
    Task->>Config: enable SSL, set provider, save certs
    Config->>Task: persisted
    Task->>CLI: complete
Loading
sequenceDiagram
    participant Scheduler as Renewal Scheduler
    participant CronJob as CronJob
    participant Task as Obtain Task
    participant DockerCompose as Docker Compose
    participant Gateway as Gateway Service

    Scheduler->>Scheduler: read existing lego cert files
    Scheduler->>Scheduler: compute renewal time (expiry - EXPIRATION_LIMIT_DAYS)
    Scheduler->>CronJob: schedule renewal
    CronJob->>Task: trigger obtainLetsEncryptCertificateTask
    Task->>Task: obtain/renew certificate
    Task->>Scheduler: return success/failure
    alt success
        Scheduler->>DockerCompose: execCommand SIGHUP to PID 1
        DockerCompose->>Gateway: reload SSL configuration
        Scheduler->>CronJob: stop job
    else failure
        Scheduler->>CronJob: schedule retry (1 hour)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 I nibbled through code by lantern light,

Lego and cron stitch the certificates tight,
Emails tucked in configs, renewals set to sing,
Docker drums beat — the gateway gains its wing,
I hop off, satisfied, ears twitched just right.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately and concisely describes the main change: adding Let's Encrypt as a new SSL provider. It is clear, specific, and directly relates to the primary objective of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@packages/dashmate/src/helper/scheduleRenewLetsEncryptCertificateFactory.js`:
- Around line 67-94: The current scheduleRenewLetsEncryptCertificate CronJob
immediately reschedules on any failure (see CronJob callback, job.stop(), and
process.nextTick(() => scheduleRenewLetsEncryptCertificate(config))) which can
create a tight retry loop; modify the error path in the catch block so that
instead of immediately calling process.nextTick to reschedule, it waits a fixed
backoff (e.g., setTimeout with a sensible delay like 1 hour) before invoking
scheduleRenewLetsEncryptCertificate(config), while leaving the success path
unchanged; update references around obtainLetsEncryptCertificateTask, job.stop,
and process.nextTick so only the failure branch uses the delayed reschedule.

In
`@packages/dashmate/src/listr/tasks/ssl/letsencrypt/obtainLetsEncryptCertificateTaskFactory.js`:
- Around line 43-44: The code uses the logical OR operator when setting
expirationDays which treats 0 as falsy; change the assignment in the
obtainLetsEncryptCertificateTaskFactory to use the nullish coalescing operator
so an explicit ctx.expirationDays = 0 is honored (i.e. replace "const
expirationDays = ctx.expirationDays || LegoCertificate.EXPIRATION_LIMIT_DAYS;"
with a nullish-coalescing version), then pass that expirationDays into
validateLetsEncryptCertificate unchanged.

In
`@packages/dashmate/src/ssl/letsencrypt/validateLetsEncryptCertificateFactory.js`:
- Around line 95-101: The current check in
validateLetsEncryptCertificateFactory.js only inspects
data.certificate.commonName and short-circuits when CN is null, so update
validate logic to inspect SubjectAltName (SAN) IP entries instead: extract
IP-type SANs from data.certificate.subjectAltName (or equivalent SAN list on
data.certificate), ensure at least one SAN IP equals data.externalIp, and return
ERRORS.CERTIFICATE_IP_MISMATCH if none match; also update the error message used
in obtainLetsEncryptCertificateTaskFactory.js (the message that currently prints
the null commonName) to reference the external IP and/or the SANs (not
commonName) so it never prints "null" as the certificate IP.
🧹 Nitpick comments (1)
packages/dashmate/src/config/configJsonSchema.js (1)

703-712: Consider adding email format validation to the schema.

The letsencrypt.email field accepts strings but has no format constraint. While the UI prompt validates email format, invalid emails could be loaded directly from config files bypassing the UI. Adding JSON Schema's format: 'email' would provide defense-in-depth validation.

♻️ Suggested improvement
                     letsencrypt: {
                       type: ['object'],
                       properties: {
                         email: {
                           type: ['string', 'null'],
+                          format: 'email',
                         },
                       },
                       required: ['email'],
                       additionalProperties: false,
                     },

Note: JSON Schema's format: 'email' requires the validator to have format validation enabled.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In
`@packages/dashmate/src/listr/tasks/ssl/letsencrypt/obtainLetsEncryptCertificateTaskFactory.js`:
- Around line 186-193: After verifying ctx.legoCertPath exists, also check that
ctx.legoKeyPath exists and throw an Error if the key file was not created
(similar message to the cert check); update the success path only after both
files exist (the block inside the obtainLetsEncryptCertificateTaskFactory task
where task.output is set) so downstream code that reads ctx.legoKeyPath will not
fail.
- Around line 80-82: The error thrown for ERRORS.CERTIFICATE_IP_MISMATCH
currently interpolates ctx.certificate.commonName which can be null when
--disable-cn is used; update the message in the throw inside the
ERRORS.CERTIFICATE_IP_MISMATCH branch to avoid printing "null" by preferring a
SAN value when available (e.g., use ctx.certificate.subjectAlternativeNames or
the first SAN) or by omitting the CN and phrasing the message generically (e.g.,
"Certificate IP does not match external IP <ctx.externalIp>"). Make the change
in the ERRORS.CERTIFICATE_IP_MISMATCH handler where ctx.certificate.commonName
and ctx.externalIp are referenced so the message never shows "null."
- Around line 145-156: The docker container creation sets User: `${uid}:${gid}`
when calling docker.createContainer (containerName, LEGO_IMAGE, legoArgs) which
will prevent binding to port 80 as a non-root user; change the container options
to either remove the User field or add the NET_BIND_SERVICE capability in
HostConfig (CapAdd: ['NET_BIND_SERVICE']) so the goacme/lego image can bind to
privileged ports, or alternatively switch the task to use a DNS challenge
instead of exposing 80/tcp; update the block that builds the container options
(the object passed to docker.createContainer) accordingly and ensure
ExposedPorts/PortBindings remain consistent with the chosen approach.
♻️ Duplicate comments (2)
packages/dashmate/src/ssl/letsencrypt/validateLetsEncryptCertificateFactory.js (1)

98-104: IP mismatch check is ineffective when CN is disabled.

The obtain flow passes --disable-cn, so commonName is always null. The AND condition short-circuits, skipping validation entirely. This was flagged in the previous review—please address by validating SubjectAltName (SAN) IP entries instead.

packages/dashmate/src/listr/tasks/ssl/letsencrypt/obtainLetsEncryptCertificateTaskFactory.js (1)

43-44: Honor explicit expiration-days = 0.

Using || treats 0 as falsy. This was flagged in the previous review—use nullish coalescing (??) instead.

@lklimek
Copy link
Contributor

lklimek commented Jan 20, 2026

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 20, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ktechmidas ktechmidas requested a review from shumkov January 20, 2026 11:14
@shumkov shumkov merged commit ed85b73 into v3.0-dev Jan 20, 2026
21 of 24 checks passed
@shumkov shumkov deleted the feat/letsencrypt branch January 20, 2026 12:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants