<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: JP</title>
    <description>The latest articles on DEV Community by JP (@jpbroeders).</description>
    <link>https://dev.to/jpbroeders</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3761671%2F485a35e7-5cbe-4b27-8c61-6209213c2b99.png</url>
      <title>DEV Community: JP</title>
      <link>https://dev.to/jpbroeders</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jpbroeders"/>
    <language>en</language>
    <item>
      <title>DevOps Do's and Don'ts: What I Wish Someone Told Me 10 Years Ago</title>
      <dc:creator>JP</dc:creator>
      <pubDate>Sat, 28 Feb 2026 09:55:23 +0000</pubDate>
      <link>https://dev.to/jpbroeders/devops-dos-and-donts-what-i-wish-someone-told-me-10-years-ago-inm</link>
      <guid>https://dev.to/jpbroeders/devops-dos-and-donts-what-i-wish-someone-told-me-10-years-ago-inm</guid>
      <description>&lt;h1&gt;
  
  
  DevOps Do's and Don'ts: What I Wish Someone Told Me 10 Years Ago
&lt;/h1&gt;

&lt;p&gt;It's 2 AM. Phone's ringing. Production's down. Users are mad. CEO's asking questions. And somewhere in your git history is a deployment that passed every test but somehow broke everything.&lt;/p&gt;

&lt;p&gt;Yeah. Been there. More times than I'd like to admit.&lt;/p&gt;

&lt;p&gt;I've been doing DevOps for about 10 years now, mostly as a freelancer. That means I've seen a LOT of companies, a LOT of different setups, and honestly... a LOT of the same mistakes over and over again.&lt;/p&gt;

&lt;p&gt;This isn't some theoretical best practices guide. This is stuff I learned by screwing up, fixing it at 3 AM, and promising myself I'd never do it again. (Spoiler: I did it again. We all do.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Be Honest About What DevOps Actually Is
&lt;/h2&gt;

&lt;p&gt;Before we get into it - DevOps isn't a job title. I know, I know, my LinkedIn says "DevOps Engineer" too. But technically it's supposed to be a culture where devs and ops work together instead of hating each other.&lt;/p&gt;

&lt;p&gt;In reality though? Companies hire us to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep servers running&lt;/li&gt;
&lt;li&gt;Build CI/CD pipelines
&lt;/li&gt;
&lt;li&gt;Deal with infrastructure&lt;/li&gt;
&lt;li&gt;Be the person who gets called at 2 AM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So let's talk about doing that without losing your mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  DO: Automate Stuff (But Don't Go Crazy)
&lt;/h2&gt;

&lt;p&gt;Here's where everyone gets it wrong. They read about DevOps, get excited, and try to automate EVERYTHING in week one.&lt;/p&gt;

&lt;p&gt;I watched a startup spend three weeks building this insane auto-scaling Kubernetes setup with service mesh, observability, the whole nine yards. For an app with like... 50 users. Meanwhile their devs were still SSH-ing into servers to deploy code manually.&lt;/p&gt;

&lt;p&gt;That's backwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you should automate first:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deployments&lt;/strong&gt; - seriously, if you're still deploying manually, stop reading this and go set up GitHub Actions or something. Even a basic pipeline will save you hours every week.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Environment setup&lt;/strong&gt; - if it takes half a day to spin up a dev environment, you're wasting time. Automate that.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Backups&lt;/strong&gt; - this should honestly be #1. Automate your backups. Test your backups. I've seen too many "oh shit we need to restore" moments where the backups were broken.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Monitoring alerts&lt;/strong&gt; - can't fix what you don't know is broken.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What can wait:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;That complex orchestration that runs once a month&lt;/li&gt;
&lt;li&gt;Edge cases that happen twice a year&lt;/li&gt;
&lt;li&gt;Stuff that keeps changing (wait till it stabilizes)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Real Talk
&lt;/h3&gt;

&lt;p&gt;I helped that startup I mentioned earlier. We ripped out the fancy Kubernetes setup and went with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker Compose (yes, really)&lt;/li&gt;
&lt;li&gt;GitHub Actions for CI/CD&lt;/li&gt;
&lt;li&gt;Basic health checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deploy time went from 30 minutes to 3 minutes. Setup took 2 days instead of 3 weeks. They're doing fine. When they actually need to scale, we'll scale. But you don't need Kubernetes for 50 users.&lt;/p&gt;

&lt;h2&gt;
  
  
  DON'T: Skip Monitoring Because "We'll Add It Later"
&lt;/h2&gt;

&lt;p&gt;This is probably the mistake I see most often. "We'll add monitoring once we're live."&lt;/p&gt;

&lt;p&gt;Then you go live. Everything SEEMS fine. Three days later you realize your database has been maxed out for 48 hours and you lost customer data.&lt;/p&gt;

&lt;p&gt;Yeah.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minimum viable monitoring:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure stuff:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU, RAM, disk space&lt;/li&gt;
&lt;li&gt;Network latency
&lt;/li&gt;
&lt;li&gt;Is the service even up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Application stuff:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Error rates&lt;/li&gt;
&lt;li&gt;How fast things respond&lt;/li&gt;
&lt;li&gt;Database connection pools&lt;/li&gt;
&lt;li&gt;Queue lengths if you use background jobs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Business stuff:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User signups&lt;/li&gt;
&lt;li&gt;Payment success rate&lt;/li&gt;
&lt;li&gt;Whatever matters to your business&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Cron Job Blind Spot
&lt;/h3&gt;

&lt;p&gt;Here's one that gets everyone: cron jobs.&lt;/p&gt;

&lt;p&gt;People monitor their web servers, their databases, their APIs. But scheduled jobs? Those just run in the background, silent and unmonitored.&lt;/p&gt;

&lt;p&gt;Until they don't run. And nobody notices for a week.&lt;/p&gt;

&lt;p&gt;I've seen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Invoice jobs that stopped for a week (customers were NOT happy)&lt;/li&gt;
&lt;li&gt;Database backups failing for 3 days (found out when we needed to restore)&lt;/li&gt;
&lt;li&gt;Data sync breaking silently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix is stupid simple. Make your cron job ping a URL when it's done. If it doesn't ping within the expected time, get an alert.&lt;/p&gt;

&lt;p&gt;That's literally it. One curl at the end of your script. I built CronGuard because I was tired of this exact problem. But there are other tools too. Just... monitor your cron jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  DO: Put Everything in Git
&lt;/h2&gt;

&lt;p&gt;If it's not in git, it doesn't exist.&lt;/p&gt;

&lt;p&gt;I mean it. Everything should be reproducible from your repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure as code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Don't click buttons in AWS console&lt;/span&gt;
&lt;span class="c1"&gt;# Write Terraform instead&lt;/span&gt;

