STAGE 5 -- "juju-client blocker" DIAGNOSIS (read-only)
Captured 2026-08-01 by agent session; repo at d555b90, branch dc-dc-stage5-preconditions.
Governing: D-138 (RULED 2026-07-30) + D-132 amendment. Ledger: SEC-026, SEC-028.
NOTHING IN THIS CAPTURE WAS MUTATED. All fix commands below are UNEXECUTED PROPOSALS.
=====================================================================
VERDICT
=====================================================================
Both reported blockers are NOT blockers. They are D-138 working as ruled.
(a) "no juju client on the jumphost" -> CORRECT under D-138. vcloud is not
where the cloud-facing client belongs. The client EXISTS, registered and
LIVE, on the dc0 rack (vvr1-dc0), which is the D-138 client host.
(b) "ssh vr1-dc0-juju -> Permission denied (publickey)" -> CORRECT and expected.
The offered key was never installed on that machine and is not needed;
`juju ssh -m controller 0` from the rack is the working access path and
was used throughout this diagnosis.
`juju add-model` is reachable from the rack TODAY. The real remaining gaps are
downstream of add-model and are listed in section 5.
=====================================================================
1. ROOT CAUSE OF THE SSH REFUSAL -- MEASURED, NO INFERENCE
=====================================================================
Alias (vcloud ~/.ssh/config.d/vr1-sites):
Host vr1-dc0-juju HostName 10.12.8.5 User ubuntu
ProxyJump vr1-dc0-rack IdentityFile ~/vr1-office1-creds/office1_svc_ed25519
`ssh -v -o BatchMode=yes vr1-dc0-juju` -- the hop chain is CORRECT end to end:
hop 1 vcloud -> voffice1 OK
hop 2 -> 172.31.0.2 (rack), key vr1-dc0_svc_ed25519 SHA256:DBkeStCyf2qGi3SY...
"Server accepts key" / "Authenticated to 172.31.0.2 (via proxy)"
hop 3 channel_connect_stdio_fwd: 10.12.8.5:22 -- reached, TCP fine
"Host '10.12.8.5' is known and matches the ED25519 host key"
"Found key in /home/jessea123/.ssh/known_hosts:28" <- host key CURRENT
Offering office1_svc_ed25519 SHA256:iUYex2kmvtdGTPiMKRZ2Nfywqa8pWacVV1GEEx6sscI
"Authentications that can continue: publickey"
ubuntu@10.12.8.5: Permission denied (publickey).
So: hostname, port, user, hop chain, per-hop identity selection and host key are
ALL correct and measured working. The single failing element is the final key.
WHAT THE TARGET ACTUALLY ACCEPTS (read via `juju ssh -m controller 0`):
/home/ubuntu/.ssh/authorized_keys -- exactly 2 lines:
4096 SHA256:VkipepSQPOmCyx1RS5HvM/F17U7c8HZAGO0LWHEgW/w Juju:juju-client-key (RSA)
4096 SHA256:pBDEJsaS7fH/JLdVWm3A9aOlBN0jiYIv4J9AjVPk7YI Juju:juju-system-key (RSA)
mtime 2026-07-31 01:00:03
SHA256:Vkipep... is byte-identical to the rack's
~/.local/share/juju/ssh/juju_id_rsa.pub (juju-client-key).
The office1 key SHA256:iUYex2... is ABSENT. Refusal is therefore CORRECT.
IDENTITY TIE -- the box read is the box that refused (hard rule 2, not "almost
certainly"). On `juju ssh -m controller 0`:
enp1s0 10.12.8.5/22
enp2s0 10.12.4.5/22
hostname: subtle-grouse
/etc/ssh/ssh_host_ed25519_key.pub -> 256 SHA256:Gr7kw5GTa+taeYiknHcLywc6Mjnwr82ly7gm2XetqXQ
That fingerprint is EXACTLY the host key `ssh -v` matched at known_hosts:28 for
10.12.8.5. Same host, proven. `juju machines -m controller` reports machine 0
at 10.12.4.5 / inst subtle-grouse -- the two addresses are two NICs on one VM.
WHY THE KEY IS ABSENT -- deploy-time evidence, and one candidate RULED OUT:
/var/lib/cloud/instance/user-data.txt mtime 2026-07-31 00:58:39
machine boot (`uptime -s`) 2026-07-31 00:58:15
ssh keys found in user-data.txt 0
/var/lib/cloud/instance/cloud-config.txt line 131 "ssh_authorized_keys:"
line 133 "Juju:juju-client-key"
grep for vr1-office1 / jessea123 in cloud-config.txt -> NO MATCH
vendor-cloud-config.txt / vendor-data.txt / vendor-data2.txt -> 0 ssh keys
`juju ssh-keys -m controller --full` -> "No keys to display."
`juju model-config -m controller authorized-keys` -> ERROR "authorized-keys" is
not a key of the currently targeted model (3.6 moved it to `juju ssh-keys`).
~/.local/share/juju/bootstrap-config.yaml:48 authorized-keys: | -> one ssh-rsa
entry only (the juju client key).
RULED OUT: "the machine was redeployed BEFORE the key import." The MAAS key
import into the new dc0 region was 2026-07-30; this machine deployed
2026-07-31 00:58 -- AFTER the import -- and STILL received only Juju keys.
Timing is not the explanation.
PROXIMATE CAUSE (measured): the deploy-time cloud-config supplied ONLY Juju's
own keys. Nothing ever put the office1 key on this VM.
DISTAL CAUSE (NOT MEASURED -- declared, not asserted): either the MAAS user
juju uses in the dc0 region (juju-vr1-dc0, minted fresh per SEC-028) has no
imported ssh key, or juju's bootstrap cloud-config supersedes the user's key
set. I could NOT distinguish these -- see section 6.
=====================================================================
2. WHERE THE JUJU CLIENT SHOULD LIVE, AND WHAT EXISTS -- ALL MEASURED
=====================================================================
D-138 RULED (1): tools that dial the CLOUD at L3 -- the Juju client and the
phase-03..06 `openstack` CLI -- run from INSIDE the DC. voffice1 stays Plane-2
only for application-proxied / qemu+ssh work. => the dc0 client host is the dc0
RACK (vvr1-dc0), consistent with SEC-028, which distributed the juju service
credential to the rack precisely because "D-138 moved the cloud-facing Juju
client INTO the DC -- so `rack` is now a credential-bearing host-role."
COMPONENT vcloud (jumphost) vr1-dc0-rack (D-138 host)
--------------------- ----------------------- -----------------------------
juju binary ABSENT (`which juju` PRESENT /snap/bin/juju
rc=1; snap list shows 3.6.27-genericlinux-amd64
only core24 + snapd) snap juju 3.6.27 rev 35621 3/stable
~/.local/share/juju ABSENT (No such file) PRESENT, 8 entries
controller reg. n/a PRESENT: vr1-dc0-controller*
credential n/a PRESENT: exactly ONE
repo scripts FULL repo (this clone) PARTIAL hand-stage, no .git
openstack CLI not checked (not the MISSING <-- D-138 gap
D-138 host)
maas CLI ABSENT, no ~/.maascli.db (not checked)
vcloud negative instrumented: `which juju` (rc=1), `snap list` (2 rows,
neither juju), `ls ~/.local/share/juju/` (No such file or directory).
Three independent instruments agree -- this is not a mis-scoped probe.
LIVE PROOF the rack client works (run 2026-08-01):
$ juju whoami
Controller: vr1-dc0-controller / Model: vr1-dc0 / User: admin
$ juju controllers
vr1-dc0-controller* vr1-dc0 admin superuser vr1-maas 1 model 1 node 3.6.27
$ juju models
controller vr1-maas maas available 1 machine 4 cores 1 unit admin
Last connection: just now <-- the controller IS UP and reachable
$ juju machines -m controller
0 started 10.12.4.5 subtle-grouse ubuntu@22.04 default Deployed
$ juju show-controller
controller-uuid 5556adb3-9cce-4b4e-8e64-f9c0a14c08f7
api-endpoints ['10.12.4.5:17070','10.12.8.5:17070','[fd50:840e:74e2:220::5]:17070']
~/.local/share/juju/clouds.yaml
vr1-maas / type maas / oauth1 / endpoint http://10.12.8.6:5240/MAAS
(the DC-LOCAL region -- D-132 amendment satisfied; no cross-fiber MAAS dep)
NOTHING IS MISSING for `add-model`: binary, registration and credential are
all present and demonstrably functional against the live controller.
=====================================================================
3. SEC-026 CONTROL (1) -- DISCHARGED BY MEASUREMENT
=====================================================================
SEC-026 requires: "Verify after any client rebuild that `juju credentials
--client` on a DC host lists exactly one."
$ juju credentials --client (on vr1-dc0-rack)
Client Credentials:
Cloud Credentials
vr1-maas vr1-dc0-cred
EXACTLY ONE. No vr1-dc1-cred present. The per-DC isolation SEC-018/-019 exist
to create is INTACT on the dc0 rack, measured 2026-08-01.
BOTH SIDES OF THE CONTROL MEASURED. SEC-026 warns that voffice1's client store
"holds BOTH vr1-dc0-cred and vr1-dc1-cred" and that copying it to a DC host
"would destroy the per-DC isolation." Confirmed on voffice1 2026-08-01:
$ juju credentials --client (on voffice1)
Client Credentials:
Cloud Credentials
vr1-maas vr1-dc0-cred, vr1-dc1-cred
Two on voffice1, ONE on the rack -> the forbidden whole-store copy did NOT
happen. The control is not merely unviolated, it is measured on both sides.
voffice1 juju state, VERIFIED (previously carried, now measured 2026-08-01):
which juju -> /snap/bin/juju
juju version -> 3.6.27-genericlinux-amd64
juju controllers-> ERROR No controllers registered.
~/.local/share/juju/ -> full store present (accounts, bootstrap-config,
clouds, controllers, credentials, models,
public-clouds, ssh)
So voffice1 has a BINARY and a CREDENTIAL STORE but no controller
registration -- it cannot perform the runbook's voffice1-labelled juju steps.
This is the measured basis for gap G1.
CREDENTIAL RESIDENCY OF THE PROPOSED FIX: NONE NEW. The fix in section 4 moves
no key, copies no credential, and creates no new residency. Every credential
location it touches is ALREADY REGISTERED in creds-manifests/vm-secret-locations:
line 123 rack vr1-dc0 ssh vr1-dc0-rack:~/vr1-dc0-creds/*
line 124 rack vr1-dc0 ssh vr1-dc0-rack:~/.local/share/juju/*
line 127 rack vr1-dc0 ssh vr1-dc0-rack:~/repo-stage/overlays/vr1-dc0-octavia-pki.yaml
NO new security-ledger row is owed for the section-4 fix.
=====================================================================
4. PROPOSED MINIMAL FIX -- UNEXECUTED, FOR OPERATOR APPROVAL
=====================================================================
FIX-0 (NO-OP, state it explicitly): do NOT install a juju client on vcloud and
do NOT add the office1 key to the controller VM. Both would work AGAINST D-138
and against the measured facts. The reported blocker requires no key movement.
FIX-1 -- HYGIENE ONLY; NOT REQUIRED FOR add-model (see the guard measurement
below before approving). Clears the dangling current-model pointer left by the
2026-07-31 teardown.
MEASURED SYMPTOM (on vr1-dc0-rack):
$ juju status
ERROR model vr1-dc0-controller:admin/vr1-dc0 not found
~/.local/share/juju/models.yaml has `current-model: admin/vr1-dc0` while its
`models:` map contains ONLY admin/controller -- the destroyed model's
pointer outlived the model.
IMPORTANT -- this does NOT block Step 3.5. Measured, in the runbook's own
guard shape:
$ juju models --format json | jq -r '.models[]?.name'
admin/controller
so the guard's `grep -qx "$DC_MODEL"` correctly finds no match and
`juju add-model vr1-dc0` proceeds. FIX-1 is hygiene for bare `juju status`,
not a precondition. Proposed command, run ON vr1-dc0-rack:
juju switch controller
Minimal because it uses juju's own state machine rather than hand-editing
models.yaml, and `add-model` will re-point current-model anyway.
REVERT: juju switch vr1-dc0 (once that model exists)
FIX-2 -- SEPARATE, OPTIONAL, DO NOT BATCH WITH FIX-1. Operator ssh to the
controller VM. Only if the operator wants `ssh vr1-dc0-juju` to work as a
convenience; it is NOT required for Stage 5, because `juju ssh -m controller 0`
already works and was the instrument for this entire diagnosis.
PREREQUISITE, MEASURED AS ABSENT -- this is a SEPARATE step, not part of the
command. The office1 PUBLIC key is NOT on the rack. Measured 2026-08-01:
vr1-dc0-rack $ ls -la ~/.ssh/
authorized_keys, known_hosts, known_hosts.old -- NO key files
vr1-dc0-rack $ find ~ -maxdepth 3 -name '*.pub'
/home/jessea123/vr1-dc0-creds/maas-virsh_ed25519.pub (only this one)
vcloud $ ls -la ~/vr1-office1-creds/
office1_svc_ed25519.pub 97 bytes, mode 0644 <- source, exists here
So FIX-2 requires FIRST staging that .pub to the rack (public key, not secret),
and only then:
juju add-ssh-key -m controller "<contents of office1_svc_ed25519.pub>"
The `-m controller` is REQUIRED and is not optional decoration: `add-ssh-key`
targets the CURRENT model, which is the dangling admin/vr1-dc0, so the bare
form fails with the same "model not found" documented under FIX-1.
NOT RECOMMENDED as part of this fix. It (a) adds an operator key to a
juju-managed authorized_keys set, (b) requires the office1 PUBLIC key to be
placed on the rack, and (c) widens shell access to the controller beyond the
juju-mediated path. If declined, the correct standing answer is: reach the
controller VM with `juju ssh -m controller 0` from the rack, and treat the
vr1-dc0-juju alias as non-functional-by-design.
If FIX-2 IS taken, the vr1-dc0-juju alias comment in
scripts/site-ssh-config.sh needs updating -- the fragment currently asserts
DC nodes carry the office1 key, which is FALSE for juju-provisioned machines
(measured above). That is a DOCFIX, not part of the mutation.
=====================================================================
5. REAL REMAINING GAPS -- DOWNSTREAM OF add-model. LOGGED, NOT FIXED.
=====================================================================
G1 -- HOST SPLIT THE RUNBOOK DOES NOT DESCRIBE.
runbooks/dc-dc-phase4-juju-bundle-per-dc.md:379 labels the add-model block
**MUTATION -- voffice1 -- create THIS DC's model (guarded)**
and :398 labels the spaces gate **CHECK (read-only) -- voffice1 --**.
Both are STALE w.r.t. D-138, which is dated 2026-07-30 and moved the juju
client into the DC. voffice1 has a juju BINARY (3.6.27) but `juju controllers`
there returns ERROR No controllers registered -- so the runbook as written
targets a host that cannot perform the step. DOCFIX owed. NOT EDITED here
(a) because hard rule 1 forbids adjacent changes mid-step and (b) because the
DOCFIX must be sized against every other voffice1-pinned juju reference, not
just these two lines. Grep next-free DOCFIX-NNN before assigning a number.
G2 -- scripts/juju-spaces-check.sh NEEDS repo + juju ON THE SAME HOST.
The runbook runs it immediately after add-model. It is present on the rack at
~/repo-stage/scripts/juju-spaces-check.sh -- and the staged copy is IDENTICAL
to the repo copy (sha256 adb9dbd38a27e663083f37b64e17ca98d870180f010b8d499a2
9f3fbfe7d1d5d on BOTH vcloud and vr1-dc0-rack). jq and python3 3.12.3 are
present on the rack. So this step CAN run today. But ~/repo-stage is a
9-file HAND-STAGED copy with NO .git (verified: `find / -xdev -maxdepth 5
-name .git -type d` on the rack returned NOTHING), so it has no provenance
and will drift silently. Drift measured 2026-08-01: 8 of 9 files byte-identical
to the repo; the 9th (vr1-dc0-octavia-pki.yaml) exists ONLY on the rack.
G3 -- overlays/vr1-dc0-octavia-pki.yaml IS ABSENT FROM THE vcloud REPO TREE.
Two independent instruments agree: `ls -la overlays/` lists 6 files and not
this one; `sha256sum overlays/vr1-dc0-octavia-pki.yaml` -> No such file.
It IS present on the rack, mode -rw------- (0600), 4682 bytes, and it is
gitignored by design (.gitignore:46 `overlays/*octavia-pki.yaml`).
vm-secret-locations registers it at BOTH locations (line 90 headend
`<repo>/overlays/vr1-dc0-octavia-pki.yaml`, line 127 rack), so the register is
correct and the rack copy's 0600 handling is correct.
CONSEQUENCE -- CITED FROM CODE, NOT EXECUTED. preflight.sh runs on the repo
host (vcloud). Its P4 delegate hard-FAILS on the missing file:
scripts/pre-flight-checks.sh:155-162 (CHECK 0)
OVL="overlays/${DC}-octavia-pki.yaml"
if [ -f "$OVL" ]; then ... else
fail "MISSING $OVL (gitignored secret; place it or regenerate via
runbook Step 1.0-GEN before deploy)"
`fail` -> preflight exit 1 -> "do NOT deploy". So the mandated pre-add-model
gate CANNOT PASS on vcloud as the tree stands. This is a CODE READ; I did not
run preflight.sh (out of mandate scope), so the FAIL is cited, not observed.
CORRECTION TO AN EARLIER DRAFT OF THIS CAPTURE: P7 does NOT depend on this
overlay and must not be cited here. scripts/octavia-pki.sh reads
overlays/${SITE}-vips.yaml (line 169) and WRITES the octavia-pki overlay; it
is the generator, not a consumer. Only P4 CHECK 0 gates on the file's presence.
Note preflight.sh:144-146 SILENTLY SKIPS a missing overlay at P2
(`[ -f "$_ovl" ] && P2_ARGS=...`), so P2 would validate a bundle merged
WITHOUT the octavia overlay and still report on it -- the failure surfaces at
P4/P7, not P2. NOT INVESTIGATED FURTHER -- out of mandate scope. Flagged so it
is not discovered at deploy time.
G4 -- `openstack` CLI is MISSING on the dc0 rack.
Measured with THREE instruments, because `which` in a non-interactive ssh
shell misses ~/.local/bin: `which openstack` -> MISSING; `ls ~/.local/bin`
-> No such file or directory (the dir does not exist at all); `snap list |
grep -i openstack` -> no openstack snap. All three agree. D-138 names the
phase-03..06 `openstack` CLI work explicitly as DC-resident. This is not a
Stage-5 add-model blocker but IS a D-138 definition-of-done gap that will
block phase-03. Also missing on the rack: yq, charmcraft. Present: python3
3.12.3, jq, git.
=====================================================================
6. WHAT I COULD NOT MEASURE, AND HOW I KNOW I COULD NOT
=====================================================================
UNMEASURED: whether MAAS user `juju-vr1-dc0` in the dc0 region has an imported
ssh key. This is the DISTAL half of section 1's root cause. It is DECLARED, not
guessed. Attempts and why each failed:
- `sudo -u postgres psql -d maasdb` on vr1-dc0-maas -> "sudo: unknown user
postgres" (snap postgresql runs as _daemon_).
- `/snap/bin/postgresql.psql -U postgres -h 127.0.0.1` -> asks for a password;
NOT pursued (would mean handling credential material).
- `sudo -u _daemon_ /snap/bin/postgresql.psql` -> snap refuses: "is not a snap
cgroup for tag snap.postgresql.psql".
- maas CLI profile: ABSENT on vcloud (no ~/.maascli.db) and EFFECTIVELY absent
on voffice1 -- ~/.maascli.db there is ZERO BYTES (Jul 29) and sqlite reports
"no such table: profiles". vr1-dc0-maas has no ~/.maascli.db either.
Operator command that would settle it (UNEXECUTED, needs a logged-in profile
pointed at http://10.12.8.6:5240/MAAS -- verify the endpoint BEFORE trusting
the answer; a profile aimed at 10.10.0.20 is the OFFICE1 region and would give
a confidently wrong answer):
maas <profile> users read
maas <profile> sshkeys read
This gap does NOT change the fix. Section 4 is correct either way, because the
proximate cause (only Juju keys were ever supplied to this machine) is measured
directly on the machine.
REFUSED BY THE PERMISSION HARNESS -- verbatim, and NOT retried in an altered
shape (the query was `ls ~/.maascli.db; maas list; ls -la ~/vr1-dc0-creds/` on
vr1-dc0-rack):
PreToolUse:Bash hook error: [python3 "$CLAUDE_PROJECT_DIR"/.claude/hooks/
guard-destructive.py]: BLOCKED by .claude/hooks/guard-destructive.py:
prints the MAAS API key (DOCFIX-016); use 'maas admin ...' directly
Consequence: the rack's maas-profile state and the contents of
~/vr1-dc0-creds/ were NOT enumerated this session. SEC-028's claim that the
credential was distributed to the rack is therefore CARRIED FROM THE LEDGER,
not re-measured here.
INSTRUMENT CORRECTION MADE MID-SESSION (recorded because the first result was a
false negative of exactly the shape this repo has been bitten by): an initial
`find ~ -maxdepth 2 -name preflight.sh` on the rack could NOT have found a repo
clone -- repo scripts sit at depth 3. Re-run as `find / -xdev` over unique
filenames, it FOUND ~/repo-stage. The corrected result is what section 5 uses.
Likewise `grep -rn ... creds-manifests/vm-secret-locations/` (trailing slash on
a FILE) returned nothing and was re-run without the slash, which found all six
rack rows. Neither false negative reached a conclusion.
=====================================================================
7. COMMANDS RUN (all read-only; none mutated state)
=====================================================================
vcloud: which juju; snap list; ls ~/.local/share/juju; ls ~/ | grep creds;
ssh -v -o BatchMode=yes vr1-dc0-juju; sha256sum <9 repo files>;
ls -la overlays/; git check-ignore -v; grep over docs/ and
creds-manifests/vm-secret-locations; sed over runbook + preflight.sh;
python3 sqlite read of ~/.maascli.db (endpoint URL only, no keys)
vr1-dc0-rack: hostname; which juju; juju version; snap list; ls ~/.local/share/juju;
juju whoami / controllers / models / credentials --client /
show-controller / machines -m controller / ssh-keys -m controller /
model-config -m controller authorized-keys / models --format json;
cat clouds.yaml models.yaml; grep accounts.yaml (password excluded);
ssh-keygen -lf juju_id_rsa.pub; find / -xdev for repo + .git;
sha256sum <9 staged files>; ls -la repo-stage; which openstack et al
subtle-grouse (via `juju ssh -m controller 0`): authorized_keys fingerprints +
comments + mtime; ip -4 -o addr; ssh-keygen -lf host key; hostname;
stat/grep over /var/lib/cloud/instance/*
vr1-dc0-maas: hostname; snap list; ps; ss -lntp; three failed psql attempts
ls -la ~/vr1-office1-creds/ (names/modes only); grep+sed over
scripts/pre-flight-checks.sh and scripts/octavia-pki.sh;
bash scripts/repo-lint.sh; git status --short; git check-ignore -v
vr1-dc0-rack (cont.): ls -la ~/.ssh/; find ~ -maxdepth 3 -name '*.pub';
ls ~/.local/bin; snap list | grep openstack
voffice1: ls ~/.maascli.db; python3 sqlite read (no profiles table);
which juju; juju version; juju controllers; juju credentials --client;
ls ~/.local/share/juju/
=====================================================================
8. REPO-LINT STATE AT CAPTURE TIME -- NOT CAUSED BY, NOT FIXABLE BY, THIS WORK
=====================================================================
$ bash scripts/repo-lint.sh
[WARN] L1 docs/design-decisions.md: 239 non-ASCII byte(s) (legacy carve-out)
[FAIL] L10 pending change set modifies state-bearing surface(s)
[docs/audit/: docs/audit/d139-carve-dryrun-20260801.txt,
docs/audit/stage5-juju-client-blocker-20260801.txt] without touching
docs/CURRENT-STATE.md -- GA-R1 rule 8 (C1)
FAIL: repo lint (1 fail, 1 warn, 645 files scanned) EXIT=1
L10 was ALREADY FAILING before this capture existed: the other file it names,
docs/audit/d139-carve-dryrun-20260801.txt, is a concurrent agent's and has
mtime 00:18 against this file's 00:25. This capture is now ALSO named in the
finding but did not create it.
NOT RESOLVED HERE, deliberately: clearing L10 requires touching
docs/CURRENT-STATE.md in the SAME commit, and this session was scoped out of
both that file and any git add/commit/push. Resolution must be coordinated by
whoever owns docs/CURRENT-STATE.md for this change set.