VNC_TROUBLESHOOTING_FINDINGS
Based on all of the below Lego blocks, how would I make an obsidian? obsidian enterprise thing where we could use chasm workspaces, something like a merger FS or other methods and have a way for employees to essentially. go to a URL that we give them and they’re given access to the things that they have access to, such as an NTFS or Onedrive and it allows them to open up an obsidian vault with access to those things and those things only seamlessly. below is some data from exploration.
Pasted Text 1
Obsidian Enterprise: Full Architecture
Section titled “Obsidian Enterprise: Full Architecture”Here’s how all the pieces snap together into a working system.
The 30-Second Version
Section titled “The 30-Second Version”text
Employee opens browser → hits obsidian.company.com → Kasm authenticates via SSO (Authentik/Azure AD) → Mount Orchestrator reads user’s groups from JWT → Assembles a per-user composite vault from permitted storage (NTFS/OneDrive/SMB) → Spins up a hardened Obsidian container with those mounts → User sees ONE vault with only their authorized folders → CRDT plugin handles multi-user conflict-free editing → User closes tab → container destroyed
Layer Diagram
Section titled “Layer Diagram”text
┌─────────────────────────────────────────────────────────────────────┐ │ EMPLOYEE’S BROWSER │ │ obsidian.company.com │ └──────────────────────────┬──────────────────────────────────────────┘ │ HTTPS/WSS ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ LAYER 0: AUTHENTICATION │ │ │ │ Authentik (or Azure AD / Okta) │ │ ├── SAML/OIDC SSO │ │ ├── Groups: engineering, finance, project-alpha, executives… │ │ └── JWT with group claims → passed to Kasm │ └──────────────────────────┬──────────────────────────────────────────┘ │ Authenticated session + groups ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ LAYER 1: KASM WORKSPACES (Orchestrator) │ │ │ │ ├── Receives user identity + group memberships │ │ ├── Looks up vault-permissions.yaml │ │ ├── Calls Mount Orchestrator API │ │ ├── Spins up per-user Obsidian container │ │ ├── Injects bind mounts based on permissions │ │ ├── Manages session lifecycle (create/hibernate/destroy) │ │ └── Enforces DLP (clipboard, file transfer, etc.) │ └──────────────────────────┬──────────────────────────────────────────┘ │ Docker API / container launch ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ LAYER 2: PER-USER OBSIDIAN CONTAINER │ │ (linuxserver/obsidian + Selkies VNC) │ │ │ │ /user-vault/ ← Obsidian opens THIS │ │ ├── .obsidian/ ← Per-user config (generated) │ │ ├── _inbox/ ← Writable space for new files │ │ ├── personal/ ← User-only storage │ │ ├── engineering/ ← Bind mount from shared storage │ │ ├── project-alpha/ ← Bind mount from shared storage │ │ └── wiki/ ← Bind mount (read-only) │ │ │ │ Environment: │ │ ├── HARDEN_DESKTOP: true │ │ ├── RESTART_APP: true │ │ ├── NO_DECOR: true │ │ ├── SELKIES_FILE_TRANSFERS: none │ │ └── SELKIES_ENABLE_SHARING: false|locked │ │ │ │ Running inside: │ │ ├── Obsidian desktop (Electron) │ │ ├── CRDT Sync Plugin (Yjs) │ │ └── Selkies VNC (browser streaming) │ └──────────────────────────┬──────────────────────────────────────────┘ │ Bind mounts ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ LAYER 3: SHARED STORAGE │ │ │ │ Option A: SMB/CIFS Shares (NTFS) │ │ ├── //fileserver/engineering → /shared/engineering │ │ ├── //fileserver/finance → /shared/finance │ │ ├── //fileserver/wiki → /shared/wiki │ │ └── //fileserver/project-alpha → /shared/project-alpha │ │ │ │ Option B: OneDrive / SharePoint (via rclone mount) │ │ ├── rclone mount onedrive:Engineering → /shared/engineering │ │ ├── rclone mount onedrive:Finance → /shared/finance │ │ └── (rclone runs as a sidecar or on the host) │ │ │ │ Option C: NFS / Local Storage │ │ ├── NFS exports mounted on Docker host │ │ └── Bind-mounted into containers │ │ │ │ .crdt/ folders live alongside content for CRDT state │ └─────────────────────────────────────────────────────────────────────┘
Component Breakdown
Section titled “Component Breakdown”1. Authentication (Authentik)
Section titled “1. Authentication (Authentik)”text
User → obsidian.company.com → Redirected to Authentik login → SSO via Azure AD / Google / LDAP / local → JWT issued with claims: { “sub”: “alice@company.com”, “groups”: [“engineering”, “project-alpha”, “all-employees”], “name”: “Alice Chen” } → Redirected back to Kasm with token
Authentik is recommended because it’s open-source, self-hosted, and integrates natively with Kasm via SAML/OIDC. If you’re already on Azure AD or Okta, those work too — Kasm supports all of them.
2. Mount Orchestrator
Section titled “2. Mount Orchestrator”This is the custom piece. It sits between Kasm and the container launch, translating group memberships into bind mounts.
yaml
folders: - name: engineering source: //fileserver/engineering # or /shared/engineering type: smb # smb | nfs | local | rclone allowed_groups: - engineering - devops access: readwrite
- name: finance source: //fileserver/finance type: smb allowed_groups: - finance - executives access: readwrite
- name: wiki source: //fileserver/wiki type: smb allowed_groups: - all-employees access: readonly
- name: project-alpha source: //fileserver/projects/alpha type: smb allowed_groups: - engineering - marketing - project-alpha-team access: readwriteWhat it does at session start:
typescript
// Pseudocodefunction assembleUserVault(user: User): DockerMounts[] { const mounts: DockerMounts[] = [];
// Always: per-user config and personal space mounts.push({ source: \`/users/${user.id}/.obsidian\`, target: "/user-vault/.obsidian", mode: "rw", }); mounts.push({ source: \`/users/${user.id}/personal\`, target: "/user-vault/personal", mode: "rw", }); mounts.push({ source: \`/users/${user.id}/inbox\`, target: "/user-vault/_inbox", mode: "rw", });
// Permission-based: check each folder for (const folder of config.folders) { if (user.groups.some((g) => folder.allowed_groups.includes(g))) { mounts.push({ source: folder.source, target: \`/user-vault/${folder.name}\`, mode: folder.access === "readonly" ? "ro" : "rw", }); } }
// Generate .obsidian config with valid paths generateObsidianConfig(user, mounts);
// Generate CRDT sync config (per-folder rooms) generateSyncConfig(user, mounts);
return mounts;}Integration with Kasm: Kasm supports Exec Config and custom launch scripts. The orchestrator either:
- Runs as a Kasm Server Side Script that executes before the container starts
- Or runs as a sidecar API that Kasm calls via webhook on session create
3. The Container (What Users See)
Section titled “3. The Container (What Users See)”Each user gets a fresh container with this shape:
yaml
# Generated dynamically per user by the orchestratorservices: obsidian-alice: image: lscr.io/linuxserver/obsidian:latest shm_size: "1gb" security_opt: - seccomp:unconfined devices: - /dev/dri:/dev/dri environment: PUID: "1000" PGID: "1000" TZ: America/Indiana/Indianapolis NO_DECOR: "true" PIXELFLUX_WAYLAND: "true" LC_ALL: "en_US.UTF-8" # Enterprise hardening HARDEN_DESKTOP: "true" RESTART_APP: "true" SELKIES_FILE_TRANSFERS: "none" SELKIES_COMMAND_ENABLED: "false|locked" SELKIES_ENABLE_SHARING: "false|locked" SELKIES_UI_SIDEBAR_SHOW_FILES: "false" SELKIES_UI_SIDEBAR_SHOW_APPS: "false" volumes: # Per-user - /users/alice/.obsidian:/user-vault/.obsidian:rw - /users/alice/personal:/user-vault/personal:rw - /users/alice/inbox:/user-vault/_inbox:rw # Permission-based shared folders - /shared/engineering:/user-vault/engineering:rw - /shared/project-alpha:/user-vault/project-alpha:rw - /shared/wiki:/user-vault/wiki:roNote: Skip
HARDEN_OPENBOX— it breaks the auto-maximize/scaling behavior you need.
4. Storage Backend Options
Section titled “4. Storage Backend Options”| Source | How It Gets to the Docker Host | Pros | Cons |
|---|---|---|---|
| NTFS/SMB shares | mount -t cifs on Docker host, then bind-mount into containers | AD-integrated, most enterprises already have this | Network dependency, SMB lock contention |
| OneDrive/SharePoint | rclone mount as FUSE on Docker host | Cloud-native, Microsoft ecosystem | Latency, rclone complexity, FUSE overhead |
| NFS | Standard NFS mount on host | Fast, Unix-native | Less common in Windows shops |
| Local disk | Direct paths | Fastest, simplest | Doesn’t scale to multiple hosts |
| MinIO/S3 | s3fs-fuse or rclone mount | Scales, cheap storage | Latency, eventual consistency |
Recommended for most enterprises: SMB shares mounted on the Docker host, then bind-mounted into per-user containers. This works with existing Windows file servers and AD permissions as a secondary enforcement layer.
bash
# On the Docker host (one-time setup or via systemd mounts)mount -t cifs //fileserver/engineering /shared/engineering \ -o credentials=/etc/samba/creds,uid=1000,gid=1000
mount -t cifs //fileserver/finance /shared/finance \ -o credentials=/etc/samba/creds,uid=1000,gid=1000For OneDrive, rclone runs as a service on the Docker host:
bash
rclone mount onedrive:Engineering /shared/engineering \ --vfs-cache-mode full \ --vfs-cache-max-age 1h \ --allow-other5. CRDT Sync (Multi-User Editing)
Section titled “5. CRDT Sync (Multi-User Editing)”The CRDT plugin handles the case where Alice and Bob both have project-alpha/ mounted and edit the same file simultaneously.
Key change from original design: Sync rooms are per-folder, not per-vault:
text
Alice’s container: CRDT Plugin connects to: → Room: folder-engineering (rw) → Room: folder-project-alpha (rw) → Room: folder-wiki (ro) → Room: user-alice-personal (private)
Bob’s container: CRDT Plugin connects to: → Room: folder-marketing (rw) → Room: folder-project-alpha (rw) ← same room as Alice! → Room: folder-wiki (ro) ← same room as Alice! → Room: user-bob-personal (private)
In file-only mode (no WebSocket server): CRDT state files in .crdt/ folders sync automatically via the shared storage. Alice saves → .crdt/project-alpha/notes/plan.md.alice-uuid.yjs appears on the SMB share → Bob’s plugin picks it up on background check.
In hybrid mode (with WebSocket server): Real-time collaboration via y-websocket, with file-based state as backup. This is better for the Kasm use case since latency matters.
6. Cross-Mount File Moves (The EXDEV Problem)
Section titled “6. Cross-Mount File Moves (The EXDEV Problem)”When folders are separate bind mounts, dragging a file between them fails with EXDEV. The solution from your exploration:
A small Obsidian plugin (or patch to the CRDT plugin) that catches the error:
typescript
// Override Obsidian's file move to handle cross-mountthis.app.vault.on("rename", async (file, oldPath) => { try { await fs.rename(oldPath, file.path); } catch (err) { if (err.code === "EXDEV") { await fs.copyFile(oldPath, file.path); await fs.unlink(oldPath); new Notice("File moved across permission boundaries"); } }});Full Deployment Stack
Section titled “Full Deployment Stack”text
┌─────────────────────────────────────────────────────────┐ │ INFRASTRUCTURE │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Authentik │ │ Kasm │ │ y-websocket │ │ │ │ (SSO/IdP) │ │ Workspaces │ │ (CRDT sync) │ │ │ │ │ │ (orchestr.) │ │ (optional) │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ │ └────────┬─────────┘ │ │ │ │ │ │ │ ┌────────▼──────────┐ │ │ │ │ Mount Orchestrator │ │ │ │ │ (custom service) │ │ │ │ │ │ │ │ │ │ Reads JWT groups │ │ │ │ │ Reads permissions │ │ │ │ │ Generates mounts │ │ │ │ │ Generates configs │ │ │ │ └────────┬──────────┘ │ │ │ │ │ │ │ ┌─────────────▼─────────────────────────────▼──────┐ │ │ │ Per-User Obsidian Containers │ │ │ │ │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │ Alice │ │ Bob │ │ Carol │ … │ │ │ │ │ eng/ │ │ mktg/ │ │ finance/│ │ │ │ │ │ proj-a/ │ │ proj-a/ │ │ exec/ │ │ │ │ │ │ wiki/ │ │ wiki/ │ │ wiki/ │ │ │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ └───────┼────────────┼────────────┼─────────────────┘ │ │ │ │ │ │ │ ┌───────▼────────────▼────────────▼─────────────────┐ │ │ │ SHARED STORAGE │ │ │ │ │ │ │ │ /shared/engineering/ (SMB: //fs/engineering) │ │ │ │ /shared/finance/ (SMB: //fs/finance) │ │ │ │ /shared/marketing/ (SMB: //fs/marketing) │ │ │ │ /shared/project-alpha/ (SMB: //fs/proj-alpha) │ │ │ │ /shared/wiki/ (SMB: //fs/wiki) │ │ │ │ /users/alice/ (per-user storage) │ │ │ │ /users/bob/ │ │ │ └────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
Implementation Roadmap
Section titled “Implementation Roadmap”Phase 1: POC (Weeks 1–2)
Section titled “Phase 1: POC (Weeks 1–2)”- Deploy Kasm Workspaces (Docker single-server)
- Import linuxserver/obsidian as a Kasm workspace image
- Configure Authentik → Kasm SAML integration
- Manual bind mounts for 2–3 test folders
- Verify: user logs in → sees Obsidian → sees correct folders
Phase 2: Mount Orchestrator (Weeks 3–4)
Section titled “Phase 2: Mount Orchestrator (Weeks 3–4)”- Build the orchestrator service (TypeScript/Node)
vault-permissions.yamlconfig format- JWT group extraction
- Dynamic Docker mount generation
- Per-user
.obsidian/config generation - Integration with Kasm launch hooks
Phase 3: Storage Integration (Weeks 5–6)
Section titled “Phase 3: Storage Integration (Weeks 5–6)”- SMB/CIFS mounts on Docker host (systemd mount units)
- OneDrive via rclone (if needed)
- Per-user storage provisioning
_inbox/writable folder setup
Phase 4: CRDT Sync (Weeks 7–8)
Section titled “Phase 4: CRDT Sync (Weeks 7–8)”- Deploy CRDT plugin to Obsidian image
- Per-folder sync room configuration
- Optional: y-websocket server for real-time
- Cross-mount move handling (EXDEV shim)
Phase 5: Hardening & Polish (Weeks 9–10)
Section titled “Phase 5: Hardening & Polish (Weeks 9–10)”- Container hardening (HARDEN_DESKTOP, file transfer lockdown)
- Audit logging (who accessed what, when)
- Dead link detection plugin (“you don’t have access to this folder”)
- Session persistence / hibernation policy
- Load testing with concurrent users
Key Decisions Still Needed
Section titled “Key Decisions Still Needed”| Decision | Options | Recommendation |
|---|---|---|
| VNC gateway | Kasm vs Guacamole vs raw Selkies | Kasm — purpose-built, SSO integration, DLP, session management out of the box |
| Storage backend | SMB vs OneDrive vs NFS | SMB for most enterprises (already have file servers); rclone for OneDrive if Microsoft-heavy |
| CRDT sync mode | File-only vs hybrid vs server-only | Hybrid — y-websocket for real-time in Kasm, file fallback for resilience |
| IdP | Authentik vs Keycloak vs Azure AD direct | Authentik if self-hosting; Azure AD direct if already Microsoft |
| Container lifecycle | Destroy on logout vs hibernate | Hibernate for fast reconnect, destroy after timeout (e.g., 4 hours idle) |
| Scaling | Single Docker host vs Kubernetes | Start single host, migrate to K8s when you need >~50 concurrent users |
What This Gives You
Section titled “What This Gives You”An employee opens obsidian.company.com in their browser. They log in with their company credentials. Within seconds, they’re looking at a full Obsidian desktop — with only the folders they’re authorized to see, cross-linked and searchable as one vault. Another employee on the same project sees the same shared folders, and edits merge automatically via CRDT. IT controls everything through group memberships and a YAML config file. No Obsidian licenses to manage, no local installs, no data on employee laptops.
What is Mount Orchestrator? Do we not need something like merge FS to do all this? I guess I’m talking about the chasm workspaces layer. Because remember, a person could have multiple groups that they’re a part of, so how does that work in this?
Mount Orchestrator = The Custom Glue Between Kasm and Storage
Section titled “Mount Orchestrator = The Custom Glue Between Kasm and Storage”The Mount Orchestrator isn’t a product you install — it’s the custom service you build that answers one question:
“This user has these groups → which folders get mounted into their container?”
You Don’t Need MergerFS
Section titled “You Don’t Need MergerFS”Here’s why. The multi-group problem is simpler than it sounds:
text
Alice’s groups: [engineering, project-alpha, all-employees]
Orchestrator reads vault-permissions.yaml: engineering/ → allowed: [engineering, devops] ✅ Alice is in engineering project-alpha/ → allowed: [engineering, marketing] ✅ Alice is in engineering finance/ → allowed: [finance, executives] ❌ nope wiki/ → allowed: [all-employees] ✅ Alice is in all-employees
Result: mount engineering/, project-alpha/, wiki/ into Alice’s container
That’s just Docker bind mounts — no union filesystem needed:
text
docker run \ -v /shared/engineering:/user-vault/engineering \ -v /shared/project-alpha:/user-vault/project-alpha \ -v /shared/wiki:/user-vault/wiki:ro \ -v /users/alice/personal:/user-vault/personal \ -v /users/alice/.obsidian:/user-vault/.obsidian \ obsidian-image
Obsidian opens /user-vault/ and sees:
text
/user-vault/ ├── .obsidian/ ← Alice’s config ├── personal/ ← Alice only ├── engineering/ ← shared, read-write ├── project-alpha/ ← shared, read-write └── wiki/ ← shared, read-only
Each folder is a separate bind mount. They don’t need to be merged. They just appear as subdirectories inside one vault. Obsidian doesn’t care that they’re separate mount points under the hood — it sees a folder tree.
Where MergerFS Would Matter (And Why You Probably Don’t Need It)
Section titled “Where MergerFS Would Matter (And Why You Probably Don’t Need It)”MergerFS solves a different problem: merging multiple directories into a flat namespace where files from different sources appear in the same folder. Example:
text
# MergerFS: everything mixes together /vault/README.md ← could be from engineering OR wiki (collision!) /vault/api-design.md ← from engineering /vault/onboarding.md ← from wiki
You don’t want that. You want subfolder separation — each permission boundary is its own folder. That’s just bind mounts. No FUSE, no MergerFS, no extra layer.
The only scenario where MergerFS helps is the cross-mount rename() problem (moving files between engineering/ and project-alpha/). But that’s better solved with the EXDEV plugin shim than by adding a FUSE filesystem to every container.
How It Works Inside Kasm Specifically
Section titled “How It Works Inside Kasm Specifically”Here’s the actual flow with Kasm in the picture:
text
1. Alice opens obsidian.company.com ↓ 2. Kasm redirects to Authentik (SSO) ↓ 3. Alice authenticates, gets JWT: { “sub”: “alice”, “groups”: [“engineering”, “project-alpha”, “all-employees”] } ↓ 4. Kasm receives the authenticated session ↓ 5. Kasm calls the Mount Orchestrator (via server-side launch hook) ↓ 6. Orchestrator: a. Reads Alice’s groups from JWT b. Reads vault-permissions.yaml c. Computes: engineering ✅, project-alpha ✅, wiki ✅, finance ❌ d. Returns a list of Docker volume mounts e. Generates Alice’s .obsidian/ config (valid paths only) ↓ 7. Kasm launches a container with those specific bind mounts ↓ 8. Alice sees Obsidian with her authorized folders
Kasm’s Role vs Orchestrator’s Role
Section titled “Kasm’s Role vs Orchestrator’s Role”| Responsibility | Who Does It |
|---|---|
| User authentication (SSO) | Kasm + Authentik |
| Spinning up/down containers | Kasm |
| Session management (reconnect, timeout) | Kasm |
| DLP (clipboard, file transfer lockdown) | Kasm |
| Deciding which folders to mount | Mount Orchestrator (custom) |
| Generating per-user .obsidian config | Mount Orchestrator (custom) |
| Passing mount list to Docker | Mount Orchestrator → Kasm API |
Kasm supports Exec Config / Server Side Scripts that run before a workspace launches. That’s where the orchestrator hooks in — it’s a script or API call that Kasm triggers at launch time, which returns the volume mount configuration for that specific user.
Multi-Group Example
Section titled “Multi-Group Example”yaml
folders: - name: engineering source: /shared/engineering allowed_groups: [engineering, devops] access: readwrite
- name: marketing source: /shared/marketing allowed_groups: [marketing] access: readwrite
- name: project-alpha source: /shared/project-alpha allowed_groups: [engineering, marketing, project-alpha-team] access: readwrite
- name: executive-reports source: /shared/executive allowed_groups: [executives] access: readonly
- name: wiki source: /shared/wiki allowed_groups: [all-employees] access: readonly| User | Groups | Sees These Folders |
|---|---|---|
| Alice (engineer) | engineering, project-alpha-team, all-employees | engineering/, project-alpha/, wiki/, personal/ |
| Bob (marketer) | marketing, project-alpha-team, all-employees | marketing/, project-alpha/, wiki/, personal/ |
| Carol (CFO) | finance, executives, all-employees | executive-reports/ (ro), wiki/, personal/ |
| Dave (DevOps) | devops, engineering, all-employees | engineering/, wiki/, personal/ |
Alice and Bob both see project-alpha/ — it’s the same data on disk, just bind-mounted into both their containers. If Alice edits a file there, Bob sees it (via CRDT sync or just filesystem-level because it’s the same underlying path).
- Mount Orchestrator = custom code that maps
user groups → folder mounts. It’s the brain. - Kasm = the container orchestrator that actually spins up Obsidian and streams it to the browser. It’s the muscle.
- MergerFS = not needed. Plain Docker bind mounts give you subfolder-level access control without any FUSE overhead. Each permitted folder just appears as a subdirectory in the user’s vault.
- Multiple groups = the orchestrator unions them. If any of your groups grants access to a folder, that folder gets mounted. Simple set logic.