Skip to content

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

Here’s how all the pieces snap together into a working system.


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


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 │ └─────────────────────────────────────────────────────────────────────┘


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.


This is the custom piece. It sits between Kasm and the container launch, translating group memberships into bind mounts.

yaml

config/vault-permissions.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: readwrite

What it does at session start:

typescript

// Pseudocode
function 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

Each user gets a fresh container with this shape:

yaml

# Generated dynamically per user by the orchestrator
services:
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:ro

Note: Skip HARDEN_OPENBOX — it breaks the auto-maximize/scaling behavior you need.


SourceHow It Gets to the Docker HostProsCons
NTFS/SMB sharesmount -t cifs on Docker host, then bind-mount into containersAD-integrated, most enterprises already have thisNetwork dependency, SMB lock contention
OneDrive/SharePointrclone mount as FUSE on Docker hostCloud-native, Microsoft ecosystemLatency, rclone complexity, FUSE overhead
NFSStandard NFS mount on hostFast, Unix-nativeLess common in Windows shops
Local diskDirect pathsFastest, simplestDoesn’t scale to multiple hosts
MinIO/S3s3fs-fuse or rclone mountScales, cheap storageLatency, 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=1000

For 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-other

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-mount
this.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");
}
}
});

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/ │ │ │ └────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘


  • 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
  • Build the orchestrator service (TypeScript/Node)
  • vault-permissions.yaml config 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
  • 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

DecisionOptionsRecommendation
VNC gatewayKasm vs Guacamole vs raw SelkiesKasm — purpose-built, SSO integration, DLP, session management out of the box
Storage backendSMB vs OneDrive vs NFSSMB for most enterprises (already have file servers); rclone for OneDrive if Microsoft-heavy
CRDT sync modeFile-only vs hybrid vs server-onlyHybrid — y-websocket for real-time in Kasm, file fallback for resilience
IdPAuthentik vs Keycloak vs Azure AD directAuthentik if self-hosting; Azure AD direct if already Microsoft
Container lifecycleDestroy on logout vs hibernateHibernate for fast reconnect, destroy after timeout (e.g., 4 hours idle)
ScalingSingle Docker host vs KubernetesStart single host, migrate to K8s when you need >~50 concurrent users

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?”


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.


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

ResponsibilityWho Does It
User authentication (SSO)Kasm + Authentik
Spinning up/down containersKasm
Session management (reconnect, timeout)Kasm
DLP (clipboard, file transfer lockdown)Kasm
Deciding which folders to mountMount Orchestrator (custom)
Generating per-user .obsidian configMount Orchestrator (custom)
Passing mount list to DockerMount 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.


yaml

vault-permissions.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
UserGroupsSees These Folders
Alice (engineer)engineering, project-alpha-team, all-employeesengineering/, project-alpha/, wiki/, personal/
Bob (marketer)marketing, project-alpha-team, all-employeesmarketing/, project-alpha/, wiki/, personal/
Carol (CFO)finance, executives, all-employeesexecutive-reports/ (ro), wiki/, personal/
Dave (DevOps)devops, engineering, all-employeesengineering/, 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.