Files
shopdb-flask/plugins/endpoints/models/bootstrap.py
cproudlock ca57b7f728
Some checks failed
CI / backend (push) Has been cancelled
CI / naming (push) Has been cancelled
CI / gates (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / lean-build (push) Has been cancelled
Install an agent on a one-off PC from a single command
An admin generates a short-lived token in Settings > Endpoints > Endpoints and
copies the command it produces. A tech runs that one line in an elevated
PowerShell on a PC with no agent:

    powershell -ExecutionPolicy Bypass -c "irm https://<server>/shopdb/api/
    endpoints/siteagent/bootstrap.ps1?t=<token> | iex"

The script refuses to run unelevated, refuses a PC that already has the agent
(a managed bay gets it from GE-Enforce), downloads the package the existing
builder produces, checks both binaries against the hashes the server baked in,
installs with NO shared key, and reports what happened. The PC then asks to
enroll and waits for an admin to approve it, which the script watches for
locally for two minutes before exiting.

- New endpointbootstraptokens table and migration
  endpoints0024bootstraptokens: hash only, expiry, use count, revoke.
- Admin routes to generate, list and revoke tokens; the secret is shown once.
- Two token-only routes: the script, and the package (which spends a use).
  Anything wrong with the token answers 404 rather than confirming it exists,
  both are rate limited per address and both are audited with the caller.
- The package builder is now shared rather than duplicated, and a bootstrap
  package can never carry a collector key.
2026-09-15 19:33:00 -04:00

79 lines
2.9 KiB
Python

"""One-line bootstrap install tokens.
An admin mints a short-lived token in the UI and hands a tech one command. The
token is only ever an authorization to FETCH the bootstrap script and the
keyless agent package: it carries no fleet key, provisions no credential, and
the PC it installs still has to be approved in Settings > Endpoints >
Enrollment before it can report anything.
Only the sha256 of the secret is stored, plus a short display prefix, the same
shape as EndpointCredential. A token is spent per package download (usecount
against maxuses) and dies at expiresat or when an admin revokes it.
"""
from datetime import datetime
from shopdb.api import db
def _utciso(value):
return value.isoformat() + 'Z' if value else None
class EndpointBootstrapToken(db.Model):
"""A short-lived authorization to fetch the bootstrap installer."""
__tablename__ = 'endpointbootstraptokens'
tokenid = db.Column(db.Integer, primary_key=True)
tokenhash = db.Column(db.CHAR(64), nullable=False, unique=True)
tokenprefix = db.Column(db.String(12), nullable=False)
createdat = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
createdby = db.Column(db.String(64), nullable=True)
expiresat = db.Column(db.DateTime, nullable=False)
maxuses = db.Column(db.Integer, nullable=False, default=1)
usecount = db.Column(db.Integer, nullable=False, default=0,
server_default='0')
revokedat = db.Column(db.DateTime, nullable=True)
lastusedat = db.Column(db.DateTime, nullable=True)
lastusedip = db.Column(db.String(45), nullable=True)
def isusable(self, now):
"""Whether this token may still serve a download at `now`."""
if self.revokedat is not None:
return False
if self.expiresat is None or self.expiresat <= now:
return False
return (self.usecount or 0) < (self.maxuses or 0)
def state(self, now):
if self.revokedat is not None:
return 'revoked'
if self.expiresat is None or self.expiresat <= now:
return 'expired'
if (self.usecount or 0) >= (self.maxuses or 0):
return 'exhausted'
return 'active'
def to_dict(self, now=None):
now = now or datetime.utcnow()
used = self.usecount or 0
maximum = self.maxuses or 0
return {
'tokenid': self.tokenid,
'tokenprefix': self.tokenprefix,
'createdat': _utciso(self.createdat),
'createdby': self.createdby,
'expiresat': _utciso(self.expiresat),
'maxuses': maximum,
'usecount': used,
'remaining': max(maximum - used, 0),
'revokedat': _utciso(self.revokedat),
'lastusedat': _utciso(self.lastusedat),
'lastusedip': self.lastusedip,
'state': self.state(now),
}
def __repr__(self):
return f'<EndpointBootstrapToken {self.tokenprefix}>'