Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 69 additions & 7 deletions packages/ns-api/files/ns.ovpnrw
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/python3

#
# Copyright (C) 2023 Nethesis S.r.l.
# Copyright (C) 2026 Nethesis S.r.l.
# SPDX-License-Identifier: GPL-2.0-only
#

Expand Down Expand Up @@ -191,6 +191,30 @@ def slurp(path):
with open(path) as fp:
return fp.read()

def get_certificate_expiration(pem_path, label):
"""Extract certificate expiration date from PEM file"""
try:
result = subprocess.run(
["openssl", "x509", "-noout", "-subject", "-nameopt", "RFC2253", "-enddate"],
stdin=open(pem_path, 'r'),
capture_output=True,
text=True,
check=True
)

# Parse notAfter line: notAfter=Feb 21 13:04:51 2036 GMT
for line in result.stdout.split('\n'):
if line.startswith("notAfter="):
date_str = line.split("=")[1].strip()
try:
expiry_dt = datetime.strptime(date_str, "%b %d %H:%M:%S %Y %Z")
return {label: int(expiry_dt.timestamp())}
except:
pass
return {}
except:
return {}

def instance2number(input_string):
numbers = re.findall(r'\d+', input_string)
return int(numbers[0]) if numbers else None
Expand Down Expand Up @@ -397,6 +421,19 @@ def get_configuration(ovpninstance):
if 'ns_public_ip' not in ret:
ret['ns_public_ip'] = ovpn.get_public_addresses(u)

# add certificate expiration dates
certificates = {}
ca_path = f'/etc/openvpn/{ovpninstance}/pki/ca.crt'
cert_path = f'/etc/openvpn/{ovpninstance}/pki/issued/server.crt'

if os.path.exists(ca_path):
certificates.update(get_certificate_expiration(ca_path, 'CA'))
if os.path.exists(cert_path):
certificates.update(get_certificate_expiration(cert_path, 'server'))

if certificates:
ret['certificates'] = certificates

return ret

def set_configuration(args):
Expand Down Expand Up @@ -547,6 +584,7 @@ def list_users(ovpninstance):
u = EUci()
ret = []
connected = ovpn.list_connected_clients(ovpninstance)
connected_lower_case = {k.lower(): v for k, v in connected.items()}
expirations = list_user_expirations(ovpninstance)
db = u.get("openvpn", ovpninstance, "ns_user_db", default=None)
if not db:
Expand All @@ -561,18 +599,18 @@ def list_users(ovpninstance):
# migrated users can have the form <user>@>domain>
# make sure to duplicate connected info also removing the domain part
normalized = {}
for user in connected:
for user in connected_lower_case:
if '@' in user:
nuser = user.split('@')[0]
normalized[nuser] = connected[user]
connected.update(normalized)
normalized[nuser] = connected_lower_case[user]
connected_lower_case.update(normalized)
for user in db_users:
# exclude users not enabled for OpenVPN
if "openvpn_enabled" not in user:
continue
if user['name'] in connected:
if user['name'].lower() in connected_lower_case:
user["connected"] = True
user = user | connected[user['name']]
user = user | connected_lower_case[user['name'].lower()]
else:
user["connected"] = False
user["expiration"] = ""
Expand Down Expand Up @@ -1041,6 +1079,24 @@ def connection_history(ovpninstance):

return connections

def renew_server_certificate(ovpninstance):
try:
subprocess.run(["/usr/sbin/ns-openvpn-renew-server", ovpninstance], check=True, capture_output=True)
subprocess.run(["/etc/init.d/openvpn", "restart", ovpninstance], check=False, capture_output=True)
except Exception as e:
print(e, file=sys.stderr)
return utils.validation_error("instance", "server_certificate_renewal_failed", ovpninstance)
return {"result": "success"}

def regenerate_all_certificates(ovpninstance):
try:
subprocess.run(["/usr/sbin/ns-openvpn-renew-ca", ovpninstance], check=True, capture_output=True)
subprocess.run(["/etc/init.d/openvpn", "restart", ovpninstance], check=False, capture_output=True)
except Exception as e:
print(e, file=sys.stderr)
return utils.validation_error("instance", "certificates_regeneration_failed", ovpninstance)
return {"result": "success"}

cmd = sys.argv[1]

if cmd == 'list':
Expand Down Expand Up @@ -1088,7 +1144,9 @@ if cmd == 'list':
"download-user-2fa": {"instance": "roadwarrior1", "username": "myuser"},
"download_all_user_configurations": {"instance": "roadwarrior1"},
"connection-history-csv": {"instance": "roadwarrior1", "timezone": "Europe/Rome"},
"connection-history": {"instance": "roadwarrior1"}
"connection-history": {"instance": "roadwarrior1"},
"renew-server-certificate": {"instance": "roadwarrior1"},
"regenerate-all-certificates": {"instance": "roadwarrior1"}
}))
else:
action = sys.argv[2]
Expand Down Expand Up @@ -1145,5 +1203,9 @@ else:
ret = connection_history_csv(args['instance'], args['timezone'])
elif action == "connection-history":
ret = connection_history(args['instance'])
elif action == "renew-server-certificate":
ret = renew_server_certificate(args["instance"])
elif action == "regenerate-all-certificates":
ret = regenerate_all_certificates(args["instance"])

print(json.dumps(ret))
9 changes: 7 additions & 2 deletions packages/ns-openvpn/files/ns-openvpn-renew-ca
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@
# SPDX-License-Identifier: GPL-2.0-only
#

# Setup EasyRSA pki
# Renew CA and all certificates for the specified OpenVPN instance
instance=$1
if [ -z $instance ]; then
exit 1
fi

cn=$(uci get system.@system[0].hostname | cut -d '.' -f 1)
if [ -z "$cn" ]; then
cn=NethSec
fi

# Set environment variables for EasyRSA
export EASYRSA_BATCH=1
export EASYRSA_CERT_EXPIRE=3650
Expand All @@ -21,7 +26,7 @@ if [ -f /etc/openvpn/$instance/pki/ca.crt ]; then
(
/usr/bin/easyrsa renew-ca
/usr/bin/easyrsa revoke-issued server
/usr/bin/easyrsa build-server-full server nopass
EASYRSA_REQ_CN=$cn /usr/bin/easyrsa build-server-full server nopass
/usr/bin/easyrsa gen-crl
for f in $(find /etc/openvpn/$instance/pki/issued -name \*.crt ! -name server.crt); do
name=$(basename $f | cut -d '.' -f 1)
Expand Down
32 changes: 32 additions & 0 deletions packages/ns-openvpn/files/ns-openvpn-renew-server
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/bin/sh

#
# Copyright (C) 2026 Nethesis S.r.l.
# SPDX-License-Identifier: GPL-2.0-only
#

# Renew server certificate for the specified OpenVPN instance
instance=$1
if [ -z $instance ]; then
exit 1
fi

cn=$(uci get system.@system[0].hostname | cut -d '.' -f 1)
if [ -z "$cn" ]; then
cn=NethSec
fi

# Set environment variables for EasyRSA
export EASYRSA_BATCH=1
export EASYRSA_CERT_EXPIRE=3650
export EASYRSA_CRL_DAYS=3650
export EASYRSA_REQ_CN=$cn

if [ -f /etc/openvpn/$instance/pki/ca.crt ]; then
cd /etc/openvpn/$instance
(
/usr/bin/easyrsa revoke server
/usr/bin/easyrsa gen-crl
/usr/bin/easyrsa build-server-full server nopass
)
fi
Loading