Architecture

Security model

Status. Flatpak sandboxing and the portal frontend work today. The Eydos portal backend implements notifications and settings. The consent dialog in the compositor, the permission store integration, the Settings page, the resource brokers, the local-executable launcher and the AppArmor policy are designed, not built. Disk encryption and ptrace_scope hardening are in the image.

Principle

A permission is only real when it is enforced outside the process it limits. A check inside the toolkit, or inside the app’s own process, is consent UX: it tells a well-behaved app what the user wants, and does nothing against a hostile one. Every mechanism below is judged by that standard, and the page says plainly where something is UX rather than a barrier.

One permission system for every application class

Decisions

Every decision the user makes is stored once, in xdg-desktop-portal’s permission store, keyed by the application’s identity: the Flatpak app id, the Android package name, the AppArmor label of a native app, or the id the launcher generated for a local executable. One store means Settings has one backend to read and one to write, whatever kind of app the row belongs to.

The consent dialog is the Access portal, implemented in Forge. It lives in the compositor because the compositor is the only process that can guarantee the dialog is really on screen, uncovered and not imitated by the app that triggered it. The same modal serves Flatpak, Android and native apps.

Resources without a portal

Cameras, microphones, location, files, printing and notifications have portals. Contacts, SMS, call logs, phone state and calendar do not. For these Eydos follows the Binder model: the service that owns the resource checks who is calling before answering. The caller is identified by its process, through a pidfd and its /.flatpak-info, or by its AppArmor label as reported by the bus. Identities derived from cgroups are not used, because a host process can forge them.

Semantics

Permissions follow Android’s shape, which users already understand:

  • A permission the app did not declare is denied without asking.
  • Normal permissions are granted at install.
  • Dangerous permissions are asked per group, with allow, only this time and deny.
  • Two denials mean the app is not asked again until the user changes it in Settings.

Settings

Settings has an App permissions page, per app and per permission group, to view and revoke, plus the persisted file and folder grants and the static Flatpak permissions from the app’s manifest.

The file chooser is the permission

Eydos does not grant apps access to the file system. When an app opens or saves a file, the portal shows the system file chooser, drawn by ltk, and the document portal hands the app only what the user picked. This is Android’s Storage Access Framework applied to Linux: the dialog is the permission. Android apps get it through the standard intents, ACTION_OPEN_DOCUMENT and friends, which rustdroid maps to the portal; the result comes back as a content URI over the chosen files. Printing works the same way: the app renders a PDF and hands it to the print portal, and never needs the printing socket.

Local executables and the single door

Code that is neither the system nor an installed app only runs through eydos-run, which wraps it in a sandbox with a generated identity, as described under application classes. That is only a barrier if nothing else can execute such code. AppArmor provides that guarantee:

  • Every process tree that is not the system’s is born with a profile and cannot leave it. Whatever a confined process launches inherits its confinement.
  • The shell launches registered applications only, each with its class and profile, never an arbitrary path.
  • The file manager opens files, it never executes them. A script opens in an editor.
  • Executing a path outside the system is denied to every confined profile. The one exception is eydos-run, a system binary, which executes it inside the sandbox.
  • Executable mappings are mediated too, so loading a library from outside the system, or running an ELF through the dynamic linker, is denied as well.
  • Indirect doors are closed on the bus: a confined process cannot ask the user’s service manager, D-Bus activation or the Flatpak development portal to run something on its behalf, and cannot elevate through pkexec or sudo.
  • kernel.yama.ptrace_scope is set so a process cannot read or inject into another user’s process, and AppArmor mediates tracing between profiles.

Mount options such as noexec on the home directory, temporary directories and removable media are applied as hygiene, but nothing relies on them: interpreters, memfd_create and JIT all bypass them.

Interpreters

An interpreter is a system binary; the program it runs is a file it reads as data. No file-based mechanism, AppArmor, IPE or noexec, sees that program. There is no way to control which script runs; there is only control over what the interpreter is allowed to do. Eydos therefore has one rule: an interpreter never gains privileges. Executing it always inherits the confinement of whoever launched it. What a script can do is then decided by its context:

ContextInterpreter runs asBarrier
System serviceThe service’s profileAppArmor
Sandboxed app (a bundled Python, ART, a web engine)The app’s identity, inside its sandboxbwrap
Local executable through eydos-runThe generated identity, inside its sandboxbwrap
File managerNever: files open, they do not executeAppArmor
Terminal on a phoneUntrusted: the terminal’s own confined identityAppArmor
Terminal on a desktop, or with developer modeUnconfined: the user takes the developer rolePolicy, by choice

The terminal is the only interactive door to an interpreter with arbitrary arguments, and so the only real policy decision. On a phone it is confined unless the device is in developer mode. On a desktop an unconfined terminal is legitimate use, and the person who opens it takes responsibility for what they run. Which policy applies follows the same layout decision Forge makes for the screen.

Two complements. First, the phone image ships as few interpreters as the base needs: sh and perl-base are unavoidable for packaging, while python3 is not essential and is kept off the image unless something the user needs pulls it in. Second, Linux 6.14 gives interpreters a way to ask the kernel whether a file may be executed, and per-process flags that tell them to check the scripts they open and to refuse interactive code. Once Debian’s interpreters honour those flags, eydos-run and the confined terminal will set them, and scripts will enter the provenance model like any binary. Eydos ships Linux 6.12 today, so this is roadmap.

Code provenance

The confinement above answers who may do what. A second, later layer answers what code may run at all: IPE, the integrity policy LSM in Linux 6.12, denies executing or mapping any file that does not come from a trusted origin, a signed dm-verity volume or a file carrying a signed fs-verity digest. Unlike path-based rules it cannot be bypassed by copying a file elsewhere, by memfd_create or by loading a library from an app’s data directory.

IPE is global: it cannot exempt sandboxed apps. With a default-deny policy it blocks every native library an app or an APK ships. The way to reconcile it with third-party apps is for the Eydos repository to sign what it serves: it computes and signs the fs-verity digest of every executable in the apps it mirrors from Flathub and F-Droid, and the installer enables fs-verity with that signature when it deploys the app. The repository then has a security role, not only a distribution one: what it serves is the only third-party code the device runs. Whether the system itself is identified by a verified root volume or by per-file signatures applied at package install is an open decision, and the system image page keeps both routes open.

Known limits

  • The microphone under Flatpak is all or nothing, because the audio socket is a single permission. A fine-grained record permission needs mediation in PipeWire.
  • Until rustdroid sandboxes the app process, its permission checks bind the Android API paths but not native libraries an APK may carry. For well-behaved apps this is consent; for a hostile one it is not a barrier.
  • Scripts are controlled by their context, never by themselves.
  • Everything under Status at the top of this page as not built is, today, not a barrier.