Files
Lucas BergerandClaude Opus 5 dad8774ed9 Document verified odin access path and Unraid API
Replace the assumed/unconfirmed environment notes with facts verified from
this box, and add docs/odin-access.md as the access reference.

Verified:
- Tailscale is the only path from dev to odin. dev (100.94.16.46) is a VPS
  with a public IP, not a LAN machine; odin is 100.101.253.105, and a subnet
  route puts 192.168.90.0/24 over tailscale0 at the same ~34ms RTT.
- The myunraid.net host is the canonical nginx vhost, not a cloud relay --
  DNS resolves it to the private 192.168.90.103, so traffic stays on the
  tailnet. The bare IP 404s on /graphql, and plain HTTP 302-redirects to the
  myunraid host while stripping the x-api-key header.
- GraphQL is live: unauthenticated POSTs return HTTP 200 with an
  UNAUTHENTICATED error body, so 200 must never be read as success.

Corrects unraid-docker-manager's claim that no SSL ignore is needed: TLS
verification fails from dev because ca-certificates 20250419 cannot chain
Let's Encrypt intermediate YR1. The cert is genuine and the claim likely
holds on odin itself, but curl needs -k here.

Container-polling field behaviours (UPPERCASE state, /-prefixed names,
PrefixedID ids, no isUpdateAvailable in 7.2) are carried over from
unraid-docker-manager, which is credited as the authoritative source.

No API key is present on this box; .env.unraid-api is gitignored there and
absent from the clone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 20:33:29 -04:00

150 lines
5.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Accessing odin
Reference for reaching the Unraid host (**odin**) from this workstation (`dev`) and
driving its API. Everything here was verified from `dev` on 2026-08-15; the "Verified"
column says how.
Related repo: **`luckberg/unraid-docker-manager`** on `git.bergerhouse.net` — an n8n
Telegram bot that manages odin's containers. It is the origin of most of the API detail
below and is worth reading before writing anything new against the Unraid API.
---
## Network path: Tailscale
`dev` and odin are both on the tailnet. `dev` is a **VPS** (public IP `51.222.234.162`),
*not* a machine on the home LAN — Tailscale is the only path to odin.
| Node | Tailscale IP | Notes |
| --- | --- | --- |
| `dev` | `100.94.16.46` | this workstation (linux) |
| `odin` | `100.101.253.105` | the Unraid host (linux) |
Other tailnet nodes seen: `duplicati`, `experience` (windows), `fedora`, `nusgwvm`,
`salt`, `google-pixel-8-pro`.
**A subnet route is advertised for the home LAN.** odin's LAN address `192.168.90.103` is
reachable from `dev` and routes over `tailscale0`:
```
$ ip route get 192.168.90.103
192.168.90.103 dev tailscale0 table 52 src 100.94.16.46
```
Both the tailnet IP and the LAN IP answer at ~34 ms RTT — the same path. This matters:
the `myunraid.net` URL below looks like a cloud relay, but **DNS resolves it to the
private `192.168.90.103`**, so traffic goes direct over Tailscale and never leaves the
tailnet. If Tailscale is down, none of it is reachable.
---
## WebGUI / API endpoint
```
https://192-168-90-103.87c90a69c53e7197560c778a63f483e675130f2a.myunraid.net:8443/
```
The hostname encodes odin's LAN IP with dashes, under a per-server hash on `myunraid.net`.
This is the **canonical vhost** — not merely a convenience alias. Use it for everything.
### Why the plain LAN IP does not work
Both failure modes are real and confirmed:
| Attempt | Result | Verified |
| --- | --- | --- |
| `https://192.168.90.103:8443/graphql` | **404** from nginx | curl |
| `http://192.168.90.103/graphql` | **302** → the `myunraid.net` URL | curl |
| `https://<myunraid-host>:8443/graphql` | **200**, GraphQL responds | curl |
nginx only serves `/graphql` on the `myunraid.net` `server_name`; the raw IP vhost returns
404. Plain HTTP redirects to that hostname — and **the redirect strips the `x-api-key`
header**, so a client that follows redirects authenticates as nobody. Always call the
`myunraid.net` URL directly rather than relying on a redirect.
### TLS caveat — correcting the other repo
`unraid-docker-manager` states "Valid certs via myunraid.net — no SSL ignore needed."
**That is not true from `dev`.** Verification fails here:
```
depth=1 C=US, O=Let's Encrypt, CN=YR1
verify error:num=20:unable to get local issuer certificate
depth=0 CN=*.87c90a69c53e7197560c778a63f483e675130f2a.myunraid.net
```
The leaf is a genuine Let's Encrypt wildcard, but the chain presents an intermediate
(`YR1`) that this box's CA bundle (`ca-certificates 20250419`) cannot chain to a trusted
root — either the server omits the intermediate or the bundle predates it. The cert is not
forged; the chain is incomplete *for this client*.
Practical consequence: **`curl` needs `-k` from `dev`**, and any script that talks to odin
must either pass the insecure flag or pin/supply the issuer. n8n running *on* odin may
well verify fine, which is likely why the other repo's claim held there. Prefer supplying
the missing intermediate over making `-k` a permanent habit — `-k` disables verification
entirely, which on a tailnet is a tolerable but real weakening.
---
## Unraid GraphQL API
- **Endpoint:** `{UNRAID_HOST}/graphql`, POST, where `UNRAID_HOST` is the `myunraid.net`
base URL **without** the `/graphql` suffix.
- **Auth:** `x-api-key: <key>` header.
- **Availability:** native in Unraid 7.2+ (odin is on 7.2); 6.97.1 needs the Connect plugin.
Unauthenticated requests return HTTP **200** with a GraphQL error body — not an HTTP 401:
```json
{"errors":[{"message":"Invalid CSRF token","extensions":{"code":"UNAUTHENTICATED",
"originalError":{"error":"Unauthorized","statusCode":401}}}],"data":null}
```
**Never treat HTTP 200 as success.** Always inspect `response.errors[]`.
### Polling running containers
```bash
curl -sS -k -X POST "${UNRAID_HOST}/graphql" \
-H 'Content-Type: application/json' \
-H "x-api-key: ${UNRAID_API_KEY}" \
-d '{"query":"query { docker { containers { id names state } } }"}'
```
```json
{"data":{"docker":{"containers":[
{"id":"<server_hash>:<container_hash>","names":["/n8n"],"state":"RUNNING"}
]}}}
```
Field behaviours that bite:
- `state` is **UPPERCASE**`RUNNING`, not `running`.
- `names` is an array and entries are **`/`-prefixed** — `/n8n`, not `n8n`.
- `id` is a `PrefixedID`: `{server_hash}:{container_hash}`, two 64-char SHA-256 hex
strings joined by a colon. The server half is identical for every container on odin.
- **`isUpdateAvailable` does not exist** in the 7.2 schema, despite appearing in that
repo's earlier research. Introspect before relying on any field.
### API keys
Not present on this box — `.env.unraid-api` is gitignored and absent from the clone. Create
a key scoped to what the script actually needs:
```bash
unraid-api apikey --create --name "<name>" --permissions "DOCKER:UPDATE_ANY" --json
```
Or WebGUI → Settings → Management Access → API Keys. Store it in the collection's own
`.env`, never in git.
---
## Gotchas carried over from `unraid-docker-manager`
- **Each Bash tool call is a fresh shell.** Source the env file in the *same* command
chain as the request: `. .env.unraid-api; curl ...`. A separate `source` call is lost.
- **Large API responses:** save to a temp file before parsing; piping a 400 KB body
straight into `python3 -c` fails silently.
- Docker API `204 No Content` means success with an empty body.