Skip to content

Commit 7105018

Browse files
authored
feat(openvpn-rw): add certificates renewal (CA and server) functionalities (#1536)
1 parent c316f4a commit 7105018

3 files changed

Lines changed: 108 additions & 9 deletions

File tree

packages/ns-api/files/ns.ovpnrw

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/python3
22

33
#
4-
# Copyright (C) 2023 Nethesis S.r.l.
4+
# Copyright (C) 2026 Nethesis S.r.l.
55
# SPDX-License-Identifier: GPL-2.0-only
66
#
77

@@ -191,6 +191,30 @@ def slurp(path):
191191
with open(path) as fp:
192192
return fp.read()
193193

194+
def get_certificate_expiration(pem_path, label):
195+
"""Extract certificate expiration date from PEM file"""
196+
try:
197+
result = subprocess.run(
198+
["openssl", "x509", "-noout", "-subject", "-nameopt", "RFC2253", "-enddate"],
199+
stdin=open(pem_path, 'r'),
200+
capture_output=True,
201+
text=True,
202+
check=True
203+
)
204+
205+
# Parse notAfter line: notAfter=Feb 21 13:04:51 2036 GMT
206+
for line in result.stdout.split('\n'):
207+
if line.startswith("notAfter="):
208+
date_str = line.split("=")[1].strip()
209+
try:
210+
expiry_dt = datetime.strptime(date_str, "%b %d %H:%M:%S %Y %Z")
211+
return {label: int(expiry_dt.timestamp())}
212+
except:
213+
pass
214+
return {}
215+
except:
216+
return {}
217+
194218
def instance2number(input_string):
195219
numbers = re.findall(r'\d+', input_string)
196220
return int(numbers[0]) if numbers else None
@@ -397,6 +421,19 @@ def get_configuration(ovpninstance):
397421
if 'ns_public_ip' not in ret:
398422
ret['ns_public_ip'] = ovpn.get_public_addresses(u)
399423

424+
# add certificate expiration dates
425+
certificates = {}
426+
ca_path = f'/etc/openvpn/{ovpninstance}/pki/ca.crt'
427+
cert_path = f'/etc/openvpn/{ovpninstance}/pki/issued/server.crt'
428+
429+
if os.path.exists(ca_path):
430+
certificates.update(get_certificate_expiration(ca_path, 'CA'))
431+
if os.path.exists(cert_path):
432+
certificates.update(get_certificate_expiration(cert_path, 'server'))
433+
434+
if certificates:
435+
ret['certificates'] = certificates
436+
400437
return ret
401438

402439
def set_configuration(args):
@@ -547,6 +584,7 @@ def list_users(ovpninstance):
547584
u = EUci()
548585
ret = []
549586
connected = ovpn.list_connected_clients(ovpninstance)
587+
connected_lower_case = {k.lower(): v for k, v in connected.items()}
550588
expirations = list_user_expirations(ovpninstance)
551589
db = u.get("openvpn", ovpninstance, "ns_user_db", default=None)
552590
if not db:
@@ -561,18 +599,18 @@ def list_users(ovpninstance):
561599
# migrated users can have the form <user>@>domain>
562600
# make sure to duplicate connected info also removing the domain part
563601
normalized = {}
564-
for user in connected:
602+
for user in connected_lower_case:
565603
if '@' in user:
566604
nuser = user.split('@')[0]
567-
normalized[nuser] = connected[user]
568-
connected.update(normalized)
605+
normalized[nuser] = connected_lower_case[user]
606+
connected_lower_case.update(normalized)
569607
for user in db_users:
570608
# exclude users not enabled for OpenVPN
571609
if "openvpn_enabled" not in user:
572610
continue
573-
if user['name'] in connected:
611+
if user['name'].lower() in connected_lower_case:
574612
user["connected"] = True
575-
user = user | connected[user['name']]
613+
user = user | connected_lower_case[user['name'].lower()]
576614
else:
577615
user["connected"] = False
578616
user["expiration"] = ""
@@ -1041,6 +1079,24 @@ def connection_history(ovpninstance):
10411079

10421080
return connections
10431081

1082+
def renew_server_certificate(ovpninstance):
1083+
try:
1084+
subprocess.run(["/usr/sbin/ns-openvpn-renew-server", ovpninstance], check=True, capture_output=True)
1085+
subprocess.run(["/etc/init.d/openvpn", "restart", ovpninstance], check=False, capture_output=True)
1086+
except Exception as e:
1087+
print(e, file=sys.stderr)
1088+
return utils.validation_error("instance", "server_certificate_renewal_failed", ovpninstance)
1089+
return {"result": "success"}
1090+
1091+
def regenerate_all_certificates(ovpninstance):
1092+
try:
1093+
subprocess.run(["/usr/sbin/ns-openvpn-renew-ca", ovpninstance], check=True, capture_output=True)
1094+
subprocess.run(["/etc/init.d/openvpn", "restart", ovpninstance], check=False, capture_output=True)
1095+
except Exception as e:
1096+
print(e, file=sys.stderr)
1097+
return utils.validation_error("instance", "certificates_regeneration_failed", ovpninstance)
1098+
return {"result": "success"}
1099+
10441100
cmd = sys.argv[1]
10451101

10461102
if cmd == 'list':
@@ -1088,7 +1144,9 @@ if cmd == 'list':
10881144
"download-user-2fa": {"instance": "roadwarrior1", "username": "myuser"},
10891145
"download_all_user_configurations": {"instance": "roadwarrior1"},
10901146
"connection-history-csv": {"instance": "roadwarrior1", "timezone": "Europe/Rome"},
1091-
"connection-history": {"instance": "roadwarrior1"}
1147+
"connection-history": {"instance": "roadwarrior1"},
1148+
"renew-server-certificate": {"instance": "roadwarrior1"},
1149+
"regenerate-all-certificates": {"instance": "roadwarrior1"}
10921150
}))
10931151
else:
10941152
action = sys.argv[2]
@@ -1145,5 +1203,9 @@ else:
11451203
ret = connection_history_csv(args['instance'], args['timezone'])
11461204
elif action == "connection-history":
11471205
ret = connection_history(args['instance'])
1206+
elif action == "renew-server-certificate":
1207+
ret = renew_server_certificate(args["instance"])
1208+
elif action == "regenerate-all-certificates":
1209+
ret = regenerate_all_certificates(args["instance"])
11481210

