Repository navigation
Conversation
The app shipped no HTTP middleware at all, so nothing set framing, sniffing or referrer policy. Without frame-ancestors the dashboard can be loaded in a hostile iframe, which is what turns a single stolen click into a power-off on real hardware. HSTS is emitted only when the request actually arrived over TLS. Sent over plain http a browser ignores it, but honouring it on a LAN hostname would strand the operator with no way back to http. The policy is scoped to frame-ancestors deliberately: a script-src policy would have to cover the bundled module script and the stylesheet, and getting that wrong breaks the UI for a finding that is about framing.
Several endpoints take no request body (SEL refresh and clear, FRU refresh, retention cleanup, connection test), so a browser can submit them cross-origin as a simple request: no preflight, session cookie attached. Nothing distinguished those from a click in the app's own UI. Three changes close it: - An Origin/Referer check on POST/PUT/PATCH/DELETE, comparing host AND port against the request's own Host header. The port matters: same-site cookie rules treat every port on a machine as one site, so another service on a different port of the same host was a valid attack position. - The restore endpoint now requires an archive content type. It reads the raw body, so the content type is the only statement of intent available, and a cross-origin form POST can never produce one. - The session cookie moves to SameSite=Strict, since Lax also ignores the port. A request carrying neither header is allowed: curl, the container health check and server-to-server callers legitimately send neither, and refusing them would break every headless integration to stop an attack only a browser can mount.
Only 127.0.0.1 is trusted to set X-Forwarded-Proto by default. A TLS proxy running in a separate container reaches the app from a bridge address, so its forwarded scheme is discarded, the request still looks like plain http, and the session cookie is issued without the Secure flag on what the user sees as an https site. server.forwarded_allow_ips lets the operator name the proxy. It is threaded into both places the server is started and left unset by default, so nothing changes for a deployment that does not use one. In the container the same setting is available as an environment variable. Startup also warns when the app binds to a non-loopback address without TLS. Refusing to start would be wrong — a trusted LAN is a legitimate deployment — but the operator should be told that cookies and BMC passwords are in the clear rather than having to work it out.
The interactive OpenAPI pages published a complete map of every route to anyone who could reach the port, and the health endpoint — unauthenticated so an orchestrator can poll it — returned the build version, the module count and the number of live connections. Together that tells a scanner which release to look up exploits for and whether an operator is currently watching. The docs pages are now off by default and turned back on for a demo instance or a debug-level run: in a demo the API is the thing being shown, and at debug level the operator explicitly asked for detail. They are re-enabled before the SPA catch-all is registered, since the catch-all shadows anything added after it. Health is reduced to a liveness status. The version and demo flag move to the authenticated config endpoint, which the two UI callers now read instead.
The login handler consulted the brute-force lockout before verifying the password. That counter is keyed on a username supplied by the caller, so anyone able to reach the login form could burn the attempt budget on a guessed name and have the operator's own correct credential rejected for the whole lockout window — a denial of service against the account owner. The password is now verified first and the lockout is only consulted on the failure path, which keeps the throttle (every wrong attempt still costs a bcrypt comparison and still records a failure) while making a valid credential impossible to refuse. A failed login also answers HTTP 401 instead of 200, so proxies, caches and scripted clients can distinguish an authentication failure from a success without parsing the body. JSONResponse keeps the response shape unchanged for existing callers, and the frontend client returns the parsed body for the login/boot paths so the localized message survives.
The host field went into the argument vector of an ipmitool subprocess after only a denylist of URL-shaped mistakes. That let through values that are not hosts at all: "-C0" is read by ipmitool as the cipher-suite flag, and suite 0 disables authentication on the IPMI session entirely. ".", "--", command substitution syntax and a 300-character string were all accepted and stored. Validation is now an allow-list — an IPv4 literal, a bracketed IPv6 literal, or a DNS hostname within the 253-character limit — because a denylist cannot enumerate what a subprocess argument may mean. The same guard was added to the credential-test endpoint, which had none at all and passes its host straight to ipmitool without touching the database. A port other than 623 is now refused with an explanation instead of being stored. Nothing ever passed that column to ipmitool, so any other value was a silent lie: the connection went to 623 regardless of what the form showed.
Exported rows reproduce sensor names and event descriptions exactly as the BMC reported them, and a cell starting with =, +, -, @, tab or carriage return is evaluated as a formula when the file is opened in a spreadsheet. The event log is the primary path: its descriptions come verbatim from 'ipmitool sel elist', so their content is not under this application's control. Such cells are now prefixed with an apostrophe, the standard treat-as-text marker, which the spreadsheet strips on display. The download filename is built from a sensor name and a server id that were interpolated raw into Content-Disposition, where a double quote would close the quoted filename and let the rest of the value be read as further header parameters. Both are now reduced to characters that cannot alter the header. The history export's range is a closed set instead of a dictionary lookup with a fallback: an unknown value returned 24 hours of data while echoing the caller's string into the filename, so the file's name described a window its contents did not come from.
bcrypt hashes at most 72 bytes and raises on anything longer instead of truncating it, so a longer password reached the hashing call as an unhandled exception: a 100-character password returned 500 from login and from first-run setup, and the credential-change endpoint echoed the raw library exception text back to the caller. Any anonymous request could trigger it. The length is now checked in bytes before hashing — a character limit would not do, since 72 multi-byte characters are already well over the byte limit — and password verification answers False rather than raising, because a password bcrypt cannot hash can never match a stored hash anyway. First-run setup was the only route without a handler for the resulting error, so it gained one. Duration parsing keeps its never-raise contract, since a malformed value in the configuration file must not stop the application from booting, but it now logs when it falls back: substituting a different session lifetime in silence left the operator with no way to discover their setting was never in effect. Values are also clamped to thirty days, so a typo like '9999d' cannot hand out a session that stays valid for centuries.
The database holds the BMC credentials and was created with the default umask, so on a typical host every local account could read it. The same applied to config.yaml. Both are now restricted to their owner. The database is fixed on every connect rather than only at creation, so an installation from an earlier version repairs itself on the next start without a migration step. The write-ahead log and shared-memory sidecars inherit the main file's mode when SQLite creates them, which is why the mode is set before the first write — but sidecars left behind by an abrupt shutdown are reused as they are, so an existing pair is fixed explicitly. Restoring a backup also widened permissions: extraction produces world-readable files and the move preserved that mode, so a restore quietly downgraded the encryption key that protects the stored credentials.
…dable Curve points are stored as free-form JSON with no per-element schema, so a point missing a key or holding a non-numeric value is accepted by the API and reaches the interpolator, which raised on it. The control loop walks its servers in a fixed order and the stored curve stays broken, so every pass aborted at the same row: every server after it was permanently starved of curve evaluation, fail-safe handling and auto-recovery, with its fans left at the last speed they were commanded while temperatures climbed. The loop kept sleeping and retrying and never healed itself. The interpolator is now total — any curve it cannot read resolves to 100%, which is the safe answer and already what an empty curve returns — and the per-server speed computation runs inside the guard that already wraps the fan write, so one unusable curve fails a single server instead of the fleet.
The image ran everything as root, so a flaw in the application or in ipmitool had the run of the container, and every file written to the data volume was owned by root on the host. The application now runs as a dedicated user with a fixed uid/gid, which also makes ownership predictable for anyone bind-mounting a host directory. There is deliberately no USER directive. Dropping privileges that way would break every existing installation on upgrade: the data volume was created by root and the process could no longer write to it, so the operator would have to chown it by hand before the container would start again. The entrypoint instead starts as root, takes ownership of the data directory, and then drops privileges, so the upgrade needs no intervention. Nothing in that adoption is fatal. Read-only mounts, NFS without no_root_squash, CIFS with a fixed uid/gid and userns-remapping all refuse chown legitimately, and a permissions problem must not turn an upgrade into an outage — the container starts anyway and warns. It also passes straight through when already started with an explicit user, where dropping privileges again would fail and the container would never start. setpriv comes with the base image, so this adds no package, and exec keeps uvicorn as PID 1 so SIGTERM still reaches it for a graceful shutdown.
config.example.yaml offered auth.enabled, auth.max_login_attempts and auth.lockout_duration. None of the three had a single reader: whether authentication is on is stored in the database, and the lockout thresholds are fixed in the login path. An operator could switch auth.enabled to false, restart, and still be asked to log in, with nothing to explain why. They are removed rather than implemented. A working enabled key would be a second and weaker switch for a security control — anyone able to edit the configuration file could turn the login off with a text editor — so the honest fix is to withdraw the promise, not to keep it. The matching environment variable goes too, since it was the same claim in another form. Deployed configuration files are unaffected: unknown keys are ignored, and auth.session_expiry, which does work, is untouched. The example's polling intervals were also wrong. command_timeout advertised 10 seconds against a real default of 30, and that default exists because a real BMC's sensor listing can take around 16 seconds — following the example would have caused timeouts. Two README claims were false. Session tokens were described as opaque, but the payload is base64url-encoded JSON that anyone holding the cookie can read; it is the signature that makes it trustworthy. The file was also said to receive every key on first run, which it does not.
The stored BMC username and password are root-equivalent on the hardware and are never returned by the API. Changing only the host address re-pointed them at a machine of the caller's choosing, and the next polling pass delivered them there — a session that could reach the settings page was enough to harvest credentials it could not read. Changing the address now requires re-sending both credentials in the same request, so the ability to redirect them depends on already knowing them. The comparison is against the stored address rather than against the presence of the field, because the edit form submits the host on every save and testing for presence would reject ordinary renames. No new fields: the form already has both inputs, and they are simply required when the address changes. The save handler also announced success unconditionally. Refusals are reported in the body with a 200 status, so an invalid address already showed "Server updated" while nothing had been saved; the reported error is now surfaced instead.
Until first-run setup completes there is no account, and nothing over HTTP tells the operator apart from anyone else who can reach the port: whoever answers the setup wizard first owns the instance. The window closes by itself the moment setup is completed, and the payoff for an attacker is lockout rather than disclosure — the database holds no BMC credentials yet. What was missing was not a mechanism but the operator knowing, so the existing startup warning now also covers this case.
The stored credential format concealed a value but did not authenticate it: anyone able to write to the database could alter a stored username or password without the change being detectable on read. Switch to AES-256-GCM and give stored values a version marker so the format can change again later without a flag day. Values written by earlier versions stay readable indefinitely, not as a courtesy but because a backup archive is swapped in before the database is opened — a value in the old format can therefore appear on any future boot. Existing installs convert on the first start. The conversion copies the database and the key file aside before writing anything, keeps them as a pair (the credentials are worthless without the key), and refuses to write at all when nothing decrypts, which is what a mismatched key file looks like. A single unreadable value is left alone rather than dropped. Failure is logged and the boot continues: both formats read, so an unconverted install keeps working instead of losing fan control at startup. Also stop the connection-test and fan-mode endpoints from answering an unreadable credential with a server error while an unreachable BMC gets an ordinary failure — that difference distinguished the two cases for anyone probing.
Enabling https previously required generating a certificate by hand first; if you turned it on without one, the embedded serve path fell back to plain HTTP silently. So the setting looked available but the honest default stayed cleartext, with the session cookie and every BMC password on the wire. Now a certificate is created on demand when https is on and none is configured, and both serve paths resolve TLS through the same helper so they cannot drift apart again. Certificates the operator supplies are never overwritten, and generation failure falls back to plain HTTP with the reason logged: locking someone out of their own dashboard over a certificate problem is worse than the cleartext they already had. The generated certificate now also covers the machine's hostname and addresses, not just localhost. A dashboard is reached at whatever the operator typed, and a name mismatch is a hard browser failure with no way to click through — unlike the unknown issuer warning, which is expected and documented. Marking it a CA is what allows removing that warning by importing the file, since macOS and Windows will not anchor trust on a plain end-entity certificate. Adds environment overrides for the three transport settings, which is the only way a container can turn TLS on, and makes the startup log state the scheme.
The telemetry socket checked authorisation once, at the handshake, and never again. A connection can stay open for days, so a session that expired — or was ended by a password change — kept streaming to whoever held the socket. It is now re-checked once a minute and closed when it no longer holds; the client sees the policy close and sends the operator back to the login page instead of retrying forever. The handshake also accepted any Origin. That is not covered by the same-origin policy, so any page the operator happened to visit could open a socket here and it would carry their cookie. The stated origin is now required to match the Host the request was addressed to, which needs no configuration and survives being reached by address, hostname or a different port. A request with no Origin is still accepted: command-line clients and the container health check send none, and rejecting them would break every non-browser caller to stop something only a browser can do. Login and the two endpoints that verify a password to change the authentication settings are now capped at five attempts a minute per source. The disable and reconfigure paths had no counter at all behind them, which made either one an unlimited guessing oracle for anyone holding a session. A slot is consumed even when the password turns out to be right, or a caller who already knows it would face no limit; the rejection reuses the existing lockout wording exactly, so it cannot be told apart from an account that is locked — which would itself reveal the account exists. Password verification now runs a throwaway comparison when the username has no row. bcrypt is slow on purpose, so returning early made an unknown name answer in well under a millisecond against tens for a real one — a difference that reads off the wire and names the only valid account without guessing a password. The rate cap alone only lowers the sample rate; it does not remove the difference. The two attacker-keyed tables that back all this are now bounded and their keys capped, since the login endpoint is reachable without a session and both the number of entries and the length of each key were caller-chosen.
Every parser read as many entries as the subprocess handed back, and the subprocess output itself had no byte limit — only a wall-clock timeout, which at LAN speed is hundreds of megabytes. A BMC that is broken, hostile, or simply answering with something other than what was asked could turn one poll cycle into unbounded memory, and the sensor payload is cached and broadcast in full, so it stayed resident and was replayed to every client that connected afterwards. The caps are four to fifty times the worst legitimate case and each one logs when it bites, naming the limit. That matters more than the number: a silently dropped sensor could make a fan curve's source disappear and trip the fail-safe on a healthy machine, so an operator with unusual hardware needs to be able to see why and raise it. The byte cap is applied after the read completes, not during it. Bounding the read would mean replacing the pipe handling wholesale for a case the existing timeout already covers; this bounds everything downstream — parsers, database rows, the broadcast cache, error strings — which is where the memory actually stayed. Deleting a server also now clears the dozen module-level trackers keyed by its id. The database rows went with the delete but none of the in-memory state did, so a removed server kept a fan controller, poll timers and counters alive for the process lifetime. The cached telemetry was the worst of it: a server the operator deleted kept reappearing on the dashboard of every newly connected client, because the existing clear-cache call had never had a caller. Two smaller leaks close with it: the power rate-limit entry was recorded before checking the server existed, so a request naming a server that does not exist left a permanent entry, and moving a server to a different address stranded the old address's command lock.
Deleting both protections from the SEL export left the whole suite green. The existing tests exercised csv_safe and safe_filename_part as pure functions, so nothing proved the endpoints still called them. These drive the real endpoints end to end. SEL is the one that matters: event descriptions and sensor names are reproduced verbatim from the BMC's own log, which is the untrusted source the sanitisation exists for. Reverting either protection now fails four of them.
The cross-origin guard compares the browser's Origin against the Host header. Behind a proxy that does not pass the browser's Host through — nginx does not, unless told to — those two can never match, so every POST/PUT/PATCH/DELETE is refused and the dashboard loads but saves nothing. There was no way to configure around it and nothing in the docs said so. server.trusted_origins (IPMIDECK_SERVER_TRUSTED_ORIGINS) is the operator's statement of the address the dashboard is really served at. It narrows nothing else: an origin that is neither this server's own authority nor on the list is still rejected, and an entry written with a scheme does not trust its cleartext twin. Also documents the companion forwarded_allow_ips, which shipped configurable but undocumented, and covers both middlewares — the guard and the defensive headers had no test at all.
An installed instance could not show its own version history: the changelog lives at the repository root, which never lands in the wheel. It is now mirrored into the package and declared as package data, with a test that fails on drift between the two copies. The root file stays authoritative — the release workflow slices the release body out of it.
… HTTP dependency Version history parsing, install-method detection and the published-version lookup for the three ways IPMIDeck is installed. Every request goes through the standard library, so the runtime dependency list keeps carrying no HTTP client — a claim anyone can check against the package metadata. Failures come back as a reason rather than an exception: an air-gapped box is an ordinary state and must not fill the log with tracebacks. A new updates.enabled config key is the switch that suppresses the socket outright.
The release notes said existing installations are repaired automatically on the next start. That held for the database, which re-applies the mode on every connect(), but not for config.yaml: it was only tightened when something wrote it back, so an install whose operator never changed a network setting kept a world-readable file holding the session secret for as long as it ran. Repaired during startup against the resolved data dir, so a config passed by path — the example file the test suite loads — is never touched.
Two routers, not one. The history and the cached state cannot reach the network, so they are always mounted. The two routes that can open a socket are mounted during startup and only when updates.enabled is on — with it off the endpoint does not exist, which is a stronger guarantee than a runtime check a later change could step past. Registration is reversible so an in-process restart that flips the switch off actually removes the endpoint. The startup log now carries the running version and the changelog address, which sends nothing and makes every bug report self-identifying.
It printed a promise that a check would arrive in a future release. It now runs one. The lookup is scheduled off the key thread — doing it inline would freeze the render loop for the length of the request, or the whole timeout on a machine with no route out — and the outcome arrives as a log line, distinguishing up to date, a newer release, a security release, and a check that could not complete. When the configuration forbids the lookup the key says so instead of pretending to try.
…te cap Conflict resolutions, all of them where the two branches touched the same decision from opposite sides: - IPMIDECK_AUTH_ENABLED stays deleted. AuthConfig no longer has an `enabled` field: whether the login is on lives in the database so write access to the configuration cannot switch it off. Restoring the variable would have created an attribute nothing reads. - The username-keyed lockout pre-check stays out of the login path. Consulting it before verifying is what let anyone lock the operator out with a guessed name. The per-source attempt cap is kept, and is now the throttle the docstring points at. - The README keeps the note that a session token's payload is readable by whoever holds the cookie, alongside the rewritten AES-256-GCM paragraph, and keeps the caveat about an interrupted key migration — that path still exists. - resolve_tls_files() supersedes the manual cert/key branch; forwarded_allow_ips is independent of it and stays on both server paths. Two things the merge itself made wrong, fixed here: - The startup line reported the scheme from the config flag. A certificate can only be handed to the server as it is built, so a process started without one serves cleartext whatever the file says, and the line would have announced https over a plaintext socket. It now reports what the launcher actually resolved, and says so loudly when https is configured but did not happen. - A throttled login answered HTTP 200. A failed one answers 401, and a caller is meant to tell them apart by status alone; the rate cap now answers exactly as the account lockout does, status and message alike. Also extends server.trusted_origins to the WebSocket handshake, which compares Origin against Host the same way and would otherwise leave a proxied dashboard loaded but with no live data.
The image started the web server directly. A certificate can only be given to that server as it is built, and only the ipmideck command resolves one, so IPMIDECK_SERVER_HTTPS=true was accepted, generated nothing, and the container went on serving cleartext. Docker is the main way this is deployed, so the one documented way to turn on encryption did not work where most people would use it. The image now starts through the console entry point, which resolves (and if needed generates) the certificate, and additionally routes SIGTERM through the graceful shutdown that restores fan control. Without a TTY the interactive console degrades to plain logs, which is what it was built to do. --host/--port go BEFORE the subcommand: they belong to the top-level parser, and the other order aborts with "unrecognized arguments" and never starts. The health check could not stay fixed on http either — it would have reported the container unhealthy precisely because the operator turned encryption on. It now tries both schemes and does not verify the certificate, since it is asking whether the app is alive, not whether the issuer is trusted. Verified end to end: the certificate is generated, the socket negotiates TLS 1.3, plain HTTP against it is refused, /api/health answers 200 over TLS and the health check exits 0.
…page The sidebar showed a hardcoded 'v2' that said nothing and did nothing. It now shows the version actually running and opens the history that explains it, over whatever page the operator is on — reading what changed is a glance, not a destination worth losing your place to. The dialog is built on the dialog primitive already in the tree, so the focus trap, Escape handling, scroll lock and focus return are the primitive's, not hand-rolled. A security release is told apart from an ordinary one by a badge that carries the word and a glyph as well as the colour. When there is no newer version, nothing is shown at all: no banner, no toast, no modal that appears by itself.
… open When no old-format credential could be decrypted but some rows were already in the new format, the message said those rows were converted "with this key" without checking. With the encryption key of another install (a half-restored backup) nothing opens at all, and the rows were blamed instead of the key. A converted row now has to decrypt before it counts as evidence that the key is right.
A failed or timed-out preflight also leaves the image job "skipped", which the PyPI and draft-release conditions took for the intended "no Docker Hub token" case, so the irreversible PyPI upload would still run. Both jobs now also require the preflight itself to have succeeded.
| except ValueError as e: | ||
| return {"success": False, "error": str(e)} | ||
| await auth.set_auth_enabled(True) | ||
| token = await auth.create_session_token_async(body.username) |
| try: | ||
| if sys.platform.startswith("linux"): | ||
| s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | ||
| s.bind((probe_host, port)) |
| fqdn = socket.getfqdn(hostname) | ||
| if fqdn and fqdn != hostname: | ||
| names.append(fqdn) | ||
| except OSError: |
| try: | ||
| for info in socket.getaddrinfo(hostname, None): | ||
| addresses.append(str(info[4][0])) | ||
| except OSError: |
Linux lets a new bind reuse a port in TIME_WAIT only when the socket that left it had SO_REUSEADDR as well. uvicorn sets it; the test's listener did not, so the test failed on Linux for a reason the real server never hits.
Resolved against dev: - Startup keeps dev's route handling. Everything added during startup, the update routes included, is dropped by the next startup and the lifespan chain is restored, so this branch's separate per-router bookkeeping is removed. Its route-count ceiling test is replaced by the existing boot-loop test. - The startup log keeps the TLS-aware line and its warnings and adds the version line. - The console keeps the live scheme and adds the update-key wiring. - The update-check bullets join the existing Added group in the changelog, and the packaged copy is re-synced. - The docs describe the setup question as what it is: asked once, box ticked, nothing contacted before the answer. They also state that the endpoint sees the caller's IP. - A console test opened /dev/null, which does not exist on Windows; it uses StringIO. - The bundled interface is rebuilt.
…releases on every channel - A redirect is followed only to https on pypi.org, hub.docker.com or api.github.com, and is logged like a request. The stock handler followed up to ten redirects to http, https or ftp, so a redirecting endpoint could move the request, version included, to a cleartext host. - A release link from a response is kept only if it is a page of this project's releases. It is stored and rendered as an anchor, so a javascript: or foreign URL no longer gets there. - The package index and the container registry carry no release notes, so a newer version found there was never marked as a security release, and most installs could not see the badge. That release's notes are now read from the published releases. If that fails, the version is still reported, just without the badge. - Sockets are opened in one place, which the tests replace, instead of through the global urlopen.
…witches The floor between unattended checks was kept in memory only, so every process start sent a request and a crash-looping container sent one per restart. The stored time of the last check now counts too. A normal start still checks; a start minutes after the last check waits out the floor. A future timestamp or an unreadable one does not suppress anything. A cached release link is re-validated when it is read. Two things had no test: the configuration switch and the recorded answer gating the unattended check, and the switch gating an on-demand check. Removing either check left the suite green. Both now have tests.
…plies it The answer was also writable through the generic app-config route. That write stored the value without starting or stopping the check, so turning it off there did not hold until a restart. The key stays readable there and is no longer writable. Every update route is now tested to answer 401 without a session, and nothing it does reaches the network. The test that the unattended check stays off under the configuration switch now records consent first, so it can fail.
If recording the answer failed, the wizard moved on silently. Nothing was stored, so no unattended check would run, whatever the box showed. A warning now tells the operator where to set it later. A 404 is not reported: it means the configuration has switched update checks off and there is nothing to record. The About section's comment named the health endpoint as the version source. The version is read from /api/config.
…heck found - A lookup never raises: an answer that is not a JSON object, a field of the wrong type, a truncated body or anything unexpected becomes a reason, not an exception. - Only a "### Security" section marks a security release; the phrase in prose no longer does. - A stable install is not offered a pre-release. - Only app containers (Docker, Podman, OCI) count as a container install; LXC and systemd-nspawn system containers do not. - When a newer version is found but its release notes cannot be read yet, the security question is asked again after an hour instead of a day. - A failed check keeps the update an earlier one found, and a check inside the rate-limit window with nothing stored reports "not_checked" rather than a blank success. - Two overlapping consents start a single unattended check, and the loop stops on its own once consent is withdrawn. - After a failure the check retries from 15 minutes, doubling up to six hours. - Tests can no longer reach the network: a session-wide backstop refuses every real request, and the two tests that started the loop now stub the lookup.
…port a known update on [g] - updates.enabled and IPMIDECK_UPDATES_ENABLED share one parser: a quoted "false", "no", "off" or "0" now switches the check off instead of loading as truthy text, and a value that reads as neither on nor off counts as off with a warning. - "updates: false", written as a value rather than a section, is honoured as the switch instead of being ignored. - The console [g] key reports an update an earlier check found even when this check fails, and no longer calls a failed check "the latest published release".
…behaves - An open page re-reads the cached update state when it becomes visible again, hourly while the unattended check is on, and once shortly after it is turned on, so what the server found shows up without a reload. Nothing here ever asks for a check on its own. - Setup no longer warns that the answer could not be saved when update checks are switched off in the configuration (the server answers 405 there, not 404). - Check now keeps showing an update an earlier check found when this one fails. - The setup hint names when and where the check asks, and the extra GitHub request; the suppressed notice names both the config key and the environment variable. - README and changelog: nothing is contacted on its own before the answer or after unticking, Check now and [g] still work when pressed, upgraded installs are not asked and stay off until turned on, the retry cadence, and how a security release is recognised.
…hostile answers - Version strings are parsed in linear time and never raise: input over 64 characters is refused, numeric fields are bounded and the suffix is unambiguous. A crafted version string could otherwise hold the interpreter lock for minutes. - Pre-releases are ordered by phase (dev, alpha, beta, release candidate), and a post-release counts as a final release. - A clone running a final release is not offered a release candidate tag either; that answer is "nothing newer", not a failed check. - Install detection never raises when a marker file cannot be inspected, and the Docker marker alone is enough to recognise a container. - A connection dropped while the answer is being read is reported as unreachable. - A lookup can be withdrawn: once withdrawn it opens no further connection, so the release-notes request is not made after the check was switched off.
…ce retries correctly - Switching the check off tells a lookup already running in its worker thread to stop before its next request, and an abandoned attempt no longer holds the rate-limit window, so switching it back on checks straight away. - A database that refuses to store the answer backs off like any other failure instead of producing a request every 15 minutes. - A newer version whose release notes cannot be read yet is checked again hourly, at most six times, whether the unattended check or Check now found it; then the daily cadence resumes.
…le without settings - The outcome of a [g] check is written to the console body like the line that announces it, so it shows at every verbosity instead of only at INFO. - A config.yaml whose top level is a list or a single value is ignored with a warning naming the file, as before, instead of stopping the server at start-up. - The decision to wire [g] to the update check is a small helper with its own tests, and the switch tests now fail if a documented spelling stops being recognised.
…al cadence - A state read that started before Check now finished, or before a consent change was saved, no longer replaces the newer answer; open pages do not re-read while a check is running. - After the unattended check is switched on, the page reads the result at 15, 30 and 60 seconds, stopping as soon as the first check has landed. - README, changelog and the setup question describe the retry cadence the server keeps, and a test pins those words to the values the service uses.
A new pre-push hook runs the i18n gate and refuses the push on failure; the gate workflow runs the same checks. - check-i18n-parity reads the languages from languages.ts (a language added there without a catalog, or a catalog not registered there, now fails) and flags empty values. - check-i18n-stale fails when an en string changed in the pushed range but a translation did not. [i18n-skip] in a commit message overrides it for a deliberate en-only change such as a typo fix. - test_backend_i18n_parity checks the backend error catalog: every message in every supported language, non-empty, same placeholders, not left in English, and SUPPORTED equal to the frontend's language list.
ipmitool (#14): - detect a missing ipmitool at startup and in the API; the app starts anyway and warns - a persistent banner with the install command for the host OS, and an install button that runs the package manager when IPMIDeck runs as root and the config allows it - routes return the i18n code ipmitool_missing instead of a raw error, and the polling loops log one parsed warning instead of a traceback Updates: - checks on by default; the CLI announces a newer version with its upgrade command - an icon button beside the version, a one-time toast and a dialog with the release notes, linked to the docs guide in the reader's language - one-click install for pip, pipx and uv (backup of database, key and config first, then an automatic restart; a manual start on Windows) and for Docker through Watchtower; other installs get the command to run - the backup folder is 0700 (it was 0600, which made the backup fail in Docker) - index.html is served no-cache and a stale lazy chunk reloads the page Also: changelog dialog formatting, CHANGELOG format test, setup hint cut to one line, setup buttons keep their width while loading, Italian wording pass.
Git on Linux and macOS ignores a hook without the executable bit, so the i18n pre-push gate would never run there.
The stale-translation check searched the whole commit message for the skip token, so any commit that merely mentions the token in its body (the commit that introduced the gate did) silently disabled the check for every range containing it. Only the subject line counts now, which also keeps the override visible in git log --oneline.
…briefly The install notice asks whether ipmitool could be installed here, and on Linux deciding that starts `sudo -n true`. It ran on the event loop on every load of the config endpoint, which every page makes, so fan control and the live telemetry stalled while sudo answered. The check now runs in a worker thread and its answer is kept for a minute; an install asks sudo again, because its rights may have just been granted.
…re they are Every update left a new folder under backups/ and none was ever removed, each one another copy of the database and the key that decrypts its BMC passwords. After a backup only the three most recent pre-update folders are kept, ordered by the time in their name; anything else in backups/ is left alone, and a folder that cannot be removed is logged without failing the update. The update dialog now shows where the backup is and that it holds the BMC credentials.
…h the Alpine index - The Windows helper waited at most two minutes for IPMIDeck to exit and then installed whether it had or not, over files the running app still held. It now installs only once the app has exited, logs that nothing was installed otherwise, and removes itself either way. - apk add on Alpine now passes --no-cache, so it reads a fresh package index instead of failing on a host that never ran apk update.
The skip token anywhere in a pushed range switched the whole check off, so one typo fix let every untranslated string pushed with it through. It now exempts only the en strings its own commit changed, and the check names them. The range is measured from the merge base, so a base branch that moved on no longer shows its own changes as untranslated ones.
The check has been on by default since installs that never saw the setup question started checking too, but the example configuration still said an unattended check needed the opt-in from setup, and the module's own principle said nothing left the machine unless asked for. Both now say what happens: the check runs unless it is turned off in setup or Settings, and updates.enabled: false removes it entirely.
| if cached is not None and time.monotonic() - cached[0] < _SUDO_ANSWER_TTL: | ||
| return cached[1] | ||
| answer = _probe_sudo() | ||
| _sudo_answer = (time.monotonic(), answer) |
| label = value.strip().strip("\"'")[:80] | ||
| if label: | ||
| return label | ||
| except OSError: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings the hardening work and the update and ipmitool work on
devtomain. Nothing is released by merging: a release still needs a version bump and a tag, and the notes come from the[Unreleased]section ofCHANGELOG.md.What changes for operators
ipmideck rotate-session-secretreplaces the signing secret after a suspected leak.Host, setserver.trusted_origins(andforwarded_allow_ips) before upgrading. Otherwise state-changing requests are refused as cross-origin.*.pre-authenc-<timestamp>.bak). Treat those copies as credential-grade.Updates and ipmitool
updates.enabled: false. A Check now button and the console[g]key check on demand.ipmi.auto_install_ipmitoolis on and IPMIDeck is privileged, the notice installs it (apt, dnf, pacman, zypper, apk, FreeBSD pkg or Homebrew). In a container it explains that the official image already includes ipmitool. On Windows it finds Dell's bundled ipmitool.exe.Main fixes
devare tested. The release smoke test reads the version from the image instead of/api/health. PyPI publishing is skipped when the Docker Hub preflight fails.npm auditreports no advisories.The full list is in the
[Unreleased]section ofCHANGELOG.md.Verification
--port 8080reports healthy and comes back afterdocker restart.ipmitool_missing, the install is refused inside Docker, and on a root Debian host it installs in about 10 seconds and is picked up without a restart.Closes #14
Closes #15