Guides, Nov 7, 2026

# OpenVPN certificate has expired: find it and renew it

The error means one certificate in the TLS handshake is past its notAfter date, or the machine that logged it has the wrong clock. The log you found it in and the depth number tell you which one: depth=0 in a client log is the server certificate, depth=0 in a server log is that user's certificate, depth=1 is the CA. Read the dates with openssl, renew with easy-rsa, and if every client fails at once while no certificate is expired, check the CRL, because it expires too.

Tahir Mehmood, 10 min read

I checked every log line below against the OpenVPN 2.6 and 2.7 source, and the easy-rsa commands against easy-rsa 3.2.7 (September 2026) and its ChangeLog. If your log shows `TLS key negotiation failed` with no `VERIFY ERROR` line before it, you have a different problem: start with [the TLS key negotiation checklist](https://tunnelhq.com/blog/openvpn-tls-key-negotiation-failed/) instead.

Why now? easy-rsa 3 issues server and client certificates for 825 days by default, the CA for 3650 days, and the CRL for 180 days. A server set up a little over two years ago, or a CRL generated six months ago and never again, is the usual story.

## What does "VERIFY ERROR: depth=0, error=certificate has expired" mean?

It means OpenSSL checked a certificate the other side sent, found its `notAfter` date in the past, and OpenVPN refused the handshake. OpenVPN prints OpenSSL's verdict in a fixed format, `VERIFY ERROR: depth=N, error=TEXT: SUBJECT, serial=N`:

```
VERIFY ERROR: depth=0, error=certificate has expired: CN=server, serial=81624310542877013290
OpenSSL: error:0A000086:SSL routines::certificate verify failed
TLS_ERROR: BIO read tls_read_plaintext error
TLS Error: TLS object -> incoming plaintext read error
TLS Error: TLS handshake failed
```

Only the first line is specific. The second follows any failed certificate check, and the last three follow any TLS failure, so don't search for them. The `error=` text comes from OpenSSL, which is why you may also see `certificate is not yet valid`, `CRL has expired` or `unable to get local issuer certificate` in the same place.

The side that rejects a certificate also tells the other side. If the server refuses an expired client certificate, the server log has the `VERIFY ERROR` line, and the client only sees a TLS alert:

```
OpenSSL: error:0A000415:SSL routines::sslv3 alert certificate expired
```

That is OpenSSL 3. OpenSSL 1.1 prints the same alert as `error:14094415:SSL routines:ssl3_read_bytes:sslv3 alert certificate expired`. In both cases it means "the other side says your certificate expired".

There is one more line worth a search. When OpenVPN loads its own certificate at startup, it checks the dates and prints `WARNING: Your certificate has expired!` or `WARNING: Your certificate is not yet valid!`. It still starts. Run `journalctl -u openvpn-server@server | grep WARNING` on the server and look at the first lines of a client log.

## Which certificate expired?

Two facts tell you: which log the line is in, and the depth. Depth 0 is the certificate the other side presented. Depth 1 is the CA that signed it (with an intermediate CA, depth 1 is the intermediate and depth 2 the root).

| Where you see it | Expired item | Who is affected | What to replace |
| --- | --- | --- | --- |
| Client log, `VERIFY ERROR: depth=0` | Server certificate | Every user | `server.crt` on the server |
| Client log, `VERIFY ERROR: depth=1` | CA certificate in the client's `<ca>` | Every user | `ca.crt` everywhere, and see the CA section below |
| Server log, `VERIFY ERROR: depth=0` | That user's certificate | One user | That user's `<cert>` |
| Client log, `sslv3 alert certificate expired` | The user's own certificate | One user | That user's `<cert>` |
| Server log, `error=CRL has expired` | The CRL in `crl-verify` | Every new connection | `crl.pem` |

Before you renew anything, check the clock on the machine that logged the error. If the dates in the certificate look fine but OpenSSL says expired or not yet valid, the clock is wrong, not the certificate. `timedatectl` shows it on Linux. The [clock drift section](https://tunnelhq.com/blog/openvpn-tls-key-negotiation-failed/) of the older post lists the usual suspects (routers without NTP, paused VMs).

## How do I read certificate dates on the server and in a .ovpn file?

On the server, find the files the config points to, then read each one:

```
grep -E '^(ca|cert|crl-verify) ' /etc/openvpn/server/server.conf

openssl x509 -in /etc/openvpn/server/server.crt -noout -subject -enddate
openssl x509 -in /etc/openvpn/server/ca.crt -noout -subject -enddate
openssl crl -in /etc/openvpn/server/crl.pem -noout -lastupdate -nextupdate
```

`notAfter` is the expiry date of a certificate. For the CRL, `nextUpdate` is the date it stops being valid.

A client `.ovpn` file usually carries its certificates inline, between `<ca>` and `<cert>` tags. `openssl x509` skips any text before the `-----BEGIN` line, so you can feed it the block directly:

```
sed -n '/<cert>/,/<\/cert>/p' client.ovpn | openssl x509 -noout -subject -enddate
```

The `<ca>` block can hold more than one certificate (an intermediate and a root), and `openssl x509` reads only the first. Split the block first:

```
sed -n '/<ca>/,/<\/ca>/p' client.ovpn \
  | awk '/BEGIN CERT/{n++; f="ca-" n ".pem"} f{print > f} /END CERT/{close(f); f=""}'
for c in ca-*.pem; do openssl x509 -in "$c" -noout -subject -enddate; done
```

If you issued the certificates with easy-rsa, it can list everything that expires soon. The default window is 90 days:

```
./easyrsa --days=30 show-expire
```

Don't try `openssl s_client` against the VPN port. OpenVPN carries TLS inside its own packet format, so a plain TLS client can't read the server's certificate from outside. Read the dates on the server.

## How do I renew a certificate with easy-rsa 3?

Check your version first with `./easyrsa --version`, because `renew` has meant different things over the years. This table follows easy-rsa's own [renewal document](https://github.com/OpenVPN/easy-rsa/blob/master/doc/EasyRSA-Renew-and-Revoke.md) and ChangeLog:

| easy-rsa version | What `renew` does |
| --- | --- |
| 3.0.6 to 3.0.8 | Builds a new certificate and a new private key; the old certificate can't be revoked |
| 3.0.9 to 3.1.0 | New certificate and new private key; revoke the old one with `revoke-renewed` |
| 3.1.1 to 3.1.7 | New certificate only, the private key stays; the old key-rebuilding command became `rebuild` |
| 3.2.0 | No `renew`: use `expire NAME`, then `sign-req TYPE NAME` |
| 3.2.1 and later | New certificate only, the key stays, supported extensions are copied from the old certificate |

The difference matters for clients. On 3.0.x, a renewed user needs a new private key as well as the new certificate. On 3.1.1 and later, the key does not change, so you only send the new `<cert>`.

On 3.2.x, renewing the server certificate looks like this:

```
cd /etc/openvpn/easy-rsa            # wherever your pki/ directory is
./easyrsa renew server
cp pki/issued/server.crt /etc/openvpn/server/server.crt
systemctl restart openvpn-server@server

./easyrsa revoke-renewed server superseded
./easyrsa gen-crl
cp pki/crl.pem /etc/openvpn/server/crl.pem
```

Some details that cost people time:

-   `renew` needs the original request file, `pki/reqs/NAME.req`, and its commonName must match the certificate. If someone deleted `pki/reqs/`, renew stops with `Missing request file`, and you need a new request and a new key (`gen-req`, then `sign-req`).
-   `renew` keeps the commonName and the certificate type. That matters for clients that use `remote-cert-tls server` or `verify-x509-name`. If you build a fresh certificate under a new name instead, clients with `verify-x509-name server name` log `VERIFY X509NAME ERROR: CN=newname, must be server` and refuse it.
-   If you sign a server certificate as type `client` by mistake, clients with `remote-cert-tls server` log `VERIFY EKU ERROR`. The line before it says which extended key usage the certificate has and which one OpenVPN expects.
-   easy-rsa itself says to revoke the old certificate only after the new one is deployed. For a user certificate that is still valid, follow that order, or the user is locked out until they get the new file.
-   `revoke-renewed` changes nothing on the server until you run `gen-crl` and copy the new `crl.pem` into place.
-   OpenVPN reads its own certificate at startup, so the server needs a restart after you replace `server.crt`. The CRL is different: OpenVPN re-reads the CRL file when a client connects and the file has changed, so a new `crl.pem` needs no restart.

For a user certificate, run `./easyrsa renew alice`. easy-rsa 3.2 also writes inline files: `pki/inline/alice.inline` without the private key, and `pki/inline/private/alice.inline` with it. Because the key is unchanged, the public one is enough: replace the `<cert>` block in the user's `.ovpn` with the new one. If your clients pin the server with `peer-fingerprint` instead of using a CA, remember that a renewed certificate has a new fingerprint.

## How do I renew an expired CA certificate?

A CA that expired breaks every connection, and there is no way around replacing it everywhere. easy-rsa 3.2.2 added `renew-ca`, which builds a new CA certificate from the existing CA key and archives the old one to `pki/expired-ca.list`. It asks for confirmation before it changes anything.

What the documentation does not warn you about: the old server and user certificates may not chain to the new CA certificate. easy-rsa's default certificate profile sets `authorityKeyIdentifier = keyid,issuer:always`, which writes the CA certificate's serial number into every certificate it signs. The new CA certificate has a new serial number, and OpenSSL checks it. I tested this with OpenSSL 3.6: a certificate signed by the old CA certificate fails against the renewed one with `unable to get local issuer certificate`, even though the key and the name are the same. You can see the field yourself:

```
openssl x509 -in pki/issued/server.crt -noout -ext authorityKeyIdentifier
```

If the output has a `serial:` line, plan to re-sign every certificate after `renew-ca`:

```
./easyrsa renew-ca
./easyrsa renew server
./easyrsa renew alice            # and every other user
./easyrsa gen-crl
```

The private keys stay the same, so each user needs the new `<ca>` and the new `<cert>`, not a new key. The server needs the new `ca.crt`, `server.crt` and `crl.pem`, then a restart. Do it before the CA expires, not after: once it has expired, every user is offline until their new file arrives.

## How do I fix "CRL has expired"?

The CRL is the list of revoked certificates that `crl-verify` points to. It has its own expiry date, `nextUpdate`. easy-rsa's [`vars.example`](https://github.com/OpenVPN/easy-rsa/blob/master/easyrsa3/vars.example) is blunt about what happens then: "When the CRL expires, an OpenVPN Server which uses a CRL will reject ALL new connections, until the CRL is replaced." The server log shows the OpenSSL error for every user who tries:

```
VERIFY ERROR: depth=0, error=CRL has expired: CN=alice, serial=10274093357266093874
```

Users who are already connected keep working until they reconnect or until their next key renegotiation, which runs a new TLS handshake (by default within an hour, `reneg-sec 3600`). So this one often looks like a slow, spreading outage. The fix is quick:

```
./easyrsa gen-crl
cp pki/crl.pem /etc/openvpn/server/crl.pem
chmod 644 /etc/openvpn/server/crl.pem
```

The `chmod` matters if the config drops privileges with `user nobody`: the [OpenVPN 2.6 manual](https://openvpn.net/community-docs/community-articles/openvpn-2-6-manual.html) notes that the CRL file is read every time a peer connects, so that user must be able to read it.

easy-rsa's default CRL lifetime is 180 days. You can set a longer one with `./easyrsa --days=3650 gen-crl`. The other way is to regenerate the CRL monthly from cron, which only works without a person present if the CA key has no passphrase. I prefer a passphrase on the CA key, a long CRL lifetime and an expiry check, but that is a trade-off for your own threat model, not a rule.

## How do I catch certificate expiry before users do?

A check that completes a real OpenVPN handshake notices the failure at the first check after the expiry. That is useful, but it is already an outage. To be warned in advance, read the dates on a schedule. This script exits with an error when the CA, the server certificate, any issued certificate, or the CRL ends within 21 days (it uses GNU `date`, so any Linux). Adjust the paths; if your PKI lives on a different machine than the VPN server, check `pki/issued/` there:

```
#!/bin/sh
# /usr/local/bin/check-ovpn-expiry.sh
WINDOW=$((21*86400))
PKI=/etc/openvpn/easy-rsa/pki
status=0
for crt in /etc/openvpn/server/ca.crt /etc/openvpn/server/server.crt "$PKI"/issued/*.crt; do
  if ! openssl x509 -in "$crt" -noout -checkend "$WINDOW" >/dev/null; then
    echo "expires soon: $crt $(openssl x509 -in "$crt" -noout -enddate)"
    status=1
  fi
done
next=$(openssl crl -in /etc/openvpn/server/crl.pem -noout -nextupdate | cut -d= -f2)
if [ $(( $(date -d "$next" +%s) - $(date +%s) )) -lt "$WINDOW" ]; then
  echo "CRL nextUpdate soon: $next"
  status=1
fi
exit $status
```

`openssl x509 -checkend SECONDS` prints `Certificate will expire` and exits 1 when the certificate ends inside the window, which is what the loop tests. easy-rsa moves revoked certificates out of `pki/issued/`, so former users do not trigger it.

Then run it daily and have it report to a heartbeat (a dead man's switch). The job calls the heartbeat URL only when every date is fine, so you get an alert when something is about to expire, and also when the job itself stops running:

```
HEARTBEAT_URL=https://your-heartbeat-url
0 6 * * * /usr/local/bin/check-ovpn-expiry.sh && curl -fsS -m 10 --retry 5 -o /dev/null "$HEARTBEAT_URL"
```

Set the heartbeat's period to one day and give it an hour or two of grace. Healthchecks.io works for this, and so do [TunnelHQ's Uptime heartbeats](https://tunnelhq.com/uptime/), which also accept `/fail` at the end of the URL if you'd rather report the failure at once. The [VPN server monitoring guide](https://tunnelhq.com/blog/vpn-server-monitoring-guide/) covers where expiry checks fit next to the other checks a server needs.

Full disclosure: TunnelHQ also runs the other half. Its [OpenVPN monitor](https://tunnelhq.com/openvpn-monitoring/) brings up a real tunnel from your `.ovpn` every 1 to 5 minutes, depending on plan, so an expired server certificate, CA or CRL shows up as a failed check and an alert, not as a support ticket. It needs a config that uses `auth-user-pass` today; certificate-only configs can't be added yet ([docs](https://docs.tunnelhq.com/protocols/openvpn/)). Every plan has a 14-day free trial.

Source: https://tunnelhq.com/blog/openvpn-certificate-has-expired/