11491211
print(json.dumps(ret))

packages/ns-openvpn/files/ns-openvpn-renew-ca

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@
55
# SPDX-License-Identifier: GPL-2.0-only
66
#
77

8-
# Setup EasyRSA pki
8+
# Renew CA and all certificates for the specified OpenVPN instance
99
instance=$1
1010
if [ -z $instance ]; then
1111
exit 1
1212
fi
1313

14+
cn=$(uci get system.@system[0].hostname | cut -d '.' -f 1)
15+
if [ -z "$cn" ]; then
16+
cn=NethSec
17+
fi
18+
1419
# Set environment variables for EasyRSA
1520
export EASYRSA_BATCH=1
1621
export EASYRSA_CERT_EXPIRE=3650
@@ -21,7 +26,7 @@ if [ -f /etc/openvpn/$instance/pki/ca.crt ]; then
2126
(
2227
/usr/bin/easyrsa renew-ca
2328
/usr/bin/easyrsa revoke-issued server
24-
/usr/bin/easyrsa build-server-full server nopass
29+
EASYRSA_REQ_CN=$cn /usr/bin/easyrsa build-server-full server nopass
2530
/usr/bin/easyrsa gen-crl
2631
for f in $(find /etc/openvpn/$instance/pki/issued -name \*.crt ! -name server.crt); do
2732
name=$(basename $f | cut -d '.' -f 1)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/bin/sh
2+
3+
#
4+
# Copyright (C) 2026 Nethesis S.r.l.
5+
# SPDX-License-Identifier: GPL-2.0-only
6+
#
7+
8+
# Renew server certificate for the specified OpenVPN instance
9+
instance=$1
10+
if [ -z $instance ]; then
11+
exit 1
12+
fi
13+
14+
cn=$(uci get system.@system[0].hostname | cut -d '.' -f 1)
15+
if [ -z "$cn" ]; then
16+
cn=NethSec
17+
fi
18+
19+
# Set environment variables for EasyRSA
20+
export EASYRSA_BATCH=1
21+
export EASYRSA_CERT_EXPIRE=3650
22+
export EASYRSA_CRL_DAYS=3650
23+
export EASYRSA_REQ_CN=$cn
24+
25+
if [ -f /etc/openvpn/$instance/pki/ca.crt ]; then
26+
cd /etc/openvpn/$instance
27+
(
28+
/usr/bin/easyrsa revoke server
29+
/usr/bin/easyrsa gen-crl
30+
/usr/bin/easyrsa build-server-full server nopass
31+
)
32+
fi

0 commit comments

Comments
 (0)