Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ gui = [

[dependencies]
# --- Core ---
nix = { version = "0.29", features = ["process", "signal", "user", "fs", "mount", "sched", "hostname", "term"] }
# `poll` is for the terminal sign-in: it waits for a keystroke without putting the
# tty in non-blocking mode, which would also apply to the frames written back to it.
# It also multiplexes the browser viewer's sockets, which is what keeps that server
# single-threaded. `net` is for `getifaddrs`: the browser viewer binds to the tailnet
# address, which is read from the interfaces rather than from the `tailscale` command.
nix = { version = "0.29", features = ["process", "signal", "user", "fs", "mount", "sched", "hostname", "term", "poll", "net"] }
anyhow = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand All @@ -69,6 +74,11 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-std", "io-
# bundled roots so it works inside the minimal container too (no system CA / curl).
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }

# --- Terminal sign-in (`login`) ---
# Reads the private display's pixels (GetImage) and types into it (XTEST). Pure
# Rust — no libX11, no C toolchain — so the statically linked CLI keeps working.
x11rb = { version = "0.13", features = ["xtest"] }

# --- CLI front-end ---
clap = { version = "4", features = ["derive"] }
rpassword = "7"
Expand Down
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,54 @@ intune-container start # (optional) run headless + seamless Teams/M365 SSO
Daily CLI use: `edge` · `status` · `update` · `doctor` · `stop`. Full walkthrough
in the **[Quickstart](docs/quickstart.md)**.

### No screen at all? Sign in from the terminal

On a headless box — an EC2 instance, a build machine — there is no display to put
the sign-in window on, and the Primary Refresh Token still expires: Entra then
answers `interaction_required` and every token call fails until a human signs in
again. `login` does that over SSH, with no VNC and no compositor:

```sh
intune-container login # the sign-in window, drawn in your terminal
```

It starts a private invisible display, runs the portal on it, draws that display
with half-block characters and sends your keys and clicks back. Type your address
and password where you are; when the two-digit Authenticator number appears, press
**F2** to zoom in and read it. **F1** lists the keys, **F4** fits the whole window,
**Ctrl+arrows** pan, **Ctrl+Q** leaves.

It needs `Xvfb` on the host (`sudo apt install xvfb`) and a terminal with 24-bit
colour. Everything it starts — the portal, the display forwarding, the X server —
it stops again, including when your SSH connection drops.

#### Or in a browser, over your tailnet

Half-block cells cost half the vertical resolution, and 13-pixel text survives that
only when you zoom. `--web` serves the same window to a browser instead, at its own
size and with a real pointer:

```sh
intune-container login --web # prints one link, with a token for this session
```

It listens on this host's **tailnet** address, so any device in your tailnet can
open it — your phone included, which is where the Authenticator prompt arrives. With
no Tailscale on the host it binds loopback instead and prints the `ssh -L` line for
it. `--bind` and `--port` override both. Every request must carry the token from the
link; nothing else is served.

The page is one file with no build step: a canvas, the tiles that changed since the
last frame (gzipped, so the browser inflates them), and your keys and clicks posted
back. **Ctrl+V** pastes — a password from your manager is typed into the display one
key at a time. **Finish** closes the window and ends the session, as **Ctrl+Q** does
in the terminal viewer.

Why not a plain text prompt? Because the identity broker allows none: it refuses a
device-code flow on Linux (`AcquireTokenWithDeviceCodeFlow is not implemented on
Linux platform`) and renders the sign-in itself, in an embedded WebKitGTK view on
an X display. The window has to exist — this brings it to the terminal.

The default container image is publicly hosted and ready to go (it already
includes everything for headless SSO) — there's nothing to build, and no
container engine is needed: the image is pulled with a built-in OCI client. Each
Expand Down Expand Up @@ -127,6 +175,12 @@ shells out, and neither needs elevated privilege.
- [x] **Compositor-agnostic** — auto-detects Wayland, abstract X11, and
Xauthority; no hardcoded socket names.
- [x] **One-command enroll** — provision, boot, and open the portal in one step.
- [x] **Terminal sign-in** (`login`) — for a host with no screen: the portal runs
on a private invisible display and is drawn in the terminal, keys and clicks
included. No VNC server, no compositor, no X forwarding.
- [x] **Browser sign-in over the tailnet** (`login --web`) — the same private
display, served to any device in your tailnet at full resolution, behind a
per-session token. One built-in HTTP server, no WebSocket and no VNC.
- [x] **Enrollment backup/restore** — survive container rebuilds without
re-enrolling.
- [x] **Microsoft Edge** in the container, with display/GPU passthrough during
Expand Down
13 changes: 13 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,22 @@ pub mod config;
pub mod display;
pub mod doctor;
pub mod lock;
pub mod login;
pub mod native_host;
pub mod ops;

/// Drawing an X display in a terminal, and typing back into it (the pure half).
pub mod termview;

/// Reading an X display's pixels and synthesising its input (the X11 half).
pub mod xscreen;

/// The private, invisible X display a terminal sign-in runs on.
pub mod xvfb;

/// Serving that same display to a browser, over the tailnet (`login --web`).
pub mod webview;

/// Pure-Rust OCI image pull + layer extraction (no docker/podman).
pub mod oci;

Expand Down
Loading
Loading