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.
79 lines
2.9 KiB
Python
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}>'
|