Fix 'x509: certificate has expired or is not yet valid' When Pulling from Docker Registry

intermediate๐Ÿณ Docker2026-05-15| Docker 20.x+, Linux/macOS/Windows, self-hosted Docker Registry or corporate registry with TLS

Error Message

Get "https://registry.example.com/v2/": x509: certificate has expired or is not yet valid: current time 2025-01-01T00:00:00Z is after 2024-12-31T23:59:59Z
#docker#ssl#certificate#x509#registry#expired

TL;DR

Three quick fixes depending on what's broken:

  • Registry cert is expired โ†’ renew it (Let's Encrypt or your CA)
  • Your system clock is wrong โ†’ sync with NTP (timedatectl set-ntp true)
  • Dev/test environment and you don't care โ†’ add registry to insecure-registries in Docker daemon config

The Full Error

Get "https://registry.example.com/v2/": x509: certificate has expired or is not yet valid: current time 2025-01-01T00:00:00Z is after 2024-12-31T23:59:59Z

You'll hit this on docker pull, docker push, or docker login โ€” any operation that opens a TLS connection to a registry. The timestamp baked into the error message is your first clue. Run date on your machine and compare it to what Docker saw. That 30-second check often tells you everything.

Root Cause

Every TLS certificate has a validity window defined by two fields: notBefore and notAfter. Docker's Go TLS stack rejects the connection the moment either boundary is crossed:

  • The cert's notAfter is in the past โ€” the registry cert genuinely expired
  • Your local clock is ahead of notAfter โ€” a valid cert looks expired due to clock skew
  • Your local clock is behind notBefore โ€” the cert looks not-yet-valid

Clock skew is sneakier than it sounds. A VM that was suspended for a few hours, a container host without NTP, or a laptop that drifted 5 minutes โ€” any of these can trigger the exact same error as a genuinely expired cert.

Step 1 โ€” Check the Certificate Expiry

Don't guess. Pull the cert directly and read its dates:

# Check the registry cert directly
openssl s_client -connect registry.example.com:443 -servername registry.example.com 2>/dev/null \
  | openssl x509 -noout -dates

# Output example:
# notBefore=Dec  1 00:00:00 2024 GMT
# notAfter=Dec 31 23:59:59 2024 GMT

If notAfter is before today, the cert is expired โ€” go to Fix A. If the dates look perfectly fine, your system clock is lying to Docker.

Step 2 โ€” Check System Clock Skew

# Check current time and NTP sync status
timedatectl status

# Or just:
date -u

Off by more than a minute or two? Fix it:

# Enable NTP sync (systemd-based Linux)
timedatectl set-ntp true

# Force an immediate step correction
sudo chronyc makestep
# or
sudo ntpdate -u pool.ntp.org

Suspended VMs are the usual culprit here โ€” the clock freezes while the host sleeps, then wakes up potentially hours behind. After syncing, retry docker pull. Clock was the problem? It works right away.

Fix A โ€” Renew the Registry Certificate

You control the registry and the cert is dead. Time to replace it.

Let's Encrypt (Certbot)

# Renew all certs
sudo certbot renew

# Force renewal even if not near expiry
sudo certbot renew --force-renewal --cert-name registry.example.com

Then restart your registry service:

# If running as a Docker container
docker restart registry

# Or systemd
sudo systemctl restart docker-registry

Self-signed cert (generate a fresh one)

openssl req -newkey rsa:4096 -nodes -sha256 \
  -keyout /certs/domain.key \
  -x509 -days 365 \
  -out /certs/domain.crt \
  -subj "/CN=registry.example.com" \
  -addext "subjectAltName=DNS:registry.example.com"

Restart the registry container with the new cert paths mounted. Every Docker client machine also needs to trust the new cert โ€” see Fix B.

Fix B โ€” Trust a Self-Signed or Internal CA Cert

Corporate registries and internal CAs aren't in Docker's default trust store. The fix is to drop the cert into Docker's per-registry directory:

# Create the directory for this registry
sudo mkdir -p /etc/docker/certs.d/registry.example.com:5000

# Copy the CA cert (or the self-signed cert)
sudo cp ca.crt /etc/docker/certs.d/registry.example.com:5000/ca.crt

No daemon restart needed โ€” Docker reads this directory on each connection.

Want system-wide trust so curl, openssl, and everything else also accepts the cert?

# Ubuntu/Debian
sudo cp ca.crt /usr/local/share/ca-certificates/registry-ca.crt
sudo update-ca-certificates

# RHEL/CentOS/Fedora
sudo cp ca.crt /etc/pki/ca-trust/source/anchors/registry-ca.crt
sudo update-ca-trust

Fix C โ€” Insecure Registry (Dev/Test Only)

Stuck in a CI pipeline or local dev loop and just need it to work right now? Mark the registry as insecure. Production use is not an option here.

Edit /etc/docker/daemon.json:

{
  "insecure-registries": ["registry.example.com:5000"]
}

Restart the daemon:

sudo systemctl restart docker

Docker will connect over HTTP or skip TLS verification for that registry. Fast to set up, genuinely dangerous in production โ€” you've been warned.

Verify the Fix

# Retry the pull
docker pull registry.example.com/myimage:latest

# Confirm the cert won't expire for at least another 24 hours
openssl s_client -connect registry.example.com:443 -servername registry.example.com 2>/dev/null \
  | openssl x509 -noout -checkend 86400
# Outputs: "Certificate will not expire" if valid

# Hit the registry v2 API directly
curl -v https://registry.example.com/v2/

Prevent This from Happening Again

  • Automate cert renewal: certbot renew via cron or systemd timer, running 30 days before expiry โ€” not the day it dies
  • Monitor expiry dates with ssl-cert-check or a Prometheus ssl_expiry exporter; page yourself at 14 days out
  • Keep NTP running on every Docker host, especially VMs โ€” timedatectl show | grep NTPSynchronized is a fast check
  • Self-signed certs on a 365-day rotation expire faster than you remember; either automate the rotation or switch to Let's Encrypt

Quick Reference

# Check cert expiry
openssl s_client -connect HOST:PORT 2>/dev/null | openssl x509 -noout -dates

# Sync clock
timedatectl set-ntp true && sudo chronyc makestep

# Trust a CA cert (Docker only)
sudo mkdir -p /etc/docker/certs.d/HOST:PORT
sudo cp ca.crt /etc/docker/certs.d/HOST:PORT/ca.crt

# Insecure registry fallback (dev only)
# /etc/docker/daemon.json โ†’ { "insecure-registries": ["HOST:PORT"] }
sudo systemctl restart docker

Related Error Notes