I had already rooted a Juniper AP33 once. I had even pulled binaries and started a local controller. Then I lost the work, and, rather helpfully, could not remember how I got in. All I had was the AP, a serial adapter, a vague memory of a response challenge, and a public comment from my past self claiming I had done it.
That is an irritating place to begin a reverse-engineering project. It is also a pretty good test of whether a result is reproducible.
The AP33 is good hardware. Mine could still boot, associate clients and pass traffic, but its management path expected Juniper Mist. I wanted to run the APs I own against a controller on my own network: sites, WLANs, client history, telemetry, a web UI, and no operational dependence on a vendor cloud. The result is Dismist. This is how I got from a locked serial console to a working WLAN on that controller.
The serial prompt that wasn't a shell
The AP exposed a 115200-baud serial console. Boot messages were easy to collect; a root shell was not. On the newer firmware, root presented a challenge. U-Boot presented a signed challenge too. The passwordless manufacturing account opened a small diagnostic menu, but not a general shell.
I spent time looking for the trick I thought I had used before. It was tempting to blame a parser overflow, an odd boot key, or an undocumented recovery menu. None of those guesses produced a verified entry. The important clue was the boot selector: this AP had two root filesystems, and the current one was not the only firmware on the NAND.
Repeated software reboots through the manufacturing menu decremented the selected image's boot counter. When that counter ran out, the bootloader fell back to the other already present image. The serial log made the transition visible: i1 became i2, and the firmware banner changed from a newer 0.14 build to an older 0.8.21758 build.
At the login prompt I entered manufacturing, chose e to reboot, and watched the boot config line on each start. That menu did not unlock anything; it gave me a controlled way to reach a recovery image the AP already had. I would not try this blindly on an AP whose other slot I had not identified.
The old image still asked root for a challenge and response, but it used a different scheme. Both generations put a B in front of the base64 challenge, which is misleading until you decode it. The old payload begins with A|...|root|...; the newer one begins with C and expects a signature. The 0.8 console_login binary used two fixed firmware strings and an HMAC-SHA256 calculation over a fresh server nonce, client nonce and the decoded challenge. I recovered that calculation, generated a response for a new challenge, and got id -u to print 0.
first = HMAC-SHA256(server_nonce, client_nonce)
proof = HMAC-SHA256(first, firmware_prefix || decoded_challenge || firmware_suffix)
response = "B" || base64(client_nonce || proof)There was no reusable password. Each login needs its own response. Dismist's AP33 guide now tells you exactly what to look for in the boot log and how to run the response generator:
dismist ap33 serial-response '<complete-challenge-from-the-console>'This path depends on an old, rootable image actually being present. It is not a claim that every AP33 has one. On the 0.14 and 0.15 images I examined, the C-mode login wants an Ed25519 signature from a private signing key I do not have. The old HMAC response cannot answer it. A unit with only C-mode images still needs a verified offline preparation path.
Root was the beginning, not the controller
With a shell, I backed up /rw, the EEPROM and the flash layout before changing trust or firmware. The AP authentication secret was the next wall. It is 32 bytes in the device's EEPROM, separate from the claim QR and label code. It is unique to that AP. A site SSH key does not provision it, and making up a new one does not make the original AP authenticate.
On the observed AP33 board, the secret starts at EEPROM offset 0x400. I read the full EEPROM first, verified its size, and copied those 32 bytes to a private file for the controller. The onboarding guide has the checked command and makes you verify the device path on your AP rather than assuming every board has the same layout.
The stock connector also verifies the TLS certificate presented by ep-terminator.mistsys.net against its own endpoint CA bundle. Pointing the hostname at my laptop was not enough; the AP correctly rejected a certificate it did not trust. The supposed public-CA shortcut did not exist in the tested trust bundle either.
I generated a local CA and WSS certificate for the hostname, kept TLS verification on, and placed an override under the AP's writable /rw/etc/mist/ep-control/ tree. The override adds my CA alongside the stock CAs and tells the connector to read that bundle. On the AP management LAN, DNS then resolves the normal endpoint hostname to Dismist. No change to the public domain is involved.
That gives a prepared AP a route to my server. It still has to prove its identity. The AP's WebSocket session starts with ep-auth; the exchange uses the EEPROM secret and a context bound to details including the negotiated TLS certificate and cipher. I reproduced the server side of that proof, answered the AP's time and ping messages, and returned a configuration when it requested ep-config. An ep-config-applied- telemetry message, tied to the config timestamp, is how Dismist knows the AP has actually accepted a revision.
The wire format took some work of its own. WebSocket frames carry a small binary envelope with a version, service class, subject and conversation handle. Some bodies are JSON, while the AP's full and fast statistics arrive as protobuf; compressed bodies have a marker before the gzip data. Once I could decode and reply to the subjects the connector actually used, the AP stopped treating my server as an impostor and began reporting real client and radio state. Echoing its ping subject mattered too: without that, the apparently successful session closed again after about 45 seconds.
AP33 -- management DNS --> Dismist's AP-scoped DNS
AP33 -- TLS + WebSocket --> dismistd (authentication, config, telemetry)
Browser -- LAN HTTP/SSE --> embedded React UI and API
|
+--> bbolt sites, APs, WLANs, clients and historyThis is local control after preparation. Factory-offline preparation for every firmware generation is still unfinished, and the onboarding guide says so plainly.
The first SSID, and the DHCP bug
The first proof I cared about was not a green status badge. It was a wireless client getting a lease on the intended VLAN and reaching the network.
Dismist built a site WLAN from a firmware-compatible AP33 template and sent it to the AP. The AP advertised the SSID on both radios, a client associated, and the telemetry showed its BSSID, signal, VLAN, IP and byte counters. I initially got a real lease on the isolated client VLAN.
Then, after more controller and firmware work, an iPhone associated perfectly but ended up with a 169.254.x.x link-local address. The phone was right: DHCP was failing. A passive capture showed its DHCP discovers entering the wireless interface, while no matching VLAN-tagged discovers appeared on the Ethernet uplink. The WLAN config named the client VLAN, but the AP's Ethernet SwitchPorts allow list contained only the native VLAN. Dismist was generating half of the bridge configuration.
I fixed the config builder to carry enabled WLAN VLANs onto the AP Ethernet ports. The phone then received a lease in the right subnet, and internet access worked. It was a useful reminder that associated and online are different states. The UI now reports actual client telemetry, and the config preview lets an operator inspect the generated switch-port VLANs before blaming the passphrase.
Getting onto 0.15 without throwing away the way back
The working AP was running 0.14, but I wanted to test the newer 0.15.35064 firmware. Mist's versions API listed build IDs, not a handy image URL. Watching an upgrade with an isolated software AP showed the actual transport: the controller carries a virtual IPv6 management path inside its WebSocket session, opens SSH, runs a Lua update helper and transfers a firmware file tree with rsync to the inactive root filesystem.
I captured the tree off the physical AP first and checked all 1,362 entries in its SHA-512 manifest. That established that the archive matched its own manifest; it was not an independent vendor signature check. I also compared the trust and connector pieces before trying a physical boot.
The slot order mattered. An ordinary upgrade while 0.14 was running would have overwritten the old 0.8 recovery image—the only serial-root route I had verified. Instead, I booted the 0.8 slot, confirmed root again, backed up the inactive 0.14 volume, and staged 0.15 into that inactive slot. I read the new tree back and compared file content and metadata before changing the boot selector. I did not rewrite SPL or U-Boot. The AP booted 0.15, reconnected to Dismist, accepted its config, kept the local CA and keys in /rw, and still had the rootable 0.8 slot available. The WLAN and client DHCP path worked again on 0.15.
That is a report of one controlled upgrade, not a general one-command AP33 firmware installer. I am not asking anyone to run a flash procedure from a blog paragraph.
Dismist as it stands
The controller is a Go daemon with an embedded React UI. It serves AP-scoped DNS, WSS control and telemetry, authenticated site and WLAN configuration, client and event views, bbolt history, site-scoped SSE updates and a loopback Prometheus endpoint. Sites can hold multiple wired APs, and a site can push SSH public keys after an AP authenticates. The AP33 base config is bundled now; dismist init creates the CA and prints the admin token. There is a v0.1.0 release for Linux and macOS, or you can install both commands with Go:
go install github.com/nicko170/dismist/cmd/...@latest
export PATH="$(go env GOPATH)/bin:$PATH"
dismist initIt is early software. Mesh backhaul is mapped but has not been validated with a physical base and relay pair. The AP33 has BLE scanning and beacon hardware, but Dismist does not yet have a verified BLE presence feed. The AP's newer signed-only serial route remains the biggest onboarding gap. I would rather have those limits in the README than call a green dashboard a finished product.
Still, the thing I wanted is real: the AP is on my network, its control session ends on my server, the WLAN works, and the client and connection history belongs to me. Mist, dismissed.