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 in the setup flow and configuration schema.
    • New obtain flow for Let's Encrypt certificates (interactive obtain + validation).
    • Automatic renewal scheduling for Let's Encrypt with expiration monitoring and restart handling.
    • Validation checks and UI prompts to capture required email and deployment details during setup.

✏️ 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 class and validators, obtain/renew task factories, CLI/provider selection updates, and scheduling for automatic renewals.

Changes

Cohort / File(s) Summary
Configuration & Constants
packages/dashmate/configs/defaults/getBaseConfigFactory.js, packages/dashmate/src/config/configJsonSchema.js, packages/dashmate/src/constants.js
Added letsencrypt to SSL provider enum, introduced providerConfigs.letsencrypt.email, and exported SSL_PROVIDERS.LETSENCRYPT.
Dependency Injection & Wiring
packages/dashmate/src/createDIContainer.js
Registered obtainLetsEncryptCertificateTask, validateLetsEncryptCertificate, and scheduleRenewLetsEncryptCertificate in the DI container.
Certificate Model
packages/dashmate/src/ssl/letsencrypt/LegoCertificate.js
New LegoCertificate class with PEM parsing, validity and expiration checks, and EXPIRATION_LIMIT_DAYS.
Validation
packages/dashmate/src/ssl/letsencrypt/validateLetsEncryptCertificateFactory.js
New validator factory returning validation results and ERRORS enum for LetsEncrypt certificates (email, IP, files, CN, expiry checks).
Obtainment Task
packages/dashmate/src/listr/tasks/ssl/letsencrypt/obtainLetsEncryptCertificateTaskFactory.js
New Listr task factory to obtain/renew certificates via Docker Lego container, validate outputs, persist config and save certificate.
Renewal Scheduler
packages/dashmate/src/helper/scheduleRenewLetsEncryptCertificateFactory.js, packages/dashmate/scripts/helper.js
New scheduler factory that reads existing certs, schedules renewals (based on expiration limit), runs obtain task on trigger, writes files and restarts gateway; helper wired to call scheduler when provider is LetsEncrypt.
Setup Task Integration
packages/dashmate/src/listr/tasks/setup/regular/configureSSLCertificateTaskFactory.js
Extended SSL setup UI to include LetsEncrypt option, prompt for email, and invoke obtainLetsEncryptCertificateTask.
CLI Command Updates
packages/dashmate/src/commands/ssl/obtain.js
Added provider flag and dynamic selection between ZeroSSL and LetsEncrypt obtain tasks; adjusted expiration-days handling and error for unsupported providers.

Sequence Diagram(s)

sequenceDiagram
    actor 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: instantiate task with dependencies
    Task->>Validator: validate existing certificate (config, expirationDays)
    Validator->>Task: 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: validity confirmation
    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
    Scheduler->>DockerCompose: execCommand SIGHUP to PID 1
    DockerCompose->>Gateway: reload SSL configuration
    Scheduler->>CronJob: stop job
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A hop, a ping, a cert in flight,

Let's Encrypt arrives to guard the night,
Docker hums and CronJobs cheer,
Renewals scheduled, errors clear,
Dashmate sleeps with SSL tight.

🚥 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 title clearly and specifically describes the main change: adding Let's Encrypt as a new SSL provider option to the dashmate project, which aligns with the comprehensive set of changes throughout the codebase.
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.

