spaceship-earth: per-host Multica PAT, root LV growth, headless Obsidian (WDWL-22)
- provision-agent-host.yml: multica_pat now comes from vault_multica_pats[inventory_hostname_short], one daemon-<host> token per host. A pre_task asserts the key exists (never prints the value) so a missing entry fails by host name instead of as a no_log "censored". - provision-wong-host.yml: spaceship-earth only. Grows the root partition, PV and LV/filesystem with growpart + community.general.lvg/lvol; idempotent (second run changed=0). Then applies obsidian_headless. - roles/obsidian_headless: Obsidian's official headless Sync client (obsidian-headless 0.0.14) as system user obsync under a sandboxed systemd unit with UMask=0077. Vault at /srv/obsidian/vault: everpure/ and wip/ obsync-only, inbox/ granted to multica via access + default ACLs, vault root traverse-only. Verifies the isolation as multica. Sign-in to Obsidian Sync stays manual (see the role README). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
04a0937910
commit
15aeb8e222
14 files changed
+634
-4
No files matched your search
@@ -0,0 +1,78 @@
|
||||
# obsidian_headless
|
||||
|
||||
Runs Obsidian's official headless Sync client
|
||||
([`obsidian-headless`](https://obsidian.md/help/sync/headless), command `ob`,
|
||||
open beta) as a dedicated unprivileged user under a systemd unit. It lays out
|
||||
the vault folder so that other local users reach only the folders granted to
|
||||
them.
|
||||
|
||||
Written for `spaceship-earth` (WDWL-22). Wong's daemon user (`multica`) may
|
||||
read and write `inbox/` and nothing else. `everpure/` and `wip/` are `obsync`
|
||||
only. Entry point: `playbooks/provision-wong-host.yml`.
|
||||
|
||||
## What it does
|
||||
|
||||
1. Creates the system user `obsync`, with a `0700` home in `/var/lib/obsync`.
|
||||
That home holds the Obsidian account token and per-vault sync state.
|
||||
2. Installs the pinned `obsidian-headless` into that user's npm prefix and
|
||||
links `ob` into `/usr/local/bin`. Node.js is **not** installed here. The
|
||||
role asserts that the runtime `multica_agent_host` installs is present and
|
||||
is at least version 22.
|
||||
3. Lays out the vault folder and its permissions:
|
||||
|
||||
| Path | Mode | ACL |
|
||||
|---|---|---|
|
||||
| `/srv/obsidian/` | `0710 obsync` | `user:multica:--x` |
|
||||
| `/srv/obsidian/vault/` | `0710 obsync` | `user:multica:--x`, so `multica` can traverse but not list |
|
||||
| `vault/everpure/`, `vault/wip/` | `0700 obsync` | none |
|
||||
| `vault/inbox/` | `0770 obsync` | `user:multica:rwx`, plus the same as a **default** entry |
|
||||
|
||||
On a directory with an ACL, the mode's group bits are the ACL mask. A
|
||||
`chmod 0700` would therefore void every named entry, which is why the
|
||||
shared paths use `0710` and `0770`. The owning group is `obsync`'s private
|
||||
group, so those bits grant nobody anything.
|
||||
4. Installs `obsidian-sync.service` (`User=obsync`, `UMask=0077`, sandboxed)
|
||||
and enables it. It is started only once a vault has been set up.
|
||||
`Condition*=` lines keep it inert until then, including across reboots.
|
||||
5. Verifies the isolation as the users themselves:
|
||||
- `multica` writes, reads and deletes in `inbox/`.
|
||||
- A folder and file created by `obsync` under umask `0077` (the way Sync
|
||||
creates them) stay readable and writable by `multica`.
|
||||
- `multica` is refused both listing the vault root and listing or writing
|
||||
`everpure/` and `wip/`.
|
||||
|
||||
## Do modes and ACLs survive Sync?
|
||||
|
||||
Obsidian Sync carries no Unix modes or ACLs. Files are created locally by
|
||||
`obsync`, so their permissions come from this host, not from the remote:
|
||||
|
||||
- In `inbox/`, the default ACL applies and the umask is ignored, so new
|
||||
content is granted to `multica`.
|
||||
- Everywhere else, `UMask=0077` applies, so new content is private to
|
||||
`obsync`. That covers any new top-level folder and `.obsidian/`.
|
||||
- If Sync deletes and recreates `inbox/` itself, the recreated folder is
|
||||
private and `multica` loses access. The failure is closed, not open, and
|
||||
re-running the play restores the grant.
|
||||
|
||||
## Manual step: sign in and set up the vault (once per host)
|
||||
|
||||
The role never handles the Obsidian account. Run these as `obsync`; `-H`
|
||||
makes `HOME` point to `/var/lib/obsync`:
|
||||
|
||||
```bash
|
||||
sudo -u obsync -H ob login
|
||||
sudo -u obsync -H ob sync-list-remote
|
||||
sudo -u obsync -H ob sync-setup --vault "<vault name>" \
|
||||
--path /srv/obsidian/vault --device-name spaceship-earth
|
||||
sudo systemctl start obsidian-sync
|
||||
journalctl -u obsidian-sync -f
|
||||
```
|
||||
|
||||
Re-running the play after this starts the unit as well. Do not also run
|
||||
desktop Obsidian Sync on this host: Obsidian warns that two Sync clients on
|
||||
one device conflict.
|
||||
|
||||
## Tags
|
||||
|
||||
`obsidian_user`, `obsidian_install`, `obsidian_layout`, `obsidian_service`,
|
||||
`obsidian_verify`. The play itself is tagged `obsidian`.
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
# =============================================================================
|
||||
# obsidian_headless — defaults
|
||||
#
|
||||
# Runs Obsidian's official headless Sync client (`ob`, npm obsidian-headless)
|
||||
# as a dedicated unprivileged user under a systemd unit, and lays out the vault
|
||||
# folder so that other local users reach only the folders granted to them.
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Service account
|
||||
# -----------------------------------------------------------------------------
|
||||
# Holds the Obsidian account token (~/.config/obsidian-headless/auth_token) and
|
||||
# the per-vault sync state, so its home is private.
|
||||
obsidian_headless_user: obsync
|
||||
obsidian_headless_group: obsync
|
||||
obsidian_headless_home: "/var/lib/{{ obsidian_headless_user }}"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Client
|
||||
# -----------------------------------------------------------------------------
|
||||
# Pinned. The package is in open beta and versions move quickly.
|
||||
obsidian_headless_version: "0.0.14"
|
||||
obsidian_headless_npm_prefix: "{{ obsidian_headless_home }}/.npm-global"
|
||||
# Node.js is NOT installed by this role. It expects the runtime that
|
||||
# multica_agent_host already puts on this host. obsidian-headless needs >= 22.
|
||||
obsidian_headless_node_bin_dir: /usr/local/bin
|
||||
obsidian_headless_node_min_version: "22.0.0"
|
||||
# Where `ob` is linked, so `sudo -u obsync -H ob ...` works without a PATH.
|
||||
obsidian_headless_link_dir: /usr/local/bin
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Vault
|
||||
# -----------------------------------------------------------------------------
|
||||
obsidian_headless_vault_path: /srv/obsidian/vault
|
||||
# Shown in Sync version history. Applied by the manual `ob sync-setup`.
|
||||
obsidian_headless_device_name: "{{ inventory_hostname_short }}"
|
||||
|
||||
# Top-level folders only obsidian_headless_user may enter (0700, no ACL).
|
||||
obsidian_headless_private_folders: []
|
||||
|
||||
# Top-level folders another local user may read and write. Granted with named
|
||||
# ACL entries plus matching DEFAULT entries, so files and folders Sync creates
|
||||
# later inherit the grant. Everything not listed here is private: the unit runs
|
||||
# with UMask=0077, and the vault root grants listed users traverse (--x) only,
|
||||
# never list.
|
||||
# - path: inbox
|
||||
# user: multica
|
||||
obsidian_headless_shared_folders: []
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Service
|
||||
# -----------------------------------------------------------------------------
|
||||
obsidian_headless_service_name: obsidian-sync
|
||||
# Enabled either way; started only once `ob login` and `ob sync-setup` have
|
||||
# been done by hand. Its Condition*= lines keep it inert until then.
|
||||
obsidian_headless_service_enabled: true
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Verification
|
||||
# -----------------------------------------------------------------------------
|
||||
# Prove the isolation as the shared users themselves: read/write in their
|
||||
# folders, denied in every private folder and on listing the vault root.
|
||||
obsidian_headless_verify: true
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
# Restarts only a unit that is already running: before the manual sign-in the
|
||||
# unit is inert, and starting it here would just trip its conditions.
|
||||
- name: Restart obsidian sync
|
||||
ansible.builtin.command:
|
||||
argv: [systemctl, try-restart, "{{ obsidian_headless_service_name }}.service"]
|
||||
changed_when: true
|
||||
listen: restart obsidian sync
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
galaxy_info:
|
||||
role_name: obsidian_headless
|
||||
author: JARVIS
|
||||
description: >-
|
||||
Obsidian's official headless Sync client as a dedicated user under a
|
||||
systemd unit, with per-folder isolation for other local users.
|
||||
license: MIT
|
||||
min_ansible_version: "2.16"
|
||||
platforms:
|
||||
- name: Ubuntu
|
||||
versions:
|
||||
- noble
|
||||
|
||||
dependencies: []
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
- name: Install | Read the installed client version
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ obsidian_headless_npm_prefix }}/lib/node_modules/obsidian-headless/package.json"
|
||||
register: _obh_pkg
|
||||
failed_when: false
|
||||
|
||||
- name: Install | Decide whether an install is needed
|
||||
ansible.builtin.set_fact:
|
||||
_obh_needs_install: >-
|
||||
{{ _obh_pkg.content is not defined
|
||||
or (_obh_pkg.content | b64decode | from_json).version != obsidian_headless_version }}
|
||||
|
||||
- name: Install | Install the pinned client as the service user
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- "{{ obsidian_headless_node_bin_dir }}/npm"
|
||||
- install
|
||||
- --global
|
||||
- --prefix
|
||||
- "{{ obsidian_headless_npm_prefix }}"
|
||||
- --no-fund
|
||||
- --no-audit
|
||||
- "obsidian-headless@{{ obsidian_headless_version }}"
|
||||
become: true
|
||||
become_user: "{{ obsidian_headless_user }}"
|
||||
environment:
|
||||
HOME: "{{ obsidian_headless_home }}"
|
||||
PATH: "{{ obsidian_headless_node_bin_dir }}:/usr/bin:/bin"
|
||||
when: _obh_needs_install | bool
|
||||
changed_when: true
|
||||
notify: restart obsidian sync
|
||||
|
||||
- name: Install | Link ob onto the default PATH
|
||||
ansible.builtin.file:
|
||||
src: "{{ obsidian_headless_npm_prefix }}/bin/ob"
|
||||
dest: "{{ obsidian_headless_link_dir }}/ob"
|
||||
state: link
|
||||
force: true
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
# Permission model
|
||||
#
|
||||
# <parent>/ obsync 0710 + user:<shared>:--x traverse, no listing
|
||||
# <vault>/ obsync 0710 + user:<shared>:--x traverse, no listing
|
||||
# <vault>/<private>/ obsync 0700 nobody else
|
||||
# <vault>/<shared>/ obsync 0770 + user:<u>:rwx, and the same as a
|
||||
# DEFAULT entry, so new children inherit
|
||||
#
|
||||
# The owning group is obsync's private group, so its bits grant nobody. On a
|
||||
# directory that carries an ACL, the group bits of the mode ARE the ACL mask,
|
||||
# which is why the modes here are 0710/0770 rather than 0700: a 0700 chmod
|
||||
# would zero the mask and silently void every named entry.
|
||||
#
|
||||
# Anything else in the vault (folders Sync creates later, .obsidian/) is born
|
||||
# private: the service runs with UMask=0077, and the umask is ignored only
|
||||
# under a directory that has a default ACL — i.e. only in the shared folders.
|
||||
|
||||
- name: Layout | Collect the users granted shared folders
|
||||
ansible.builtin.set_fact:
|
||||
_obh_shared_users: "{{ obsidian_headless_shared_folders | map(attribute='user') | unique | list }}"
|
||||
|
||||
- name: Layout | Create the vault and its parent
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ obsidian_headless_user }}"
|
||||
group: "{{ obsidian_headless_group }}"
|
||||
mode: "{{ '0710' if _obh_shared_users | length > 0 else '0700' }}"
|
||||
loop:
|
||||
- "{{ obsidian_headless_vault_path | dirname }}"
|
||||
- "{{ obsidian_headless_vault_path }}"
|
||||
|
||||
- name: Layout | Let shared users traverse (not list) the vault and its parent
|
||||
ansible.posix.acl:
|
||||
path: "{{ item.0 }}"
|
||||
entity: "{{ item.1 }}"
|
||||
etype: user
|
||||
permissions: x
|
||||
state: present
|
||||
loop: >-
|
||||
{{ [obsidian_headless_vault_path | dirname, obsidian_headless_vault_path]
|
||||
| product(_obh_shared_users) | list }}
|
||||
|
||||
- name: Layout | Create the private folders
|
||||
ansible.builtin.file:
|
||||
path: "{{ obsidian_headless_vault_path }}/{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ obsidian_headless_user }}"
|
||||
group: "{{ obsidian_headless_group }}"
|
||||
mode: "0700"
|
||||
loop: "{{ obsidian_headless_private_folders }}"
|
||||
|
||||
- name: Layout | Create the shared folders
|
||||
ansible.builtin.file:
|
||||
path: "{{ obsidian_headless_vault_path }}/{{ item.path }}"
|
||||
state: directory
|
||||
owner: "{{ obsidian_headless_user }}"
|
||||
group: "{{ obsidian_headless_group }}"
|
||||
mode: "0770"
|
||||
loop: "{{ obsidian_headless_shared_folders }}"
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
|
||||
- name: Layout | Grant shared folders (access and default entries)
|
||||
ansible.posix.acl:
|
||||
path: "{{ obsidian_headless_vault_path }}/{{ item.0.path }}"
|
||||
entity: "{{ item.0.user }}"
|
||||
etype: user
|
||||
permissions: rwx
|
||||
default: "{{ item.1 }}"
|
||||
state: present
|
||||
loop: "{{ obsidian_headless_shared_folders | product([false, true]) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.0.path }} -> {{ item.0.user }}{{ ' (default)' if item.1 else '' }}"
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
# Not `always`: that would also fire under unrelated tags of the calling
|
||||
# playbook (e.g. --tags wong_disk). Every tag of this role carries it instead.
|
||||
- name: Preflight
|
||||
ansible.builtin.import_tasks: preflight.yml
|
||||
tags: [obsidian, obsidian_user, obsidian_install, obsidian_layout, obsidian_service, obsidian_verify]
|
||||
|
||||
- name: Service account
|
||||
ansible.builtin.import_tasks: user.yml
|
||||
tags: [obsidian_user]
|
||||
|
||||
- name: Client
|
||||
ansible.builtin.import_tasks: install.yml
|
||||
tags: [obsidian_install]
|
||||
|
||||
- name: Vault layout
|
||||
ansible.builtin.import_tasks: layout.yml
|
||||
tags: [obsidian_layout]
|
||||
|
||||
- name: Service
|
||||
ansible.builtin.import_tasks: service.yml
|
||||
tags: [obsidian_service]
|
||||
|
||||
- name: Verify
|
||||
ansible.builtin.import_tasks: verify.yml
|
||||
when: obsidian_headless_verify | bool
|
||||
tags: [obsidian_verify]
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
- name: Preflight | Read the Node.js version
|
||||
ansible.builtin.command:
|
||||
argv: ["{{ obsidian_headless_node_bin_dir }}/node", "--version"]
|
||||
register: _obh_node
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
|
||||
- name: Preflight | Require Node.js new enough for obsidian-headless
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- _obh_node.rc == 0
|
||||
- (_obh_node.stdout | trim | regex_replace('^v', '')) is version(obsidian_headless_node_min_version, '>=')
|
||||
fail_msg: >-
|
||||
{{ obsidian_headless_node_bin_dir }}/node is missing or older than
|
||||
{{ obsidian_headless_node_min_version }} (found:
|
||||
{{ _obh_node.stdout | default('nothing') | trim }}). This role uses the
|
||||
runtime multica_agent_host installs; run provision-agent-host.yml first.
|
||||
quiet: true
|
||||
|
||||
- name: Preflight | Look up the users granted shared folders
|
||||
ansible.builtin.getent:
|
||||
database: passwd
|
||||
key: "{{ item }}"
|
||||
loop: "{{ obsidian_headless_shared_folders | map(attribute='user') | unique | list }}"
|
||||
|
||||
- name: Preflight | Require shared folders to stay disjoint from private ones
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- obsidian_headless_shared_folders | map(attribute='path') | intersect(obsidian_headless_private_folders) | length == 0
|
||||
fail_msg: A folder cannot be both private and shared.
|
||||
quiet: true
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
- name: Service | Install the unit
|
||||
ansible.builtin.template:
|
||||
src: obsidian-sync.service.j2
|
||||
dest: "/etc/systemd/system/{{ obsidian_headless_service_name }}.service"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: restart obsidian sync
|
||||
|
||||
- name: Service | Enable it at boot
|
||||
ansible.builtin.systemd_service:
|
||||
name: "{{ obsidian_headless_service_name }}"
|
||||
enabled: "{{ obsidian_headless_service_enabled | bool }}"
|
||||
daemon_reload: true
|
||||
|
||||
# Starting it before the manual sign-in would be a no-op (its conditions fail)
|
||||
# that reports "changed" on every run. Start it only once it can actually sync.
|
||||
- name: Service | Check whether the manual sign-in and vault setup are done
|
||||
ansible.builtin.find:
|
||||
paths: "{{ obsidian_headless_home }}/.config/obsidian-headless/sync"
|
||||
file_type: directory
|
||||
register: _obh_sync_configs
|
||||
|
||||
- name: Service | Start it once a vault is set up
|
||||
ansible.builtin.systemd_service:
|
||||
name: "{{ obsidian_headless_service_name }}"
|
||||
state: started
|
||||
when:
|
||||
- obsidian_headless_service_enabled | bool
|
||||
- _obh_sync_configs.matched | default(0) > 0
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
- name: User | Create the group
|
||||
ansible.builtin.group:
|
||||
name: "{{ obsidian_headless_group }}"
|
||||
system: true
|
||||
state: present
|
||||
|
||||
# nologin: nobody needs a shell as this user. `sudo -u obsync -H ob ...` runs
|
||||
# the command directly and does not consult the login shell.
|
||||
- name: User | Create the user
|
||||
ansible.builtin.user:
|
||||
name: "{{ obsidian_headless_user }}"
|
||||
group: "{{ obsidian_headless_group }}"
|
||||
home: "{{ obsidian_headless_home }}"
|
||||
shell: /usr/sbin/nologin
|
||||
system: true
|
||||
create_home: true
|
||||
state: present
|
||||
|
||||
- name: User | Keep the home private (holds the Obsidian account token)
|
||||
ansible.builtin.file:
|
||||
path: "{{ obsidian_headless_home }}"
|
||||
state: directory
|
||||
owner: "{{ obsidian_headless_user }}"
|
||||
group: "{{ obsidian_headless_group }}"
|
||||
mode: "0700"
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
# Each check runs AS the user it is about, because file modes and ACLs only
|
||||
# mean something when exercised. Probes are cleaned up; nothing is left behind.
|
||||
|
||||
- name: Verify | Shared users can write, read and delete in their folders
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- 'f="$1/.obh-verify.$$"; echo ok > "$f" && grep -q ok "$f" && rm -f "$f"'
|
||||
- sh
|
||||
- "{{ obsidian_headless_vault_path }}/{{ item.path }}"
|
||||
become: true
|
||||
become_user: "{{ item.user }}"
|
||||
loop: "{{ obsidian_headless_shared_folders }}"
|
||||
loop_control:
|
||||
label: "{{ item.user }} -> {{ item.path }}"
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
|
||||
# Simulates the service: obsync, umask 0077, creating a folder and a file the
|
||||
# way Sync does. The shared user must still reach both, via the default ACL.
|
||||
- name: Verify | Content the service creates in a shared folder inherits the grant (create)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- 'umask 0077; d="$1/.obh-verify-dir"; mkdir -p "$d" && echo ok > "$d/note.md"'
|
||||
- sh
|
||||
- "{{ obsidian_headless_vault_path }}/{{ item.path }}"
|
||||
become: true
|
||||
become_user: "{{ obsidian_headless_user }}"
|
||||
loop: "{{ obsidian_headless_shared_folders }}"
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
|
||||
- name: Verify | Content the service creates in a shared folder inherits the grant (use)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- 'd="$1/.obh-verify-dir"; grep -q ok "$d/note.md" && echo ok > "$d/reply.md"'
|
||||
- sh
|
||||
- "{{ obsidian_headless_vault_path }}/{{ item.path }}"
|
||||
become: true
|
||||
become_user: "{{ item.user }}"
|
||||
loop: "{{ obsidian_headless_shared_folders }}"
|
||||
loop_control:
|
||||
label: "{{ item.user }} -> {{ item.path }}"
|
||||
register: _obh_inherit
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
|
||||
- name: Verify | Remove the inheritance probes
|
||||
ansible.builtin.file:
|
||||
path: "{{ obsidian_headless_vault_path }}/{{ item.path }}/.obh-verify-dir"
|
||||
state: absent
|
||||
loop: "{{ obsidian_headless_shared_folders }}"
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Verify | Inheritance held
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- _obh_inherit.results | rejectattr('rc', 'equalto', 0) | list | length == 0
|
||||
fail_msg: >-
|
||||
A shared user could not read or write content created by
|
||||
{{ obsidian_headless_user }} under umask 0077; the default ACL is not
|
||||
being inherited.
|
||||
quiet: true
|
||||
|
||||
# Every shared user must be refused: listing the vault root, and listing,
|
||||
# reading into or writing into each private folder.
|
||||
- name: Verify | Shared users are denied the vault root listing and every private folder
|
||||
ansible.builtin.command:
|
||||
argv: >-
|
||||
{{ ['/bin/sh', '-c', _obh_deny_script, 'sh', obsidian_headless_vault_path]
|
||||
+ obsidian_headless_private_folders }}
|
||||
vars:
|
||||
_obh_deny_script: |
|
||||
v="$1"; shift
|
||||
ls "$v" >/dev/null 2>&1 && { echo "can list $v"; exit 1; }
|
||||
for p in "$@"; do
|
||||
ls "$v/$p" >/dev/null 2>&1 && { echo "can list $p"; exit 1; }
|
||||
touch "$v/$p/.obh-verify" 2>/dev/null && { rm -f "$v/$p/.obh-verify"; echo "can write $p"; exit 1; }
|
||||
done
|
||||
exit 0
|
||||
become: true
|
||||
become_user: "{{ item }}"
|
||||
loop: "{{ obsidian_headless_shared_folders | map(attribute='user') | unique | list }}"
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
@@ -0,0 +1,34 @@
|
||||
# {{ ansible_managed }}
|
||||
[Unit]
|
||||
Description=Obsidian Headless Sync ({{ obsidian_headless_vault_path }})
|
||||
Documentation=https://obsidian.md/help/sync/headless
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
# Inert until `ob login` and `ob sync-setup` have been run by hand.
|
||||
ConditionPathExists={{ obsidian_headless_home }}/.config/obsidian-headless/auth_token
|
||||
ConditionDirectoryNotEmpty={{ obsidian_headless_home }}/.config/obsidian-headless/sync
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User={{ obsidian_headless_user }}
|
||||
Group={{ obsidian_headless_group }}
|
||||
# Everything Sync creates is private unless it lands under a folder with a
|
||||
# default ACL (the shared folders), where the umask does not apply.
|
||||
UMask=0077
|
||||
Environment=HOME={{ obsidian_headless_home }}
|
||||
Environment=PATH={{ obsidian_headless_node_bin_dir }}:/usr/bin:/bin
|
||||
WorkingDirectory={{ obsidian_headless_vault_path }}
|
||||
ExecStart={{ obsidian_headless_link_dir }}/ob sync --continuous --path {{ obsidian_headless_vault_path }}
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
# 2: missing encryption key, 3: no sync configuration. Both need a human.
|
||||
RestartPreventExitStatus=2 3
|
||||
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
ReadWritePaths={{ obsidian_headless_vault_path }} {{ obsidian_headless_home }}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in new issue
Block a user