&lt;span class="s"&gt;resource "aws_instance" "app" {&lt;/span&gt;
  &lt;span class="s"&gt;ami           = "ami-0c55b159cbfafe1f0"&lt;/span&gt;
  &lt;span class="s"&gt;instance_type = "t3.micro"&lt;/span&gt;

  &lt;span class="s"&gt;tags = {&lt;/span&gt;
    &lt;span class="s"&gt;Name = "production-app"&lt;/span&gt;
  &lt;span class="s"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Configuration management:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Don't SSH in and edit nginx.conf&lt;/span&gt;
&lt;span class="c1"&gt;# Use Ansible&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure Nginx&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx.conf.j2&lt;/span&gt;
    &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/nginx/nginx.conf&lt;/span&gt;
  &lt;span class="na"&gt;notify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;restart nginx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Database migrations:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Don't run random SQL scripts&lt;/span&gt;
&lt;span class="c"&gt;# Use proper migrations&lt;/span&gt;

npx prisma migrate deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why This Actually Matters
&lt;/h3&gt;

&lt;p&gt;Client of mine had a production server die last year. Hardware failure. Completely dead.&lt;/p&gt;

&lt;p&gt;They had backups. They had monitoring. What they DIDN'T have was any record of how that server was configured.&lt;/p&gt;

&lt;p&gt;Two years of manual tweaks. Undocumented changes. Some guy who left 6 months ago had set up "some stuff" but nobody knew what.&lt;/p&gt;

&lt;p&gt;Took 3 days to get back online instead of 3 hours.&lt;/p&gt;

&lt;p&gt;If everything was in git - Terraform configs, Ansible playbooks, whatever - they could've rebuilt that server in an hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  DON'T: Treat Production Like a Playground
&lt;/h2&gt;

&lt;p&gt;"I'll just quickly fix this in prod..."&lt;/p&gt;

&lt;p&gt;Famous last words.&lt;/p&gt;

&lt;p&gt;I've said them. You've probably said them. We've all said them. Sometimes it works out. Sometimes you take down production for 4 hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When something breaks in prod:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is it on fire RIGHT NOW? If yes, do what you gotta do. If no, keep reading.&lt;/li&gt;
&lt;li&gt;Can you reproduce it in staging?&lt;/li&gt;
&lt;li&gt;Fix it in a branch&lt;/li&gt;
&lt;li&gt;Test it&lt;/li&gt;
&lt;li&gt;Code review&lt;/li&gt;
&lt;li&gt;Deploy through your normal pipeline&lt;/li&gt;
&lt;li&gt;Verify it worked&lt;/li&gt;
&lt;li&gt;Write down what happened so it doesn't happen again&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;For actual emergencies:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yeah okay sometimes you DO need to hotfix prod. When you do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write down EXACTLY what you changed&lt;/li&gt;
&lt;li&gt;Tell your team what you did&lt;/li&gt;
&lt;li&gt;Create a ticket to do it properly later&lt;/li&gt;
&lt;li&gt;Add a test so it can't happen again&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Feature Flags Are Your Friend
&lt;/h3&gt;

&lt;p&gt;Want to deploy without the stress? Feature flags.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;featureFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;newCheckout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NewCheckoutFlow&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;OldCheckoutFlow&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy the new code. Turn it on for yourself. Then a few internal users. Then 10% of traffic. Then everyone.&lt;/p&gt;

&lt;p&gt;Something breaks? Flip the switch back. No emergency deploys. No 3 AM rollbacks. Just... flip it off.&lt;/p&gt;

&lt;h2&gt;
  
  
  DO: Have a Rollback Plan (And Actually Test It)
&lt;/h2&gt;

&lt;p&gt;Every deployment should be reversible. Every single one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database migrations need down migrations:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Going up&lt;/span&gt;
ALTER TABLE &lt;span class="nb"&gt;users &lt;/span&gt;ADD COLUMN email_verified BOOLEAN&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c"&gt;# Going back down&lt;/span&gt;
ALTER TABLE &lt;span class="nb"&gt;users &lt;/span&gt;DROP COLUMN email_verified&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Keep your old docker images.&lt;/strong&gt; Tag releases properly. Document how to rollback.&lt;/p&gt;

&lt;p&gt;And here's the thing - TEST YOUR ROLLBACKS.&lt;/p&gt;

&lt;p&gt;Don't wait till production is on fire to find out your rollback procedure doesn't work. Do it in staging. Time how long it takes. Fix the slow parts.&lt;/p&gt;

&lt;p&gt;We do a quarterly drill where we deploy something, intentionally break it, and practice rolling back. Sounds paranoid but it's saved us multiple times.&lt;/p&gt;

&lt;h2&gt;
  
  
  DON'T: Skip Security Because It's Annoying
&lt;/h2&gt;

&lt;p&gt;"We'll fix the security issues after launch."&lt;/p&gt;

&lt;p&gt;No you won't.&lt;/p&gt;

&lt;p&gt;Security isn't a feature you add later. It's a requirement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't do this:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Secrets in git&lt;/span&gt;
&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgresql://admin:password123@prod-db.com/myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Do this:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Environment variables&lt;/span&gt;
&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use AWS Secrets Manager, HashiCorp Vault, or at minimum just environment variables. But get secrets out of your code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't give everything admin access:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Bad&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Good&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::my-bucket/*"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Least privilege. Always.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use HTTPS everywhere.&lt;/strong&gt; Even in dev. mkcert makes it easy to get trusted local certificates. In production, Let's Encrypt is free.&lt;/p&gt;

&lt;p&gt;And monitor your SSL certificates so they don't expire. (Yeah I built CertGuard for this too. Expired certificates are embarrassing.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limit your APIs.&lt;/strong&gt; Even internal ones. You'll thank me later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't run containers as root.&lt;/strong&gt; Just don't.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; node&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  DO: Write Things Down
&lt;/h2&gt;

&lt;p&gt;"The code is self-documenting" is a lie we tell ourselves.&lt;/p&gt;

&lt;p&gt;Future you (or the poor person who replaces you) needs to understand how this works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Document:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How services talk to each other&lt;/li&gt;
&lt;li&gt;Database schemas&lt;/li&gt;
&lt;li&gt;External dependencies&lt;/li&gt;
&lt;li&gt;How to deploy&lt;/li&gt;
&lt;li&gt;How to rollback&lt;/li&gt;
&lt;li&gt;How to debug common issues&lt;/li&gt;
&lt;li&gt;Where the logs are&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep it close to the code. README files work great. Architecture Decision Records are good too. Just... write it down.&lt;/p&gt;

&lt;p&gt;And keep it updated. Outdated docs are worse than no docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  DON'T: Build For Scale You Don't Have
&lt;/h2&gt;

&lt;p&gt;"We need Kubernetes from day one."&lt;/p&gt;

&lt;p&gt;No you don't.&lt;/p&gt;

&lt;p&gt;Most apps don't need Kubernetes. Most databases don't need sharding. Most APIs don't need Redis caching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scale when you have data showing it's actually a problem.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not when you THINK it might be a problem someday. When your monitoring shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Response times above your SLA&lt;/li&gt;
&lt;li&gt;Database queries timing out&lt;/li&gt;
&lt;li&gt;Servers hitting resource limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Start simple:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One server handles 10k users easily&lt;/li&gt;
&lt;li&gt;SQLite handles millions of rows&lt;/li&gt;
&lt;li&gt;You don't need a CDN till you have global users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've watched startups spend 6 months building distributed systems for apps that never got past 100 users. That time could've been spent building features people actually wanted.&lt;/p&gt;

&lt;h2&gt;
  
  
  DO: Break Things (In Staging)
&lt;/h2&gt;

&lt;p&gt;Staging should be where things break safely.&lt;/p&gt;

&lt;p&gt;Kill services randomly. Simulate slow databases. Fill up the disk. Cut network connections.&lt;/p&gt;

&lt;p&gt;See what breaks. Fix it. Repeat.&lt;/p&gt;

&lt;p&gt;You don't need Netflix's fancy Chaos Monkey. Just manually break stuff and see what happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  DON'T: Ignore Technical Debt
&lt;/h2&gt;

&lt;p&gt;"We'll refactor this later."&lt;/p&gt;

&lt;p&gt;You won't.&lt;/p&gt;

&lt;p&gt;That hacky fix becomes critical infrastructure. That "temporary" workaround is still there 2 years later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Managing tech debt:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create tickets for known issues&lt;/li&gt;
&lt;li&gt;Estimate the actual cost (time + risk)&lt;/li&gt;
&lt;li&gt;Dedicate 20% of your sprint to fixing it&lt;/li&gt;
&lt;li&gt;Don't let it pile up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And explain to stakeholders WHY you're "not building features." Show them the cost of NOT fixing the debt.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It All Comes Down To
&lt;/h2&gt;

&lt;p&gt;Strip away all the fancy tools and methodologies and it's really just:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Automate the painful stuff&lt;/li&gt;
&lt;li&gt;Monitor what matters&lt;/li&gt;
&lt;li&gt;Make it reproducible&lt;/li&gt;
&lt;li&gt;Plan for failure&lt;/li&gt;
&lt;li&gt;Secure by default&lt;/li&gt;
&lt;li&gt;Write down the why&lt;/li&gt;
&lt;li&gt;Scale when needed (not before)&lt;/li&gt;
&lt;li&gt;Pay down your debt&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;DevOps isn't about having the coolest tools or the most complex setup. It's about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shipping faster&lt;/li&gt;
&lt;li&gt;Breaking less&lt;/li&gt;
&lt;li&gt;Recovering quicker when you do break&lt;/li&gt;
&lt;li&gt;Actually sleeping at night&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start small. Automate ONE thing this week. Add ONE monitor. Write ONE runbook. Ship ONE improvement.&lt;/p&gt;

&lt;p&gt;Do that every week and in a year you'll be way ahead of teams still "planning their DevOps transformation."&lt;/p&gt;

&lt;p&gt;Trust me. I've been doing this for 10 years and I'm still learning. We all are. That's kind of the point.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;About me:&lt;/strong&gt; I'm Jean-Pierre, a freelance DevOps engineer in the Netherlands. I help teams build infrastructure that doesn't suck. More stuff on my blog at &lt;a href="https://www.freelyit.nl" rel="noopener noreferrer"&gt;FreelyIT.nl&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Got DevOps war stories? Drop them in the comments. I want to hear what you've learned the hard way too.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>automation</category>
      <category>monitoring</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Why Your SSL Certificate Will Expire at 3 AM on a Saturday (And How to Stop It)</title>
      <dc:creator>JP</dc:creator>
      <pubDate>Thu, 19 Feb 2026 14:20:38 +0000</pubDate>
      <link>https://dev.to/jpbroeders/why-your-ssl-certificate-will-expire-at-3-am-on-a-saturday-and-how-to-stop-it-5ecm</link>
      <guid>https://dev.to/jpbroeders/why-your-ssl-certificate-will-expire-at-3-am-on-a-saturday-and-how-to-stop-it-5ecm</guid>
      <description>&lt;p&gt;Let me tell you about the worst Saturday morning of my career.&lt;/p&gt;

&lt;p&gt;3:17 AM. My phone explodes with alerts. The main production site is down. API is down. Mobile app can't connect. Customer support is getting flooded.&lt;/p&gt;

&lt;p&gt;The culprit? An expired SSL certificate.&lt;/p&gt;

&lt;p&gt;Not a server crash. Not a database failure. Not a DDoS attack. A &lt;strong&gt;certificate that expired at 3:00 AM&lt;/strong&gt; on a Saturday morning, taking down a multi-million dollar e-commerce platform.&lt;/p&gt;

&lt;p&gt;The certificate had been valid for a year. We knew it would expire. There were emails. There were reminders. But between team changes, inbox filters, and "I'll do it tomorrow" syndrome, it slipped through the cracks.&lt;/p&gt;

&lt;p&gt;That incident cost the company six figures in lost revenue and taught me that &lt;strong&gt;SSL certificate monitoring isn't optional -- it's survival.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with SSL Certificates
&lt;/h2&gt;

&lt;p&gt;SSL/TLS certificates are one of those things that work perfectly until they don't. And when they don't, they fail catastrophically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Silent expiry&lt;/strong&gt; -- No warning. Your site just stops working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser warnings&lt;/strong&gt; -- "This connection is not private" scares away customers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API breakage&lt;/strong&gt; -- Mobile apps and integrations stop working&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero grace period&lt;/strong&gt; -- The second it expires, everything breaks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even worse, modern infrastructure has &lt;strong&gt;dozens or hundreds&lt;/strong&gt; of certificates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Main domain (&lt;code&gt;example.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;WWW subdomain (&lt;code&gt;www.example.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;API endpoints (&lt;code&gt;api.example.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Admin panels (&lt;code&gt;admin.example.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CDN origins&lt;/li&gt;
&lt;li&gt;Wildcard certificates (&lt;code&gt;*.example.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Internal services&lt;/li&gt;
&lt;li&gt;Load balancers&lt;/li&gt;
&lt;li&gt;VPNs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one is a ticking time bomb.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Certificates Expire (And Why That's Good)
&lt;/h2&gt;

&lt;p&gt;Before we dive into monitoring, let's understand &lt;em&gt;why&lt;/em&gt; certificates expire:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt; -- Shorter validity periods limit damage if a private key is compromised&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cryptographic aging&lt;/strong&gt; -- Today's "secure" algorithms become tomorrow's vulnerabilities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ownership changes&lt;/strong&gt; -- Domains change hands, organizations restructure&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Certificate authorities (CAs) have been steadily &lt;em&gt;reducing&lt;/em&gt; validity periods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2015:&lt;/strong&gt; 5 years maximum&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2018:&lt;/strong&gt; 2 years maximum (825 days)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2020:&lt;/strong&gt; 1 year maximum (398 days)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2024:&lt;/strong&gt; Proposals for 90 days or less&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's Encrypt already issues 90-day certificates by default. The industry is moving toward &lt;strong&gt;automated, short-lived certificates&lt;/strong&gt; -- which means monitoring becomes even more critical.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Anatomy of an SSL Certificate
&lt;/h2&gt;

&lt;p&gt;To monitor certificates effectively, you need to understand what you're monitoring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check a certificate with OpenSSL&lt;/span&gt;
openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; example.com:443 &lt;span class="nt"&gt;-servername&lt;/span&gt; example.com &amp;lt; /dev/null 2&amp;gt;/dev/null | openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-dates&lt;/span&gt;

&lt;span class="c"&gt;# Output:&lt;/span&gt;
&lt;span class="nv"&gt;notBefore&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Jan 15 00:00:00 2026 GMT
&lt;span class="nv"&gt;notAfter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Apr 15 23:59:59 2026 GMT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key fields to monitor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Subject&lt;/strong&gt; -- Who the cert is issued to (&lt;code&gt;CN=example.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issuer&lt;/strong&gt; -- Who issued it (&lt;code&gt;Let's Encrypt&lt;/code&gt;, &lt;code&gt;DigiCert&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Valid From&lt;/strong&gt; (&lt;code&gt;notBefore&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Valid Until&lt;/strong&gt; (&lt;code&gt;notAfter&lt;/code&gt;) -- THE critical field&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subject Alternative Names (SANs)&lt;/strong&gt; -- Additional domains covered&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signature Algorithm&lt;/strong&gt; -- Is it still secure?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The SAN Trap
&lt;/h3&gt;

&lt;p&gt;Here's a gotcha that bit me: &lt;strong&gt;Subject Alternative Names (SANs)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Modern certificates often cover multiple domains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CN=example.com
SAN=example.com, www.example.com, api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you only check the primary domain but monitor from a different SAN, you might miss expiry warnings. Always verify the certificate covers the specific hostname you're checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a Certificate Monitor
&lt;/h2&gt;

&lt;p&gt;The simplest version is a shell script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nv"&gt;DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"example.com"&lt;/span&gt;
&lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;443
&lt;span class="nv"&gt;ALERT_DAYS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30

&lt;span class="c"&gt;# Get expiry date&lt;/span&gt;
&lt;span class="nv"&gt;EXPIRY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; | openssl s_client &lt;span class="nt"&gt;-servername&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOMAIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-connect&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOMAIN&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$PORT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="se"&gt;\&lt;/span&gt;
         openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-enddate&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-f2&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Convert to epoch timestamp&lt;/span&gt;
&lt;span class="nv"&gt;EXPIRY_EPOCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EXPIRY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;NOW_EPOCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;DAYS_LEFT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$EXPIRY_EPOCH&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$NOW_EPOCH&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;86400&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$DAYS_LEFT&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; &lt;span class="nv"&gt;$ALERT_DAYS&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Certificate for &lt;/span&gt;&lt;span class="nv"&gt;$DOMAIN&lt;/span&gt;&lt;span class="s2"&gt; expires in &lt;/span&gt;&lt;span class="nv"&gt;$DAYS_LEFT&lt;/span&gt;&lt;span class="s2"&gt; days!"&lt;/span&gt;
    &lt;span class="c"&gt;# Send alert (email, Slack, etc.)&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this daily via cron:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;0 9 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /usr/local/bin/check-cert.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;But this has problems:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Only checks when the cron runs&lt;/li&gt;
&lt;li&gt;No historical data&lt;/li&gt;
&lt;li&gt;Doesn't handle multiple domains&lt;/li&gt;
&lt;li&gt;No retry logic for network blips&lt;/li&gt;
&lt;li&gt;Alert fatigue (you get the same alert every day)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's level up.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Production-Grade Solution
&lt;/h2&gt;

&lt;p&gt;Here's a Node.js version that handles edge cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tls&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;https&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkCertificate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;servername&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// SNI support&lt;/span&gt;
            &lt;span class="na"&gt;rejectUnauthorized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Check even invalid certs&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPeerCertificate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valid_to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No certificate found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validTo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valid_to&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validFrom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valid_from&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;daysLeft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;validTo&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;validFrom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;validFrom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;validTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;validTo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;daysLeft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;daysLeft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;validFrom&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;validTo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;subjectAltNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subjectaltname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;serialNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serialNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fingerprint&lt;/span&gt;
            &lt;span class="p"&gt;};&lt;/span&gt;

            &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Connection timeout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage&lt;/span&gt;
&lt;span class="nf"&gt;checkCertificate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Certificate for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`  Expires: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validTo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`  Days left: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`  Issuer: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;O&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;sendAlert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Check failed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Python Alternative
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_certificate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_default_context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_connection&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wrap_socket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;server_hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ssock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ssock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getpeercert&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="c1"&gt;# Parse expiry
&lt;/span&gt;            &lt;span class="n"&gt;not_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;notAfter&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%b %d %H:%M:%S %Y %Z&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;days_left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;not_after&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hostname&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;subject&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;subject&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;issuer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;issuer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;valid_to&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;not_after&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;days_left&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;days_left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;serial_number&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;serialNumber&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;san&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;subjectAltName&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Usage
&lt;/span&gt;&lt;span class="n"&gt;cert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;check_certificate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;example.com&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Certificate expires in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;days_left&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;days_left&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;send_alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Alert Thresholds: A Practical Strategy
&lt;/h2&gt;

&lt;p&gt;Don't just alert at "30 days". Use &lt;strong&gt;multiple thresholds&lt;/strong&gt; with escalating urgency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;60 days&lt;/strong&gt; Informational (Slack channel, low priority)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;30 days&lt;/strong&gt; Warning (Email to team)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;14 days&lt;/strong&gt; Urgent (Email + Slack mention)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7 days&lt;/strong&gt; Critical (SMS/PagerDuty to on-call)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3 days&lt;/strong&gt; Emergency (Wake everyone up)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAlertLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EMERGENCY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CRITICAL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;URGENT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WARNING&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INFO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendAlert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAlertLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: Certificate for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; expires in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; days (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validTo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;switch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EMERGENCY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;sendSMS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;sendSlack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CRITICAL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;sendSlack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@channel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;URGENT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;sendSlack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WARNING&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INFO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;sendSlack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// No email spam&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Edge Cases That Will Bite You
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Certificate Chains
&lt;/h3&gt;

&lt;p&gt;A valid server cert with an expired &lt;strong&gt;intermediate certificate&lt;/strong&gt; will fail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Root CA (trusted)
  Intermediate CA (EXPIRED!)
    Server Cert (valid)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always check the &lt;strong&gt;entire chain&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPeerCertificate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// true = include chain&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issuerCertificate&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issuerCertificate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issuerCertificate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issuerCertificate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Check each cert in the chain&lt;/span&gt;
&lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validTo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valid_to&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;daysLeft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;validTo&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;86400000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`  [&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; - &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; days left`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Wildcard Certificates
&lt;/h3&gt;

&lt;p&gt;A wildcard cert (&lt;code&gt;*.example.com&lt;/code&gt;) covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;api.example.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;www.example.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;admin.example.com&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But &lt;strong&gt;NOT&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;example.com&lt;/code&gt; (root domain)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;api.staging.example.com&lt;/code&gt; (nested subdomain)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Monitor both the wildcard AND the root domain separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. SNI (Server Name Indication)
&lt;/h3&gt;

&lt;p&gt;Modern servers host multiple domains on one IP. Without SNI, you might check the wrong certificate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Wrong (no SNI)&lt;/span&gt;
openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; 192.0.2.1:443

&lt;span class="c"&gt;# Right (with SNI)&lt;/span&gt;
openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; 192.0.2.1:443 &lt;span class="nt"&gt;-servername&lt;/span&gt; example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always specify &lt;code&gt;-servername&lt;/code&gt; or the equivalent in your programming language.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Load Balancers and CDNs
&lt;/h3&gt;

&lt;p&gt;You have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Origin server cert (your server)&lt;/li&gt;
&lt;li&gt;Load balancer cert (AWS ELB, Cloudflare, etc.)&lt;/li&gt;
&lt;li&gt;CDN edge cert (Cloudflare, Fastly, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Monitor the certificate that users actually see&lt;/strong&gt;, not just your origin. Check from outside your infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Internal Services
&lt;/h3&gt;

&lt;p&gt;Don't forget:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database SSL connections&lt;/li&gt;
&lt;li&gt;Redis TLS&lt;/li&gt;
&lt;li&gt;Internal APIs&lt;/li&gt;
&lt;li&gt;VPNs&lt;/li&gt;
&lt;li&gt;LDAP/LDAPS&lt;/li&gt;
&lt;li&gt;SMTP with STARTTLS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These often use self-signed or internal CA certs that expire quietly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's Encrypt: Automation &amp;amp; Monitoring
&lt;/h2&gt;

&lt;p&gt;Let's Encrypt changed the game with free, automated certificates. But automation &lt;strong&gt;doesn't mean "set and forget"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  ACME Renewal Monitoring
&lt;/h3&gt;

&lt;p&gt;Certbot (the Let's Encrypt client) auto-renews certs. But what if renewal fails?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check last renewal attempt&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; certbot.timer &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"7 days ago"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; error

&lt;span class="c"&gt;# Or parse certbot's logs&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Renewal failed"&lt;/span&gt; /var/log/letsencrypt/letsencrypt.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Better:&lt;/strong&gt; Monitor the certificate expiry &lt;em&gt;and&lt;/em&gt; renewal success separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkLetsEncryptRenewal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;renewalConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`/etc/letsencrypt/renewal/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.conf`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;renewalConfig&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Renewal config not found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;statSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;renewalConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;daysSinceUpdate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mtime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;86400000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// LE renews certs older than 60 days&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;daysSinceUpdate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
            &lt;span class="na"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Renewal config not updated in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;daysSinceUpdate&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; days`&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Common Let's Encrypt Failures
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;HTTP-01 challenge blocked&lt;/strong&gt; -- Firewall rules, CDN config&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS-01 fails&lt;/strong&gt; -- API rate limits, DNS propagation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webroot permissions&lt;/strong&gt; -- Certbot can't write to &lt;code&gt;.well-known/acme-challenge/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits hit&lt;/strong&gt; -- Too many renewals in a week&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Monitor renewal logs and alert on failure patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dashboard: Making Data Actionable
&lt;/h2&gt;

&lt;p&gt;Raw alerts are good. A dashboard is better. Track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Certificates expiring soon&lt;/strong&gt; (sorted by days left)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recently renewed&lt;/strong&gt; (confirm automation is working)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issuer distribution&lt;/strong&gt; (Are you still using that old CA?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expiry timeline&lt;/strong&gt; (visual calendar of upcoming expirations)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simple HTML dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateDashboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;checkCertificate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; 
            &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; 
        &lt;span class="p"&gt;})))&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Sort by days left (ascending)&lt;/span&gt;
    &lt;span class="nx"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;999&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;999&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;h1&amp;gt;SSL Certificate Dashboard&amp;lt;/h1&amp;gt;
    &amp;lt;table&amp;gt;
        &amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;Domain&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Days Left&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Expires&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Issuer&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;
        &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`
            &amp;lt;tr class="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getRowClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;
                &amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/td&amp;gt;
                &amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ERROR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/td&amp;gt;
                &amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validTo&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;toLocaleDateString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/td&amp;gt;
                &amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;O&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
        `&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
    &amp;lt;/table&amp;gt;
    `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getRowClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;critical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;urgent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;daysLeft&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pro Tips from Production
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Check from Multiple Locations
&lt;/h3&gt;

&lt;p&gt;Your cert might be valid from your office but expired on your CDN edge nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;locations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-east.probe.example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eu-west.probe.example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ap-southeast.probe.example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;probe&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;locations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;checkCertificateViaProxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Compare results&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Track Certificate Changes
&lt;/h3&gt;

&lt;p&gt;Alert when certificates change unexpectedly (potential security issue):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastKnownFingerprint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cert_fingerprint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentFingerprint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastKnownFingerprint&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;lastKnownFingerprint&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;currentFingerprint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;sendSecurityAlert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Certificate changed for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;!`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cert_fingerprint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentFingerprint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Test Renewal Before It's Urgent
&lt;/h3&gt;

&lt;p&gt;Don't wait until 7 days before expiry to test renewal. Run a &lt;strong&gt;dry-run&lt;/strong&gt; monthly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;certbot renew &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Renewal will fail!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Document Your Renewal Process
&lt;/h3&gt;

&lt;p&gt;When the cert expires at 3 AM and the person who set it up left the company 6 months ago, you need documentation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# SSL Certificate Renewal - example.com&lt;/span&gt;

&lt;span class="gs"&gt;**Provider:**&lt;/span&gt; Let's Encrypt  
&lt;span class="gs"&gt;**Renewal method:**&lt;/span&gt; Certbot with HTTP-01 challenge  
&lt;span class="gs"&gt;**Auto-renewal:**&lt;/span&gt; Yes (certbot.timer systemd service)  
&lt;span class="gs"&gt;**Manual renewal:**&lt;/span&gt; &lt;span class="sb"&gt;`sudo certbot renew --force-renewal`&lt;/span&gt;  
&lt;span class="gs"&gt;**Webroot:**&lt;/span&gt; &lt;span class="sb"&gt;`/var/www/example.com/`&lt;/span&gt;  
&lt;span class="gs"&gt;**On-call:**&lt;/span&gt; ops@example.com  
&lt;span class="gs"&gt;**Last manual renewal:**&lt;/span&gt; 2026-01-15 by @john
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tools Worth Knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SSL Labs&lt;/strong&gt; -- Free online checker (&lt;a href="https://www.ssllabs.com/ssltest/" rel="noopener noreferrer"&gt;https://www.ssllabs.com/ssltest/&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;testssl.sh&lt;/strong&gt; -- Comprehensive SSL/TLS scanner&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certbot&lt;/strong&gt; -- Let's Encrypt client&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cert-manager&lt;/strong&gt; -- Kubernetes cert automation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Certificate Manager&lt;/strong&gt; -- Managed certs for AWS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And of course, dedicated monitoring services exist (I'll leave product names out of this, but they're worth Googling).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Post-Incident Checklist
&lt;/h2&gt;

&lt;p&gt;After that 3 AM disaster, here's what we implemented:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Automated monitoring&lt;/strong&gt; for all domains (check daily)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-threshold alerts&lt;/strong&gt; (60/30/14/7/3 days)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple alert channels&lt;/strong&gt; (email, Slack, SMS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard&lt;/strong&gt; showing all certs at a glance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation&lt;/strong&gt; for manual renewal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated renewal&lt;/strong&gt; (Let's Encrypt)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Renewal monitoring&lt;/strong&gt; (separate from expiry monitoring)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-call runbook&lt;/strong&gt; for cert emergencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly dry-run&lt;/strong&gt; renewal tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certificate inventory&lt;/strong&gt; (every cert, every service)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We've had &lt;strong&gt;zero certificate-related outages&lt;/strong&gt; in the two years since.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;SSL certificates will expire. It's not &lt;em&gt;if&lt;/em&gt;, it's &lt;em&gt;when&lt;/em&gt;. The question is: will you know about it 30 days in advance, or at 3 AM on a Saturday when everything is on fire?&lt;/p&gt;

&lt;p&gt;Monitoring isn't hard:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check expiry dates regularly&lt;/strong&gt; (daily is fine)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Alert with escalating urgency&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automate renewal where possible&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monitor renewal success separately&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Document everything&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The 3 AM outage is 100% preventable. Don't let it happen to you.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Have you had a certificate disaster story?&lt;/strong&gt; Share it in the comments. Misery loves company, and we all learn from each other's mistakes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building certificate monitoring?&lt;/strong&gt; What challenges are you facing? Drop a comment and let's discuss.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>ssl</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Hidden Costs of Skipping Code Reviews (And How Different Teams Handle Them)</title>
      <dc:creator>JP</dc:creator>
      <pubDate>Tue, 17 Feb 2026 10:17:58 +0000</pubDate>
      <link>https://dev.to/jpbroeders/the-hidden-costs-of-skipping-code-reviews-and-how-different-teams-handle-them-2ia3</link>
      <guid>https://dev.to/jpbroeders/the-hidden-costs-of-skipping-code-reviews-and-how-different-teams-handle-them-2ia3</guid>
      <description>&lt;p&gt;&lt;em&gt;A practical look at code review approaches, what they catch, and why most teams still get it wrong.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Every developer has been there. You ship a feature, it works in staging, you deploy on a Friday afternoon, and by Monday morning you're dealing with a production incident that a basic code review would have caught in fifteen minutes.&lt;/p&gt;

&lt;p&gt;Code reviews aren't just about catching bugs. They're about knowledge transfer, consistency, and -- increasingly important -- security. But not all code reviews are created equal. Different types catch different problems, and most teams pick one approach and call it a day.&lt;/p&gt;

&lt;p&gt;Let's break down the main types of code review, what they're actually good at, and where they fall short.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Classic Peer Review
&lt;/h2&gt;

&lt;p&gt;The most common type. A colleague reads your code, leaves comments, you argue in the PR thread about variable naming for 45 minutes, and eventually someone approves it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it's good at:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logic errors and edge cases&lt;/li&gt;
&lt;li&gt;Readability and maintainability&lt;/li&gt;
&lt;li&gt;Knowledge sharing between team members&lt;/li&gt;
&lt;li&gt;Catching obvious architectural mistakes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where it falls short:&lt;/strong&gt;&lt;br&gt;
Peer reviews are only as good as your team. If nobody on your team knows the OWASP Top 10 by heart, SQL injection vulnerabilities will sail through undetected. Same goes for subtle memory leaks, race conditions, or dependency vulnerabilities. Humans are great at reading business logic, but terrible at consistently catching security issues under deadline pressure.&lt;/p&gt;

&lt;p&gt;There's also the familiarity problem: if you've been staring at the same codebase for two years, you stop seeing the things that are obviously wrong to a fresh set of eyes.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Automated Static Analysis
&lt;/h2&gt;

&lt;p&gt;Tools like ESLint, SonarQube, ReSharper, or Semgrep run against your code and flag issues automatically. These are fantastic â€” and completely table stakes at this point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it's good at:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code style and formatting consistency&lt;/li&gt;
&lt;li&gt;Known anti-patterns&lt;/li&gt;
&lt;li&gt;Basic security rules (hardcoded secrets, unsafe functions)&lt;/li&gt;
&lt;li&gt;Cyclomatic complexity warnings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where it falls short:&lt;/strong&gt;&lt;br&gt;
Static analysis tools generate a &lt;em&gt;lot&lt;/em&gt; of noise. Teams learn to ignore the warnings, which defeats the purpose. They also can't understand context: a tool might flag a &lt;code&gt;eval()&lt;/code&gt; call without understanding that it's intentionally sandboxed and reviewed.&lt;/p&gt;

&lt;p&gt;More importantly, static analysis works at the file level. It doesn't understand your architecture, your deployment environment, or how your dependencies interact with each other.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Security-Focused Code Audits
&lt;/h2&gt;

&lt;p&gt;This is where things get serious. Security audits go beyond "does this look clean" and ask "can this be exploited?"&lt;/p&gt;

&lt;p&gt;A proper security audit covers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OWASP Top 10 vulnerabilities:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Injection attacks (SQL, NoSQL, command injection)&lt;/li&gt;
&lt;li&gt;Broken authentication&lt;/li&gt;
&lt;li&gt;Sensitive data exposure&lt;/li&gt;
&lt;li&gt;XML External Entities (XXE)&lt;/li&gt;
&lt;li&gt;Broken access control&lt;/li&gt;
&lt;li&gt;Security misconfiguration&lt;/li&gt;
&lt;li&gt;Cross-Site Scripting (XSS)&lt;/li&gt;
&lt;li&gt;Insecure deserialization&lt;/li&gt;
&lt;li&gt;Using components with known vulnerabilities&lt;/li&gt;
&lt;li&gt;Insufficient logging and monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What makes security audits different&lt;/strong&gt; is the adversarial mindset. You're not asking "does this work?" -- you're asking "how could someone break this?" That requires a completely different way of reading code.&lt;/p&gt;

&lt;p&gt;The challenge: real security expertise is expensive and hard to find. Most teams schedule a security audit once a year (if that) and hope for the best between audits. Meanwhile, they're shipping new code every week.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Dependency Audits
&lt;/h2&gt;

&lt;p&gt;This one gets overlooked constantly, and it's becoming one of the most critical review types.&lt;/p&gt;

&lt;p&gt;Modern applications import hundreds of packages. Each one is a potential attack vector. The &lt;code&gt;log4shell&lt;/code&gt; vulnerability from 2021 brought down major enterprises because of a transitive dependency nobody knew was there. The &lt;code&gt;event-stream&lt;/code&gt; npm compromise injected malicious code into a popular package. This is not theoretical risk.&lt;/p&gt;

&lt;p&gt;A proper dependency audit checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Known CVEs (Common Vulnerabilities and Exposures) in your dependencies&lt;/li&gt;
&lt;li&gt;Outdated packages with security patches available&lt;/li&gt;
&lt;li&gt;License compliance (GPL in a commercial product is a legal problem)&lt;/li&gt;
&lt;li&gt;Transitive dependencies (what your packages depend on)&lt;/li&gt;
&lt;li&gt;Abandoned packages with no maintenance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most teams run &lt;code&gt;npm audit&lt;/code&gt; or &lt;code&gt;pip check&lt;/code&gt; occasionally and consider that sufficient. It's not. These tools only catch known vulnerabilities that have been formally reported. Zero-days and newly discovered issues require more comprehensive scanning.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Performance Reviews
&lt;/h2&gt;

&lt;p&gt;Separate from security, performance reviews look at your code through a completely different lens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What gets checked:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;N+1 query problems (the classic database performance killer)&lt;/li&gt;
&lt;li&gt;Unoptimized loops and algorithmic complexity&lt;/li&gt;
&lt;li&gt;Memory leaks and resource cleanup&lt;/li&gt;
&lt;li&gt;Unnecessary re-renders in frontend applications&lt;/li&gt;
&lt;li&gt;Missing database indexes&lt;/li&gt;
&lt;li&gt;Inefficient data structures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Performance issues are notoriously hard to catch in code review because they often only manifest at scale. Code that works fine with 100 users can grind to a halt at 10,000. A good performance review combines static code analysis with understanding of production load patterns.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Infrastructure and Container Reviews
&lt;/h2&gt;

&lt;p&gt;As teams move to containerized deployments, a whole new category of review has emerged.&lt;/p&gt;

&lt;p&gt;Docker images and Kubernetes configurations introduce their own class of vulnerabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Running containers as root (bad idea)&lt;/li&gt;
&lt;li&gt;Exposing unnecessary ports&lt;/li&gt;
&lt;li&gt;Missing resource limits (hello, runaway processes)&lt;/li&gt;
&lt;li&gt;Hardcoded secrets in Dockerfiles or environment variables&lt;/li&gt;
&lt;li&gt;Overly permissive RBAC configurations&lt;/li&gt;
&lt;li&gt;Images built from unverified base images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is specialized knowledge that most developers don't have. And yet, most teams ship Kubernetes configs that have never been reviewed by someone who knows what they're looking at.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Problem: Review Gaps
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable truth about most engineering teams: you're probably doing peer review reasonably well, you have some static analysis in your CI pipeline, and you've run &lt;code&gt;npm audit&lt;/code&gt; recently. But you have large gaps in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Systematic security auditing (not just occasional penetration tests)&lt;/li&gt;
&lt;li&gt;Thorough dependency vulnerability scanning&lt;/li&gt;
&lt;li&gt;Performance review at the architecture level&lt;/li&gt;
&lt;li&gt;Infrastructure security review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These gaps exist for a predictable reason: expertise is expensive, time is scarce, and manual comprehensive reviews don't scale with shipping velocity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where AI Changes the Calculus
&lt;/h2&gt;

&lt;p&gt;This is where the landscape is genuinely shifting. AI-powered code review tools can now perform comprehensive audits that would previously require a team of specialists.&lt;/p&gt;

&lt;p&gt;One tool worth looking at is &lt;strong&gt;&lt;a href="https://www.scanmycode.dev" rel="noopener noreferrer"&gt;ScanMyCode&lt;/a&gt;&lt;/strong&gt; -- it runs AI-powered audits covering all the categories above: code quality, security (OWASP Top 10), dependency vulnerabilities (CVE lookup and license compliance), performance bottlenecks, and Docker/K8s configuration. Results come back within 24 hours, at a fraction of the cost of a human consultant.&lt;/p&gt;

&lt;p&gt;This doesn't replace human expertise -- but it closes the gap between expensive periodic audits and the continuous shipping reality that modern teams live in. Think of it as a comprehensive first pass that surfaces the issues worth a human's attention.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building a Review Culture That Actually Works
&lt;/h2&gt;

&lt;p&gt;The teams with the strongest review cultures share a few common traits:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They layer their reviews.&lt;/strong&gt; Automated tools catch what they're good at. Peers review logic and context. Dedicated audits (manual or AI-assisted) catch what humans miss under deadline pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They review infrastructure as code.&lt;/strong&gt; Dockerfiles, Kubernetes manifests, and CI/CD configurations go through the same rigor as application code. Because they should.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They treat dependencies as first-class citizens.&lt;/strong&gt; Regular dependency audits aren't a one-time event â€” they're part of the development lifecycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They create psychological safety.&lt;/strong&gt; Code review culture dies when people feel attacked in PR threads. Comments should be about the code, not the author. "This query could cause an N+1 issue" is useful. "Why would you write it this way?" is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They follow up on findings.&lt;/strong&gt; A code review that surfaces issues but doesn't result in fixes is just documentation of technical debt. The value is in the action that follows.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Code review is not a checkbox. It's a discipline that, done well, catches the kind of issues that make the news â€” and the ones that quietly drain performance and maintainability over years.&lt;/p&gt;

&lt;p&gt;The teams that get it right don't rely on any single approach. They build layered review processes that match their risk profile, shipping velocity, and resource constraints. And increasingly, they're supplementing human review with AI-powered auditing to close the gaps that manual processes inevitably leave.&lt;/p&gt;

&lt;p&gt;Start with whatever you're not doing. Add a dependency audit to your CI pipeline. Schedule a security review for your most critical service. Run your Docker configuration through something that actually understands container security.&lt;/p&gt;

&lt;p&gt;The breach you prevent is the one you never hear about.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have a codebase that needs a thorough once-over? &lt;a href="https://www.scanmycode.dev" rel="noopener noreferrer"&gt;ScanMyCode&lt;/a&gt; does comprehensive AI-powered audits covering security, performance, dependencies, and Docker/K8s -- starting from €39.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>security</category>
      <category>codequality</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built a Cron Job Monitor Because Silence Kills Production</title>
      <dc:creator>JP</dc:creator>
      <pubDate>Mon, 16 Feb 2026 14:38:40 +0000</pubDate>
      <link>https://dev.to/jpbroeders/i-built-a-cron-job-monitor-because-silence-kills-production-56h1</link>
      <guid>https://dev.to/jpbroeders/i-built-a-cron-job-monitor-because-silence-kills-production-56h1</guid>
      <description>&lt;h1&gt;
  
  
  I Built a Cron Job Monitor Because Silence Kills Production
&lt;/h1&gt;

&lt;p&gt;Three months ago, my client's daily database backup hadn't run in 11 days. The cron job was still scheduled. No errors in the logs. The monitoring dashboard was green. Everything looked fine.&lt;/p&gt;

&lt;p&gt;Until someone tried to restore from a backup that didn't exist.&lt;/p&gt;

&lt;p&gt;That's when I learned the hard way: &lt;strong&gt;traditional monitoring is terrible at catching things that don't happen&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Traditional Monitoring
&lt;/h2&gt;

&lt;p&gt;Most monitoring tools are great at telling you when something breaks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server is down? Alert.&lt;/li&gt;
&lt;li&gt;API returns 500? Alert.&lt;/li&gt;
&lt;li&gt;Disk is full? Alert.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But what about when your nightly backup job silently stops running? Or your data sync task fails to start? Or your cleanup script never executes?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silence.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Traditional monitoring watches for events. Cron jobs that don't run don't generate events. They just... don't happen. And you don't find out until it's too late.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Dead Man's Switch" Approach
&lt;/h2&gt;

&lt;p&gt;After the backup incident, I started thinking differently about monitoring scheduled tasks. Instead of watching &lt;em&gt;for&lt;/em&gt; failures, what if we watched for &lt;em&gt;missing&lt;/em&gt; successes?&lt;/p&gt;

&lt;p&gt;The concept is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your cron job pings an endpoint when it runs successfully&lt;/li&gt;
&lt;li&gt;If the ping doesn't arrive within the expected window, you get alerted&lt;/li&gt;
&lt;li&gt;Silence = failure&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's like a dead man's switch on a train. If the operator stops pressing the button (the "heartbeat"), the train stops. If your job stops checking in, you get alerted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building CronGuard
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://cronguard.app" rel="noopener noreferrer"&gt;CronGuard&lt;/a&gt; to solve this for myself and my clients. The core idea is dead simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every monitor gets a unique ping URL. Your job hits that URL when it completes. If we don't get a ping within the expected schedule, we alert you.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's what a basic integration looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# Your backup script&lt;/span&gt;
pg_dump mydb &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; backup.sql
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-czf&lt;/span&gt; backup-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;.tar.gz backup.sql
aws s3 &lt;span class="nb"&gt;cp &lt;/span&gt;backup-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;.tar.gz s3://my-backups/

&lt;span class="c"&gt;# Ping CronGuard when done&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; https://cronguard.app/api/ping/your-monitor-id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. If the backup fails, the ping doesn't happen. If the cron job stops running, the ping doesn't happen. If the server dies, the ping doesn't happen.&lt;/p&gt;

&lt;p&gt;In all cases: &lt;strong&gt;you get alerted&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Technical Choices That Mattered
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Keep the Ping Endpoint Stupid Simple
&lt;/h3&gt;

&lt;p&gt;The ping endpoint is just an HTTP GET. No authentication required (the URL itself is the secret). No JSON body. No headers. Just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://cronguard.app/api/ping/abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why? Because I wanted it to work from anywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bash scripts&lt;/li&gt;
&lt;li&gt;Python scripts&lt;/li&gt;
&lt;li&gt;Inside Docker containers&lt;/li&gt;
&lt;li&gt;Lambda functions&lt;/li&gt;
&lt;li&gt;GitHub Actions&lt;/li&gt;
&lt;li&gt;Cron jobs on a Raspberry Pi&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can make an HTTP request, you can monitor your job. No SDK. No dependencies. No authentication dance.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Grace Periods Are Critical
&lt;/h3&gt;

&lt;p&gt;Here's a mistake I made early: treating cron schedules as exact.&lt;/p&gt;

&lt;p&gt;If a job is scheduled for 03:00, and runs at 03:02, that's not a failure. Servers reboot. Tasks queue. Execution time varies.&lt;/p&gt;

&lt;p&gt;CronGuard uses &lt;strong&gt;grace periods&lt;/strong&gt;: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Daily job at 03:00? Alert if no ping by 04:00.&lt;/li&gt;
&lt;li&gt;Hourly job? Alert if no ping after 70 minutes.&lt;/li&gt;
&lt;li&gt;Every 5 minutes? Alert after 7 minutes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This eliminated false positives and made the system actually useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The First Ping Problem
&lt;/h3&gt;

&lt;p&gt;When you create a new monitor, you haven't sent your first ping yet. Should the system immediately alert that the job is "down"?&lt;/p&gt;

&lt;p&gt;No. That's annoying.&lt;/p&gt;

&lt;p&gt;Solution: monitors are in a "waiting" state until they receive their first ping. After that, the clock starts ticking.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Recovery Notifications Matter
&lt;/h3&gt;

&lt;p&gt;Early version: alert when job stops checking in. Done.&lt;/p&gt;

&lt;p&gt;Reality: you also want to know when it starts working again.&lt;/p&gt;

&lt;p&gt;Now CronGuard sends recovery notifications too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Backup job missed check-in (expected by 04:00)"&lt;/li&gt;
&lt;li&gt;"Backup job recovered (checked in at 04:15)"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This helps you confirm your fix actually worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons from Running It in Production
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Lesson 1: Cron Jobs Fail More Than You Think
&lt;/h3&gt;

&lt;p&gt;After deploying CronGuard for my own infrastructure and a handful of clients, I learned that &lt;strong&gt;cron jobs are fragile&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Things I've seen cause silent failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server rebooted, cron daemon didn't restart properly&lt;/li&gt;
&lt;li&gt;Environment variables missing in cron context&lt;/li&gt;
&lt;li&gt;Disk full, job can't write temp files&lt;/li&gt;
&lt;li&gt;Database credentials rotated, job can't connect&lt;/li&gt;
&lt;li&gt;Dependencies updated, script breaks&lt;/li&gt;
&lt;li&gt;Path issues (&lt;code&gt;/usr/local/bin&lt;/code&gt; not in cron's PATH)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these would trigger traditional monitoring. All of them stopped critical jobs from running.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson 2: Most People Don't Monitor Their Cron Jobs At All
&lt;/h3&gt;

&lt;p&gt;I thought everyone had sophisticated monitoring setups. Turns out, most developers and small teams just... hope their cron jobs work.&lt;/p&gt;

&lt;p&gt;They schedule it once, see it run once, and assume it'll run forever. Until it doesn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson 3: The Real Value Is Peace of Mind
&lt;/h3&gt;

&lt;p&gt;The best feedback I got wasn't "this caught a bug." It was "I sleep better knowing I'll find out if something stops working."&lt;/p&gt;

&lt;p&gt;That's the real value: &lt;strong&gt;confidence that silence won't kill production&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Dead Man's Switch Monitoring Makes Sense
&lt;/h2&gt;

&lt;p&gt;This approach isn't for everything. Here's when it works:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scheduled tasks&lt;/strong&gt;: backups, data syncs, cleanup jobs, report generation&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Async workers&lt;/strong&gt;: if you expect jobs to complete regularly&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Periodic data ingestion&lt;/strong&gt;: RSS feeds, API polling, scraping&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-time services&lt;/strong&gt;: use traditional uptime monitoring&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event-driven systems&lt;/strong&gt;: if execution is unpredictable&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternative Approaches (and Why I Didn't Use Them)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Option 1: Log parsing&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Parse cron output for errors. Problem: no output = no detection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2: Process monitoring&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Check if the process is running. Problem: cron spawns processes, they finish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 3: File timestamps&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Check modification time on output files. Problem: requires filesystem access, brittle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 4: Traditional uptime monitoring&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Ping the endpoint yourself. Problem: doesn't tell you if the &lt;em&gt;job&lt;/em&gt; ran, just if the endpoint responds.&lt;/p&gt;

&lt;p&gt;Dead man's switch monitoring is the only approach that directly answers: &lt;strong&gt;"Did my job complete successfully?"&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;I run CronGuard as a free service for basic monitoring (5 monitors, 5-minute checks). If you've got cron jobs, backups, or scheduled tasks you care about, give it a shot:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cronguard.app" rel="noopener noreferrer"&gt;cronguard.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can literally start monitoring in 30 seconds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a monitor&lt;/li&gt;
&lt;li&gt;Copy the ping URL&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;curl -fsS &amp;lt;url&amp;gt;&lt;/code&gt; to the end of your script&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. Now you'll know if it stops working.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Traditional monitoring watches for things that happen. But some of the most critical failures are things that &lt;em&gt;don't&lt;/em&gt; happen.&lt;/p&gt;

&lt;p&gt;If you've got scheduled tasks keeping your infrastructure alive, you need to monitor for silence.&lt;/p&gt;

&lt;p&gt;Because in production, &lt;strong&gt;silence kills&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Questions? Running into silent failures with your own infrastructure?&lt;/strong&gt; Drop a comment. I'd love to hear your war stories about cron jobs that stopped working and how long it took to notice.&lt;/p&gt;

&lt;p&gt;Built something similar? Using a different approach? Let me know. I'm always curious how other teams solve this.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>monitoring</category>
      <category>infrastructure</category>
      <category>saas</category>
    </item>
  </channel>
</rss>
