get2knowio / get2knowio/remo

Code review findings: 15 defects from /code-review of #32 (005-credential-broker)

Open
#33 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
2
Forks
0
Avg merge
15h 59m
Merged PRs (30d)
9

Description

This issue collects the 15 confirmed/plausible defects surfaced by a high-effort code review (5 finder angles + 1 verifier + 1 sweep) over the credential-broker work in #32 / branch `005-credential-broker`.

Most-severe first. Each entry is independently actionable; consider splitting into per-finding issues before triage if a single thread gets unwieldy.

---

## 1. Showstopper — `broker_install` URL `v0.1.0` returns 404 today

- **File**: `ansible/roles/broker_install/defaults/main.yml:7`
- **Failure**: `curl -sI -L https://github.com/get2knowio/remo-broker/releases/download/v0.1.0/...` returns HTTP/2 404 for both the binary and `.sha256`. Every `remo {aws,hetzner,incus,proxmox} create` now includes `broker_install` unconditionally, so the very first user to upgrade hits a play failure during their first create.
- **Fix sketch**: Coordinate with `get2knowio/remo-broker` to cut v0.1.0 (see companion remo-broker prompt). Gate the role on \`REMO_BROKER_BACKEND\` being set until then.

## 2. Security — mint exceptions leak the freshly-minted token via `{payload!r}`

- **File**: `src/remo_cli/providers/broker.py:86` (`_onepassword_mint`) and `:146` (`_vault_mint`)
- **Failure**: 1Password / Vault response returns the token under an unexpected key (schema drift, future API version). Token is present in \`payload\` but extractor misses it; \`raise BackendError(f\"...{payload!r}\")\` formats the entire payload — including the live token — into the exception message. \`providers/hetzner.py:215\` then prints it via \`print_error(f\"Bootstrap minting failed: {exc}\")\`. Token ends up in shell scrollback / CI logs / terminal recording.
- **Fix**: Never format \`payload\` into the exception message. Log only field-presence info (\"missing 'token_id'; saw keys: [...]\").

## 3. Showstopper — `lookup('pipe', 'fnox get …')` hard-fails when fnox absent

- **File**: `ansible/group_vars/all.yml:4` (also `ansible/roles/aws_server/defaults/main.yml:16-17`, `ansible/aws_teardown.yml:26-27`)
- **Failure**: Pipe lookups evaluate eagerly during variable resolution. If `fnox` is not on PATH or the key isn't set, Ansible raises before any user-facing validation task can fire. Previously \`lookup('env', ...)\` returned empty gracefully and a friendly-error \`assert\` produced the install-pointer message.
- **Fix**: Wrap the pipe call in a default and surface a friendly error in a pre-flight task. Example: \`lookup('pipe', 'fnox get hetzner_api_token 2>/dev/null || true') | default('', true)\`.

## 4. Subsystem dead — `@cli.result_callback` never fires because subcommands `sys.exit()`

- **File**: `src/remo_cli/cli/main.py:18`
- **Failure**: Every subcommand in `cli/providers/*.py`, `cli/audit.py`, `cli/rotate.py`, `cli/init.py` ends with `sys.exit(rc)`. SystemExit propagates past Click's \`MultiCommand.invoke\`, so the result_callback never runs. Consequence: \`overdue_reminders()\` (T083a passive overdue-rotation warning) AND the pre-existing passive update-check are dead code in practice. Tests cover \`overdue_reminders()\` directly, hiding the dead-hook bug.
- **Fix**: Either move the post-command hook to a Click `result_callback` that returns from subcommands (refactor all `sys.exit(rc)` → `return rc`), or wrap commands in a `try/except SystemExit` that runs the hook before re-raising. Add a CLI integration test that asserts the hook actually fires.

## 5. Destructive — `_aws_sm_revoke` deletes the per-developer IAM role shared across instances

- **File**: `src/remo_cli/providers/broker.py:195`
- **Failure**: \`_ensure_broker_instance_role\` names the role by \`dev_id\` alone, so two \`remo aws create\` calls share the role + profile. When the user destroys instance A, \`_aws_sm_revoke\` calls \`_attach_broker_deny_all_policy\` then \`_delete_broker_instance_role\` against that shared role. Deny-all immediately breaks IMDS credentials on instance B/C/…; the delete then removes the role permanently. No \`list-instance-profiles-for-role\` guard.
- **Fix**: Make the role per-instance (or check for in-use instance profiles before tearing down). Document the per-developer-vs-per-instance scoping decision in `contracts/bootstrap-delivery.md`.

## 6. FR-020 silently broken — Hetzner label keys reject `:`

- **File**: `src/remo_cli/providers/hetzner.py:228` (write site); `:172` (silent swallow)
- **Failure**: Hetzner Cloud's label-key regex disallows \`:\`. PUTs of \`remo:bootstrap-token-id\` / \`remo:rotation-cadence-days\` / \`remo:last-rotation-at\` return HTTP 400. \`_set_server_label\` catches \`urllib.error.URLError\` silently → label never lands → \`_lookup_token_id\` returns None → \`revoke_before_destroy\` skips revocation → backend token orphaned forever. Same defect makes \`_read_rotation_metadata\` permanently return (7, None, None), marking every Hetzner host overdue.
- **Fix**: Use `_`/`.`/`-` instead of `:` in label keys (e.g., `remo_bootstrap_token_id`). Audit the entire `remo:*` label namespace; update tests + reader.

## 7. FR-020 silently broken — `revoke_before_destroy` treats `None` from `_lookup_token_id` as success

- **File**: `src/remo_cli/core/broker_revoke.py:33` (and `:84` for the lookup that swallows)
- **Failure**: \`_lookup_token_id\` wraps the entire Hetzner GET in \`except Exception: return None\`, returning the same value when the API is unreachable as when no label was ever set. Caller then \`return True\` and destroy proceeds. Asymmetric: a successful lookup followed by a revoke failure correctly blocks with exit 5, but a lookup failure silently succeeds → a Hetzner outage during destroy = guaranteed leaked token.
- **Fix**: Distinguish \"no token to revoke\" (return None) from \"lookup failed\" (raise; caller honors `--force`). Drop the bare except.

## 8. FR-020 only enforced on Hetzner — AWS/Incus/Proxmox `destroy` skip the hook

- **File**: `src/remo_cli/providers/aws.py:665`, `incus.py:247`, `proxmox.py:356`
- **Failure**: \`grep -rn revoke_before_destroy src/remo_cli/providers/\` shows the only caller is `hetzner.py:344`. The other three providers' \`destroy()\` paths run the teardown playbook / API call without ever invoking \`broker.revoke_bootstrap_token\`. The exit-5 contract in `contracts/cli-surface.md` is unreachable on three of four providers.
- **Fix**: Wire \`revoke_before_destroy\` into every provider's `destroy()`. See deferred T074 in `specs/005-credential-broker/tasks.md`.

## 9. Service starts in `failed` state — handler always fires on fresh install

- **File**: `ansible/roles/broker_install/handlers/main.yml:2` (paired with `tasks/main.yml:70,100,107`)
- **Failure**: The template renders on every play (changed=true on first install), notifying \`restart remo-broker\`. The handler unconditionally \`state: restarted\`s the unit at end-of-play, but with no token on a fresh install the daemon exits non-zero → systemd transitions to \`failed\`. The post-install verify task (line 107) is gated on \`broker_token_present\` and skips, so Ansible reports green. Operator sees success; broker is broken.
- **Fix**: Either gate the handler on \`broker_token_present | default(false)\`, or make the verify task unconditional with a friendly skip-message when no token is present yet.

## 10. Helper permanently empty — `add_node` `touch` blocks the real install

- **File**: `src/remo_cli/providers/incus.py:706` (and `providers/proxmox.py:128`); blocks `ansible/roles/incus_bootstrap/tasks/main.yml` copy task with `force: false`
- **Failure**: \`add_node\` SSH-runs \`sudo touch /usr/local/libexec/remo-broker-tokens; sudo chmod 0755 ...\`, creating an empty zero-byte file. The later \`incus_bootstrap\` role's helper copy task uses \`force: false\` so it never overwrites. The broker daemon's expected node-side dispatcher exits 0 with no output. Every per-developer token-management on Incus/Proxmox is silently broken.
- **Fix**: Drop the `touch`; let the Ansible role install the real helper. Or change the Ansible task to `force: true` once the helper script content is known.

## 11. Supply-chain — whitespace-only `.sha256` body disables integrity check

- **File**: `ansible/roles/broker_install/tasks/main.yml:44`
- **Failure**: \`failed_when: content == ''\` only catches a literally empty body. A \`.sha256\` containing only \`\\n\` or spaces passes the check; \`(content).split() | first | default('')\` yields \`''\`; \`get_url\` runs with \`checksum: \"sha256:\"\` which Ansible accepts as no-verify. A compromised mirror could substitute a tampered broker binary and the install would succeed silently.
- **Fix**: Validate the extracted checksum against `^[0-9a-fA-F]{64}$` before passing to `get_url`.

## 12. `remo init` is a no-op — config.yml not bridged to env

- **File**: `src/remo_cli/cli/init.py:37` (and every consumer that reads `os.environ.get(\"REMO_BROKER_BACKEND\", \"\")`)
- **Failure**: \`_save_config\` persists \`broker.backend\` to \`~/.config/remo/config.yml\`. But \`providers/hetzner.py:298\`, \`providers/aws.py:561\`, \`core/broker_revoke.py:26\`, \`cli/rotate.py:85\` all read the env var. After \`remo init --backend 1password\` the broker code paths remain inert until the user separately exports \`REMO_BROKER_BACKEND=1password\`.
- **Fix**: Add `core/broker_config.py::get_backend()` with env-first / file-fallback resolution; replace every direct `os.environ.get(\"REMO_BROKER_BACKEND\", ...)` with the helper. Same treatment for `REMO_BROKER_ADMIN_SA_KEY`.

## 13. JSONC strip corrupts string literals containing `//`

- **File**: `src/remo_cli/core/devcontainer.py:59`
- **Failure**: Regex \`(^|[^:])//[^\\n]*\` strips \`//\` line-comments but also matches inside string literals whenever the preceding char isn't \`:\` (e.g. \`\"foo\": \"a//b\"\`). \`json.loads\` then fails; \`ensure_socket_mount\` returns False silently. Currently masked by the helper having no production callers; will bite the moment integration ships.
- **Fix**: Use a proper JSONC tokenizer (or commentjson / json5) that skips quoted strings. Add a regression test for `\"path\": \"a//b\"`.

## 14. Naive vs aware datetime — `--since` filter and freshness check crash uncaught

- **File**: `src/remo_cli/cli/rotate.py:78` (`_parse_iso`) and `src/remo_cli/core/audit.py:100` (`_parse_ts`)
- **Failure**: \`datetime.fromisoformat\` returns a NAIVE datetime when input lacks \`Z\` or explicit offset. \`_now() - last_rotation\` (rotate.py:78 freshness check, :71 `_is_overdue`) and \`_parse_ts(ln.ts) >= cutoff\` (audit.py:68) then raise \`TypeError: can't subtract offset-naive and offset-aware datetimes\`. \`remo rotate-bootstrap\` and \`remo audit --since 1h\` crash uncaught if any broker-written timestamp lacks the offset suffix.
- **Fix**: Normalize to aware in `_parse_iso`/`_parse_ts` (`tzinfo=timezone.utc` if absent). Add tests with bare-ISO inputs.

## 15. SSH MITM window — `accept-new` on a freshly-allocated public IP

- **File**: `src/remo_cli/providers/hetzner.py:132`
- **Failure**: First SSH push uses \`-o StrictHostKeyChecking=accept-new\`. No out-of-band host-key verification. An on-path attacker (BGP hijack, hostile network, stale IP reuse) presenting their own host key gets the bootstrap token piped on stdin; \`accept-new\` pins it without complaint, subsequent runs continue silently against the wrong endpoint.
- **Fix**: Hetzner Cloud's API surfaces the host fingerprint after server creation — fetch it and verify against the live key before the push. Failing that, document the residual risk in \`docs/credential-broker.md\` threat model.

---

**Method**: 5 parallel finder angles (line-by-line / removed-behavior / cross-file / language-pitfalls / wrapper-correctness) → 8 candidates each → dedup → 1 verifier with quote-the-line evidence → sweep for gaps. Of 40 raw candidates, 24 verified CONFIRMED, 2 PLAUSIBLE, 1 REFUTED; 5 from the sweep made the top-15 cut.

cc / context: #32 (PR introducing these), `specs/005-credential-broker/`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Contributor guide

Open the contributing guide

Research direction

Split the collection into per-finding issues, then read the referenced files and the related contracts and tasks under specs/005-credential-broker/. Start with the highest-severity findings and compare each claimed failure with the cited entry point; run the relevant existing tests and add the regression coverage named in the findings. Done means each confirmed defect has a focused change, verification, and an unambiguous result.

Written by the indexing model from the issue text.

Assessment

Tech stack
ansible, python
Domain
backend, cli, devops, infrastructure, security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.