Comment on lines +67 to +94
const job = new CronJob(renewAt, async () => {
try {
const tasks = obtainLetsEncryptCertificateTask(config);

await tasks.run({
expirationDays: LegoCertificate.EXPIRATION_LIMIT_DAYS,
noRetry: true,
});

// Write config files
configFileRepository.write(configFile);
writeConfigTemplates(config);

// Restart Gateway to catch up new SSL certificates
await dockerCompose.execCommand(config, 'gateway', 'kill -SIGHUP 1');

// eslint-disable-next-line no-console
console.log("Let's Encrypt certificate renewed successfully");
} catch (e) {
// eslint-disable-next-line no-console
console.error(`Failed to renew Let's Encrypt certificate: ${e.message}`);
}

job.stop();
}, async () => {
// Schedule new cron task after completion
process.nextTick(() => scheduleRenewLetsEncryptCertificate(config));
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid a tight retry loop after renewal failure.
On failure, the flow immediately reschedules; with an expiring cert this retries every ~3s, which can thrash Docker/LE and hit rate limits. Add a fixed delay before rescheduling on error.

🔧 Proposed fix (delay reschedule on failure)
-    const job = new CronJob(renewAt, async () => {
-      try {
+    let hadError = false;
+    const job = new CronJob(renewAt, async () => {
+      try {
         const tasks = obtainLetsEncryptCertificateTask(config);
 
         await tasks.run({
           expirationDays: LegoCertificate.EXPIRATION_LIMIT_DAYS,
           noRetry: true,
         });
@@
-      } catch (e) {
+      } catch (e) {
+        hadError = true;
         // eslint-disable-next-line no-console
         console.error(`Failed to renew Let's Encrypt certificate: ${e.message}`);
       }
 
       job.stop();
     }, async () => {
       // Schedule new cron task after completion
-      process.nextTick(() => scheduleRenewLetsEncryptCertificate(config));
+      const delayMs = hadError ? 60 * 60 * 1000 : 0;
+      setTimeout(() => scheduleRenewLetsEncryptCertificate(config), delayMs);
     });
Based on learnings, prefer a fixed delay between retries.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const job = new CronJob(renewAt, async () => {
try {
const tasks = obtainLetsEncryptCertificateTask(config);
await tasks.run({
expirationDays: LegoCertificate.EXPIRATION_LIMIT_DAYS,
noRetry: true,
});
// Write config files
configFileRepository.write(configFile);
writeConfigTemplates(config);
// Restart Gateway to catch up new SSL certificates
await dockerCompose.execCommand(config, 'gateway', 'kill -SIGHUP 1');
// eslint-disable-next-line no-console
console.log("Let's Encrypt certificate renewed successfully");
} catch (e) {
// eslint-disable-next-line no-console
console.error(`Failed to renew Let's Encrypt certificate: ${e.message}`);
}
job.stop();
}, async () => {
// Schedule new cron task after completion
process.nextTick(() => scheduleRenewLetsEncryptCertificate(config));
});
let hadError = false;
const job = new CronJob(renewAt, async () => {
try {
const tasks = obtainLetsEncryptCertificateTask(config);
await tasks.run({
expirationDays: LegoCertificate.EXPIRATION_LIMIT_DAYS,
noRetry: true,
});
// Write config files
configFileRepository.write(configFile);
writeConfigTemplates(config);
// Restart Gateway to catch up new SSL certificates
await dockerCompose.execCommand(config, 'gateway', 'kill -SIGHUP 1');
// eslint-disable-next-line no-console
console.log("Let's Encrypt certificate renewed successfully");
} catch (e) {
hadError = true;
// eslint-disable-next-line no-console
console.error(`Failed to renew Let's Encrypt certificate: ${e.message}`);
}
job.stop();
}, async () => {
// Schedule new cron task after completion
const delayMs = hadError ? 60 * 60 * 1000 : 0;
setTimeout(() => scheduleRenewLetsEncryptCertificate(config), delayMs);
});
🤖 Prompt for AI Agents
In `@packages/dashmate/src/helper/scheduleRenewLetsEncryptCertificateFactory.js`
around lines 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.

Comment on lines +43 to +44
const expirationDays = ctx.expirationDays || LegoCertificate.EXPIRATION_LIMIT_DAYS;
const { error, data } = await validateLetsEncryptCertificate(config, expirationDays);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Honor explicit expiration-days = 0.
||_toggle treats 0 as falsy, so the CLI can’t request “only renew if already expired.” Use nullish coalescing instead.

🔧 Proposed fix
-          const expirationDays = ctx.expirationDays || LegoCertificate.EXPIRATION_LIMIT_DAYS;
+          const expirationDays = ctx.expirationDays ?? LegoCertificate.EXPIRATION_LIMIT_DAYS;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const expirationDays = ctx.expirationDays || LegoCertificate.EXPIRATION_LIMIT_DAYS;
const { error, data } = await validateLetsEncryptCertificate(config, expirationDays);
const expirationDays = ctx.expirationDays ?? LegoCertificate.EXPIRATION_LIMIT_DAYS;
const { error, data } = await validateLetsEncryptCertificate(config, expirationDays);
🤖 Prompt for AI Agents
In
`@packages/dashmate/src/listr/tasks/ssl/letsencrypt/obtainLetsEncryptCertificateTaskFactory.js`
around lines 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.

Comment on lines +95 to +101
// Check if certificate IP matches external IP
if (data.certificate.commonName && data.certificate.commonName !== data.externalIp) {
return {
error: ERRORS.CERTIFICATE_IP_MISMATCH,
data,
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for --disable-cn usage
rg "\-\-disable-cn" --type js --type ts

Repository: dashpay/platform

Length of output: 180


🏁 Script executed:

# Find the obtain flow for Let's Encrypt
fd ".*letsencrypt.*" -t f --include="*.js" --include="*.ts" | head -20

Repository: dashpay/platform

Length of output: 280


🏁 Script executed:

# Look at the validateLetsEncryptCertificateFactory.js file
cat -n packages/dashmate/src/ssl/letsencrypt/validateLetsEncryptCertificateFactory.js | head -120

Repository: dashpay/platform

Length of output: 4433


🏁 Script executed:

# Look at LegoCertificate.js
cat -n packages/dashmate/src/ssl/letsencrypt/LegoCertificate.js

Repository: dashpay/platform

Length of output: 2953


🏁 Script executed:

# Search for subjectAltName usage
rg "subjectAltName" --type js --type ts

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

# Check the obtain flow more carefully
cat -n packages/dashmate/src/listr/tasks/ssl/letsencrypt/obtainLetsEncryptCertificateTaskFactory.js | head -100

Repository: dashpay/platform

Length of output: 4761


🏁 Script executed:

# Search for forge extensions usage to understand how to extract SubjectAltName
rg "getExtension|altNames" --type js --type ts

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

# Check if there's any documentation or comments about the certificate validation
rg "commonName|CN|disable-cn" -B 2 -A 2 --type js --type ts packages/dashmate/src/ssl/

Repository: dashpay/platform

Length of output: 3807


🏁 Script executed:

# Check if there are any tests or examples of certificate parsing
fd "*.test.js" -o "*.spec.js" | xargs grep -l "certificate\|LegoCertificate" 2>/dev/null | head -5

Repository: dashpay/platform

Length of output: 197


🏁 Script executed:

# Look at lego task to see the full context of --disable-cn
cat -n packages/dashmate/src/listr/tasks/ssl/letsencrypt/obtainLetsEncryptCertificateTaskFactory.js | grep -A 20 -B 5 "\-\-disable-cn"

Repository: dashpay/platform

Length of output: 1125


🏁 Script executed:

# Search for any forge extension handling patterns
rg "extensions|getExtension" --type js --type ts -A 3

Repository: dashpay/platform

Length of output: 50373


🏁 Script executed:

# Check package.json for node-forge version
cat packages/dashmate/package.json | grep -A 5 -B 5 "forge"

Repository: dashpay/platform

Length of output: 353


🏁 Script executed:

# Check if there are any tests or examples related to certificate parsing
find packages/dashmate -name "*.js" -type f | xargs grep -l "certificate\|Certificate" | head -10

Repository: dashpay/platform

Length of output: 698


🏁 Script executed:

# Look for any certificate-related tests
find . -path "*/node_modules" -prune -o -name "*.test.js" -o -name "*.spec.js" | grep -i certif

Repository: dashpay/platform

Length of output: 143


🌐 Web query:

node-forge certificate getExtension subjectAltName altNames example

💡 Result:

Use pki.certificateFromPem(...) then read the subjectAltName extension's altNames array. Example:

Code:

const forge = require('node-forge');
const pki = forge.pki;

// parse PEM cert
const certPem = `-----BEGIN CERTIFICATE-----\n...base64...\n-----END CERTIFICATE-----`;
const cert = pki.certificateFromPem(certPem);

// find subjectAltName extension (OID 2.5.29.17) or by name
const sanExt = cert.extensions.find(e => e.name === 'subjectAltName' || e.id === '2.5.29.17');

if (sanExt && sanExt.altNames) {
  sanExt.altNames.forEach(alt => {
    // types: 1=email, 2=DNS, 6=URI, 7=IP
    if (alt.type === 2) console.log('DNS:', alt.value);
    else if (alt.type === 7) console.log('IP:', alt.ip);
    else if (alt.type === 6) console.log('URI:', alt.value);
    else console.log('other type', alt);
  });
}

Notes:

  • When creating certs, subjectAltName is set with { name: 'subjectAltName', altNames: [...] } where altNames entries use type and value/ip. [1]
  • For more complex otherName parsing you may need to inspect ASN.1 fields (examples available). [2]

Sources:

  • node-forge pki docs / examples for subjectAltName and altNames. [1]
  • Example parsing otherName in subjectAltName (Stack Overflow). [2]

🏁 Script executed:

# Check if there are any existing certificate extension handling in the repo
rg "extensions\s*\." packages/dashmate --type js -B 2 -A 2

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

# Look for forge certificate parsing examples
rg "certificateFromPem\|pki\.certificate" packages/dashmate --type js -B 2 -A 5

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

# Check the test file for ZeroSSL to see certificate validation patterns
cat -n packages/dashmate/test/unit/ssl/zerossl/validateZeroSslCertificateFactory.spec.js | head -150

Repository: dashpay/platform

Length of output: 6368


🏁 Script executed:

# Check the error message handling in obtainLetsEncryptCertificateTaskFactory
cat -n packages/dashmate/src/listr/tasks/ssl/letsencrypt/obtainLetsEncryptCertificateTaskFactory.js | grep -A 2 "CERTIFICATE_IP_MISMATCH"

Repository: dashpay/platform

Length of output: 360


🏁 Script executed:

# Let me verify the correct API for accessing certificate extensions in node-forge
# by looking at actual usage patterns in the codebase
rg "forge\.pki\|pki\.certificate" packages/dashmate --type js -A 10 | head -50

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

# Check if there are any comments or documentation about certificate structure
rg "commonName|CN|certificate" packages/dashmate/src/ssl --type js -B 1 -A 1 | grep -i "subject\|extension\|alt" | head -20

Repository: dashpay/platform

Length of output: 241


🏁 Script executed:

# Check if forge's certificate object has getExtension method
node -e "
const forge = require('node-forge');
const pki = forge.pki;
// Try to see what methods are available on certificate objects
console.log('Checking forge pki API...');
try {
  const testCert = pki.createCertificate();
  console.log('Methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(testCert)).filter(m => m.includes('Extension')));
  console.log('Has getExtension:', typeof testCert.getExtension);
  console.log('Has extensions property:', 'extensions' in testCert);
} catch (e) {
  console.log('Error:', e.message);
}
" 2>&1 || echo "Node.js test failed, checking package.json instead"

Repository: dashpay/platform

Length of output: 948


🏁 Script executed:

# Let me check how the ZeroSSL certificate validation works for comparison
cat -n packages/dashmate/src/ssl/zerossl/validateZeroSslCertificateFactory.js | head -80

Repository: dashpay/platform

Length of output: 3185


🏁 Script executed:

# Check node-forge version and see if we can find documentation about the API
cat packages/dashmate/package.json | grep -A 2 -B 2 "node-forge"

Repository: dashpay/platform

Length of output: 206


🏁 Script executed:

# Look for any other certificate parsing or validation code that might give hints
rg "forge\.pki\.certificate" packages/dashmate/src --type js -B 2 -A 10

Repository: dashpay/platform

Length of output: 1564


🏁 Script executed:

# Search for certificate validation tests to understand the structure
cat -n packages/dashmate/test/unit/ssl/letsencrypt/validateLetsEncryptCertificateFactory.spec.js 2>/dev/null | head -100

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

# Search for letsencrypt test files
find packages/dashmate/test -name "*letsencrypt*" -type f

Repository: dashpay/platform

Length of output: 42


🏁 Script executed:

# Look at the self-signed certificate creation to understand the certificate structure better
cat -n packages/dashmate/src/ssl/selfSigned/createSelfSignedCertificate.js

Repository: dashpay/platform

Length of output: 1115


🌐 Web query:

node-forge certificate extensions property altNames structure API

💡 Result:

Short summary — node-forge SubjectAltName (extensions.altNames) structure

  • Where: put an extension object in cert/csr extensions with name: 'subjectAltName' and altNames: an array. Example:
    { name: 'subjectAltName', altNames: [{ type: 2, value: 'example.com' }, { type: 7, ip: '127.0.0.1' }] }. [1][2]

  • Per-altName fields:

    • type: number (GeneralName type): common values:
      • 0 = otherName (complex ASN.1 value)
      • 1 = rfc822Name (email)
      • 2 = dNSName (DNS string)
      • 6 = uniformResourceIdentifier (URI)
      • 7 = iPAddress (use ip property for human-readable IP)
      • 8 = registeredID (OID; altName.oid available when parsed)
    • value: string (used for types like 1,2,6; otherName uses a value array)
    • ip: string (for type 7; node-forge converts bytes to human IP string)
    • oid: string (for type 8 when parsing). [1][2]
  • Notes:

    • For otherName (type 0) the value is an ASN.1 sequence (node-forge exposes the raw value; you must decode OID and inner value). Example parsing shown in community answers. [2][3]

Example (creating a cert/csr):
cert.setExtensions([
{ name: 'subjectAltName', altNames:[
{ type: 2, value: 'test.domain.com' },
{ type: 7, ip: '127.0.0.1' },
{ type: 6, value: 'http://example.org/webid#me' }
]}
]); [1][2]

Sources:

  • node-forge pki docs / examples showing altNames schema. [1]
  • node-forge x509 parsing code & examples (how altNames are parsed, types and ip/oid handling). [2]
  • StackOverflow example for parsing otherName (type 0) in node-forge. [3]

IP mismatch check is ineffective when CN is disabled.

The obtain flow passes --disable-cn, so commonName is always null in the parsed certificate. The condition at line 96 uses AND logic (data.certificate.commonName &&), which short-circuits and skips the mismatch validation entirely—allowing stale certificates for different IPs to pass validation.

Additionally, the error message in obtainLetsEncryptCertificateTaskFactory.js (line 81) will display "Certificate IP null does not match..." when this error is thrown, since it references the null commonName.

Validate SubjectAltName entries (IP type) instead:

🔧 Proposed fix (validate SANs for IP)
// packages/dashmate/src/ssl/letsencrypt/LegoCertificate.js
@@
   /**
    * `@type` {string}
    */
   commonName;
+
+  /**
+   * `@type` {string[]}
+   */
+  subjectAltNames;
@@
-   * `@param` {string} data.commonName
+   * `@param` {string|null} data.commonName
+   * `@param` {string[]} data.subjectAltNames
    */
   constructor(data) {
     this.expires = data.expires;
     this.created = data.created;
     this.commonName = data.commonName;
+    this.subjectAltNames = data.subjectAltNames ?? [];
   }
@@
     const commonNameAttr = cert.subject.attributes.find(
       (attr) => attr.shortName === 'CN',
     );
+    const altNameExt = cert.extensions?.find(e => e.name === 'subjectAltName');
+    const subjectAltNames = (altNameExt?.altNames ?? [])
+      .map((alt) => alt.ip || alt.value)
+      .filter(Boolean);
 
     return new LegoCertificate({
       expires: cert.validity.notAfter,
       created: cert.validity.notBefore,
       commonName: commonNameAttr ? commonNameAttr.value : null,
+      subjectAltNames,
     });
// packages/dashmate/src/ssl/letsencrypt/validateLetsEncryptCertificateFactory.js
@@
-    if (data.certificate.commonName && data.certificate.commonName !== data.externalIp) {
+    const matchesIp = data.certificate.subjectAltNames?.includes(data.externalIp)
+      || data.certificate.commonName === data.externalIp;
+    if (!matchesIp) {
       return {
         error: ERRORS.CERTIFICATE_IP_MISMATCH,
         data,
       };
     }
🤖 Prompt for AI Agents
In
`@packages/dashmate/src/ssl/letsencrypt/validateLetsEncryptCertificateFactory.js`
around lines 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.

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.

Comment on lines +80 to +82
case ERRORS.CERTIFICATE_IP_MISMATCH:
throw new Error(`Certificate IP ${ctx.certificate.commonName} does not match external IP ${ctx.externalIp}.\n`
+ 'Please change the external IP in config or use --force to obtain a new certificate.');
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Error message will display "null" when CN is disabled.

Since --disable-cn is used (line 119), ctx.certificate.commonName will be null. The error message should reference the expected IP from SANs or simply state the mismatch without the null value.

🔧 Proposed fix
             case ERRORS.CERTIFICATE_IP_MISMATCH:
-              throw new Error(`Certificate IP ${ctx.certificate.commonName} does not match external IP ${ctx.externalIp}.\n`
+              throw new Error(`Certificate does not match external IP ${ctx.externalIp}.\n`
                 + 'Please change the external IP in config or use --force to obtain a new certificate.');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case ERRORS.CERTIFICATE_IP_MISMATCH:
throw new Error(`Certificate IP ${ctx.certificate.commonName} does not match external IP ${ctx.externalIp}.\n`
+ 'Please change the external IP in config or use --force to obtain a new certificate.');
case ERRORS.CERTIFICATE_IP_MISMATCH:
throw new Error(`Certificate does not match external IP ${ctx.externalIp}.\n`
'Please change the external IP in config or use --force to obtain a new certificate.');
🤖 Prompt for AI Agents
In
`@packages/dashmate/src/listr/tasks/ssl/letsencrypt/obtainLetsEncryptCertificateTaskFactory.js`
around lines 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."

Comment on lines +145 to +156
const container = await docker.createContainer({
name: containerName,
Image: LEGO_IMAGE,
Cmd: legoArgs,
User: `${uid}:${gid}`,
ExposedPorts: { '80/tcp': {} },
HostConfig: {
AutoRemove: true,
Binds: [`${ctx.legoDir}:/data`],
PortBindings: { '80/tcp': [{ HostPort: '80' }] },
},
});
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 18, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

goacme/lego docker image non-root user support documentation

💡 Result:

Short answer: The official goacme/lego Docker image does not document a non-root user or show a USER in its published metadata — it expects the default (root) runtime. See the image page and installation docs. [1][2]

If you need a rootless/root‑nonroot build, use a community rootless image (example: 11notes/lego) or build your own Dockerfile that adds a non‑root user and sets USER. [3][4]

Sources

  • goacme/lego Docker Hub page. [1]
  • lego installation (Docker) in official docs. [2]
  • Example community rootless image (11notes/lego) on Docker Hub. [3]
  • Public image metadata showing no USER configured for goacme/lego. [4]

🌐 Web query:

goacme lego GitHub dockerfile USER instruction

💡 Result:

Short answer: lego’s Docker image can run non-root, but the binary needs permission to bind ports 80/443 (or you must avoid binding those ports). Use the Dockerfile USER instruction to drop privileges after preparing files and ownership, or give the binary CAP_NET_BIND_SERVICE with setcap. See lego docs on "Running without root privileges" and the official image. [1][2]

Example patterns:

  1. Create a non-root user and run as it (common):
  • create user, chown .lego (or other runtime dirs), then switch USER.

Dockerfile snippet:
FROM golang:1.20-alpine AS build

build steps...

FROM alpine:3.18

add lego binary and any certs

RUN addgroup -S lego && adduser -S -G lego lego
&& mkdir -p /home/lego/.lego
&& chown -R lego:lego /home/lego /etc/ssl
USER lego
WORKDIR /home/lego
ENTRYPOINT ["/usr/local/bin/lego"]

Use --http.port/--tls.port or DNS challenge if you cannot bind 80/443 as non-root. [2]

  1. Grant CAP_NET_BIND_SERVICE to allow binding 80/443 while running non-root:
  • setcap must be applied to the binary (on the final image or during runtime build). Example:
    RUN apk add --no-cache libcap && setcap 'cap_net_bind_service=+ep' /usr/local/bin/lego

then optionally drop to non-root USER as above.

Notes:

  • If you run lego as non-root without capabilities you must either forward host ports 80/443 to higher ports (and run lego with --http.port/--tls.port) or use the DNS challenge. [2]
  • Official goacme/lego image and docs: Docker Hub & project site. [1][2]

References:
[1] goacme/lego Docker image (official).
[2] lego — Running without root privileges / Port Usage (official docs).


The User option will cause a runtime failure with the official goacme/lego image.

The official goacme/lego Docker image runs as root by default and does not support non-root execution without additional configuration. Setting User: ${uid}:${gid} combined with PortBindings: { '80/tcp': [{ HostPort: '80' }] } will fail—non-root users cannot bind to privileged ports (< 1024) without the CAP_NET_BIND_SERVICE capability.

To fix this, either:

  • Remove the User option and accept running as root
  • Add CAP_NET_BIND_SERVICE to the container (using CapAdd: ['NET_BIND_SERVICE'])
  • Switch to a DNS challenge instead of HTTP challenge to avoid binding port 80
  • Use a community rootless image variant instead of the official goacme/lego image
🤖 Prompt for AI Agents
In
`@packages/dashmate/src/listr/tasks/ssl/letsencrypt/obtainLetsEncryptCertificateTaskFactory.js`
around lines 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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmmm... unsure about this, it appears to work when I tested it as the dashmate user?

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🌐 Web query:

Docker container non-root user bind port 80 inside container network namespace

💡 Result:

Short answer — give the process the CAP_NET_BIND_SERVICE capability (or give that capability to the binary) or bind on a high port and map host port 80 -> container port.

Options and commands (pick one):

  1. Add capability to the container process at runtime
  • docker run --cap-add=NET_BIND_SERVICE --user 1000 ... image
    This grants the process the capability to bind <1024. Note: some runtimes/contexts (rootless Docker, certain security settings) may still prevent it unless ambient/inheritable caps are set correctly. [1][2]
  1. Give the application binary the capability inside the image (recommended for least privilege)
  • In Dockerfile:
    • install libcap (libcap2-bin / libcap)
    • setcap 'cap_net_bind_service=+ep' /path/to/executable
    • switch to non-root user and run
      This makes that specific executable able to bind port 80 without running as UID 0. Example pattern: setcap on /usr/bin/python3 or your app binary. [3][4]
  1. Use an explicit bind + port mapping (no special capabilities)
  • Run the app on an unprivileged port (e.g. 8080) as non-root and expose/map host port 80:
    docker run -p 80:8080 image
    This keeps the container non-root and avoids capabilities but requires clients to use the mapped host port or internal services to use the higher port. [4]

Special/rootless cases and caveats

  • Rootless Docker/podman: privileged port bind may be blocked by rootless network stack or kernel sysctl net.ipv4.ip_unprivileged_port_start (default 1024). Workarounds include adjusting that sysctl or granting capabilities to rootless helper (rootlesskit) or using setcap on the binary — behaviour varies by runtime. [5][1]
  • Kubernetes: adding NET_BIND_SERVICE in securityContext is common, but ensure capability ends up in the container's effective/ambient set; otherwise binding still fails. Some setups need setcap on the executable or allowPrivilegeEscalation=false and correct ambient support. [2][5]

References

  • How to bind non-root process to privileged port inside Docker (example Dockerfile + setcap). [1]
  • Using CAP_NET_BIND_SERVICE via securityContext / runtime caveats (Kubernetes issues, ambient capabilities). [2]
  • Practical guide: setcap in Dockerfile to grant single binary ability to bind <1024. [3]
  • Explanation and history of capability issues and workarounds (blog / troubleshooting). [4]
  • Rootless Docker notes: rootlesskit and net.unprivileged_port_start implications. [5]

If you want, tell me which runtime (Docker rootful vs rootless, Kubernetes, podman) and your app language/binary and I’ll give a concrete Dockerfile and docker run example.


🌐 Web query:

goacme lego docker image capabilities NET_BIND_SERVICE setcap

💡 Result:

Short answer: lego supports running without root by either using setcap on the lego binary (cap_net_bind_service), running on non‑privileged ports, or using DNS challenges — and for containers you normally either grant the capability to the container (docker run --cap-add=NET_BIND_SERVICE / compose cap_add) or set the capability on the binary during image build. See notes and links below. [1][2][3][4]

Key points and practical options

  • lego docs: lego can bind low ports without sudo by (a) setcap 'cap_net_bind_service=+ep' /path/to/lego, (b) change --http.port/--tls.port, or (c) use DNS challenge. Use whichever fits your deployment. [1]
  • Granting capability to the container: give the container CAP_NET_BIND_SERVICE at runtime (docker run --cap-add=NET_BIND_SERVICE or docker-compose cap_add / Kubernetes securityContext add: ["NET_BIND_SERVICE"]). This lets a non‑root process bind ports <1024. [3]
  • Using setcap in an image: you can run setcap in your Dockerfile (install libcap and run setcap on the lego binary) so the binary has cap_net_bind_service. However, be aware of environment caveats: some runtimes or host security policies (e.g., platforms that drop file capabilities, certain container runtimes, or mounting behavior) can make setcap ineffective at runtime — granting the capability to the container is more robust. [3][2]
  • Docker image notes: Docker’s capability model and filesystem handling mean capability bits on files in images may not behave the same across runtimes; audit with getcap/getpcaps and prefer runtime cap_add when possible for portability. [2]

References

  • lego docs — running without root / options (setcap, ports, DNS). [1]
  • Docker / capabilities explanation (file capability limits, getcap, and runtime cap_add examples). [2]
  • Practical examples: using setcap in Dockerfile and adding NET_BIND_SERVICE in Kubernetes / Docker. [3]
  • goacme/lego Docker Hub page (image tags/usage). [4]

If you want, I can:

  • show a minimal Dockerfile that installs libcap and runs setcap on lego, or
  • show docker run / docker-compose / Kubernetes snippets to give CAP_NET_BIND_SERVICE to the container. Which would you prefer?

@ktechmidas Thanks for testing! That's interesting that it worked in your environment. Let me clarify what might be happening:

When Docker does port mapping with PortBindings: { '80/tcp': [{ HostPort: '80' }] }, the Docker daemon (running as root) handles the host-side port 80 binding. However, inside the container, the lego process still needs to bind to port 80 in the container's network namespace, which typically requires either root privileges or the CAP_NET_BIND_SERVICE capability.

Your test might have succeeded due to:

  1. Recent kernel versions or Docker configurations that handle capabilities differently
  2. The net.ipv4.ip_unprivileged_port_start sysctl being set below 80 on your system
  3. Some other environment-specific configuration

Recommendation: To ensure this works reliably across different environments and production setups, I suggest explicitly adding the capability:

HostConfig: {
  AutoRemove: true,
  Binds: [`${ctx.legoDir}:/data`],
  PortBindings: { '80/tcp': [{ HostPort: '80' }] },
  CapAdd: ['NET_BIND_SERVICE'],
},

This makes the requirement explicit and ensures consistent behavior regardless of the host's kernel configuration. Could you verify whether your test environment has any special Docker/kernel settings, or would you prefer to add the explicit capability for safety?


🧠 Learnings used
Learnt from: shumkov
Repo: dashpay/platform PR: 2297
File: packages/dashmate/src/listr/tasks/ssl/zerossl/obtainZeroSSLCertificateTaskFactory.js:148-150
Timestamp: 2024-10-31T14:41:42.158Z
Learning: In the `obtainZeroSSLCertificateTaskFactory` function in `packages/dashmate/src/listr/tasks/ssl/zerossl/obtainZeroSSLCertificateTaskFactory.js`, the application already fails appropriately if `configFileRepository.write(configFile)` fails, so additional error handling around this operation is unnecessary.

Learnt from: shumkov
Repo: dashpay/platform PR: 2298
File: packages/dashmate/src/listr/tasks/ssl/zerossl/cleanupZeroSSLCertificatesTaskFactory.js:27-48
Timestamp: 2024-10-31T15:53:06.937Z
Learning: In `cleanupZeroSSLCertificatesTask` located in `packages/dashmate/src/listr/tasks/ssl/zerossl/cleanupZeroSSLCertificatesTaskFactory.js`, if the certificate listing API call fails, we prefer the entire command to fail without additional error handling.

Learnt from: shumkov
Repo: dashpay/platform PR: 2298
File: packages/dashmate/src/listr/tasks/ssl/zerossl/cleanupZeroSSLCertificatesTaskFactory.js:74-74
Timestamp: 2024-10-31T15:54:57.897Z
Learning: In the `cleanupZeroSSLCertificatesTask` function located in `packages/dashmate/src/listr/tasks/ssl/zerossl/cleanupZeroSSLCertificatesTaskFactory.js`, the team prefers to include a small fixed delay between all requests to avoid overloading ZeroSSL, rather than implementing exponential backoff.

Learnt from: shumkov
Repo: dashpay/platform PR: 2298
File: packages/dashmate/src/commands/ssl/cleanup.js:33-50
Timestamp: 2024-10-31T15:37:28.986Z
Learning: In `packages/dashmate/src/commands/ssl/cleanup.js`, the `cleanupZeroSSLCertificatesTask` function already includes progress reporting within its nested `Listr` tasks, so additional progress reporting in the `CleanupCommand` is unnecessary.

Learnt from: shumkov
Repo: dashpay/platform PR: 2365
File: packages/dashmate/src/listr/tasks/ssl/zerossl/obtainZeroSSLCertificateTaskFactory.js:206-213
Timestamp: 2024-12-03T05:47:30.524Z
Learning: In `obtainZeroSSLCertificateTaskFactory.js`, when handling ZeroSSL API errors during domain verification, the error detail structure is consistent when an error occurs, so additional null checks on `e.details[ctx.externalIp]` may not be necessary.

Learnt from: shumkov
Repo: dashpay/platform PR: 2107
File: packages/dashmate/src/listr/tasks/setup/local/configureCoreTaskFactory.js:116-124
Timestamp: 2024-11-22T10:28:33.588Z
Learning: In `configureCoreTaskFactory` in `packages/dashmate/src/listr/tasks/setup/local/configureCoreTaskFactory.js`, the `local_seed` configuration is already established earlier in the code, so adding error handling for its absence in the 'Generating funds to use as a collateral for masternodes' task is unnecessary.

Comment on lines +186 to +193
// Verify certificate was created
if (!fs.existsSync(ctx.legoCertPath)) {
throw new Error('Certificate file was not created by lego');
}

// eslint-disable-next-line no-param-reassign
task.output = 'Certificate obtained successfully';
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Also verify the key file was created.

Only the certificate file existence is verified, but the key file is also required and read on line 201.

🔧 Proposed fix
           // Verify certificate was created
           if (!fs.existsSync(ctx.legoCertPath)) {
             throw new Error('Certificate file was not created by lego');
           }
+
+          if (!fs.existsSync(ctx.legoKeyPath)) {
+            throw new Error('Private key file was not created by lego');
+          }
🤖 Prompt for AI Agents
In
`@packages/dashmate/src/listr/tasks/ssl/letsencrypt/obtainLetsEncryptCertificateTaskFactory.js`
around lines 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.

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.

2 participants