feature: users and their certificates no longer need a P2S system enabled.
Release / release (push) Successful in 21s
Release / release (push) Successful in 21s
This commit is contained in:
1 parent
0ff74cc8c9
commit
0bf78b16b1
7 files changed
+67
-40
No files matched your search
@@ -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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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()],
|
||||||
|
|||||||
Reference in new issue
Block a user