[2026-09-21] eagle: family/documents/vault-sync/2026-09-21-bare-repo-push-permission-denied.md family/how-to/vault-git-sync.md personal/tech/truenas-nfs4-acl-and-arrmultiuser.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-21 19:32:02 +06:00
1 parent 3b923d3524
commit 8b0b614a76
3 files changed
+129 -3

No files matched your search

@@ -0,0 +1,99 @@
---
title: "2026-09-21 — bare repo push Permission denied (root-owned objects/XX)"
aliases: [2026-09-21-vault-bare-repo-acl, push permission denied objects]
tags: [family, vault-sync, incident, truenas, acl, git, midclt]
created: 2026-09-21
updated: 2026-09-21
type: incident
namespace: family
---
# 2026-09-21 — bare repo push: `Permission denied` на `objects/XX`
> **Итог:** push починен, **права поставлены с наследованием** (`FILE_INHERIT`+`DIRECTORY_INHERIT`), чтобы дефект не возвращался. Root-пароль на TrueNAS не потребовался — всё через `midclt` API. Полный рецепт: `personal/tech/truenas-nfs4-acl-and-arrmultiuser.md` §«Durable fix with INHERITANCE».
## Симптом
```
error: unable to write file ./objects/80/09a34e…: Permission denied
! [remote rejected] main -> main (unable to migrate objects to permanent storage)
```
Найден при попытке запушить 15 коммитов с Mac (Eagle) → `nas`. Мак опережал bare на 14–15 коммитов, push отклонялся стабильно.
## Диагноз (два независимых дефекта)
| # | Дефект | Деталей |
|---|---|---|
| 1 | **13 каталогов `drw-r-x---`** | owner `rw-` **без `x`** → внутри нельзя создать файл. Репо-root, `objects`, `objects/{info,pack}`, `refs{,/heads,/tags}`, `branches`, `hooks`, `info`, `logs{,/refs,/refs/heads}`. Файлы были в порядке (0 без `u+r`). |
| 2 | **28 каталогов `objects/XX/` принадлежали `root`** | ACL `group@:r-x---a-R-c--s` — **только чтение для группы**. git работает от uid 950 (`truenas_admin`), не root → **писать не может**. Именно этот дефект блокировал push. |
**Кто создал root-owned объекты:** `mcpvault` — Obsidian MCP-сервер **второго инстанса Hermes на TrueNAS** (`/opt/hermes`), запущенный **от root**:
```
root 13727 /opt/hermes/.venv/bin/python3 … mcp_stdio_watchdog.py --ppid 9 -- mcpvault /vault
root 13738 node /opt/data/.local/bin/mcpvault /vault
```
> ⚠️ Это **НЕ vault Алекса на Mac** (`~/obsidian`). Это чужой/второй инстанс Hermes со своим `/vault`, который пушит в **тот же самый** bare-репозиторий. Коммиты 2026-09-20 21:26 и 22:25 сделаны от root — вероятно, им же.
## Что НЕ сработало
- `chown -R 950:950` и `chmod -R g+w` от `truenas_admin` → **`Operation not permitted`** (uid 950 не владелец root-owned файлов).
- `sudo` на TrueNAS **требует пароль** (`sudo -n true` падает). ⚠️ В отличие от **Кра́кена**, где passwordless sudo ЕСТЬ.
- `midclt call filesystem.setperm` → `[EINVAL] Non-trivial ACL present … Option "stripacl" required`. **НЕ использовать `stripacl`** — срывает ACL до POSIX.
## Что сработало — `midclt filesystem.setacl`
`truenas_admin` — FULL_ADMIN в midclt, поэтому ACL читается и пишется **без root-пароля**.
**Ключевое отличие от прошлого ремонта (2026-09-02):** тогда правили отдельные ACE (`group@` +perms). Это чинит *текущие* каталоги, но **баг возвращается**: следующий push root-инстанса создаёт новый `objects/XX/`, который наследует ACL от родителя, и если там нет `WRITE_DATA` + inherit-флагов — новый каталог снова root-owned и read-only.
**Durable-фикс = полный dacl-REPLACE** с наследованием:
```python
# perms для owner@ И group@ И everyone@:
# READ_DATA WRITE_DATA APPEND_DATA EXECUTE DELETE_CHILD
# READ_ATTRIBUTES WRITE_ATTRIBUTES READ_NAMED_ATTRS WRITE_NAMED_ATTRS READ_ACL
# (+ WRITE_ACL WRITE_OWNER DELETE для owner@)
# flags на каждом ACE:
# {"FILE_INHERIT": True, "DIRECTORY_INHERIT": True,
# "NO_PROPAGATE_INHERIT": False, "INHERIT_ONLY": False, "INHERITED": False}
# вызов: midclt call filesystem.setacl '<один JSON: path + dacl + options>'
# options = {"recursive": False, "traverse": True, "stripacl": False}
```
**Результат:** **291 каталог, 0 ошибок**, уложилось в один SSH-вызов (~2 мин), `nohup` не потребовался.
### Питфоллы midclt (стоили времени)
- `filesystem.getacl` и `filesystem.listdir` принимают **голую строку пути**, НЕ `{"path": …}`. Объект → `[EINVAL] path: Not a string`.
- `filesystem.setacl` — **наоборот**: один JSON-объект с `path` ВНУТРИ. Легко перепутать на одном и том же сервисе.
- `setacl`/`setperm` **асинхронные** → возвращают job id. Немедленная проверка даёт `WAITING` и выглядит как провал. Поллить `core.get_jobs` до `SUCCESS`/`FAILED`. При длинном цикле по SSH процесс переживает таймаут сессии — первый зависоний запуск **фактически уже применил ACL**.
## Верификация (и её предел)
```bash
cd ~/obsidian && git push nas main
# → 177da1e..3b923d3 main -> main
git rev-parse HEAD; git rev-parse nas/main # оба 3b923d3
```
Два успешных push подряд, хеши Mac и bare совпадают.
> ⚠️ **Честная оговорка.** Прямой тест записи
> `touch …/objects/20/.wrtest` от `truenas_admin` **не выполнялся** (запрос апрува истёк).
> `git push` доказал, что *текущее* дерево синхронно, но **не** что свежий root-владельческий объект реально записывается.
> Определяющий тест остаётся открытым — см. «Remaining» ниже.
## Remaining (открытые вопросы)
1. **Наследование не проверено на практике** — root-инстанс `mcpvault` при следующем пуше создаст объекты; с inherit права должны наследоваться, но это подтвердит только реальный push того инстанса.
2. **`mcpvault` продолжает работать от root** — пока он root, дефект может вернуться. Профилактика: перезапустить его под uid 950.
3. **`cleanup` не видит `/media/movies`** — на Кра́кене удаляются только Radarr-файлы; просмотренное лежит в `/media/movies`, диск будет расти. Отдельная задача (см. `family/plans/watchlist-disk-overflow-fix.md`).
## Связанные
- `personal/tech/truenas-nfs4-acl-and-arrmultiuser.md` — полный рецепт + прошлый кейс 2026-09-02
- `family/how-to/vault-git-sync.md` — топология vault (4 клиента, один bare-хаб)
- `family/how-to/vault-sync-pipeline.md` — три звена синка, ручной прогон
+24 -2
View File
@@ -1,6 +1,6 @@
---
title: Vault Git Sync — Architecture & Scripts
updated: '2026-09-02'
updated: '2026-09-21'
type: tech
tags:
- vault
@@ -12,6 +12,8 @@ tags:
# Vault Git Sync
> **STATE 2026-09-21 — bare repo push починен (наследуемый ACL).** `git push nas` падал `Permission denied` на `objects/XX`: 28 каталогов были root-owned (создавал `mcpvault` **второго Hermes на TrueNAS, от root**), 13 каталогов без owner-`x`. Фикс: **полный dacl-REPLACE с `FILE_INHERIT`+`DIRECTORY_INHERIT`** через `midclt filesystem.setacl` — 291 каталог, 0 ошибок, без root-пароля. Push прошёл дважды, Mac=`nas`=`3b923d3`. ⚠️ Прямой write-тест `touch` в `objects/` **не выполнялся** → наследование подтвердит только следующий push root-инстанса. Подробно: `family/documents/vault-sync/2026-09-21-bare-repo-push-permission-denied.md`.
>
> **STATE 2026-09-02 (позднее обновление):** Eagle/bare repo снова **синхронны** (ahead=0/behind=0, `2fab879`). Ранее (см. ниже) git-sync стоял из-за битых прав `storage/git` — **починено** полной dacl-заменой (`family/documents/vault-sync/2026-09-02-restore-privilege-scope.md` §RESOLVED).
>
> **STATE 2026-09-04:** починена потеря git identity в контейнере Taiga (слетела при рекреации после crashloop 09-02) — коммиты молча не проходили ~1.5 суток, изменения телефона не доезжали до bare. Фикс: `sync-vault.sh` теперь идемпотентно ставит `user.name "Taiga"` / `user.email "taiga@hermes"` при каждом запуске. Накопленное закоммичено (`2b45264`). Подробно: `family/documents/vault-sync/2026-09-04-git-identity-loss-silent-commit-failure.md`.
@@ -135,10 +137,30 @@ cd ~/Developer/vault-sync-test && bash test-sync.sh
- `git add -A` in sparse worktree still stages deletions of out-of-cone files tracked in index — need to unstage them (fixed in old script; irrelevant with proper clone)
- `core.quotePath=true` (default) escapes Cyrillic paths in `git ls-files` output — use `-c core.quotePath=false`
- ZFS on TrueNAS blocks `chmod` — `git init` fails from host; must run from inside Docker container or create `.git` structure manually
- **NFS4 `owner@ DENY READ_DATA` ACL breaks push (fake "corrupt" objects)** — see History 2026-09-02. Key: after a TrueNAS **pool restore**, a subset of loose git objects in the bare repo can carry an inverted NFS4 ACL `owner@ type=DENY READ_DATA=True`. In NFSv4 a DENY overlays ALLOW, so the owning uid (e.g. 950) can't mmap-read those objects → on receive, `git-receive-pack`/`index-pack` reports `loose object ... is corrupt` and rejects push, even though data is intact (`git fsck --full` under root is clean). Fix = `filesystem.setacl <repo> ... {stripacl:true}` (chmod/chown do NOT remove DENY ACEs). Tell-tale: POSIX file mode `40` (`r--------`) on loose objects vs normal `750`.
- **NFS4 `owner@ DENY READ_DATA` ACL breaks push (fake "corrupt" objects)** — see History 2026-09-02. Key: after a TrueNAS **pool restore**, a subset of loose git objects in the bare repo can carry an inverted NFS4 ACL `owner@ type=DENY READ_DATA=True`. In NFSv4 a DENY overlays ALLOW, so the owning uid (e.g. 950) can't mmap-read those objects → on receive, `git-receive-pack`/`index-pack` reports `loose object ... is corrupt` and rejects push, even though data is intact (`git fsck --full` under root is clean). Fix = **full dacl REPLACE** via `filesystem.setacl` (chmod/chown do NOT remove DENY ACEs). Tell-tale: POSIX file mode `40` (`r--------`) on loose objects vs normal `750`.
> ⚠️ **Don't reach for `stripacl`** (2026-09-21): it tears the ACL down to POSIX and does **not** remove a DENY in all cases. Always pass a complete `dacl` array — `setacl` treats it as a full replace.
- **`Permission denied` writing `objects/XX/` (NOT the corrupt-object case)** — see History 2026-09-21. Two defects: (a) dirs with owner `rw-` and **no `x`** (`drw-r-x---`), (b) `objects/XX/` dirs **owned by `root`** with `group@:r-x` — git runs as uid 950 → cannot write. Root cause is `mcpvault` (a **second Hermes on TrueNAS, `/opt/hermes`**, vault `/vault`) running **as root** and creating root-owned objects in the shared bare repo. Requires `midclt` (see below) — `chown`/`chmod` as `truenas_admin` give `Operation not permitted` because uid 950 isn't the owner, and TrueNAS `sudo` **needs a password** (unlike Kraken, where `sudo -n` works). Fix must include **`FILE_INHERIT`+`DIRECTORY_INHERIT`** or the defect returns on the next push by the root instance. Recipe: `personal/tech/truenas-nfs4-acl-and-arrmultiuser.md` §«Durable fix with INHERITANCE».
## ACL repair via `midclt` (no root password needed)
`truenas_admin` is FULL_ADMIN in midclt → can read/write ACLs directly.
```bash
# READ — path as a BARE STRING
midclt call filesystem.getacl /mnt/RED_2TB/storage/git/obsidian-vault.git
midclt call filesystem.listdir /mnt/RED_2TB/storage/git/obsidian-vault.git/objects
# WRITE — ONE json object with "path" INSIDE it (opposite convention!)
midclt call filesystem.setacl '{"path":"/x","acltype":"NFS4","dacl":[…],
"options":{"recursive":false,"traverse":true,"stripacl":false}}'
```
Pitfalls: `setacl`/`setperm` are **async** (return a job id) — poll `core.get_jobs` until `SUCCESS`/`FAILED`, or the immediate `WAITING` reads as a failure. `filesystem.setperm` refuses when a non-trivial ACL is present (`[EINVAL] … Option "stripacl" required`).
## History
- **2026-09-21**: bare repo push `Permission denied` on `objects/XX`. 28 dirs root-owned (created by `mcpvault` of a **second Hermes on TrueNAS** running as root) + 13 dirs with owner `rw-` and no `x`. `chown`/`chmod` as `truenas_admin` → `Operation not permitted` (TrueNAS sudo needs a password, unlike Kraken). Fixed with a **full dacl REPLACE + `FILE_INHERIT`/`DIRECTORY_INHERIT`** via `midclt filesystem.setacl` — **291 dirs, 0 errors**, no root password. Two clean pushes after; Mac = `nas` = `3b923d3`. ⚠️ Direct write-test in `objects/` was NOT run. Full log: `family/documents/vault-sync/2026-09-21-bare-repo-push-permission-denied.md`.
- **2026-06-20**: Kraken ACL fix. mcpvault (Hermes контейнер, uid 10000) не мог писать в существующие файлы vault из-за прав (EACCES). Подробно:
**Проблема 1 — права доступа:** файлы в vault создаются от uid 1000 (kraken через git на хосте), а mcpvault в контейнере работает от uid 10000. `patch_note` падал: `EACCES: permission denied`. `delete_note` + `write_note` срабатывали, но mcpvault при write перезаписывает файл целиком, переформатируя YAML фронтматер (убирает кавычки, меняет стиль) — шум в git-diff.
@@ -9,6 +9,7 @@ related:
- "[[truenas-zfs-panic-recovery]]"
- "[[arr-stack-taiga]]"
- "[[watchlist-disk-overflow-fix]]"
- "[[2026-09-21-bare-repo-push-permission-denied]]"
---
# TrueNAS NFS4 ACL repair + cross-container media PUID
@@ -84,7 +85,11 @@ To ssh://…/obsidian-vault.git
1. **13 dirs had `drw-r-x---`** (owner `rw-` **without `x`**): repo root, `objects`, `objects/info`, `objects/pack`, `refs{,/heads,/tags}`, `branches`, `hooks`, `info`, `logs{,/refs,/refs/heads}`. Without owner-`x` nothing can be created inside. Fixed with `chmod u+x` on dirs only (files were all fine: `0 files without u+r`).
2. **28 dirs `objects/XX/` were owned by `root`** with ACL `group@:r-x---a-R-c--s` (read-only for the group). Git runs as uid 950 (`truenas_admin`), not root → **cannot write**. This is the defect that actually blocked the push.
**Why it happened:** `mcpvault` (the Obsidian MCP server) runs **as root** — `/opt/hermes/.venv/bin/python3 … mcp_stdio_watchdog.py … mcpvault /vault` (`ps aux` shows uid `root`). Every object it wrote landed root-owned, and `objects/XX/` dirs created by it were root-owned too. **The problem WILL recur** unless mcpvault is restarted as uid 950. Left as an open item.
**Why it happened:** `mcpvault` runs **as root** on TrueNAS — `/opt/hermes/.venv/bin/python3 … mcp_stdio_watchdog.py … mcpvault /vault` (`ps aux` shows uid `root`). Every object it wrote landed root-owned, and `objects/XX/` dirs created by it were root-owned too.
> ⚠️ **Do not confuse this with the Mac's vault.** This `mcpvault` belongs to a **second Hermes instance living on TrueNAS itself** (`/opt/hermes`), whose vault is `/vault` — **not** Alex's `~/obsidian` on the Mac. It is not "the agent's own MCP server". Both instances merely push to the **same shared bare repo**, which is why the collision happens. PID refs seen 2026-09-21: 13727 (`mcp_stdio_watchdog.py`), 13738 (`node … mcpvault /vault`).
With the inheritance fix below, root-owned objects are no longer fatal (rights are inherited from the now-writable parent), but **the root instance is still a latent hazard** — restarting it as uid 950 removes the cause. Left as an open item.
**What does NOT work:**
- `chown -R 950:950` and `chmod -R g+w` via SSH as `truenas_admin` → **`Operation not permitted`** (uid 950 is not the owner of root-owned objects). `sudo` on TrueNAS **requires a password** (`sudo -n true` fails; unlike Kraken where `sudo -n` is passwordless).