Compare commits

...
1 Commits
Author SHA1 Message Date
slawek 0bf78b16b1 feature: users and their certificates no longer need a P2S system enabled.
Release / release (push) Successful in 21s
2026-10-24 00:00:44 +02:00
7 changed files with 67 additions and 40 deletions

No files matched your search

+17
View File
@@ -215,3 +215,20 @@ reaches the browser. The application checks the shape of every answer of the rou
reaches the manager's port manages the router, so on another system it listens on `127.0.0.1`
unless told otherwise. The router's firewall depends on whether the manager is installed, and a
changed manager port needs `vpn-router-setup` to run again.
## ADR-10: Users and their certificates do not depend on a P2S system being enabled
Status: Accepted
Context: The user directory and the CA were provisioned, and `/users` was served, only while
OpenVPN or road-warrior access was enabled. Users could not be prepared before a P2S system was
switched on, and the Router Manager could not manage them on a router without one.
Decision: Once a platform is chosen, `vpn-router-setup` always provisions the user directory in
slapd and the CA with its CRL; the server certificate follows once `wan.local_fqdn` is set.
`/users` and `vpn-router-user` rely only on the directory and the CA. Only the OpenVPN client
profile needs OpenVPN to be enabled.
Consequences: slapd always holds the directory and the CA always exists on a configured router,
even when no P2S system uses them. An operator who wants a CA of their own must place it before
choosing the platform.
+12 -11
View File
@@ -272,12 +272,12 @@ ca_cert.pem ca_key.pem ca_bundle.pem <label>_cert.pem <label>_key.pem crl.p
`<label>` is the first component of the router FQDN, so `router.example.com` gives
`router_cert.pem`.
If road-warrior or OpenVPN access is enabled and this directory is empty, a certificate
authority, a server certificate and a CRL valid for ten years are created there
automatically; a CA without a CRL gets one. User certificates are issued by
`vpn-router-user` into `users/`. To use your own instead, place the
files in that layout before enabling the feature; nothing is generated or overwritten when
material is already present.
Once a platform is chosen and while this directory is empty, a certificate authority and a CRL
valid for ten years are created there automatically, whether or not road-warrior or OpenVPN
access is enabled. A CA of the router's own (with `ca_key.pem`) without a CRL gets one, and
without a server certificate gets one once `wan.local_fqdn` is set. User certificates are issued
by `vpn-router-user` into `users/`. To use your own CA instead, place the files in that layout
before choosing the platform; nothing present is overwritten.
Either way, the package copies what each consumer needs into place: the CA into
`/etc/swanctl/x509ca/`, the server certificate and key into `/etc/swanctl/x509/` and
@@ -314,8 +314,9 @@ certificate names:
### The user directory
While OpenVPN or road-warrior access is configured, `vpn-router-setup` keeps a directory of
users in the local slapd:
Once a platform is chosen, `vpn-router-setup` keeps a directory of users in the local slapd,
whether or not OpenVPN or road-warrior access is enabled, so users can be managed before either
is switched on:
- its own database, `dc=vpn-router,dc=local` in `/var/lib/ldap/vpn-router`, next to the one the
slapd package creates; root manages it through the local socket, with no administrator
@@ -328,7 +329,7 @@ users in the local slapd:
(`pwdAccountLockedTime`);
- slapd listens on its local socket only (`SLAPD_SERVICES` in `/etc/default/slapd`).
The directory is kept when OpenVPN and road-warrior access are switched off, and on purge.
The directory is kept on purge.
### Managing users
@@ -484,8 +485,8 @@ and answer bodies are JSON; a request body is at most 4096 bytes.
| `GET /users/<name>/profile` | | `{"name", "profile"}`, the OpenVPN client profile |
A failure is `{"error": ...}` with 400 for an invalid request, 404 for a user that does not
exist, 409 for a user that exists and 500 otherwise. While neither OpenVPN nor road-warrior
access is configured, `/users` answers 404. Passwords and private keys cross the internal
exist, 409 for a user that exists and 500 otherwise. `/users` works whether or not OpenVPN or
road-warrior access is enabled; the profile needs OpenVPN. Passwords and private keys cross the internal
network in plain HTTP, like the token.
```bash
+14
View File
@@ -1,3 +1,17 @@
vpn-router (0.5.5-1) unstable; urgency=medium
feature: users and their certificates no longer need a P2S system enabled.
* vpn-router-setup provisions the user directory, the CA and its CRL once
a platform is chosen, whether or not OpenVPN or road-warrior access is
enabled. The server certificate is issued once wan.local_fqdn is set,
also for an existing CA that has none.
* /users and vpn-router-user manage users and issue and revoke their
certificates without a P2S system; only the OpenVPN client profile
needs OpenVPN.
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Sat, 24 Oct 2026 16:00:00 +0200
vpn-router (0.5.4-1) unstable; urgency=medium
feature: the firewall lets the Router Manager in on the internal interface.
+3 -9
View File
@@ -347,13 +347,8 @@ def manage_wireguard(ctx, changed):
run(['systemctl', 'restart', unit], check=False)
def manage_directory(ctx):
"""Provision the user directory in slapd while a P2S system that has users is configured.
The directory is left in place when they are switched off, so the users are kept.
"""
if not (vpnrouter.p2s_ready(ctx) or vpnrouter.openvpn_ready(ctx)):
return
def manage_directory():
"""Provision the user directory in slapd, whether or not a P2S system is configured."""
if vpnrouter_ldap.ensure_services() or not unit_is_active('slapd'):
run(['systemctl', 'restart', 'slapd'])
try:
@@ -422,7 +417,6 @@ def refresh_web(cfg, ctx):
'p2s': bool(vpnrouter.p2s_ready(ctx)),
'wireguard': bool(vpnrouter.wg_ready(ctx)),
'openvpn': bool(vpnrouter.openvpn_ready(ctx)),
'users': bool(vpnrouter.p2s_ready(ctx) or vpnrouter.openvpn_ready(ctx)),
'configuration': public,
'network': {
'external_interface': ctx['wan_iface'],
@@ -469,7 +463,7 @@ def main():
elif rules_changed:
run(['ufw', 'reload'], check=False)
manage_directory(ctx)
manage_directory()
manage_strongswan(ctx, changed)
manage_wireguard(ctx, changed)
manage_openvpn(ctx, changed)
@@ -565,24 +565,28 @@ def _simple_ca(*args):
def provision_pki(ctx):
"""Create the CA, server certificate and CRL when the PKI directory is empty; a CA without a CRL gets one."""
"""Create the CA and CRL when the PKI directory is empty, and the server certificate once wan.local_fqdn is set.
A CA of the router's own (ca_key.pem present) without a CRL gets one, and without a
certificate for wan.local_fqdn gets one too; nothing present is overwritten.
"""
PKI_DIR.mkdir(parents=True, exist_ok=True)
os.chmod(PKI_DIR, 0o700)
if any(PKI_DIR.iterdir()):
if (PKI_DIR / 'ca_key.pem').is_file() and not (PKI_DIR / 'crl.pem').is_file():
_simple_ca('make-crl', '--ca-dir', str(PKI_DIR), '--days', str(CRL_DAYS))
return
if not ctx['p2s_ca_name']:
warn('p2s.ca_name is empty, not creating a local CA')
return
if not SIMPLE_CA.is_file():
fail(f'{SIMPLE_CA} is missing')
_simple_ca('make-ca', '--ca-dir', str(PKI_DIR), ctx['p2s_ca_name'])
_simple_ca('make-cert', '--ca-dir', str(PKI_DIR),
ctx['local_fqdn'], ctx['local_fqdn'])
_simple_ca('make-crl', '--ca-dir', str(PKI_DIR), '--days', str(CRL_DAYS))
if not any(PKI_DIR.iterdir()):
if not ctx['p2s_ca_name']:
warn('p2s.ca_name is empty, not creating a local CA')
return
_simple_ca('make-ca', '--ca-dir', str(PKI_DIR), ctx['p2s_ca_name'])
if not (PKI_DIR / 'ca_key.pem').is_file():
return
if not (PKI_DIR / 'crl.pem').is_file():
_simple_ca('make-crl', '--ca-dir', str(PKI_DIR), '--days', str(CRL_DAYS))
label = ctx['local_fqdn'].split('.', 1)[0] if ctx['local_fqdn'] else ''
if label and not (PKI_DIR / f'{label}_cert.pem').is_file():
_simple_ca('make-cert', '--ca-dir', str(PKI_DIR), ctx['local_fqdn'], ctx['local_fqdn'])
def distribute_pki(ctx):
@@ -628,8 +632,7 @@ def configure(cfg=None, local_id_override=None):
changed = set()
if p2s_ready(ctx) or openvpn_ready(ctx):
provision_pki(ctx)
provision_pki(ctx)
if distribute_pki(ctx):
changed.add('swanctl')
+1 -3
View File
@@ -10,7 +10,7 @@ Pages:
/status the status as JSON
/config the configuration without its secrets, and what the router resolved and
runs, as JSON
/users the users of the P2S systems: list, add, set the password, disable, enable,
/users the users in the router's directory, whether or not a P2S system is enabled: list, add, set the password, disable, enable,
remove, and the OpenVPN client profile (see users())
/config and /users need an HS256 JWT in an Authorization: Bearer header that is signed
@@ -433,8 +433,6 @@ def users(state, method, path, body):
DELETE /users/<name> remove the user and revoke the certificate
GET /users/<name>/profile the OpenVPN client profile
"""
if not state.get('users'):
return 404, {'error': 'no P2S system with users is configured'}
if body == 'invalid' or (body is not None and not isinstance(body, dict)):
return 400, {'error': 'the body must be a JSON object'}
body = body or {}
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/python3
"""Manage the users of the router's P2S systems.
"""Manage the users of the router's directory, whether or not a P2S system is enabled.
A user is an entry in the router's directory, whose password OpenVPN checks, and a client
certificate of the router CA with the user name as its common name, which OpenVPN and
@@ -103,7 +103,7 @@ def list_users(connection):
def add(connection, name):
secret = password()
if not (vpnrouter.PKI_DIR / 'ca_key.pem').is_file():
raise Failure('the router CA does not exist; enable OpenVPN or IKEv2 road-warrior access first')
raise Failure('the router CA does not exist; vpn-router-setup creates it once a platform is chosen')
try:
connection.add_s(dn_of(name), ldap.modlist.addModlist({
'objectClass': [b'inetOrgPerson'], 'uid': [name.encode()], 'cn': [name.encode()], 'sn': [name.encode()],