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.
This commit is contained in:
@@ -29,6 +29,7 @@ lowercase hex SHA-256 of the leaf SubjectPublicKeyInfo), optionally with a
|
||||
second "next" pin for a planned certificate rotation.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
@@ -873,6 +874,109 @@ else {{
|
||||
)
|
||||
|
||||
|
||||
def build_package_zip(baseurl, collectorkey, cadenceseconds, appsettings,
|
||||
certfilename=None, cert_pem=None, certpath=None,
|
||||
serverpins=None, includesharedkey=True,
|
||||
trustcertinroot=True):
|
||||
"""Assemble the agent package ZIP in memory.
|
||||
|
||||
The single place the archive is laid out, so the admin download route and
|
||||
the token-authenticated bootstrap download cannot drift apart in what they
|
||||
ship. Callers own validation, pin resolution and the appsettings they pass.
|
||||
"""
|
||||
buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as archive:
|
||||
archive.writestr('appsettings.json',
|
||||
json.dumps(appsettings, indent=2) + '\n')
|
||||
archive.writestr('install.ps1',
|
||||
_install_script(baseurl.rstrip('/'), collectorkey,
|
||||
cadenceseconds, certfilename,
|
||||
serverpins=serverpins,
|
||||
includesharedkey=includesharedkey,
|
||||
trustcertinroot=trustcertinroot))
|
||||
archive.writestr('README.txt', _readme(includesharedkey))
|
||||
|
||||
if os.path.isfile(UNINSTALLER_PATH):
|
||||
archive.write(UNINSTALLER_PATH, 'uninstall-shopdbagent.ps1')
|
||||
|
||||
agent = agentstore.current_path('agent')
|
||||
if agent and os.path.isfile(agent):
|
||||
archive.write(agent, 'shopdbagent.exe')
|
||||
tray = agentstore.current_path('tray')
|
||||
if tray and os.path.isfile(tray):
|
||||
archive.write(tray, 'shopdb-endpoint-tray.exe')
|
||||
if cert_pem:
|
||||
archive.writestr('server.crt', cert_pem)
|
||||
elif certpath:
|
||||
archive.write(certpath, 'server.crt')
|
||||
|
||||
buffer.seek(0)
|
||||
return buffer
|
||||
|
||||
|
||||
def package_download_name():
|
||||
"""Versioned download filename for the package, or the plain one."""
|
||||
agentversion = agentstore.current_version('agent')
|
||||
return ('shopdb-endpoint-agent-{0}.zip'.format(agentversion)
|
||||
if agentversion else 'shopdb-endpoint-agent.zip')
|
||||
|
||||
|
||||
# Filenames the package ships the staged binaries under, keyed by store binkey.
|
||||
STAGED_EXE_NAMES = {
|
||||
'agent': 'shopdbagent.exe',
|
||||
'tray': 'shopdb-endpoint-tray.exe',
|
||||
}
|
||||
|
||||
|
||||
def staged_binary_hashes():
|
||||
"""{binkey: lowercase sha256} for every staged binary, '' when none.
|
||||
|
||||
The bootstrap script bakes these in and re-checks the downloaded ZIP
|
||||
against them on the endpoint, so a package swapped in transit is refused
|
||||
before anything is executed. Hashed from the file rather than read out of
|
||||
the store manifest so a legacy flat-file staging is covered too.
|
||||
"""
|
||||
hashes = {}
|
||||
for binkey in STAGED_EXE_NAMES:
|
||||
path = agentstore.current_path(binkey)
|
||||
digest = ''
|
||||
if path and os.path.isfile(path):
|
||||
hasher = hashlib.sha256()
|
||||
with open(path, 'rb') as handle:
|
||||
for chunk in iter(lambda: handle.read(65536), b''):
|
||||
hasher.update(chunk)
|
||||
digest = hasher.hexdigest()
|
||||
hashes[binkey] = digest
|
||||
return hashes
|
||||
|
||||
|
||||
def build_keyless_package(baseurl, cadenceseconds=900):
|
||||
"""The package the bootstrap installer downloads: never a collector key.
|
||||
|
||||
Pins this server exactly as the admin builder does with no upload - the
|
||||
server's own leaf certificate - and bundles that certificate when the app
|
||||
can read it, which is the self-signed topology where the endpoint would
|
||||
otherwise fail its own TLS validation.
|
||||
"""
|
||||
appsettings = dict(_BASE_APPSETTINGS)
|
||||
appsettings['cadenceseconds'] = cadenceseconds
|
||||
appsettings['apikey'] = ''
|
||||
|
||||
certpath = _server_cert_path()
|
||||
certfilename = 'server.crt' if certpath else None
|
||||
currentpin, pinisca = _current_server_facts()
|
||||
serverpins = [pin for pin in (currentpin,) if pin]
|
||||
appsettings['serverpins'] = serverpins
|
||||
|
||||
buffer = build_package_zip(
|
||||
baseurl, '', cadenceseconds, appsettings,
|
||||
certfilename=certfilename, certpath=certpath,
|
||||
serverpins=serverpins, includesharedkey=False,
|
||||
trustcertinroot=not pinisca)
|
||||
warning = CA_CERT_BUILD_WARNING if (currentpin and pinisca) else None
|
||||
return buffer, warning
|
||||
|
||||
|
||||
def _siteagent_status_payload():
|
||||
"""Whether a prebuilt agent binary is staged for packaging.
|
||||
|
||||
@@ -1035,41 +1139,18 @@ def package_siteagent():
|
||||
serverpins = list(dict.fromkeys(serverpins))
|
||||
appsettings['serverpins'] = serverpins
|
||||
|
||||
buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as archive:
|
||||
archive.writestr('appsettings.json',
|
||||
json.dumps(appsettings, indent=2) + '\n')
|
||||
archive.writestr('install.ps1',
|
||||
_install_script(baseurl.rstrip('/'), collectorkey,
|
||||
cadenceseconds, certfilename,
|
||||
serverpins=serverpins,
|
||||
includesharedkey=includesharedkey,
|
||||
trustcertinroot=trustcertinroot))
|
||||
archive.writestr('README.txt', _readme(includesharedkey))
|
||||
buffer = build_package_zip(
|
||||
baseurl, collectorkey, cadenceseconds, appsettings,
|
||||
certfilename=certfilename, cert_pem=cert_pem, certpath=certpath,
|
||||
serverpins=serverpins, includesharedkey=includesharedkey,
|
||||
trustcertinroot=trustcertinroot)
|
||||
|
||||
if os.path.isfile(UNINSTALLER_PATH):
|
||||
archive.write(UNINSTALLER_PATH, 'uninstall-shopdbagent.ps1')
|
||||
|
||||
agent = agentstore.current_path('agent')
|
||||
if agent and os.path.isfile(agent):
|
||||
archive.write(agent, 'shopdbagent.exe')
|
||||
tray = agentstore.current_path('tray')
|
||||
if tray and os.path.isfile(tray):
|
||||
archive.write(tray, 'shopdb-endpoint-tray.exe')
|
||||
if cert_pem:
|
||||
archive.writestr('server.crt', cert_pem)
|
||||
elif certpath:
|
||||
archive.write(certpath, 'server.crt')
|
||||
|
||||
buffer.seek(0)
|
||||
# Stamp the staged agent version into the download filename so a DT operator
|
||||
# can see which agent build a package carries at a glance. This is cosmetic:
|
||||
# install.ps1 finds the exe by its fixed name inside the ZIP, so the download
|
||||
# name never affects the install. Falls back to the unversioned name when no
|
||||
# managed version is current (only a legacy flat file is staged).
|
||||
agentversion = agentstore.current_version('agent')
|
||||
download_name = ('shopdb-endpoint-agent-{0}.zip'.format(agentversion)
|
||||
if agentversion else 'shopdb-endpoint-agent.zip')
|
||||
download_name = package_download_name()
|
||||
response = send_file(
|
||||
buffer,
|
||||
mimetype='application/zip',
|
||||
|
||||
Reference in New Issue
Block a user