AutomationFlowsGeneral › SSL Certificate Expiry Watcher (Three-Tier Alerts)

SSL Certificate Expiry Watcher (Three-Tier Alerts)

SSL Certificate Expiry Watcher (Three-Tier Alerts). Uses stickyNote, scheduleTrigger, httpRequest. Scheduled trigger; 10 nodes.

Cron / scheduled trigger★★★★☆ complexity10 nodesHttp Request
General Trigger: Cron / scheduled Nodes: 10 Complexity: ★★★★☆

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "name": "SSL Certificate Expiry Watcher (Three-Tier Alerts)",
  "nodes": [
    {
      "parameters": {
        "content": "## SSL Certificate Expiry Watcher\n\nDaily TLS check across multiple domains. Three-tier alert: warning (<30 days), urgent (<14 days), critical (<7 days). Slack post per affected domain, only when threshold is crossed.\n\nNo memory, no LLM, no webhook. Free to run.",
        "height": 240,
        "width": 380,
        "color": 6
      },
      "id": "note-intro",
      "name": "Sticky Note - Intro",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -200,
        -100
      ]
    },
    {
      "parameters": {
        "content": "### >> SET ME <<\n\n1. Set `SSL_DOMAINS` env var, comma-separated list (e.g. `example.com,api.example.com,studiomeyer.io`).\n2. Set `SLACK_OPS_WEBHOOK` for alerts.\n3. Adjust schedule (default daily 09:00 UTC).\n4. Adjust thresholds in `Check Expiry` Code node (default 30 / 14 / 7 days).",
        "height": 240,
        "width": 380,
        "color": 5
      },
      "id": "note-setup",
      "name": "Sticky Note - Setup",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -200,
        200
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9 * * *"
            }
          ]
        }
      },
      "id": "ssl-1-trigger",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        240,
        60
      ]
    },
    {
      "parameters": {
        "jsCode": "// Read domains from SSL_DOMAINS env (comma-separated). Emit one item per domain.\n\nconst raw = $env.SSL_DOMAINS || '';\nconst domains = raw.split(',').map(d => d.trim()).filter(Boolean);\n\nif (domains.length === 0) {\n  return [{ json: { skipped: true, reason: 'no domains configured' } }];\n}\n\nreturn domains.map(d => ({ json: { domain: d } }));"
      },
      "id": "ssl-2-load-domains",
      "name": "Load Domains",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        60
      ]
    },
    {
      "parameters": {
        "jsCode": "// TLS-connect to the domain, read the peer certificate, compute days_until_expiry.\n// Three-tier alert: warning (<30 days), urgent (<14 days), critical (<7 days).\n//\n// Default rejectUnauthorized=true (CA-validated). Set\n// SSL_ACCEPT_SELFSIGNED=1 to opt in to inspecting self-signed / chain-broken\n// certs (useful for staging or internal mTLS endpoints where you want to\n// monitor expiry even if the cert chain does not validate).\n\nconst tls = require('tls');\n\nconst domain = $input.first().json.domain;\nconst host = domain.replace(/^https?:\\/\\//, '').split('/')[0].split(':')[0];\nconst port = 443;\nconst acceptSelfSigned = $env.SSL_ACCEPT_SELFSIGNED === '1';\n\nfunction connect() {\n  return new Promise((resolve, reject) => {\n    const socket = tls.connect({\n      host,\n      port,\n      servername: host,\n      timeout: 10000,\n      rejectUnauthorized: !acceptSelfSigned,\n    }, () => {\n      const cert = socket.getPeerCertificate();\n      socket.end();\n      if (!cert || !cert.valid_to) {\n        reject(new Error('No peer certificate or no valid_to'));\n      } else {\n        resolve(cert);\n      }\n    });\n    socket.on('error', reject);\n    socket.on('timeout', () => {\n      socket.destroy();\n      reject(new Error('TLS connection timeout'));\n    });\n  });\n}\n\nlet cert;\ntry {\n  cert = await connect();\n} catch (e) {\n  return [{ json: {\n    domain,\n    host,\n    error: true,\n    errorMessage: e.message,\n    severity: 'error',\n    shouldAlert: true,\n    checkedAt: new Date().toISOString(),\n  } }];\n}\n\nconst expiryDate = new Date(cert.valid_to);\nconst now = Date.now();\nconst daysLeft = Math.floor((expiryDate.getTime() - now) / (1000 * 60 * 60 * 24));\n\nlet severity = 'ok';\nlet shouldAlert = false;\nif (daysLeft < 7) { severity = 'critical'; shouldAlert = true; }\nelse if (daysLeft < 14) { severity = 'urgent'; shouldAlert = true; }\nelse if (daysLeft < 30) { severity = 'warning'; shouldAlert = true; }\n\nreturn [{ json: {\n  domain,\n  host,\n  daysLeft,\n  expiryDate: expiryDate.toISOString().slice(0, 10),\n  issuer: (cert.issuer && cert.issuer.O) || 'unknown',\n  subject: (cert.subject && cert.subject.CN) || host,\n  severity,\n  shouldAlert,\n  checkedAt: new Date().toISOString(),\n} }];"
      },
      "id": "ssl-3-check",
      "name": "Check Expiry",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        640,
        60
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "loose",
            "version": 2
          },
          "combinator": "and",
          "conditions": [
            {
              "leftValue": "={{ $json.shouldAlert }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ]
        }
      },
      "id": "ssl-4-if-alert",
      "name": "Should Alert?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        840,
        60
      ]
    },
    {
      "parameters": {
        "jsCode": "// Build a severity-coded Slack Block Kit message.\n\nconst e = $input.first().json;\n\nconst emoji = {\n  warning: ':warning:',\n  urgent: ':alarm_clock:',\n  critical: ':rotating_light:',\n  error: ':boom:',\n}[e.severity] || ':bell:';\n\nconst color = {\n  warning: '#daa038',\n  urgent: '#d93f0b',\n  critical: '#a30200',\n  error: '#666666',\n}[e.severity] || '#666666';\n\nconst headline = e.error\n  ? `${emoji} SSL check failed for ${e.domain}: ${e.errorMessage || 'unknown error'}`\n  : `${emoji} ${e.severity.toUpperCase()}: ${e.domain} expires in ${e.daysLeft} days`;\n\nconst blocks = [\n  { type: 'section', text: { type: 'mrkdwn', text: `*${headline}*` } },\n];\n\nif (!e.error) {\n  blocks.push({\n    type: 'section',\n    fields: [\n      { type: 'mrkdwn', text: `Expires: *${e.expiryDate}*` },\n      { type: 'mrkdwn', text: `Issuer: *${e.issuer}*` },\n      { type: 'mrkdwn', text: `Days left: *${e.daysLeft}*` },\n      { type: 'mrkdwn', text: `Subject: *${e.subject}*` },\n    ],\n  });\n}\n\nblocks.push({\n  type: 'context',\n  elements: [{ type: 'mrkdwn', text: `Checked at ${e.checkedAt}` }],\n});\n\nreturn [{ json: {\n  text: headline,\n  attachments: [{ color, blocks }],\n} }];"
      },
      "id": "ssl-5-build-message",
      "name": "Build Slack Message",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1040,
        -60
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.SLACK_OPS_WEBHOOK }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {}
      },
      "id": "ssl-6-slack",
      "name": "Slack Alert",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1240,
        -60
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "// Fallback for Slack delivery failure. Log structured error.\nconst err = ($input.first().json && $input.first().json.error) || {};\nreturn [{ json: {\n  ok: false,\n  fallback: true,\n  errorMessage: err.message || 'slack delivery failed',\n  loggedAt: new Date().toISOString(),\n} }];"
      },
      "id": "ssl-err-fallback",
      "name": "Error Fallback",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1440,
        200
      ]
    },
    {
      "parameters": {
        "content": "## Production Patterns\n\n- **Schedule throttle (built-in):** daily cron, no missed-run backfill.\n- **Three-tier alerting:** warning <30d, urgent <14d, critical <7d. Per-domain severity gates downstream alerts.\n- **TLS-error path:** if `tls.connect` itself fails, severity is `error` and the alert message includes the network error message.\n- **Error branch (always on):** Slack delivery failure does not crash workflow. Falls through to structured error log.",
        "height": 280,
        "width": 380,
        "color": 7
      },
      "id": "note-production-patterns",
      "name": "Sticky Note - Production Patterns",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        840,
        -260
      ]
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Load Domains",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Domains": {
      "main": [
        [
          {
            "node": "Check Expiry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Expiry": {
      "main": [
        [
          {
            "node": "Should Alert?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Should Alert?": {
      "main": [
        [
          {
            "node": "Build Slack Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Slack Message": {
      "main": [
        [
          {
            "node": "Slack Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack Alert": {
      "main": [
        [],
        [
          {
            "node": "Error Fallback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}

About this workflow

SSL Certificate Expiry Watcher (Three-Tier Alerts). Uses stickyNote, scheduleTrigger, httpRequest. Scheduled trigger; 10 nodes.

Source: https://github.com/studiomeyer-io/n8n-workflows/blob/main/templates/04-ssl-certificate-expiry-watcher/workflow.json — original creator credit. Request a take-down →

More General workflows → · Browse all categories →