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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c6be0dc9d6
commit
dad8774ed9
@@ -43,13 +43,13 @@ Copy `_template/` to the new name, fill in its README, and add one line to the r
|
|||||||
|
|
||||||
## The environment
|
## The environment
|
||||||
|
|
||||||
> **⚠ Confirm the gaps marked `?` below, then delete this warning.** The infrastructure
|
**odin** — the Unraid server; the single host everything self-hosted runs on. Reached from
|
||||||
> facts are carried over from the FamilySync project, where they are documented and
|
this box over **Tailscale** (`100.101.253.105`, LAN `192.168.90.103`).
|
||||||
> verified. Only the mapping of the *name* `odin` to that host is assumed — the name
|
|
||||||
> appears in no memory, transcript, or config on this box.
|
|
||||||
|
|
||||||
**odin** — the Unraid server; the single host everything self-hosted runs on. Verified
|
> **See [docs/odin-access.md](docs/odin-access.md)** for the access path, the WebGUI/API
|
||||||
characteristics:
|
> endpoint, TLS caveats, and the Unraid GraphQL API — all verified from this box.
|
||||||
|
|
||||||
|
Verified characteristics:
|
||||||
|
|
||||||
| Aspect | Detail |
|
| Aspect | Detail |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -60,9 +60,12 @@ characteristics:
|
|||||||
| Auth | Authelia — OIDC/OAuth2 provider for internal apps |
|
| Auth | Authelia — OIDC/OAuth2 provider for internal apps |
|
||||||
| Ingress | Pangolin/Newt tunnel — **no open inbound ports** |
|
| Ingress | Pangolin/Newt tunnel — **no open inbound ports** |
|
||||||
| DNS | Split-DNS on `bergerhouse.net`; private IPs resolve internally |
|
| DNS | Split-DNS on `bergerhouse.net`; private IPs resolve internally |
|
||||||
| Git forge | Self-hosted Gitea/Forgejo at `git.bergerhouse.net` (user `luckberg`) |
|
| Git forge | Self-hosted Gitea/Forgejo at `git.bergerhouse.net` (user `luckberg`, SSH port 222) |
|
||||||
| CI | Gitea Actions with a self-hosted runner |
|
| CI | Gitea Actions with a self-hosted runner |
|
||||||
| Mail / calendar | Fastmail (paid) — JMAP/CalDAV, the source of truth for calendars |
|
| Mail / calendar | Fastmail (paid) — JMAP/CalDAV, the source of truth for calendars |
|
||||||
|
| Remote access | Tailscale; odin `100.101.253.105`, subnet route for `192.168.90.0/24` |
|
||||||
|
| Unraid API | GraphQL at `{myunraid.net host}:8443/graphql`, `x-api-key` auth (Unraid 7.2+) |
|
||||||
|
| Automation | n8n on odin — drives the Docker-manager Telegram bot |
|
||||||
|
|
||||||
Known service hostnames: `git.bergerhouse.net`, `familysync.bergerhouse.net`,
|
Known service hostnames: `git.bergerhouse.net`, `familysync.bergerhouse.net`,
|
||||||
`familysync-dev.bergerhouse.net`.
|
`familysync-dev.bergerhouse.net`.
|
||||||
@@ -72,14 +75,26 @@ PostgreSQL, any cloud provider.
|
|||||||
|
|
||||||
### This box vs. odin
|
### This box vs. odin
|
||||||
|
|
||||||
The machine this repo is edited on is `dev` — a **separate workstation**, not the server.
|
The machine this repo is edited on is `dev` (Tailscale `100.94.16.46`) — a **separate
|
||||||
Scripts written here usually *target* odin (over SSH, or by being deployed into a
|
workstation**, and in fact a VPS with a public IP, not a machine on the home LAN. Scripts
|
||||||
container/user-script on it). Never assume a script runs on the same host it was authored
|
written here usually *target* odin. Never assume a script runs on the same host it was
|
||||||
on: take the target as a parameter or read it from config.
|
authored on: take the target as a parameter or read it from config.
|
||||||
|
|
||||||
`?` — SSH access method to odin (no `~/.ssh/config` entry exists on this box yet), and
|
Tailscale is the **only** path from `dev` to odin — if it is down, nothing below resolves.
|
||||||
|
|
||||||
|
`?` — still unconfirmed: the SSH login to odin (no `~/.ssh/config` entry on this box) and
|
||||||
where deployed scripts are expected to live on the Unraid host.
|
where deployed scripts are expected to live on the Unraid host.
|
||||||
|
|
||||||
|
### Sibling repos on `git.bergerhouse.net`
|
||||||
|
|
||||||
|
`luckberg/` holds `scripts` (this repo), `familysync`, `unraid-docker-manager`,
|
||||||
|
`azure-epac`, `_cargo-index`. Two are load-bearing context:
|
||||||
|
|
||||||
|
- **`unraid-docker-manager`** — n8n + Telegram bot managing odin's containers. The
|
||||||
|
authoritative source on the Unraid GraphQL API and container polling; distilled into
|
||||||
|
[docs/odin-access.md](docs/odin-access.md).
|
||||||
|
- **`familysync`** — the origin of most of the stack facts in the table above.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Prior context: MemPalace
|
## Prior context: MemPalace
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ Docker Compose behind Authelia and a Pangolin/Newt tunnel).
|
|||||||
Each script collection lives in its own top-level folder and is fully self-contained —
|
Each script collection lives in its own top-level folder and is fully self-contained —
|
||||||
its own README, config, and dependencies. Collections never import from each other.
|
its own README, config, and dependencies. Collections never import from each other.
|
||||||
|
|
||||||
See [CLAUDE.md](CLAUDE.md) for the environment details and repo conventions.
|
See [CLAUDE.md](CLAUDE.md) for the environment details and repo conventions, and
|
||||||
|
[docs/odin-access.md](docs/odin-access.md) for how to reach odin and drive its API.
|
||||||
|
|
||||||
## Collections
|
## Collections
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# 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.9–7.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.
|
||||||
Reference in New Issue
Block a user