feature: users and their certificates no longer need a P2S system enabled.
Release / release (push) Successful in 21s

This commit is contained in:
slawek committed 2026-10-24 00:00:44 +02:00
1 parent 0ff74cc8c9
commit 0bf78b16b1
7 files changed
+67 -40

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` 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 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. 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 `<label>` is the first component of the router FQDN, so `router.example.com` gives
`router_cert.pem`. `router_cert.pem`.
If road-warrior or OpenVPN access is enabled and this directory is empty, a certificate Once a platform is chosen and while this directory is empty, a certificate authority and a CRL
authority, a server certificate and a CRL valid for ten years are created there valid for ten years are created there automatically, whether or not road-warrior or OpenVPN
automatically; a CA without a CRL gets one. User certificates are issued by access is enabled. A CA of the router's own (with `ca_key.pem`) without a CRL gets one, and
`vpn-router-user` into `users/`. To use your own instead, place the without a server certificate gets one once `wan.local_fqdn` is set. User certificates are issued
files in that layout before enabling the feature; nothing is generated or overwritten when by `vpn-router-user` into `users/`. To use your own CA instead, place the files in that layout
material is already present. before choosing the platform; nothing present is overwritten.
Either way, the package copies what each consumer needs into place: the CA into 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 `/etc/swanctl/x509ca/`, the server certificate and key into `/etc/swanctl/x509/` and
@@ -314,8 +314,9 @@ certificate names:
### The user directory ### The user directory
While OpenVPN or road-warrior access is configured, `vpn-router-setup` keeps a directory of Once a platform is chosen, `vpn-router-setup` keeps a directory of users in the local slapd,
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 - 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 slapd package creates; root manages it through the local socket, with no administrator
@@ -328,7 +329,7 @@ users in the local slapd:
(`pwdAccountLockedTime`); (`pwdAccountLockedTime`);
- slapd listens on its local socket only (`SLAPD_SERVICES` in `/etc/default/slapd`). - 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 ### 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 | | `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 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 exist, 409 for a user that exists and 500 otherwise. `/users` works whether or not OpenVPN or
access is configured, `/users` answers 404. Passwords and private keys cross the internal road-warrior access is enabled; the profile needs OpenVPN. Passwords and private keys cross the internal
network in plain HTTP, like the token. network in plain HTTP, like the token.
```bash ```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 vpn-router (0.5.4-1) unstable; urgency=medium
feature: the firewall lets the Router Manager in on the internal interface. 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) run(['systemctl', 'restart', unit], check=False)
def manage_directory(ctx): def manage_directory():
"""Provision the user directory in slapd while a P2S system that has users is configured. """Provision the user directory in slapd, whether or not a P2S system 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
if vpnrouter_ldap.ensure_services() or not unit_is_active('slapd'): if vpnrouter_ldap.ensure_services() or not unit_is_active('slapd'):
run(['systemctl', 'restart', 'slapd']) run(['systemctl', 'restart', 'slapd'])
try: try:
@@ -422,7 +417,6 @@ def refresh_web(cfg, ctx):
'p2s': bool(vpnrouter.p2s_ready(ctx)), 'p2s': bool(vpnrouter.p2s_ready(ctx)),
'wireguard': bool(vpnrouter.wg_ready(ctx)), 'wireguard': bool(vpnrouter.wg_ready(ctx)),
'openvpn': bool(vpnrouter.openvpn_ready(ctx)), 'openvpn': bool(vpnrouter.openvpn_ready(ctx)),
'users': bool(vpnrouter.p2s_ready(ctx) or vpnrouter.openvpn_ready(ctx)),
'configuration': public, 'configuration': public,
'network': { 'network': {
'external_interface': ctx['wan_iface'], 'external_interface': ctx['wan_iface'],
@@ -469,7 +463,7 @@ def main():
elif rules_changed: elif rules_changed:
run(['ufw', 'reload'], check=False) run(['ufw', 'reload'], check=False)
manage_directory(ctx) manage_directory()
manage_strongswan(ctx, changed) manage_strongswan(ctx, changed)
manage_wireguard(ctx, changed) manage_wireguard(ctx, changed)
manage_openvpn(ctx, changed) manage_openvpn(ctx, changed)
@@ -565,24 +565,28 @@ def _simple_ca(*args):
def provision_pki(ctx): 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) PKI_DIR.mkdir(parents=True, exist_ok=True)
os.chmod(PKI_DIR, 0o700) 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(): if not SIMPLE_CA.is_file():
fail(f'{SIMPLE_CA} is missing') fail(f'{SIMPLE_CA} is missing')
_simple_ca('make-ca', '--ca-dir', str(PKI_DIR), ctx['p2s_ca_name']) if not any(PKI_DIR.iterdir()):
_simple_ca('make-cert', '--ca-dir', str(PKI_DIR), if not ctx['p2s_ca_name']:
ctx['local_fqdn'], ctx['local_fqdn']) warn('p2s.ca_name is empty, not creating a local CA')
_simple_ca('make-crl', '--ca-dir', str(PKI_DIR), '--days', str(CRL_DAYS)) 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): def distribute_pki(ctx):
@@ -628,8 +632,7 @@ def configure(cfg=None, local_id_override=None):
changed = set() changed = set()
if p2s_ready(ctx) or openvpn_ready(ctx): provision_pki(ctx)
provision_pki(ctx)
if distribute_pki(ctx): if distribute_pki(ctx):
changed.add('swanctl') changed.add('swanctl')
+1 -3
View File
@@ -10,7 +10,7 @@ Pages:
/status the status as JSON /status the status as JSON
/config the configuration without its secrets, and what the router resolved and /config the configuration without its secrets, and what the router resolved and
runs, as JSON 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()) remove, and the OpenVPN client profile (see users())
/config and /users need an HS256 JWT in an Authorization: Bearer header that is signed /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 DELETE /users/<name> remove the user and revoke the certificate
GET /users/<name>/profile the OpenVPN client profile 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)): if body == 'invalid' or (body is not None and not isinstance(body, dict)):
return 400, {'error': 'the body must be a JSON object'} return 400, {'error': 'the body must be a JSON object'}
body = body or {} body = body or {}
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/python3 #!/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 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 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): def add(connection, name):
secret = password() secret = password()
if not (vpnrouter.PKI_DIR / 'ca_key.pem').is_file(): 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: try:
connection.add_s(dn_of(name), ldap.modlist.addModlist({ connection.add_s(dn_of(name), ldap.modlist.addModlist({
'objectClass': [b'inetOrgPerson'], 'uid': [name.encode()], 'cn': [name.encode()], 'sn': [name.encode()], 'objectClass': [b'inetOrgPerson'], 'uid': [name.encode()], 'cn': [name.encode()], 'sn': [name.encode()],