Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e3cbe40
docs(hover): headless-browser auth design + ADR 0001 (defeat Imperva …
intel352 May 31, 2026
ea3ddd3
docs(hover): harden browser auth design review
intel352 May 31, 2026
7a8e9e9
docs(hover): plan browser-backed auth implementation
intel352 May 31, 2026
b85a12c
docs(hover): review browser auth implementation plan
intel352 May 31, 2026
4b9d685
docs(hover): align browser auth plan with design
intel352 May 31, 2026
88e300a
chore: lock scope for hover browser auth
intel352 May 31, 2026
2b5dcea
test(hoverclient): add live browser auth viability gate
intel352 May 31, 2026
1b8b276
docs(hover): backport go-rod driver spike evidence into design
intel352 May 31, 2026
9d75788
docs(hover): record 2026-05-30 Imperva-bypass re-check (go-rod unchan…
intel352 May 31, 2026
66ed5e1
fix(hoverclient): keep profile dir on local launcher (Kill, not Cleanup)
intel352 May 31, 2026
2656e49
docs(hover): backport live-gate result — Imperva cleared, 2FA model
intel352 May 31, 2026
14fb2f8
chore(hover): Go 1.26.3 + x/net v0.55.0 + gopls modernize
intel352 Jun 1, 2026
30e10f5
docs(hover): ADR 0002 fork go-rod -> GoCodeAlone/rod + design backport
intel352 Jun 1, 2026
5887f54
feat(hoverclient): use GoCodeAlone/rod fork for the browser driver
intel352 Jun 1, 2026
87ae0aa
docs(hover): backport production live proof (Imperva cleared, TOTP, 3…
intel352 Jun 1, 2026
0bbc20b
refactor(hoverclient): add browser backend configuration seam
intel352 Jun 1, 2026
693ce9f
feat(hoverclient): browser login mints Imperva clearance + hands cook…
intel352 Jun 1, 2026
1ef7e46
feat(hoverclient): execute hover dns writes in-browser (hybrid write …
intel352 Jun 1, 2026
07267fe
docs(hover): browser-auth README + v0.5.0 manifests + live plugin test
intel352 Jun 1, 2026
6330454
docs(hover): security review + harden bot-challenge classification
intel352 Jun 1, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.claude/
.release/
dist/

.hover-browser-profile/
192 changes: 130 additions & 62 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,45 @@
[![CI](https://github.com/GoCodeAlone/workflow-plugin-hover/actions/workflows/ci.yml/badge.svg)](https://github.com/GoCodeAlone/workflow-plugin-hover/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

> 🧪 **Experimental** — Hover DNS provider for the GoCodeAlone/workflow IaC surface.
> Hover has no official API; this plugin mimics the browser auth flow used by
> [pjslauta/hover-dyn-dns](https://github.com/pjslauta/hover-dyn-dns). Watch out
> for UI changes on hover.com that may break CSRF token parsing.

## Auth flow

1. GET `/signin` → parse `<input name="_token">` (CSRF token).
2. POST `/signin` with `username`, `password`, `_token`.
3. GET `/signin/totp` to probe for MFA:
- If the page contains a `_token`: account has MFA enabled → POST `/signin/totp`
with `code` (RFC 6238 TOTP) + `_token`.
- If no `_token`: MFA is disabled → skip the TOTP submission (the
GET probe itself still runs).
4. Session cookies are stored in-memory for subsequent `/api/dns*` calls.

Re-auth fires whenever the in-memory session is older than 1 hour.
> **Experimental** — Hover DNS provider for the GoCodeAlone/workflow IaC surface.
> Hover has no official API. This plugin drives a real Chrome browser to
> authenticate, then reuses session cookies for read operations and runs writes
> in-browser.

## Why Chrome?

Hover's signin endpoint is protected by Imperva Advanced Bot Protection (ABP).
A cold Go `http.Client` receives a 401/block response because it cannot run
Imperva's JavaScript sensor to mint the clearance cookie. A real Chrome instance
runs the JS sensor, passes the Imperva challenge, and completes the full login
flow (including TOTP 2FA). The resulting session + clearance cookies are then
reused by a lightweight Go `http.Client` for all read operations; writes that
require an in-page flow run in the same browser instance.

## Hybrid architecture

1. **Browser login**: Chrome drives the Hover signin page, passes the Imperva
challenge, and completes TOTP 2FA when configured.
2. **HTTP reads**: Session + Imperva clearance cookies are transferred to a Go
`http.Client`; all read operations (`ListDomains`, `GetDomain`, `ListRecords`,
`GetDomainDelegation`) use the cookie-reuse path.
3. **In-browser writes**: Mutations (`SetNameservers`, `CreateRecord`,
`UpdateRecord`, `DeleteRecord`) run in the browser where the full Imperva
context is live.

The session is considered stale after 1 hour; a fresh browser login fires
automatically on the next operation.

## Chrome acquisition

The plugin needs a Chrome binary. Resolution order:

1. `browser_path` config key (or `HOVER_BROWSER_PATH` / `ROD_BROWSER_PATH` env
var) — absolute path to an existing Chrome/Chromium binary.
2. Auto-download: if no binary is found and `browser_download` is `true` (or
`HOVER_BROWSER_DOWNLOAD=true`), go-rod downloads a compatible Chromium build
to a local cache. **Default: `true`.**
3. If neither finds a binary, `ErrChromeUnavailable` is returned.

## Configuration

Expand All @@ -31,7 +53,13 @@ modules:
provider: hover
username: ${HOVER_USERNAME}
password: ${HOVER_PASSWORD}
totp_secret: ${HOVER_TOTP_SECRET}
totp_secret: ${HOVER_TOTP_SECRET} # omit if account has no MFA

# Browser options (all optional; shown with their defaults)
# browser_path: "" # absolute path to Chrome binary
# browser_download: true # auto-download Chromium if not found
# browser_headless: true # run Chrome headlessly
# browser_profile_dir: "" # persistent profile dir (see below)

- name: example-com
type: infra.dns
Expand All @@ -43,70 +71,110 @@ modules:
- { type: CNAME, name: 'www', content: example.com., ttl: 900 }
```

The `records` key is **required**. The plugin treats your declared
list as authoritative: on apply, records present upstream but
absent from `records` are deleted, records present in `records`
but absent upstream are created, and records that differ are
updated. To deliberately drop every record from a zone, set
`records: []` — that explicit empty list is the only way to ask
for a wipe. Omitting `records` entirely is rejected at Plan time
to avoid the "I forgot the key and lost my zone" failure mode.
### Browser config env vars

| Config key | Env alias | Default |
|----------------------|--------------------------|--------------------------------------------|
| `browser_path` | `HOVER_BROWSER_PATH`, `ROD_BROWSER_PATH` | (none — auto-discover or download) |
| `browser_download` | `HOVER_BROWSER_DOWNLOAD` | `true` |
| `browser_headless` | `HOVER_BROWSER_HEADLESS` | `true` |
| `browser_profile_dir`| `HOVER_BROWSER_PROFILE_DIR` | `${XDG_STATE_HOME:-$HOME/.local/state}/wfctl/plugins/hover/browser-profile` |

Explicit config keys take precedence over env vars; env vars take precedence
over built-in defaults.

## 2FA / TOTP

Supply `HOVER_TOTP_SECRET` (base32-encoded seed from the Hover "Set up
authenticator" QR-code page) when your account has 2FA enabled. The plugin
computes RFC 6238 (SHA-1, 30 s step, 6 digits) codes in-process.

**Email / non-TOTP 2FA is not supported.** If Hover reports `need_2fa` and no
TOTP secret is configured, the plugin returns `ErrEmail2FARequired`. To resolve:
either switch the account to an authenticator-app 2FA method and supply the
base32 seed, or pre-trust a persistent browser profile (see below) so the
security checkpoint is skipped on subsequent runs.

## Browser profile directory

The profile dir stores Imperva clearance cookies, Hover session cookies, and
other browser state across runs. It is sensitive — treat it like a credential
file and keep it out of version control.

Default location:

```
${XDG_STATE_HOME:-$HOME/.local/state}/wfctl/plugins/hover/browser-profile
```

Override via `browser_profile_dir` or `HOVER_BROWSER_PROFILE_DIR`. A warm
profile skips re-authentication against Imperva on subsequent runs and allows
the plugin to work even after a TOTP secret is no longer available.

## Required secrets

| Name | Sensitive | Source |
|------|-----------|--------|
| `HOVER_USERNAME` | no | Hover account login |
| Name | Sensitive | Purpose |
|------|-----------|---------|
| `HOVER_USERNAME` | no | Hover account login (email) |
| `HOVER_PASSWORD` | **yes** | Hover account password |
| `HOVER_TOTP_SECRET` | **yes** | Base32 seed from Hover 2FA setup (the QR-code page shows a "Secret Key" field; copy that) |
| `HOVER_TOTP_SECRET` | **yes** | Base32 TOTP seed (required when account has authenticator 2FA) |

`wfctl secrets setup --plugin workflow-plugin-hover` prompts for each;
sensitive fields are masked.
```sh
wfctl secrets setup --plugin workflow-plugin-hover
```

## Importing existing state
Sensitive fields are masked during interactive prompts.

## Typed errors

| Error | Meaning |
|-------|---------|
| `ErrBotChallenge` | Imperva blocked the browser session. Check network egress rules or rotate the browser profile. |
| `ErrChromeUnavailable` | No Chrome binary found and `browser_download=false`. Install Chrome or set `HOVER_BROWSER_DOWNLOAD=true`. |
| `ErrEmail2FARequired` | Account uses email/non-TOTP 2FA. Switch to an authenticator app or pre-trust a persistent browser profile. |

The plugin supports read-only import for existing Hover domains:
### Bot-challenge notice

Driving a real browser against Imperva-protected pages is a best-effort
approach. Imperva may update its JS sensor in ways that break the clearance
flow; this is a known gray area in Imperva's terms of service. The plugin
documents this honestly and makes no guarantees of perpetual bypass.

## Importing existing state

```sh
wfctl infra import --config infra.yaml --name example-com-dns --id example.com
wfctl infra import --config infra.yaml --name example-com-delegation --id example.com
```

Declare the target resource in config first so `wfctl` can resolve the Hover
provider and resource type. `infra.dns` imports the zone records returned by
Hover. `infra.dns_delegation` imports the current registrar nameservers.
Imported state is marked as adoption-shaped state so follow-up plans can
compare against live outputs without treating the imported record set as a
user-authored apply config.
Declare the target resource in config first so `wfctl` resolves the Hover
provider and resource type. `infra.dns` imports zone records; `infra.dns_delegation`
imports registrar nameservers. Imported state is adoption-shaped so follow-up
plans compare against live outputs.

## TOTP
## DNS record semantics

In-process RFC 6238 (SHA-1, 30s step, 6 digits). The seed is decoded
once at plugin start; codes are computed on each login. Tested
against [RFC 6238 Appendix B vectors](https://datatracker.ietf.org/doc/html/rfc6238#appendix-B).

## Caveats

- **UI brittleness**: Hover's signin page can change. The plugin
fails loud with `CSRF token not found at /signin` when the regex
no longer matches.
- **CAPTCHA**: Hover may serve a CAPTCHA challenge on suspicious
logins. The plugin doesn't solve CAPTCHAs; you'll need to log
in manually from the same IP to seed trust, OR use a static
egress IP for the plugin runner.
- **Rate limit**: Stick to small zones; Hover's account portal
isn't optimised for bulk DNS edits.
The `records` key is **required**. The plugin treats your declared list as
authoritative: records present upstream but absent from `records` are deleted;
records in `records` but absent upstream are created; records that differ are
updated. To drop every record from a zone, set `records: []`. Omitting `records`
entirely is rejected at Plan time to prevent accidental zone wipes.

## Limitations

- **No zone delete**: Hover exposes no API to drop a DNS zone.
Resource `Delete` is a no-op — the IaC state is cleared but
upstream records remain. Operators who want to drop the zone
must do so manually via Hover's UI.
- **No zone delete**: Hover exposes no API to drop a DNS zone. Resource `Delete`
is a no-op — IaC state is cleared but upstream records remain. Drop the zone
manually via Hover's UI if needed.
- **Rate limit**: Stick to small zones; Hover's account portal is not optimised
for bulk DNS edits.

## Development

```sh
GOWORK=off go build ./...
GOWORK=off go test ./... -race -count=1
GOWORK=off GOTOOLCHAIN=auto go build ./...
GOWORK=off GOTOOLCHAIN=auto go test ./... -race -count=1
```

Browser unit tests in `pkg/hoverclient` launch real Chrome locally and may take
~20 s. They skip automatically when no Chrome binary is available and
`HOVER_BROWSER_DOWNLOAD` is not set.
Comment on lines +178 to +180
2 changes: 1 addition & 1 deletion cmd/workflow-plugin-hover/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "workflow-plugin-hover",
"version": "0.0.0",
"version": "0.5.0",
"description": "Hover DNS provider (browser-style login + TOTP, no official SDK)",
"author": "GoCodeAlone",
"license": "MIT",
Expand Down
28 changes: 28 additions & 0 deletions decisions/0001-real-browser-auth-for-imperva.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 0001. Drive Hover auth with a real browser (go-rod) to defeat Imperva ABP

**Status:** Accepted
**Date:** 2026-05-30
**Decision-makers:** codingsloth@pm.me (approved), Claude (Opus 4.8)
**Related:** docs/plans/2026-05-30-headless-browser-auth-design.md

## Context

Hover has no public API; the plugin scrapes the React signin (`/signin/auth.json`). Hover is behind **Imperva Advanced Bot Protection** (ex-Distil): a JS sensor (`/c<uuid>`) mints clearance cookies (`__uzma/b/c/d/e`, `uzmx`) that the auth endpoint requires. A cold Go `http.Client` never runs the sensor → Imperva returns a generic 401 even with valid credentials (root-caused live 2026-05-30). No header/token tweak can pass it: the clearance is a JS-computed, server-validated, rotating fingerprint, additionally backed by TLS/JA3 + behavioral signals. The prior HTTP login never actually worked against live Hover — only against the test stub.

## Decision

We will authenticate by **driving a real Chrome via go-rod** (CDP) so Imperva's JS executes and mints clearance, performing the **full** Hover flow in-browser (login + API calls) to keep TLS/JS/cookies consistent.

Alternatives rejected:
- **Keep HTTP-scrape** — cannot pass Imperva; proven non-functional against live Hover.
- **Managed solver services (Scrapfly/ZenRows/2captcha)** — would route Hover *login credentials* through a third party; unacceptable for auth.
- **Python tooling (nodriver/zendriver/SeleniumBase-UC)** — the 2026 SOTA, but wrong language for a Go gRPC plugin; would bolt a Python runtime onto the plugin.
- **Reverse-engineer the Imperva sensor** in Go — fragile, breaks on every Imperva update, unmaintainable, more ToS-hostile than driving a real browser.

## Consequences

- Defeats Imperva on a **best-effort** basis (arms race): works today; Imperva updates may break it. Mitigated by full-browser (max longevity) + periodic re-evaluation of maintained solutions.
- Adds a **Chrome runtime dependency** (system / go-rod-cached / container) — cannot compile into the Go binary; heavier CI (download/cache or container; tens of seconds per login vs the old sub-second HTTP).
- ToS gray area (passing bot-protection); shipped best-effort with a README disclaimer; public MIT plugin, documented not hidden.
- The `hoverclient` interface + gRPC surface are unchanged — drivers/provider untouched; the change is internal + revertible by version pin (rollback = Hover automation disabled, since v0.4.2 can't auth live).
- Negative: go-rod's CDP + stealth has known gaps vs Imperva's behavioral/ML; viability is gated by a live login test before the full build.
28 changes: 28 additions & 0 deletions decisions/0002-fork-go-rod-for-maintenance-and-dep-control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 0002. Fork go-rod to GoCodeAlone/rod for maintenance + dep control

**Status:** Accepted
**Date:** 2026-05-31
**Decision-makers:** codingsloth@pm.me (directed), Claude (Opus 4.8)
**Related:** decisions/0001-real-browser-auth-for-imperva.md (amends the driver source), docs/plans/2026-05-30-headless-browser-auth-design.md

## Context

ADR 0001 chose go-rod (CDP) to defeat Imperva. But `github.com/go-rod/rod` is **stale**: no release since 2024, 179 open issues, 27 open PRs, 425 forks. The Imperva arms race means we will eventually need our own patches and dep bumps, and upstream has no active fix channel. `govulncheck` on the pinned tree is clean (no *called* vulns), but GitHub Dependabot flags 2 moderate `golang.org/x/sys` advisories, and we cannot get upstream to act. We need durable control over the browser driver our DNS automation depends on.

## Decision

Fork to **`github.com/GoCodeAlone/rod`** and maintain it ourselves. We **rename the module path** (not just a filesystem replace) so consumers can `require`/`replace` the fork with a matching module path in CI — a versioned `replace` to a differently-pathed module fails Go's module-path check. Bump the go directive 1.21 → 1.26.3. Keep divergence from upstream **minimal** to preserve mergeability: do NOT bleeding-edge-bump deps that break the API for no vuln benefit (fetchup 0.5.x breaks the launcher), and do NOT run whole-repo `modernize` (it churns 2600+ lines of generated proto). Address Dependabot (x/sys). `workflow-plugin-hover` imports `github.com/GoCodeAlone/rod` directly.

Alternatives rejected:
- **Stay on go-rod/rod** — stale, no fix channel, no control; the original problem.
- **Filesystem/replace without rename** — `replace ... => GoCodeAlone/rod vX` fails the module-path match; filesystem replace isn't CI-portable.
- **Switch driver (chromedp / playwright-go)** — chromedp is not stealth-focused; playwright-go drags a Node-driven Playwright runtime onto a Go gRPC plugin (rejected in ADR 0001). The driver spike already picked go-rod.

## Consequences

- We own go-rod maintenance: periodically merge upstream, apply our patches/dep bumps, run our own vuln gate (CodeQL on the fork + `govulncheck` + Dependabot).
- Divergence kept small → upstream merges stay feasible; the rename is the one large mechanical delta (104 files).
- Vuln posture: `govulncheck` clean + Dependabot x/sys addressed.
- The 3 nested example/tooling modules stay on the upstream path (not maintained surface; not in hover's dep graph).
- Undo cost: low — revert hover's import/require back to `github.com/go-rod/rod`.
- Negative: a maintained fork is ongoing work; if upstream revives, re-evaluate whether to drop the fork.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Headless-browser Hover auth — Adversarial Design Review 1

**Date:** 2026-05-30
**Phase:** design
**Design:** `docs/plans/2026-05-30-headless-browser-auth-design.md`
**Status:** PASS after design corrections

## Findings

| sev | class | loc | issue | resolution |
|---|---|---|---|---|
| Critical | validation | `Multi-Component Validation` | Browser implementation could proceed against local stubs even if the verified Hover account or go-rod Imperva pass failed. | Design now requires live viability as the plan's first task and blocks stub-only implementation when the test account is unavailable. |
| Important | testability | `Architecture` | Replacing the HTTP path outright would destroy existing `httptest` coverage or force every unit test to launch Chrome. | Design now requires an internal execution-backend seam: production browser backend, tests may keep local HTTP backend. |
| Important | config | `Chrome acquisition` | Runtime knobs were named inconsistently and not clearly exposed at provider config/env level. | Design now requires `browser_path`, `browser_download`, `browser_headless`, `browser_profile_dir` plus `HOVER_*` env aliases and `ROD_BROWSER_PATH` compatibility. |
| Important | secrets | `Stealth` | Persistent browser profile could be created inside repo and accidentally committed; profile contains auth cookies. | Design now requires sensitive default under `${XDG_STATE_HOME:-$HOME/.local/state}/wfctl/plugins/hover/browser-profile` and gitignore coverage for local test paths. |
| Important | docs drift | `Multi-Component Validation` | README still documents obsolete CSRF form login, causing operators to debug the wrong failure mode. | Design now requires README update for browser auth, Chrome/runtime config, bot challenge behavior, and sensitive profile handling. |
| Minor | optimization | `Browser scope` | Browser-login + Go HTTP API reuse may be viable, but adopting it before evidence would reintroduce TLS/JA3 risk. | Design keeps full-browser as default and makes login-only optimization evidence-gated. |

## Clean Checks

| area | result |
|---|---|
| Public contract | unchanged `hoverclient` + provider surface preserved. |
| Ecosystem fit | Go plugin implementation; no Python harness or standalone demo tool. |
| Security posture | no managed CAPTCHA/solver service; secrets redacted; profile treated as sensitive state. |
| Rollback | rollback is honest: v0.4.2 disables live Hover automation, not a working fallback. |

## Verdict

PASS. Remaining risk is empirical, not design-structural: go-rod may still fail Imperva where Playwright succeeds. The implementation plan must make that the first executable gate and stop if it fails.
Loading