fix: internal service accounts are the router's own, of which the metrics service's is the one at present, and the documentation says what a service account is.
Release / release (push) Successful in 13s
Release / release (push) Successful in 13s
This commit is contained in:
1 parent
f01be41191
commit
68ebd541b2
5 files changed
+60
-40
No files matched your search
@@ -558,7 +558,7 @@ purge. A view shows only what its level keeps, so a shorter retention empties th
|
|||||||
view. A month view needs the hourly rows and a year view the monthly rows. `/metrics` has only the
|
view. A month view needs the hourly rows and a year view the monthly rows. `/metrics` has only the
|
||||||
last sample; history is not scraped.
|
last sample; history is not scraped.
|
||||||
|
|
||||||
## ADR-23: /metrics takes HTTP Basic authentication of an internal service account that setup creates
|
## ADR-23: /metrics takes HTTP Basic authentication of an internal service account the router manages
|
||||||
|
|
||||||
Status: Accepted
|
Status: Accepted
|
||||||
|
|
||||||
@@ -567,20 +567,21 @@ the router's tokens, which last minutes (ADR-3). The scraper should not be able
|
|||||||
API or the Manager, and its password should be checked and locked out like any other. The account
|
API or the Manager, and its password should be checked and locked out like any other. The account
|
||||||
belongs to the package's metrics, not to the people using the router.
|
belongs to the package's metrics, not to the people using the router.
|
||||||
|
|
||||||
Decision: A service account is a Manager-only account (ADR-18) with `vpnRouterScope` `service`.
|
Decision: A service account is a Manager-only account (ADR-18) with `vpnRouterScope` `service`,
|
||||||
Administrators add service accounts with `vpn-router-user add --service` and the web service's
|
which another system signs in with to use a router service. Administrators add service accounts
|
||||||
`/service-accounts`, for services of their own that check passwords against the directory.
|
with `vpn-router-user add --service` and the web service's `/service-accounts`.
|
||||||
`/service-accounts` is a path of its own, apart from `/users`, which does not list or serve them:
|
`/service-accounts` is a path of its own, apart from `/users`, which does not list or serve them:
|
||||||
a service account has no names, certificate, profile or TOTP, and is not a person an
|
a service account has no names, certificate, profile or TOTP, and is not a person an
|
||||||
administrator manages with the users. The metrics service's is the internal one, created by
|
administrator manages with the users. Internal service accounts are those the router creates and
|
||||||
`vpn-router-setup`: while metrics are enabled it adds the account `metrics.service_account` names,
|
manages for its own services (`vpnrouter_users.internal_accounts`), so the system, not an
|
||||||
`metrics` by default, with a random password it writes to `/etc/vpn-router/metrics.secret` (root
|
administrator, owns them; at present that is the metrics service's. `/service-accounts` marks
|
||||||
only), and sets a new random password, ending a lockout, when the file is missing or no longer
|
them as `internal` and only lists them: every other request on one, and putting one in or taking
|
||||||
holds the password. `/service-accounts` marks it as `internal` and only lists it: every other
|
one out of a group at `/groups`, gets 403, whatever the token. The router changes them through the
|
||||||
request on it, and putting it in or taking it out of a group at `/groups`, gets 403, whatever the
|
directory's user-manager account, not the API, and root may still with `vpn-router-user`. For the
|
||||||
token. Setup
|
metrics service, `vpn-router-setup` adds the account `metrics.service_account` names, `metrics` by
|
||||||
changes it through the directory's user-manager account, not the API, and root may still with
|
default, while metrics are enabled, with a random password it writes to
|
||||||
`vpn-router-user`. `/auth` refuses every service account with the message of a wrong password. `/metrics` accepts HTTP Basic only for that account, and
|
`/etc/vpn-router/metrics.secret` (root only), and sets a new random password, ending a lockout,
|
||||||
|
when the file is missing or no longer holds the password. `/auth` refuses every service account with the message of a wrong password. `/metrics` accepts HTTP Basic only for that account, and
|
||||||
only when it is a service account; the password is checked with a bind as the account, so the
|
only when it is a service account; the password is checked with a bind as the account, so the
|
||||||
password policy's lockout applies (ADR-14). TLS is Caddy's, on `metrics.https_port` (ADR-21);
|
password policy's lockout applies (ADR-14). TLS is Caddy's, on `metrics.https_port` (ADR-21);
|
||||||
plain HTTP is served on the interfaces `metrics.listen` names, `internal` by default.
|
plain HTTP is served on the interfaces `metrics.listen` names, `internal` by default.
|
||||||
|
|||||||
@@ -427,12 +427,14 @@ it is added:
|
|||||||
name, the password and an optional e-mail address, for notifications: no names and no
|
name, the password and an optional e-mail address, for notifications: no names and no
|
||||||
certificate, so no VPN access. It can sign in to `/auth` and, as a member of `admins`, manage
|
certificate, so no VPN access. It can sign in to `/auth` and, as a member of `admins`, manage
|
||||||
the router. `add` without the first and the last name adds one; its names cannot be set later;
|
the router. `add` without the first and the last name adds one; its names cannot be set later;
|
||||||
- a service account, a Manager-only account whose `vpnRouterScope` is `service`, for a service
|
- a service account, a Manager-only account whose `vpnRouterScope` is `service`, which another
|
||||||
that checks passwords against the directory; it cannot sign in to `/auth`. `add --service` adds
|
system signs in with to use a router service, such as an OpenTelemetry Collector reading
|
||||||
one, and the web service manages them at `/service-accounts`, not at `/users`. The internal one
|
`/metrics`; it cannot sign in to `/auth`. `add --service` adds one, and the web service manages
|
||||||
is the account `metrics.service_account` names while metrics are enabled: `vpn-router-setup` adds
|
them at `/service-accounts`, not at `/users`. Internal service accounts are those the router
|
||||||
it and keeps its password (see Metrics). The API only lists it: `/service-accounts` refuses every
|
creates and manages for its own services; at present that is the metrics service's, the account
|
||||||
other request on it, and `/groups` refuses to put it in or take it out of a group, with 403.
|
`metrics.service_account` names while metrics are enabled (see Metrics). The API only lists
|
||||||
|
them: `/service-accounts` refuses every other request on one, and `/groups` refuses to put one
|
||||||
|
in or take one out of a group, with 403.
|
||||||
|
|
||||||
Passwords and TOTP secrets are read from the first line of standard input. The answer is text: a
|
Passwords and TOTP secrets are read from the first line of standard input. The answer is text: a
|
||||||
table of the users, the client profile, a TOTP secret or a short message, and a failure is a
|
table of the users, the client profile, a TOTP secret or a short message, and a failure is a
|
||||||
@@ -759,8 +761,8 @@ curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
|||||||
| `POST /service-accounts/<name>/unlock` | | the account |
|
| `POST /service-accounts/<name>/unlock` | | the account |
|
||||||
| `DELETE /service-accounts/<name>` | | `{"name", "removed": true}` |
|
| `DELETE /service-accounts/<name>` | | `{"name", "removed": true}` |
|
||||||
|
|
||||||
The internal account, `internal` true, is only listed: every other request on it gets 403, as
|
The internal service accounts, `internal` true, are only listed: every other request on one gets
|
||||||
`vpn-router-setup` adds and keeps it. A failure is otherwise as for `/users`, with 404 also for a
|
403, as the router manages them. A failure is otherwise as for `/users`, with 404 also for a
|
||||||
name that is not a service account.
|
name that is not a service account.
|
||||||
|
|
||||||
### The /account API
|
### The /account API
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
vpn-router (0.18.3-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
fix: internal service accounts are the router's own, of which the metrics service's is the one at present, and the documentation says what a service account is.
|
||||||
|
|
||||||
|
* /service-accounts, /groups and the internal field of the answers
|
||||||
|
take every internal service account, the accounts the router creates
|
||||||
|
and manages for its own services, instead of only the metrics
|
||||||
|
service's; the 403 answers say that the router manages them.
|
||||||
|
* The README, the ADR and the users module describe a service account
|
||||||
|
as the account another system, such as an OpenTelemetry Collector
|
||||||
|
reading /metrics, signs in with, and the internal ones as managed by
|
||||||
|
the router.
|
||||||
|
|
||||||
|
-- Slawomir Koszewski <slawek@koszewscy.waw.pl> Mon, 26 Oct 2026 19:00:00 +0100
|
||||||
|
|
||||||
vpn-router (0.18.2-1) unstable; urgency=medium
|
vpn-router (0.18.2-1) unstable; urgency=medium
|
||||||
|
|
||||||
fix: service accounts are managed at /service-accounts, not /users, and the internal one of the metrics service is only listed.
|
fix: service accounts are managed at /service-accounts, not /users, and the internal one of the metrics service is only listed.
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ common name, which OpenVPN and IKEv2 accept. A Manager-only account is an accoun
|
|||||||
simpleSecurityObject and vpnRouterAccount (the package's schema) with a name, a password and
|
simpleSecurityObject and vpnRouterAccount (the package's schema) with a name, a password and
|
||||||
optionally an e-mail address, no names, no certificate and so no VPN access; it is added when
|
optionally an e-mail address, no names, no certificate and so no VPN access; it is added when
|
||||||
neither the first nor the last name is given, and can sign in to the web service. A service
|
neither the first nor the last name is given, and can sign in to the web service. A service
|
||||||
account is a Manager-only account with the scope SERVICE_SCOPE in vpnRouterScope, which cannot
|
account is a Manager-only account with the scope SERVICE_SCOPE in vpnRouterScope, which another
|
||||||
sign in to the web service; an administrator adds one for a service that checks passwords
|
system signs in with to use a router service, and which cannot sign in to the web service. The
|
||||||
against the directory. The internal one is the account metrics.service_account names while
|
internal service accounts are those the router creates and manages for its own services
|
||||||
metrics are enabled, which vpn-router-setup adds and whose password it keeps (metrics_account).
|
(internal_accounts); vpn-router-setup keeps the metrics service's.
|
||||||
Users exist whether or not a P2S system is enabled.
|
Users exist whether or not a P2S system is enabled.
|
||||||
|
|
||||||
A group is a groupOfNames in ou=groups with a name, which follows the rules of user names, an
|
A group is a groupOfNames in ou=groups with a name, which follows the rules of user names, an
|
||||||
@@ -194,19 +194,20 @@ def service(attributes):
|
|||||||
return SERVICE_SCOPE.encode() in (value.lower() for value in attributes.get('vpnRouterScope', []))
|
return SERVICE_SCOPE.encode() in (value.lower() for value in attributes.get('vpnRouterScope', []))
|
||||||
|
|
||||||
|
|
||||||
def metrics_account():
|
def internal_accounts():
|
||||||
"""The internal service account: the one metrics.service_account names while metrics are enabled, else None."""
|
"""The names of the internal service accounts, which the router creates and manages for its own services: the one
|
||||||
|
metrics.service_account names while metrics are enabled."""
|
||||||
try:
|
try:
|
||||||
metrics = vpnrouter.load_config()['metrics']
|
metrics = vpnrouter.load_config()['metrics']
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
return None
|
return set()
|
||||||
return metrics['service_account'] if metrics['enabled'] else None
|
return {metrics['service_account']} if metrics['enabled'] else set()
|
||||||
|
|
||||||
|
|
||||||
def summary(attributes):
|
def summary(attributes):
|
||||||
"""A user as the answers show it; a person attribute the entry does not have is None.
|
"""A user as the answers show it; a person attribute the entry does not have is None.
|
||||||
|
|
||||||
internal is whether it is the internal service account (metrics_account).
|
internal is whether it is an internal service account (internal_accounts).
|
||||||
"""
|
"""
|
||||||
name = attributes['uid'][0].decode()
|
name = attributes['uid'][0].decode()
|
||||||
lock = attributes.get('pwdAccountLockedTime', [None])[0]
|
lock = attributes.get('pwdAccountLockedTime', [None])[0]
|
||||||
@@ -214,7 +215,7 @@ def summary(attributes):
|
|||||||
'name': name,
|
'name': name,
|
||||||
'manager_only': manager_only(attributes),
|
'manager_only': manager_only(attributes),
|
||||||
'service': service(attributes),
|
'service': service(attributes),
|
||||||
'internal': service(attributes) and name == metrics_account(),
|
'internal': service(attributes) and name in internal_accounts(),
|
||||||
'enabled': lock != LOCKED,
|
'enabled': lock != LOCKED,
|
||||||
'locked': lock not in (None, LOCKED) and locked_since(lock) > time.time() - vpnrouter_ldap.LOCKOUT_SECONDS,
|
'locked': lock not in (None, LOCKED) and locked_since(lock) > time.time() - vpnrouter_ldap.LOCKOUT_SECONDS,
|
||||||
'certificate': certificate(name)[0].is_file(),
|
'certificate': certificate(name)[0].is_file(),
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Pages:
|
|||||||
profile, and TOTP enrolment (see users()); service accounts are not among them
|
profile, and TOTP enrolment (see users()); service accounts are not among them
|
||||||
/service-accounts
|
/service-accounts
|
||||||
the service accounts: list, add, change the e-mail address, set the password,
|
the service accounts: list, add, change the e-mail address, set the password,
|
||||||
disable, enable, unlock and remove; the internal one is only listed (see
|
disable, enable, unlock and remove; the internal ones are only listed (see
|
||||||
service_accounts())
|
service_accounts())
|
||||||
/groups the groups of the router's directory: list, add, rename or describe, remove, and
|
/groups the groups of the router's directory: list, add, rename or describe, remove, and
|
||||||
add and take out members (see groups())
|
add and take out members (see groups())
|
||||||
@@ -516,8 +516,8 @@ def service_accounts(method, path, body):
|
|||||||
POST /service-accounts/<name>/unlock end a lockout after failed logins
|
POST /service-accounts/<name>/unlock end a lockout after failed logins
|
||||||
DELETE /service-accounts/<name> remove it
|
DELETE /service-accounts/<name> remove it
|
||||||
|
|
||||||
A name that is not a service account is 404. The internal one (vpnrouter_users.metrics_account), which
|
A name that is not a service account is 404. The internal service accounts (vpnrouter_users.internal_accounts),
|
||||||
vpn-router-setup adds and keeps, is only listed: every other request on it is refused with 403.
|
which the router manages, are only listed: every other request on one is refused with 403.
|
||||||
"""
|
"""
|
||||||
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'}
|
||||||
@@ -540,8 +540,8 @@ def service_accounts(method, path, body):
|
|||||||
}
|
}
|
||||||
if route not in actions:
|
if route not in actions:
|
||||||
return 404, {'error': f'no route {method} {path}'}
|
return 404, {'error': f'no route {method} {path}'}
|
||||||
if name is not None and name == vpnrouter_users.metrics_account():
|
if name is not None and name in vpnrouter_users.internal_accounts():
|
||||||
return 403, {'error': f'{name} is the internal service account of the metrics service; only vpn-router-setup changes it'}
|
return 403, {'error': f'{name} is an internal service account, which the router manages'}
|
||||||
try:
|
try:
|
||||||
with vpnrouter_users.session() as connection:
|
with vpnrouter_users.session() as connection:
|
||||||
if name is not None and not vpnrouter_users.service(vpnrouter_users.entry(connection, name)):
|
if name is not None and not vpnrouter_users.service(vpnrouter_users.entry(connection, name)):
|
||||||
@@ -564,7 +564,7 @@ def groups(state, method, path, body):
|
|||||||
POST /groups/<name>/members add a member: {"user": ...}
|
POST /groups/<name>/members add a member: {"user": ...}
|
||||||
DELETE /groups/<name>/members/<user> take a member out; the last member cannot leave
|
DELETE /groups/<name>/members/<user> take a member out; the last member cannot leave
|
||||||
|
|
||||||
The internal service account (vpnrouter_users.metrics_account) is not put in or taken out of a group: such a
|
An internal service account (vpnrouter_users.internal_accounts) is not put in or taken out of a group: such a
|
||||||
request is refused with 403.
|
request is refused with 403.
|
||||||
"""
|
"""
|
||||||
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)):
|
||||||
@@ -573,11 +573,12 @@ def groups(state, method, path, body):
|
|||||||
parts = path.strip('/').split('/')
|
parts = path.strip('/').split('/')
|
||||||
name = parts[1] if len(parts) > 1 else None
|
name = parts[1] if len(parts) > 1 else None
|
||||||
route = (method, len(parts), parts[2] if len(parts) > 2 else None)
|
route = (method, len(parts), parts[2] if len(parts) > 2 else None)
|
||||||
internal = vpnrouter_users.metrics_account()
|
|
||||||
members = body.get('members') if route == ('POST', 1, None) else [body.get('user')] if route == ('POST', 3, 'members') \
|
members = body.get('members') if route == ('POST', 1, None) else [body.get('user')] if route == ('POST', 3, 'members') \
|
||||||
else [parts[3]] if route == ('DELETE', 4, 'members') else []
|
else [parts[3]] if route == ('DELETE', 4, 'members') else []
|
||||||
if internal is not None and isinstance(members, list) and internal in members:
|
internal = sorted(vpnrouter_users.internal_accounts().intersection(
|
||||||
return 403, {'error': f'{internal} is the internal service account of the metrics service; only vpn-router-setup changes it'}
|
member for member in (members if isinstance(members, list) else []) if isinstance(member, str)))
|
||||||
|
if internal:
|
||||||
|
return 403, {'error': f'{", ".join(internal)}: internal service accounts, which the router manages, are not group members'}
|
||||||
actions = {
|
actions = {
|
||||||
('GET', 1, None): lambda connection: vpnrouter_users.list_groups(connection),
|
('GET', 1, None): lambda connection: vpnrouter_users.list_groups(connection),
|
||||||
('POST', 1, None): lambda connection: vpnrouter_users.group_add(
|
('POST', 1, None): lambda connection: vpnrouter_users.group_add(
|
||||||
|
|||||||
Reference in new issue
Block a user