MTDA-TV + preliminary hardening - #607
Merged
Merged
Conversation
chombourger
commented
Aug 20, 2026
Collaborator
Signed-off-by: Cedric Hombourger <cedric.hombourger@siemens.com>
_GrpcImpl.storage_open() ignored the session kwarg passed by callers (e.g. mtda-www's per-request Client instance) and always sent its own default/generated session as gRPC metadata instead. This caused the server to open storage and fire the OPENED event under a session id the browser never saw, so the client's OPENED-session check in index.html never matched and no upload data was ever sent (silent 0-byte timeout). Client.__getattr__'s generic wrapper had the same issue in the other direction: it unconditionally overwrote any explicitly-passed session kwarg with the client's own default, instead of only filling it in when missing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Cedric Hombourger <cedric.hombourger@siemens.com>
Several URLs in the per-agent web UI were hardcoded as absolute-root paths (the /mtda WebSocket, pyodide wheel/console URLs, the swagger doc URL). These break whenever the page is served under a path prefix by a reverse proxy (e.g. Traefik with mtda-tv's gateway support routing /agents/<name>/), since the browser computes these URLs from window.location and has no notion of what prefix the proxy stripped. Introduce window.MTDA_BASE, derived from the page's own path, and use it to build these URLs relative to the page instead of the origin root. Also respect https:/wss: when building the WebSocket URL. Signed-off-by: Cedric Hombourger <cedric.hombourger@siemens.com>
chombourger
force-pushed
the
ch/feat/mtda-tv
branch
from
August 20, 2026 18:18
2515442 to
279dd15
Compare
chombourger
force-pushed
the
ch/feat/mtda-tv
branch
from
August 20, 2026 18:26
279dd15 to
58ac263
Compare
chombourger
force-pushed
the
ch/feat/mtda-tv
branch
from
August 20, 2026 18:30
58ac263 to
35eb12f
Compare
chombourger
force-pushed
the
ch/feat/mtda-tv
branch
from
August 20, 2026 18:38
35eb12f to
aa4c3fd
Compare
Add mtda-tv, a Tornado service that discovers MTDA agents via ZeroConf, polls video snapshots over gRPC, and presents them in a TV-style grid, redirecting to each agent's mtda-www on click. Add an AgentInfo RPC exposing agent metadata (version, name, www_port) needed by mtda-tv. Add transparent gateway/proxy support: a [gateway] config section lets mtda-tv act as a dynamic discovery source for a reverse proxy (e.g. Traefik), so agents are reached through a single hostname instead of each agent's own mtda-www instance. Includes a /api/traefik-config endpoint, a gateway_url per agent in /api/agents, and an optional mtda-tv-gateway Debian package with sample Traefik config and a postinst to restart mtda-tv on (re)configuration. A docker-compose.yml is included alongside the sample Traefik config so Traefik keeps running (restart: unless-stopped) across crashes and machine reboots. A new base_url gateway option lets tile links keep pointing at the reverse proxy even when the grid page is browsed directly on mtda-tv's own port instead of through the proxy. Debian packaging is reworked to share resources between mtda-tv and mtda-www. Signed-off-by: Cedric Hombourger <cedric.hombourger@siemens.com>
…efik The gRPC control channel had no auth/TLS (CWE-306/CWE-862/CWE-319): a client-supplied 'mtda-session' string was trusted as-is, letting anyone forge a session identity and call privileged RPCs (power, console, storage, USB, ...) with no credentials. Opt-in, defaults to prior (insecure) behavior. - mtda/tls.py: mTLS-aware client channels (mtdas:// scheme), server credentials, -r/$MTDA_REMOTE parsing, and grpc.aio channel support. - mtda/grpc/servicer.py: when [security] trust_proxy_identity is set, derive the session identity from a reverse-proxy-verified client certificate CN (e.g. Traefik's passTLSClientCert) instead of the spoofable 'mtda-session' metadata, through the shared _session() choke point used by all RPCs. - mtda-service: configurable bind address and TLS/mTLS termination ([security] bind, server_tls) for the separate-node deployment case; mDNS advertisement now respects the configured bind address. - mtda-cli/mtda/client.py/mtda/console/remote.py: client-side TLS/mTLS support so mtda-cli/consoles can reach an agent through Traefik. - mtda-tv/mtda-www: TLS-aware gRPC clients ([security] client settings, build_channel()/build_aio_channel()); new [gateway] tls_entry_point option so a single agent can be reachable via both plain and TLS Traefik entry points. - configs/, docs/hardening.rst: sample Traefik/mTLS configs and a full hardening guide (threat model, deployment shapes, client setup, firewalling, production Traefik deployment under systemd). Signed-off-by: Cedric Hombourger <cedric.hombourger@siemens.com>
Document MTDA's intended security model (trusted/controlled environments only, not a security boundary for untrusted networks), deployment assumptions, vulnerability reporting process, and point to docs/hardening.rst for the optional TLS/mTLS hardening now available. Signed-off-by: Cedric Hombourger <cedric.hombourger@siemens.com>
The Command RPC forwarded arbitrary caller-supplied strings straight into QEMU's HMP monitor, allowing any authenticated client to read/ write host files or run host processes (e.g. via pmemsave, migrate exec:). This authorization gap is independent of the mTLS/identity hardening added earlier, which only verifies who is calling, not what they can do. No other backend implemented Command beyond a no-op stub, and the only documented use (hostfwd_add) has no other caller. Remove the RPC, client/agent/servicer plumbing, all power backend stubs, and the mtda-cli command subcommand. mtda-cli monitor send/wait remains as the supported way to interact with the QEMU monitor. Signed-off-by: Cedric Hombourger <cedric.hombourger@siemens.com>
Subscribe was the only RPC that never extracted the caller's session identity, so it streamed all console/monitor bytes and events to any client able to reach the gRPC endpoint, bypassing the identity check every other RPC performs. When trust_proxy_identity (mTLS hardening) is enabled, reject subscribers with no verified client identity; otherwise track the session like every other call for consistency. Signed-off-by: Cedric Hombourger <cedric.hombourger@siemens.com>
console_locked/power_locked/_check_locked all actually answer 'does this session hold the exclusive lock', not 'is this locked out of the session'. Rename to console_is_owner/power_is_owner/ _is_lock_owner to remove the ambiguity before fixing how callers use this value. Signed-off-by: Cedric Hombourger <cedric.hombourger@siemens.com>
…ower/storage Every consumer of console_is_owner/power_is_owner used it as an 'is locked' style gate (proceed only when the check is False), but the check actually returns True precisely when the caller IS the lock owner. As a result the lock owner was blocked from their own console/monitor input, power control and storage swap while any other, non-owning session was let through unrestricted - the exact opposite of what target_lock is supposed to enforce. Add _locked_out(session), which correctly returns True only when the device is locked by a *different* session, and switch every affected call site (console clear/dump/flush/prompt/run/send/tail/wait, monitor send/wait, power on/off/toggle/firmware, storage_locked) to use it. Signed-off-by: Cedric Hombourger <cedric.hombourger@siemens.com>
keyboard_press, keyboard_write and mouse_move had no lock check at all, so any session could type or move the mouse on a target that another session holds an exclusive lock on. Gate them the same way as console/monitor input: proceed only when the target is unlocked or the caller holds the lock. Signed-off-by: Cedric Hombourger <cedric.hombourger@siemens.com>
/storage-set-bmap (mtda-www) and the client's bmap auto-discovery both parsed uploaded/local bmap files with plain xml.etree.ElementTree.fromstring(), which does not limit expat's internal entity expansion. Add utils.SafeXml.fromstring(), built directly on xml.parsers.expat, which rejects any DOCTYPE declaration outright. Since custom entities can only be declared from a DOCTYPE's internal subset, this blocks entity-expansion DoS with no new dependency, while still parsing well-formed bmap files that never use DOCTYPE. Use it in both mtda-www and mtda/client.py instead of the raw ElementTree call. Signed-off-by: Cedric Hombourger <cedric.hombourger@siemens.com>
chombourger
force-pushed
the
ch/feat/mtda-tv
branch
from
August 20, 2026 20:05
aa4c3fd to
cdfc804
Compare
baprusty
approved these changes
Aug 25, 2026
Collaborator
|
Great addition. LGTM, thanks |
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.