+++ /dev/null
-<!DOCTYPE html>
-<html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>AccountTree 5.0 Design</title>
- <script>
- {
- let dirpr_library = null;
- try{ dirpr_library = localStorage.getItem("RT.dirpr_library"); }catch(e){}
- window.RT = window.RT || {};
- RT.dirpr_library = RT.dirpr_library || dirpr_library || "https://style.ReasoningTechnology.com/Manuscript";
- document.write('<script src="' + RT.dirpr_library + '/Core/RT-Manuscript_make.js"><\/script>');
- }
- </script>
- <script>
- window.RT.theme_preference('inverse_wheat');
- window.RT.load('Layout/paginate');
- window.RT.load('Layout/article_tech_ref');
- window.RT.load('Element/theme_selector');
- </script>
- </head>
- <body>
- <RT·theme-selector></RT·theme-selector>
- <RT·article>
- <RT·title
- author="Thomas Walker Lynch"
- date="2026-11-10"
- title="AccountTree 5.0 Design">
- </RT·title>
-
- <RT·TOC level="1"></RT·TOC>
-
- <h1>Purpose</h1>
-
- <p>
- AccountTree keeps light containers made of Linux accounts. A person's work is divided among accounts, each with its own files and processes, so that a web browser, an email program and a development project cannot read each other's files, without the cost of a virtual machine. This document gives the design as it is currently envisioned, for the standard numbered 5.0. It covers the accounts, the storage that keeps their data, the systems the storage is attached to, the databases, the requests through which accounts and mounts are changed, the programs that make them, and login. Where a decision is still open it is marked as open. The features planned for later standards are listed at the end.
- </p>
-
- <p>
- <RT·code>document/doc_2.html</RT·code> gives the reasoning behind the flat home directories and the idmapped views. It was written in the earlier terms described below; this document builds on it.
- </p>
-
- <h1>Ontology</h1>
-
- <h2>Why the terms changed</h2>
-
- <p>
- The project was called SubU, for subordinate user, and spoke of a MasU, a master user, owning its SubUs. In Linux the word user is overloaded. It names the person at the keyboard, human or AI, and it names the account in <RT·code>/etc/passwd</RT·code> that the person uses, as in <RT·code>useradd</RT·code> and user name. The two are different things: one person uses many accounts, and an account has no will of its own. The design came to need the difference everywhere. Two accounts of one person are not the same account, while an account on a system and an account in a storage database can be one account in two places. With one word for both, every sentence about them was ambiguous.
- </p>
-
- <p>
- The MasU and SubU terms had a second fault. Some passages spoke as though there were one MasU at the top, and others as though every SubU could be a MasU. Both were true, because master and subordinate are relative, but the terms gave no word for the top, or for an account with nothing below it.
- </p>
-
- <p>
- So the word user is dropped. A person is the agent; an account is the account. The structure is described in the terms of a tree, which are relative where they should be and fixed where they should be. The project is renamed AccountTree, for what it keeps, and SubU and MasU are retired. The word user survives only in names the project does not own: uid, which is short for user id, <RT·code>useradd</RT·code> and <RT·code>userdel</RT·code>, and the kernel's user namespaces.
- </p>
-
- <h2>The account tree</h2>
-
- <p>
- Every account on a system has one parent, and root is at the top. In Linux root already administers every account, so making root the top of the tree describes what is already so, and the tree fits into Linux without changing its shape. Root keeps its one meaning.
- </p>
-
- <ul>
- <li><RT·term>person</RT·term>: the agent who logs in and owns data in the world, human or AI.</li>
- <li><RT·term>account</RT·term>: a Linux account, with its own uid, home, processes and sessions.</li>
- <li><RT·term>account tree</RT·term>: the accounts of a system, with root at the top.</li>
- <li><RT·term>parent account</RT·term>: the account directly above an account. <RT·term>child account</RT·term>: an account directly below one.</li>
- <li><RT·term>login account</RT·term>: a child account of root.</li>
- <li><RT·term>leaf account</RT·term>: an account with no child accounts.</li>
- </ul>
-
- <p>
- Every account is entered through its parent. Root enters its child accounts through the system's login programs, the display manager, <RT·code>login</RT·code> at the console and <RT·code>sshd</RT·code>, which run as root and start a session for a person who gives a password or a key. A login account enters its child accounts through AccountTree's request gate, which starts sessions and programs in them; an account below a login account has no password, and the login programs do not enter it. The rule is the same at every level of the tree. Only the means differ, and the means for root's children are the ones Linux already has.
- </p>
-
- <p>
- A parent account reaches a child account's files through a view.
- </p>
-
- <ul>
- </ul>
-
- <p>
- An account's name is its parent's name, the middle dot, and its own short name: <RT·code>Thomas·developer·scratch</RT·code> is a child of <RT·code>Thomas·developer</RT·code>, which is a child of the login account <RT·code>Thomas</RT·code>, which is a child of root. In identifiers the wider category comes first, so prose says child account and parent account, and code says <RT·code>account_child</RT·code> and <RT·code>account_parent</RT·code>.
- </p>
-
- <h1>Term</h1>
-
- <ul>
- <li><RT·term>storage</RT·term>: a persistent storage device carrying accounts' data, such as the SSD inside a laptop or an encrypted USB drive. Its root is the directory it is mounted at. A storage can be moved between machines.</li>
- <li><RT·term>tree dir</RT·term>: the directory <RT·code><storage root>/home/AccountTree</RT·code>, holding the storage database and the accounts' homes. On the disk a system runs from it is <RT·code>/home/AccountTree</RT·code>. AccountTree_admin is given a tree dir to name a storage.</li>
- <li><RT·term>system</RT·term>: an installed, running Linux, with its own <RT·code>/etc/passwd</RT·code>, its own uid numbering, and its own <RT·code>/home</RT·code>, into which the accounts kept on attached storage are mounted.</li>
- <li><RT·term>system account</RT·term>: an account in a system's <RT·code>/etc/passwd</RT·code>. <RT·term>storage account</RT·term>: an account in a storage database.</li>
- <li><RT·term>storage database</RT·term>: the SQLite database in a tree dir, describing the storage accounts kept on that storage.</li>
- <li><RT·term>system database</RT·term>: the SQLite database at <RT·code>/var/lib/AccountTree/system.sqlite3</RT·code>, a system's own record. Its system map is the authority for which storage accounts are the same accounts as system accounts.</li>
- <li><RT·term>database uuid</RT·term>: the random identifier a storage database is given when it is made. It names the database, not the storage: a copy of the database carries it too. It is not the storage uuid that a file system is given when it is formatted, which AccountTree does not use.</li>
- <li><RT·term>system uuid</RT·term>: the random identifier a system database is given when it is made, which names the system.</li>
- <li><RT·term>database uid</RT·term>: a storage account's uid in its storage database's own numbering. The account's files on the storage carry it.</li>
- <li><RT·term>system uid</RT·term>: a system account's uid, in <RT·code>/etc/passwd</RT·code>.</li>
- <li><RT·term>account specifier</RT·term>: the name of an account in one place: <RT·code>system <host name> <system account></RT·code>, or <RT·code>storage <tree dir> <storage account></RT·code>.</li>
- <li><RT·term>same account</RT·term>: two account specifiers that name one account in two places. The system map records which system accounts are the same accounts as which storage accounts.</li>
- <li><RT·term>view</RT·term>: an idmapped mount through which a parent account sees a child account's files as its own.</li>
- <li><RT·term>request</RT·term>: what a person asks of AccountTree, such as making a child account. A request is an instruction of the request language, given in a journal to the request gate.</li>
- <li><RT·term>gate</RT·term>, <RT·term>guard</RT·term>, <RT·term>interpreter</RT·term>: Man_In_Grey's parts. A journal given to a gate is checked by its guard, and played by its interpreter only when the guard approves it. The gate invoking account is the account that started the gate, which the kernel reports as its real uid; the gate owner account is the one whose effective uid the guard and interpreter run with. <RT·code>Man_In_Grey.html</RT·code> gives the design.</li>
- <li><RT·term>database account</RT·term>: the system account <RT·code>AccountTree</RT·code>, which owns AccountTree's databases and is the only account that opens them, besides root. It holds no other privilege.</li>
- <li><RT·term>answer</RT·term>: the result one interpreter hands back to the program that invoked its gate, on descriptor 3, which that program checks before it acts on it.</li>
- <li><RT·term>unmanaged account</RT·term>: an account AccountTree does not manage, such as root, a service's account, or an account made with <RT·code>useradd</RT·code> by hand.</li>
- </ul>
-
- <h1>Account and name</h1>
-
- <h2>Account name</h2>
-
- <p>
- The middle dot (U+00B7, written <RT·code>·</RT·code>) is not permitted in a short name, so a full name divides in exactly one way, and the conventional characters <RT·code>_</RT·code>, <RT·code>-</RT·code> and <RT·code>.</RT·code> remain free for use in names. It lies outside the POSIX portable character set. Making an account needs the <RT·code>--badname</RT·code> flag of <RT·code>useradd</RT·code>, and a tool that checks account names against the portable set needs its check widened. In UTF-8 the middle dot is two bytes, and an account name is currently limited to 32 bytes. That limit is set in user space, by the fixed name field of the <RT·code>utmp</RT·code> login records and by <RT·code>useradd</RT·code>, not by the kernel, which knows accounts only by uid. pkg-config prints paths holding the middle dot in a form that is not valid UTF-8; builds that pass such paths through pkg-config need the workaround given in <RT·code>shared/project-dependency/Python.org</RT·code>.
- </p>
-
- <p>
- The name <RT·code>AccountTree</RT·code> is reserved, because on a running system <RT·code>/home/AccountTree</RT·code> holds AccountTree's own files. A system account made for a branch of a storage database is named with a tilde and eight hex digits, as <RT·code>Thomas·try~a7b3247f</RT·code>, described under Copies. That is a default, not a rule on names: the system map, not the name, records which system accounts belong to a branch.
- </p>
-
- <h2>Identity of an account</h2>
-
- <p>
- A storage account has an identity in its storage database, and, when the storage is attached to a running system, an extended identity on that system.
- </p>
-
- <p>
- In its storage database, an account is identified by the database uuid together with its database uid. Its files on the storage are owned by that database uid, and root, mounting the storage by hand without AccountTree, sees that database uid on them. This pair is all the storage database needs to identify the account.
- </p>
-
- <p>
- On a running system, the account also needs a system uid, because that numbering is the one <RT·code>/etc/passwd</RT·code> and the kernel use there. A row of the system map gives it: the row pairs the storage account's identity with a system account. The account's extended identity is its identity in its storage database, joined through the system map to the system.
- </p>
-
- <h1>Storage and system</h1>
-
- <h2>Storage layout</h2>
-
- <p>
- Every storage has the same layout under its root, where the root is the directory the storage is mounted at: <RT·code>/</RT·code> for the disk a system runs from, or a mount point such as <RT·code>/mnt/Eagle</RT·code> for a drive plugged in. Its tree dir is <RT·code>home/AccountTree</RT·code> under the root, so <RT·code>/home/AccountTree</RT·code> or <RT·code>/mnt/Eagle/home/AccountTree</RT·code>.
- </p>
-
- <RT·code>
- home/AccountTree/ the tree dir: root's, mode 0711
- home/AccountTree/database/ the database account's, mode 0700
- home/AccountTree/database/storage.sqlite3 the storage database
- home/AccountTree/account_data/ root's, mode 0700
- home/AccountTree/account_data/<name> the home of each storage account, by name
- home/<other> anything else, such as the homes of unmanaged accounts
- </RT·code>
-
- <p>
- Everything AccountTree keeps on a storage is under its tree dir, so the storage can carry other things beside it, and the storage database sits with the data it describes. <RT·code>account_data</RT·code> holds the accounts' homes; on a running system each is reached through an idmapped mount at <RT·code>/home/<system account></RT·code>. An account named <RT·code>database</RT·code> cannot collide with the database, because the two are at different levels.
- </p>
-
- <p>
- The ownership divides the tree dir between the two accounts that act on it. SQLite writes a journal file beside a database while a transaction is open, so the account that writes a database needs to write the directory it is in; the database is therefore in a directory of its own, <RT·code>database/</RT·code>, which the database account owns. Everything else is root's. The database account can pass through the tree dir, mode 0711, to reach its directory, but cannot enter <RT·code>account_data</RT·code>, so it cannot rename, move or read a home. A root interpreter that checks a home's owner on disk is then checking something the database account could not have arranged; see Answer check.
- </p>
-
- <p>
- On the disk a system runs from, AccountTree's files are therefore under <RT·code>/home</RT·code>, beside the homes of unmanaged accounts. Distributions already keep <RT·code>/home</RT·code> apart from the operating system: openSUSE on btrfs makes it a subvolume outside the root snapshots, so a rollback of the root leaves it alone, and installers on other file systems commonly offer it as a partition of its own, kept when the system is reinstalled. AccountTree's files inherit this without depending on any file system. Were they on the root file system, a rollback of the root would roll back the storage database and the accounts' files with it.
- </p>
-
- <p>
- A tool that walks <RT·code>/home</RT·code> sees each mounted account's files twice: at <RT·code>/home/<system account></RT·code>, and at <RT·code>/home/AccountTree/account_data/<name></RT·code>. A bind mount of the same file system reports the same device number, so options such as <RT·code>rsync -x</RT·code> do not stop at the mount. A backup of <RT·code>/home</RT·code> excludes one of the two.
- </p>
-
- <h2>Every storage is the same</h2>
-
- <p>
- A storage is a repository of accounts' data, whether it is built into the machine or plugged in, and a machine can have several. The SSD inside a laptop is attached at boot, before any login, in the same way a USB drive is attached when it is plugged in. Taken out of the laptop and put into a USB enclosure, the same SSD can be attached to another machine, and its accounts used there.
- </p>
-
- <h2>Copy and branch</h2>
-
- <p>
- The database uuid names the database, not the storage it is on. A copy of a tree dir, made for a backup or an archive, carries the same database uuid, and is the same database until one of the two is changed. Because a tree dir can be anywhere, an archive kept at <RT·code>/<storage name>/<date>/home/AccountTree</RT·code> can be attached by giving that tree dir.
- </p>
-
- <p>
- A storage moved between systems, plugged into a desktop, then a laptop, then the desktop again, is the ordinary case. A copy used apart from its original is the case to notice: a storage restored from a backup after the first was lost, or a lost drive found again after work went on on its replacement. The record of transactions tells these apart, as git's history does:
- </p>
-
- <ul>
- <li>Every attach and every detach is a transaction in the storage database. Two copies used apart therefore have different heads, even when no account was made or changed on either.</li>
- <li>The system records every head of each storage database it sees, in its <RT·code>storage_head_seen</RT·code> table: when one of its accounts is made the same account as a system account, and at each attach and detach.</li>
- </ul>
-
- <p>
- At attach, the storage's record is compared with those heads:
- </p>
-
- <ul>
- <li><em>forward</em>: its record holds the last head seen. It is the storage seen before, moved on, perhaps on another system. It attaches without comment. The system does not care how far along the storage has gone, only that it has not gone back, or sideways.</li>
- <li><em>backwards</em>: its head is one seen earlier, and another state of the database has been attached since. It is an older copy, or the other line of a branch.</li>
- <li><em>sideways</em>: its record holds an earlier head seen, but not the last one. It is a branch, two copies each changed after they parted, or an older copy whose head this system never saw.</li>
- <li><em>unrelated</em>: its record holds no head seen here; it parted before this system first saw it.</li>
- </ul>
-
- <p>
- A storage gone backwards, sideways or unrelated is attached with a warning, and the warning and the kind are recorded. No one is kept from working: a person restoring from a backup attaches the restored storage and goes on, and its line becomes the one this system follows. Merging the two lines afterwards is done by hand in 5.0.
- </p>
-
- <p>
- The two lines can also be kept apart, so that both can be attached at once and their files compared. <RT·code>attach <tree dir> branch</RT·code> makes the storage a branch: it gives the database a new database uuid, records in it the database it came from and the head they parted after, and makes a system account for each storage account that had one on the line it came from, named <RT·code><system account>~<eight hex digits></RT·code>. The eight digits begin the hash of the transaction that made the branch, which no other branch has, and which does not change as either line grows. The branch is then a storage database in its own right.
- </p>
-
- <p>
- The same database attached twice would mount two homes at one place. When a storage's homes are already mounted from another copy of it, attach mounts nothing, and says to detach the other copy or attach this one as a branch.
- </p>
-
- <p>
- Bringing two lines together again is a merge. For the storage database, a merge replays the transactions of one line onto the other where they do not conflict. For the accounts' files, it is the integration problem of 7.0, of which merging two lines of one storage is the simplest case.
- </p>
-
- <p>
- These checks establish continuity, not authenticity. The hashes are not secret: they are in the storage database and the system database, and anyone who can read either can copy them into a fabricated database. Authenticity needs a secret, which is the subject of the keys planned for 6.0.
- </p>
-
- <h2>Uids are a storage database's own</h2>
-
- <p>
- The uids in a storage database are that database's own numbering. Files on the storage carry them. A storage database never adopts the uids of a system it is attached to; its accounts are mounted onto the system through idmapped mounts, which translate database uids to system uids as the files are used. A storage can therefore be moved from machine to machine without changing its files.
- </p>
-
- <p>
- A storage database gives out database uids counting down from 1999999999, while systems give out system uids counting up from low numbers. A database uid is therefore not also the system uid of an account, and a file on a storage, seen without its mount, does not appear to belong to a system account it does not belong to, except in the cases given under Limit of a uid. The band 1879048192 to 2147352575 is left unused by systemd's allocation of uids, so a storage database has about 120 million database uids before it leaves the band. An account brought into AccountTree from an existing one can keep its existing uid, with the keyword <RT·code>uid</RT·code> of <RT·code>account make</RT·code>; for that account the coincidence remains, behind the root-only directories of the tree dir.
- </p>
-
- <p>
- A storage database never reuses a database uid. A database uid given up by a deallocated account stays retired, because files carrying it can remain on the storage, in a backup, or on another storage they were copied to.
- </p>
-
- <h2>Limit of a uid</h2>
-
- <p>
- Two numberings meet on a running system: database uids, on the files of storage, and system uids, in <RT·code>/etc/passwd</RT·code>. Each has limits, set by different parties, and each limit is checked where the party that sets it is known.
- </p>
-
- <p>
- <em>The kernel.</em> Linux uids are 32 bits. 4294967295 is (uid_t)−1 and means no uid; 65534 is the overflow uid shown for a uid that cannot be mapped; neither is given out. A database uid is never used as a system uid. At run time it meets only the idmapped mount, which accepts any uid below 4294967295, so the kernel's limit is the same on every system and a storage moved between systems is unaffected by it.
- </p>
-
- <p>
- <em>The file system.</em> ext4, xfs and btrfs store 32-bit uids. JFFS2 and Minix store 16 bits, and FAT and exFAT store none; such storage cannot carry AccountTree accounts. This limit belongs to the storage and travels with it, so it is checked once, when the storage is made: <RT·code>storage make</RT·code> sets the owner of a test file to the first database uid, reads it back, and refuses the storage if the two differ. That check is not yet built.
- </p>
-
- <p>
- <em>The system.</em> <RT·code>UID_MAX</RT·code> in <RT·code>/etc/login.defs</RT·code> bounds only the uids <RT·code>useradd</RT·code> picks on its own; AccountTree gives the system uid. A system's range of system uids is set by <RT·code>system make</RT·code>, 2000 to 59999 by default, and is the system's own. A storage moved to a system with a smaller range keeps its database uids, since they never become that system's uids; the smaller range limits only how many of the storage's accounts the system can make system accounts for, and <RT·code>is-same-account</RT·code> reports when the range is used up.
- </p>
-
- <p>
- <em>Where the two numberings can still meet.</em> A database uid equals the system uid of some account when:
- </p>
-
- <ul>
- <li>an account was brought in with the keyword <RT·code>uid</RT·code> of <RT·code>account make</RT·code>, keeping its existing uid. This is deliberate.</li>
- <li>a system's own range reaches into the band. <RT·code>system make</RT·code> refuses a range that reaches 1879048192.</li>
- <li>accounts from outside AccountTree have uids in the band. A directory service mapping Active Directory accounts through SSSD gives out slices of 200000 uids up to 2000200000 by default, which is inside the band, and container uid ranges in <RT·code>/etc/subuid</RT·code> can be set anywhere.</li>
- <li>a storage database has given out nearly all its band, after about 120 million accounts. <RT·code>account make</RT·code> refuses below 1879048192. Should a database ever need more, a later standard chooses the next band and records it in the database, so that databases already made keep theirs.</li>
- </ul>
-
- <p>
- At attach, the database uids of the storage are to be compared with the system uids in use, from the system's account database, which includes directory services, and from <RT·code>/etc/subuid</RT·code>. Each coincidence is logged with the system account that would appear to own the raw files. A coincidence is not refused: through the mounts each account sees its own files correctly, and the raw files are behind <RT·code>account_data</RT·code>, which only root can enter. It is reported because a tool that works by uid on the raw files, run as root, would treat them as that account's. This check is not yet built.
- </p>
-
- <p>
- <em>Tools that copy files.</em> Backups and archives copy uids with the files. The old ustar format of tar holds uids up to 2097151, and the odc format of cpio up to 262143; database uids do not fit. GNU tar's default format, the POSIX pax format, cpio's newc format, rsync and btrfs send hold 32-bit uids, and archives of AccountTree storage use one of them.
- </p>
-
- <h2>The system</h2>
-
- <p>
- A system is a party in its own right, with its own database. Its system database, at <RT·code>/var/lib/AccountTree/system.sqlite3</RT·code>, holds its system uuid, the range of system uids AccountTree gives out, the system map, every head of each storage database it has seen, and the limits. The system owns the uid numbering of <RT·code>/etc/passwd</RT·code>: it alone decides which storage accounts get system accounts on it and with which system uids, so that record is its own and not any storage's. A storage plugged in can claim nothing; what it names in its own database grants it no system account.
- </p>
-
- <p>
- A system never reuses a system uid. Its own disk keeps files carrying system uids, in <RT·code>/tmp</RT·code>, <RT·code>/var/spool</RT·code>, crontabs and logs, and a reused uid would hand them to another account. The system map therefore keeps every row it has held, retired rows included, and a system account AccountTree makes is never given a system uid that appears in it.
- </p>
-
- <p>
- The system database is under <RT·code>/var</RT·code>, which holds the state programs keep, and not under <RT·code>/etc</RT·code>, which holds configuration. A system whose root file system is snapshotted, as openSUSE's is, keeps <RT·code>/var</RT·code> outside the snapshots. A rollback of the root then rolls back <RT·code>/etc/passwd</RT·code> and leaves the system map as it was: a system account made after the snapshot is lost from <RT·code>/etc/passwd</RT·code>, but its system uid stays recorded, is not given out again, and the system account can be made again from the system map. Were the system database under <RT·code>/etc</RT·code>, the rollback would also forget the system uid, and it could be given to another account while files carrying it remained under <RT·code>/var</RT·code>.
- </p>
-
- <h2>Deleting storage</h2>
-
- <p>
- <RT·code>AccountTree_admin storage delete <tree dir></RT·code> undoes, on this system, what AccountTree did for the accounts of the storage database, then deletes the tree dir. It makes every check first, and a failed check changes nothing:
- </p>
-
- <ul>
- <li>the storage database names no other system that has seen it, since that system's map would name a database that no longer exists, unless the keyword <RT·code>forget-other-systems</RT·code> is given;</li>
- <li>no system account that is the same account as one of its accounts has a login session;</li>
- <li>nothing is mounted inside the tree dir;</li>
- <li>the tree dir holds only the storage database and <RT·code>account_data</RT·code>, and <RT·code>account_data</RT·code> holds only homes of the database's accounts;</li>
- <li>homes that hold files are deleted only when the keyword <RT·code>delete-account-data</RT·code> is given.</li>
- </ul>
-
- <p>
- For each row of this system's map naming one of its accounts, it then unmounts the home, deletes the mount point attach made, recognized as an empty directory owned by root with mode 0000, deletes the system account and its group when AccountTree made them, keeps an adopted system account, and retires the row. Last it deletes <RT·code>account_data</RT·code>, the storage database and the tree dir. Run a second time, it finds nothing to delete and says so. <RT·code>system delete</RT·code> deletes the system database, and is refused while any row of the system map is in use.
- </p>
-
- <h1>Database</h1>
-
- <p>
- There are two kinds of database, with two schemas, <RT·code>schema_storage.sql</RT·code> and <RT·code>schema_system.sql</RT·code>. Both keep the same record of changes, described below, and are checked the same way. Each fact is kept by the party with authority over it: a storage database keeps its accounts and their database uids; the system database keeps which of those accounts are the same accounts as system accounts.
- </p>
-
- <h2>Storage database table</h2>
-
- <ul>
- <li><RT·code>identity</RT·code>: the database uuid, the uuid it was made with, from which its chain begins, and for a branch, the database it came from and the head they parted after; the series number, the head hash, and when it was made. One row. A storage database has no name for people. A name would travel with every copy of the database, and a name taken from the storage it was made on would come to mislead once the database was copied elsewhere. A database is shown by its database uuid, its series number, its head hash and the time of its last change, and log lines give the first eight digits of the database uuid.</li>
- <li><RT·code>standard</RT·code>: the AccountTree standards this database follows, one row each. Currently <RT·code>5.0</RT·code>.</li>
- <li><RT·code>account</RT·code>: one row per storage account: database uid, name, the database uid and name of its parent, state (<RT·code>active</RT·code>, <RT·code>deprecated</RT·code>, <RT·code>deallocated</RT·code>), and times. The parent's database uid is empty when the parent is root or is carried by another database.</li>
- <li><RT·code>database_uid</RT·code>: every database uid this database has ever given out, with whether it is in use or retired.</li>
- <li><RT·code>system_seen</RT·code>: the systems that have seen this database, with when that was first and last. For the owner of the storage, and for <RT·code>storage delete</RT·code>, which will not leave a system mapping a database that no longer exists. Never authority.</li>
- <li><RT·code>acquaintance_database</RT·code> and <RT·code>same_account_database</RT·code>: other storage databases this one knows, and which of their accounts are the same accounts as accounts here, by database uid. Written into both databases. Not used in 5.0.</li>
- <li><RT·code>transaction_log</RT·code>: the record of every change, described below.</li>
- </ul>
-
- <h2>System database table</h2>
-
- <ul>
- <li><RT·code>identity</RT·code>: the system uuid, the series number, the head hash, and when the database was made. One row. The system is shown by its host name, read when it is shown, so a name for people is not kept.</li>
- <li><RT·code>standard</RT·code>: as for a storage database.</li>
- <li><RT·code>system_uid_range</RT·code>: the first and last system uid AccountTree gives a system account it makes. One row.</li>
- <li><RT·code>system_map</RT·code>: one row for each time a system account has been made the same account as a storage account: the system uid and gid, the system account's name, the database uuid and database uid, whether AccountTree made the system account or adopted one that existed, whether the row is in use or retired, and when. At most one row in use names a given system uid, and at most one names a given storage account. A row is retired, never deleted. An adopted system account can be made the same account again once its row is retired; a system uid that appears in any row is never given to a system account AccountTree makes.</li>
- <li><RT·code>storage_head_seen</RT·code>: every head of each storage database this system has seen, with the event (mapped, attached, detached), and at attach, the continuity found.</li>
- <li><RT·code>account_limit</RT·code>: the number of child accounts each system account is allowed below it, by system uid. A system account with no row is allowed none.</li>
- <li><RT·code>transaction_log</RT·code>: the record of every change.</li>
- </ul>
-
- <h2>Series number and hash chain</h2>
-
- <p>
- A transaction is one change to one database: the SQL statements a command of <RT·code>AccountTree_admin</RT·code>, or a request, runs against that database, recorded together. A command can make transactions in both kinds of database; <RT·code>is-same-account</RT·code> makes one in the storage database and one in the system database.
- </p>
-
- <p>
- Each transaction increments the series number, and records its statements, a description, a random number, and a hash. The hash is taken over the previous transaction's hash, the series number, the description, the statements, and the random number. The first transaction follows a hash made from the database's kind and the uuid it was made with, so no two databases' chains can coincide, and a record cannot be moved from one database into another. A branch keeps its record, and the transaction that made it a branch is in that record. The <RT·code>identity</RT·code> row holds the series number and hash of the last transaction, the head of the chain.
- </p>
-
- <p>
- The chain gives order, integrity and identity of history. It does not give authenticity. Anyone holding a database can extend its chain, or rewrite it and compute new hashes. What a rewrite cannot do is keep the hashes that others have already seen: a system that remembers a head finds that the rewritten record does not hold it. Each party that remembers a head is a witness. A storage database and the system database are readable only by the database account and root, so the record is the database's own history, as a git repository's history is, rather than a log for persons to read.
- </p>
-
- <p>
- The series number orders a database's history; the hash identifies it. A copy that is used goes on to make its own transactions, with the same series numbers as the original's and different contents. Their hashes differ, because the random number differs even when the same change is made to both. So two databases with the same database uuid and series number but different heads are known to have parted.
- </p>
-
- <p>
- Times recorded in a database are for people to read. They are not authoritative, because clocks differ between machines. The series number is authoritative.
- </p>
-
- <h2>Standard</h2>
-
- <p>
- A program that opens a database first reads its <RT·code>standard</RT·code> table, and refuses a database that follows no standard the program follows. During a change from one standard to the next, a database lists both.
- </p>
-
- <h2>Fallback tool</h2>
-
- <p>
- The databases can be built by hand from SQL scripts kept in the project. They are the means of starting over if a database is lost or damaged.
- </p>
-
- <ul>
- <li>A schema script builds an empty database where there was none.</li>
- <li>Initialization scripts put in the starting data: the identity row, the standard, existing accounts, the uid range, and limits.</li>
- <li>Scripts with their data written into them can write the system map rows for existing system accounts, for a system whose system database is lost.</li>
- </ul>
-
- <h1>Same account</h1>
-
- <p>
- A storage account's home, <RT·code>account_data/<name></RT·code> in its tree dir, carries only its database uid. What lets it be used on a system is a row of that system's map, saying that a system account there is the same account as the storage account. The administrator states it:
- </p>
-
- <RT·code>
- AccountTree_admin is-same-account system <host name> <system account> storage <tree dir> <storage account> [make-system-account]
- </RT·code>
-
- <p>
- A keyword such as <RT·code>make-system-account</RT·code> is a word given after a command's other arguments, as the command itself is a word, not a flag. The two account specifiers are accepted in either order. In 5.0 the pair is a system account and a storage account; correspondences between two storage databases come later. The host name must be this system's, which confirms that the administrator is on the machine he means. When the system account exists, it is adopted: it must be an account of a person, with a system uid at or above <RT·code>UID_MIN</RT·code>, its home must be <RT·code>/home/<system account></RT·code>, and it must not already be the same account as another storage account. When it does not exist, the keyword <RT·code>make-system-account</RT·code> makes it, with a system uid from the system's range that no account, no group and no row of the system map has had, provided <RT·code>/home/<system account></RT·code> is absent or empty, since the new system account would take whatever is there as its home. Without the keyword the command refuses, so that a mistyped name does not make a system account.
- </p>
-
- <p>
- The first time a storage account is made the same account as a system account on a system, the storage database records that system in <RT·code>system_seen</RT·code>. That and the system map row together are what 5.0 calls an introduction; in 6.0 it is also when keys are exchanged. <RT·code>account make</RT·code> with <RT·code>make-system-account</RT·code> makes a storage account and, in the same command, a system account of the same name, the same account as it.
- </p>
-
- <p>
- <RT·code>is-not-same-account</RT·code> undoes it: it unmounts the home if it is mounted from that storage, deletes the mount point attach made, deletes the system account if AccountTree made it and keeps an adopted one, and retires the row. <RT·code>same-account show</RT·code> lists which accounts are the same, with the storage accounts and the persons' system accounts that are not. <RT·code>same-account suggest</RT·code> prints <RT·code>is-same-account</RT·code> commands proposed by matching names, for the administrator to read and run; it changes nothing. Matching names is a guess, and is used nowhere else.
- </p>
-
- <p>
- Someone else's storage, plugged into this system, gets nothing until the administrator makes its accounts the same accounts as system accounts here. There is no guest attach in 5.0: every storage is introduced by the administrator. A storage account named <RT·code>Thomas</RT·code> on that storage is no one here until the system map says so.
- </p>
-
- <p>
- Two storage databases can record that an account of one is the same account as an account of the other, and can record it while a system's map says otherwise. Such a claim grants nothing until a system acts on it, as in mounting a parent account's view of a child account kept on other storage. A system acts only on its own record: it does not infer that two accounts are the same from what storage databases say, and where the claims of storage databases and its own map disagree, it reports the disagreement. A child account made through a request on this system was made by this system, which records the parent relation itself.
- </p>
-
- <p>
- In 5.0 storage databases and systems recognize each other by database uuid and system uuid alone. A copy of a storage database is taken for the original, and gains the system accounts mapped to the original's accounts. That is enough among one person's machines and storage. It is not enough among strangers, because either kind of uuid can be copied; keys are given under standard 6.0.
- </p>
-
- <h1>Request</h1>
-
- <h2>No server</h2>
-
- <p>
- A person's program asks for what it wants by composing a journal and invoking a gate with it: AccountTree's program is its own Man_In_Grey orchestrator, and no server stands between the person and the gate. The gate's real uid is the account that asked, reported by the kernel, so the guard knows who asks as a fact rather than as a claim in the journal. The place where authority is decided and the place where privilege changes are then one place, the gate, which is what Man_In_Grey was built to be.
- </p>
-
- <p>
- An earlier form of this design had a server, running under an account of its own and receiving requests on a socket. The server learned the requesting account from <RT·code>SO_PEERCRED</RT·code>, but every journal it composed reached a root gate with the server as the gate invoking account, so the root guard would have had to trust the server's word for who asked. The server also wrote the system database, while the root guards checked their constraints against that database, so a compromised server could have written a record and then asked root to act on it. Taking the server away removes both.
- </p>
-
- <p>
- A gate is a function call. The program that invokes it waits, and is given back the gate's exit status and, on descriptor 3, the interpreter's result. A request's answer is therefore tied to the request by the kernel: it arrives on the descriptor of the gate the program started.
- </p>
-
- <h2>The gate</h2>
-
- <ul>
- <li><em>The request gate</em>, <RT·code>AccountTree_gate·request</RT·code>, setuid root. Any account invokes it. Its guard and interpreter run with root's effective uid.</li>
- <li><em>The record gate</em>, <RT·code>AccountTree_gate·record</RT·code>, setuid to the database account, owned by it with the group root and mode 4750, so that only root can start it. A root interpreter invokes it to have the databases read or written; its guard and interpreter run with the database account's effective uid.</li>
- <li><em>The administrator's gate</em>, <RT·code>AccountTree_gate·admin</RT·code>, not setuid, mode 0700, owned by root. <RT·code>AccountTree_admin</RT·code> invokes it under root; its guard's constraints allow only root.</li>
- </ul>
-
- <p>
- The administrator's gate needs no setuid bit, since root invokes it already holding root's rights, and without the bit the administrator's instructions have no setuid program to reach them by. It is a gate all the same, so that the administrator's journals pass a guard as a person's do, and the code that changes the system lives in one place, the interpreters. Making accounts the same, attaching, detaching and setting limits all go through it, at boot as at any other time.
- </p>
-
- <h2>The database account</h2>
-
- <p>
- The databases are opened by the database account and by no root process. A storage database is input from a drive: a copy, a branch, or a drive found again after it was out of its owner's hands, whose file anyone could have rewritten. Opening it means parsing its pages and its schema and walking its whole hash chain, and SQLite's own guidance treats a database file from an untrusted source as an attack surface. Under the database account, a fault that a crafted database reaches gives that account, not root. This is privilege separation in the manner of OpenSSH: the complex parsing of untrusted input runs unprivileged, and the privileged program does a few simple things with what it is told.
- </p>
-
- <p>
- The database account owns <RT·code>/var/lib/AccountTree</RT·code>, where the system database is, and on each storage the tree dir's <RT·code>database/</RT·code> directory. Its interpreter opens the databases with SQLite's defensive settings, <RT·code>trusted_schema</RT·code> off among them.
- </p>
-
- <h2>Who is asking</h2>
-
- <p>
- The request gate records the gate invoking account, its real uid, for its guard and interpreter, as every Man_In_Grey gate does; the account cannot misstate it. That account's name is the prefix of any child account it makes. At the record gate the gate invoking account is root, and the journal names the requesting account as an argument. The record guard trusts that argument because the account that gave it is root, which could do anything anyway; its constraints allow only root to give it journals.
- </p>
-
- <h2>The request language</h2>
-
- <p>
- The request language is made of requests, not of the primitive steps that carry them out. A journal that asked for a system uid to be made, or a path to be mounted, would leave the guard to work out what request the steps add up to, and to check every uid and path the composer chose against records it would have to consult again. A request names the work, and the uids, paths and mount options are the interpreter's to choose; none of them can be expressed in the language.
- </p>
-
- <p>
- Each request acts on the requesting account or on an account in the tree below it, with the authority of the requesting account; this is how a child account's window, opened from its parent's, acts with the parent's authority. Each has a result, which the person's program shows.
- </p>
-
- <ul>
- <li><RT·code>make_child_account</RT·code> <short name>: make a child account of the requesting account. Checks: the short name is valid and not in use; the requesting account's limit, and the limit of every account above it in its tree, allow one more. The child account is made in the storage database of the requesting account unless another attached storage is named. Steps: a database uid, never given before, and the account row in the storage database; a system uid, never given before, and the row in the system map; the system account; the home in <RT·code>account_data</RT·code>, owned by the database uid, mode 0700; the mount of it at <RT·code>/home/<name></RT·code>; the parent's view. Result: the account's name, database uid and system uid.</li>
- <li><RT·code>rename</RT·code> <account> <new short name>: rename a child account. The account, and every account below it, whose names begin with its name, must have no session; all are renamed together. The uids and files do not change; the homes, mount points and views are renamed with them.</li>
- <li><RT·code>deprecate</RT·code> <account>: end the account's sessions, lock it, and mark it deprecated. Its data, name and uid are kept. It still counts against the limits. A deprecated account can be restored.</li>
- <li><RT·code>deallocate</RT·code> <account>: remove a deprecated account. Its system account and views are removed, and its database uid and system uid are retired. What becomes of its data is open: the proposal is to move it to a holding directory on the storage, from which only the administrator removes it.</li>
- <li><RT·code>view</RT·code> [<account>]: report an account and its tree: names, states, storage, and whether each is mounted. Changes nothing. The report is the result, so a person's program learns the records without being able to open the databases.</li>
- <li><RT·code>set_views</RT·code>: mount the requesting account's views of its child accounts that are on attached storage. Views already mounted are left as they are.</li>
- <li><RT·code>enter</RT·code> <account> [<program>]: start a session, or a program, in an account below the requesting account.</li>
- </ul>
-
- <p>
- <RT·code>reconcile</RT·code>, which compares the databases with <RT·code>/etc/passwd</RT·code> and the mount table, completes what an interrupted request left undone, makes again any system account lost from <RT·code>/etc/passwd</RT·code>, and reports what differs, is an instruction of the administrator's language, run at boot and on demand.
- </p>
-
- <h2>A request's course</h2>
-
- <p>
- For <RT·code>make_child_account</RT·code>:
- </p>
-
- <ol>
- <li>The person's program composes a journal of the one request, and invokes the request gate with descriptor 3 open.</li>
- <li>The request guard approves it: the journal is well formed, the gate invoking account is a person's account, with a system uid at or above <RT·code>UID_MIN</RT·code>, and the short name is valid. Whether the account is managed, and what it is allowed to do, are questions for the records, asked next.</li>
- <li>The request interpreter composes a journal for the record gate, naming the requesting account and the short name, and invokes it.</li>
- <li>The record interpreter checks the tree and the limits against the records, gives out the database uid and the system uid, each within its database's write transaction, records the account and its intended state, and answers with the name, the two uids, and the series number and head hash of each transaction.</li>
- <li>The request interpreter checks the answer, Answer check below, then makes the system account, the home, the mount point, the mount and the parent's view.</li>
- <li>The request gate hands back the result: the account's name and uids.</li>
- </ol>
-
- <p>
- A refusal from the record interpreter, for a limit for instance, comes before anything on the system has changed, since the records are asked first. The series numbers and head hashes in the answer are logged with the changes the request interpreter makes, so that every change to the system is tied to the database transaction that recorded it.
- </p>
-
- <h2>Giving out a uid</h2>
-
- <p>
- Each database gives out its own uids, and its write lock is what keeps two requests from being given the same one. The choice of the next uid is made inside the write transaction, opened with <RT·code>BEGIN IMMEDIATE</RT·code>, which takes the lock before anything is read; a uid chosen before the transaction opens could be chosen by two requests at once. The code as it stands chooses first and opens the transaction after, in <RT·code>account make</RT·code> and in <RT·code>is-same-account</RT·code>, and is to be changed.
- </p>
-
- <p>
- One check stays outside the lock: that no account made by hand already has the system uid. An account made with <RT·code>useradd</RT·code> between the record interpreter's choice and the request interpreter's <RT·code>useradd -u</RT·code> would hold it. <RT·code>useradd</RT·code> then fails rather than making a duplicate, the request fails, and since the system database recorded the uid first, <RT·code>reconcile</RT·code> finds the request half done.
- </p>
-
- <h2>Answer check</h2>
-
- <p>
- The request interpreter trusts the database account less than it trusts itself, so it checks each answer the way a guard checks a journal, against what it asked and against facts the database account cannot forge:
- </p>
-
- <ul>
- <li>the account's name is the requesting account's name, the middle dot, and the short name asked for;</li>
- <li>the system uid lies in the system's range and is held by no account in the system's account database, and the database uid lies in the band;</li>
- <li>before a home is mounted, its owner on disk, read with <RT·code>stat</RT·code>, is the database uid the answer gives; the database account cannot enter <RT·code>account_data</RT·code>, so it cannot have arranged the owner.</li>
- </ul>
-
- <p>
- The third check is the one that matters most. An answer that gave one account another's database uid would otherwise mount the second account's files as the first's.
- </p>
-
- <h2>Instruction language and constraint</h2>
-
- <ul>
- <li><em>Request</em>, at the request gate: the requests above. Guard: the journal is well formed; the gate invoking account is a person's account; names are valid. The checks that need the records, the tree and the limits, are the record interpreter's, made before anything changes.</li>
- <li><em>Record</em>, at the record gate: the record work each request and each of the administrator's commands needs, such as allocating a child account, mapping a system account to a storage account, recording an attach or a detach, and reading the tree for a view. Guard: the journal is well formed; the gate invoking account is root.</li>
- <li><em>Administrator</em>, at the administrator's gate: <RT·code>system make</RT·code>, <RT·code>storage make</RT·code>, <RT·code>account make</RT·code>, <RT·code>is-same-account</RT·code>, <RT·code>attach</RT·code>, <RT·code>detach</RT·code>, <RT·code>limit set</RT·code>, <RT·code>reconcile</RT·code> and the others of <RT·code>AccountTree_admin</RT·code>. Guard: the journal is well formed; the gate invoking account is root.</li>
- </ul>
-
- <p>
- The constraints that limit what root's interpreters do with what they are told stay with them: a new system uid lies in AccountTree's range and appears in no row of the system map; a system account is deleted only when the system map records that AccountTree made it; a home is mounted only from <RT·code><tree dir>/account_data/<storage account></RT·code> onto <RT·code>/home/<system account></RT·code>, with the uid translation of the row of the system map that pairs them; a view is mounted only of a child account's home, at <RT·code>~/account_child/<short name></RT·code> of its parent; every mount is <RT·code>nosuid</RT·code> and <RT·code>nodev</RT·code>; a mount point is an empty directory owned by root with mode 0000, made and deleted only at <RT·code>/home/<system account></RT·code>; a home is deleted only during the deletion of its storage, or of a deallocated account.
- </p>
-
- <p>
- A single request touches <RT·code>/etc/passwd</RT·code>, a home and a mount, so the request gate needs every part of root's rights those take, and the separate account, home, mount and session languages of the earlier form of this design are folded into it. The loss is small: the right to mount, <RT·code>CAP_SYS_ADMIN</RT·code>, is nearly root by itself. The account, home, mount and session code stays in separate modules within the request interpreter.
- </p>
-
- <h2>Partial failure</h2>
-
- <p>
- A request makes several changes. The journal for a request is approved whole before any instruction executes, so a request that breaks a constraint changes nothing on the system, and the record interpreter's refusals come before the request interpreter changes anything. A step can still fail while executing, after earlier steps succeeded. The databases record the intended state of each account, so such a failure leaves the intended state recorded and the system short of it; <RT·code>reconcile</RT·code> finishes the work or reports why it cannot. A request is complete only when the system matches the databases.
- </p>
-
- <h2>At boot</h2>
-
- <p>
- <RT·code>attach</RT·code> and <RT·code>reconcile</RT·code> are run at boot, before logins are allowed, by a systemd unit that runs <RT·code>AccountTree_admin</RT·code> under root. Two kinds of systemd option are to be left out of that unit, because each would quietly stop the work:
- </p>
-
- <ul>
- <li>options that give the unit a mount namespace of its own, such as <RT·code>PrivateMounts</RT·code> or <RT·code>ProtectHome</RT·code>, since a mount made there exists only for the unit, and no account ever sees it;</li>
- <li><RT·code>NoNewPrivileges</RT·code>, since with it set the kernel ignores the setuid bit of the record gate, and the record interpreter would run under root, opening the databases with root's rights.</li>
- </ul>
-
- <h1>Limit</h1>
-
- <p>
- Making a child account takes a database uid from a storage database, and a system uid and a system account from the system, and the system's are finite. Without a limit, one account could take all of them. Each system account has a limit on the number of accounts in its tree below it, counting accounts at every depth and counting deprecated ones. Limits are kept in the system database. The administrator sets them with <RT·code>AccountTree_admin limit set</RT·code>. A system account with no limit set has a limit of 0, and cannot make child accounts.
- </p>
-
- <p>
- A child account made by a child account counts against the limits of every account above it. The starting value suggested for a login account is 100.
- </p>
-
- <h1>Mount</h1>
-
- <h2>Idmapped mount</h2>
-
- <p>
- Every AccountTree mount is an idmapped mount: a bind mount that carries a translation of uids, applied by the kernel to every permission check and to every file made through it. Idmapped mounts need Linux 5.12 or later, and a file system that supports them, such as ext4, btrfs or xfs. An idmapped mount is kernel state, with no process behind it, so it survives the end of the program that made it and of any session. A mount through FUSE, such as bindfs, does neither.
- </p>
-
- <p>
- The request interpreter, and the administrator's, make the mount with the kernel's own calls, not by running <RT·code>mount</RT·code>. <RT·code>open_tree</RT·code> clones the source as a mount not yet attached anywhere; <RT·code>mount_setattr</RT·code> gives it <RT·code>nosuid</RT·code>, <RT·code>nodev</RT·code> and the idmap; <RT·code>move_mount</RT·code> places it. The kernel is given the idmap as a user namespace: a child process is made in a new user namespace, its uid and gid maps are written, and the namespace is handed to <RT·code>mount_setattr</RT·code>. A map line reads <RT·code><id inside> <id outside> <count></RT·code>; for an idmapped mount the kernel takes the uid on disk, the database uid, as the id inside, and the id outside, the system uid, as the uid seen through the mount.
- </p>
-
- <h2>Attaching storage</h2>
-
- <p>
- <RT·code>AccountTree_admin attach <tree dir></RT·code> verifies the storage database's standards and hash chain, compares its record with the heads seen here, as described under Copy and branch, then reads this system's map for the rows naming that database uuid, and nothing else. For each, it checks that the storage account is active and that the system account exists with its system uid and with its home at <RT·code>/home/<system account></RT·code>, and mounts the storage account's home there, translating the database uid to the system uid. Storage accounts with no row are logged as skipped. The mounts are <RT·code>nosuid</RT·code> and <RT·code>nodev</RT·code>, since a storage can carry set-user-id programs and device files. Files on the storage owned by database uids the mount does not translate appear owned by <RT·code>nobody</RT·code>.
- </p>
-
- <h2>View</h2>
-
- <p>
- A parent account sees each of its child accounts at <RT·code>~/account_child/<short name></RT·code>: an idmapped mount of the child account's home that translates the child's uid to the parent's. Files there appear to be the parent's, and files the parent makes there belong to the child. The directory is named for the property each of its entries has, being a child account, so the name is singular. Views are set when storage is attached, for the parent accounts already in session, and at each session's start, through <RT·code>set views</RT·code>. Views are designed in outline only, and come next after this document's other parts are built.
- </p>
-
- <h2>Mount point</h2>
-
- <p>
- A mount point must exist before a mount, so <RT·code>/home/<system account></RT·code> exists for every known account, attached or not. While nothing is mounted on it, it is an empty directory owned by root with mode 0000, and marked immutable. No account can enter it, and nothing can be written into it to be hidden later by the mount.
- </p>
-
- <h1>Login and entering</h1>
-
- <p>
- A login account whose storage is not attached has no home. A login into the empty mount point finds no initialization files, and does not read them later when the storage arrives. Login is therefore refused for a managed login account whose home is not mounted.
- </p>
-
- <p>
- The check is in PAM, which every way of logging in passes through: the display manager, the console, ssh, <RT·code>su</RT·code> and <RT·code>machinectl shell</RT·code>.
- </p>
-
- <RT·code>
- account sufficient pam_succeed_if.so uid = 0 quiet
- account required pam_exec.so quiet /usr/local/lib/AccountTree/home_present
- </RT·code>
-
- <p>
- <RT·code>home_present</RT·code> allows the login when the account is not managed by AccountTree, or when <RT·code>/home/<name></RT·code> is a mount point, and refuses it otherwise. The first line keeps root able to log in whatever the check does.
- </p>
-
- <p>
- An account below a login account is entered through its parent: the parent gives an <RT·code>enter</RT·code> request to the request gate, whose interpreter allows the session only when the gate invoking account is above the account in the tree. The interpreter hands the session to systemd to start, and returns: the gate waits for its interpreter's verdict, so the interpreter cannot be the session itself. Whether such an account is ever to be entered through the login programs as well is open.
- </p>
-
- <p>
- A session, when it starts, gives a <RT·code>set_views</RT·code> request. The gate knows the account from its real uid, so a session can set only its own views.
- </p>
-
- <h1>Program and module</h1>
-
- <ul>
- <li><RT·code>AccountTree</RT·code>: the person's program. Its commands compose requests and give them to the request gate; its <RT·code>window</RT·code> command opens the resource window, which shows the account's child accounts and their states and gives the same requests. Runs under the person's account.</li>
- <li><RT·code>AccountTree_admin</RT·code>: the administrator's program, run under root. Its commands: <RT·code>system make</RT·code>, <RT·code>system show</RT·code> and <RT·code>system delete</RT·code>; <RT·code>storage make</RT·code>, <RT·code>storage show</RT·code> and <RT·code>storage delete</RT·code>, each given a tree dir; <RT·code>account make</RT·code>; <RT·code>is-same-account</RT·code>, <RT·code>is-not-same-account</RT·code>, <RT·code>same-account show</RT·code> and <RT·code>same-account suggest</RT·code>; <RT·code>attach</RT·code> and <RT·code>detach</RT·code>; <RT·code>limit set</RT·code>; and <RT·code>reconcile</RT·code>. Each composes a journal for the administrator's gate. Detaching is refused while a system account it would unmount has a session. Unlocking an encrypted storage and mounting it come later.</li>
- <li>The three gates, each with its guard and interpreter, installed owned by root, in directories only root can write, except the record gate, owned by the database account.</li>
- <li><RT·code>home_present</RT·code>: the PAM check.</li>
- <li>The schema and initialization scripts.</li>
- </ul>
-
- <p>
- Each program keeps the RT convention of a command line interface apart from its work functions, and the modules are imported as libraries: a window or a test calls the functions directly, never the command line. There are two layers, and one boundary between them, the gate. The person's functions, and the administrator's, compose journals and invoke a gate; <RT·code>AccountTree</RT·code>, the window and <RT·code>AccountTree_admin</RT·code> import them. At the gate the kernel says who asks, privilege changes, and the guard decides, by the constraints, what the privilege is used for. The interpreters hold the functions that change the system and the records.
- </p>
-
- <p>
- The modules follow Man_In_Grey's naming: a module is a namespace, a file named for a namespace alone is its main module, and a file specific to it carries the namespace before a middle dot. Each of AccountTree's three instruction languages is a namespace whose files parallel Man_In_Grey's own:
- </p>
-
- <RT·code>
- Request·Language.py the request language's definition
- Request·Guard.py its guard, with its constraints at its top
- Request·GuardCheck.py its checks for the guard
- Request·Interpreter.py its interpreter
- Request·LibraryInstruction.py the implementation of its instructions
- Record·... the same five for the record language
- Admin·... the same five for the administrator's language
- </RT·code>
-
- <p>
- The code that does the work is in modules of its own, which the libraries of instructions import. Where a direct call is not yet written, a wrapper function stands in its place, named for what it does here and commented with the direct call it will become. The names are proposed:
- </p>
-
- <ul>
- <li><RT·code>Kernel.py</RT·code>: direct calls, made through ctypes. The idmapped mount through <RT·code>open_tree</RT·code>, <RT·code>mount_setattr</RT·code> and <RT·code>move_mount</RT·code>; unmounting through <RT·code>umount2</RT·code>; the mount table from <RT·code>/proc/self/mountinfo</RT·code>; the immutable flag through the <RT·code>FS_IOC_SETFLAGS</RT·code> ioctl; login sessions through <RT·code>sd_get_uids</RT·code> in libsystemd. Imported by the request and administrator's interpreters.</li>
- <li><RT·code>Account·System.py</RT·code>: reading system accounts directly, through the C library, and changing them through wrappers, <RT·code>system_account_make</RT·code>, <RT·code>system_account_delete</RT·code> and <RT·code>system_group_delete</RT·code>, which run the shadow suite's commands for now. In the future each will be a direct call: the lines of <RT·code>/etc/passwd</RT·code>, <RT·code>/etc/shadow</RT·code>, <RT·code>/etc/group</RT·code> and <RT·code>/etc/gshadow</RT·code> edited under the C library's lock, <RT·code>lckpwdf</RT·code>, each new file written beside the old one and renamed into place.</li>
- <li><RT·code>Database.py</RT·code>, <RT·code>Database·Storage.py</RT·code>, <RT·code>Database·System.py</RT·code> and <RT·code>Database·SameAccount.py</RT·code>: the databases, their hash chain, and the work functions on them. Imported by the record interpreter only, so that no root process opens a database.</li>
- <li><RT·code>Window.py</RT·code>, and the person's side of the resource kinds: the person's program.</li>
- </ul>
-
- <p>
- In the code as it stands, <RT·code>AccountTree_admin</RT·code> calls the changing functions directly, as root, and opens the databases itself. Moving the changing functions into the interpreters, and the database work into the record interpreter, is the work this design calls for. Keeping the changing functions small and their calls direct also lets them later be moved into the kernel, where an account tree with root at its top fits Linux as it is.
- </p>
-
- <h1>Open question</h1>
-
- <ul>
- <li>What deallocation does with an account's data.</li>
- <li>When the request gate moves from setuid root to only the capabilities its requests need: <RT·code>CAP_SYS_ADMIN</RT·code> to mount, <RT·code>CAP_SETUID</RT·code> and <RT·code>CAP_SETGID</RT·code> for sessions, and the file capabilities for accounts and homes.</li>
- <li>How AccountTree's gates, guards and interpreters are installed beside Man_In_Grey's library, and how a guard's check of its modules' ownership extends to that library. Man_In_Grey's <RT·code>install_gate</RT·code> installs Man_In_Grey's own tree; an application's tree is not yet provided for.</li>
- <li>Whether the record gate is a Man_In_Grey gate, as proposed, or the request interpreter instead starts a child that gives up root's uid for the database account's and answers on a pipe. The gate keeps the record work in the same form as every other change, logged and guarded; the child needs no setuid program.</li>
- <li>A deadline for each gate, so that a hung interpreter, which account A cannot signal once the gate has isolated itself, does not hold a request until root ends it. Man_In_Grey lists it as a future feature.</li>
- <li>Whether an account below a login account is ever to be entered through the login programs as well.</li>
- <li>Whether a parent account's views are removed when its last session ends, or left until the storage is detached.</li>
- <li>How resources other than accounts and storage, such as network zones and devices, are recorded in the database.</li>
- </ul>
-
- <h1>Road map</h1>
-
- <p>
- Standard 5.0 is the design above. Later standards add the following, each described in its own section. A database lists the standards it follows in its <RT·code>standard</RT·code> table, so a database moves to a later standard by listing it, and a program that follows only an earlier one refuses it.
- </p>
-
- <ul>
- <li>6.0: longer account names; keys, for unlocking storage and recognizing it; signed heads; deduplication; deep freeze.</li>
- <li>7.0: integration of archives and of child copies, and merging of lines; a property file system.</li>
- <li>Later, version not yet assigned: guests.</li>
- </ul>
-
- <p>
- <em>Guests.</em> A person who plugs storage into someone else's machine and works there, with no administrator introducing it first, makes the machine a display with a processor for whatever storage is brought to it. The guest's storage accounts would be given system accounts at attach, in a band of system uids of their own, recorded in the system map as transient, and deleted at detach, with no rights to anything of the machine's owners. In 5.0 every storage is introduced by the administrator. Guests wait until keys, automatic attach, and the protection of a machine from what a stranger's storage carries, are understood.
- </p>
-
- <h1>Standard 6.0</h1>
-
- <h2>Longer account name</h2>
-
- <p>
- Account names built from a chain of parent names grow long, and 32 bytes, with two for each middle dot, is soon reached. 6.0 is to allow names of 128 bytes. The limit is in user space: the login records and <RT·code>useradd</RT·code>. Distributions are replacing the <RT·code>utmp</RT·code> records with <RT·code>wtmpdb</RT·code> and <RT·code>lastlog2</RT·code>, which have no fixed name field; openSUSE has done so. What remains is the check in <RT·code>useradd</RT·code>, which goes away when AccountTree edits the account files itself. Whether a system already accepts longer names is found by making a test account with a name past 32 bytes.
- </p>
-
- <h2>Key</h2>
-
- <p>
- In 5.0, databases and systems recognize each other by database uuid and system uuid. 6.0 adds two uses of keys.
- </p>
-
- <p>
- <em>Unlocking storage.</em> A storage's file system is encrypted with LUKS2, which keeps up to 32 key slots, any of which opens it. Each person whose accounts are on the storage has a slot, opened by a passphrase or by a hardware token such as a FIDO2 key. When a storage is introduced to a system, the administrator opens it once, and the system adds a slot of its own, with a key it keeps, so that the storage is opened and attached on that system without asking anyone for a password. systemd-cryptenroll already enrolls such slots, and can seal a key in the machine's TPM, the security chip on its motherboard, so that the key is useless away from that machine. A slot can be removed from any machine that can open the storage, which is how a stolen laptop is cut off from it. Opening a storage with a key the system enrolled is also the proof that it is the storage that was introduced, which uuids and hashes cannot give: a fabricated storage cannot be opened with that key. The keys a system keeps make the place they are kept a target in the way <RT·code>/etc/shadow</RT·code> is; sealing them in the TPM is the protection. The disk inside the machine can be treated the same way, its <RT·code>/home</RT·code> opened by the system's own slot at boot, which keeps every storage alike.
- </p>
-
- <p>
- <em>Signed heads.</em> Each database and each system also holds a key pair. Each transaction's hash, which covers every transaction before it, is signed with the private key of the party that wrote it, so that a head can be shown to have been written by whoever holds that key, and not only to follow from the heads before it. The signature is checked with the public key, and the first <RT·code>is-same-account</RT·code> between a system and a storage database records each one's public key in the other's database, alongside its database uuid or system uuid. On attach, each side checks that the other holds the private key matching the public key recorded for it, so a copied database uuid or system uuid alone is refused. A storage is not a running party, so a database's private key is a file on its storage, and a full copy of the database copies the key too; protecting the key, for example inside the storage's encryption, is part of the 6.0 design.
- </p>
-
- <p>
- The migration touches only the identity and acquaintance tables and the programs that read them. A 6.0 database adds the key columns and lists both <RT·code>5.0</RT·code> and <RT·code>6.0</RT·code> in its standard table while keys are exchanged. The system map, uids and the account table are unchanged, because an account is still identified by its database and its database uid.
- </p>
-
- <h2>Deduplication</h2>
-
- <p>
- A storage gathers copies of the same files, from old archives and from copies made between accounts. On btrfs, two files with the same content can be made to share their extents, after which the content is stored once while the files stay separate: a later write to one of them is written apart from the other. The kernel call that does this, <RT·code>FIDEDUPERANGE</RT·code>, compares the two ranges byte for byte before sharing them, so a false match on a hash cannot corrupt data. Tools such as duperemove and bees find candidates by hashing and leave the comparison to that call.
- </p>
-
- <p>
- A background pass over attached storage, started by a systemd timer through the administrator's gate, does the work. The pass keeps an index of content hashes, gives the kernel call the pairs whose hashes match, and so keeps each storage compacted as it is used. On a file system without shared extents, the pass reports duplicates and changes nothing. The same index serves integration in 7.0.
- </p>
-
- <h2>Deep freeze</h2>
-
- <p>
- A person's accounts, archives included, do not always fit on one portable storage: 500 GB, say, against more data than that. Most of it is seldom read. Deep freeze divides an account's home into a working part and a frozen part by access time. Files not read for a long time move to the frozen part, which is kept on other storage, and a copy to a storage that cannot hold all of it takes the working part only. Whether the frozen part's directory tree stays visible, with its content brought in when its storage is attached, is open.
- </p>
-
- <p>
- Access times are recorded only when a file system is mounted with <RT·code>atime</RT·code> or <RT·code>relatime</RT·code>. <RT·code>relatime</RT·code>, the Linux default, updates a file's access time at most once a day, which is enough here; under <RT·code>noatime</RT·code> nothing is recorded, and deep freeze cannot work. Bringing a working part back after it was changed elsewhere is the integration problem of 7.0.
- </p>
-
- <h1>Standard 7.0</h1>
-
- <h2>Integration</h2>
-
- <p>
- Two kinds of integration are needed. The first brings the files of old archives into the current accounts. The old archives have older, less mature directory structures, and the aim is to place their files in the current structure, not to keep the old structures. The content-hash index kept for deduplication shows which old files already exist in the current tree, under any name, and leaves the rest to be placed.
- </p>
-
- <p>
- The second brings back a child copy: part of an account's home copied to a portable storage, changed there, and returned. Its changes are merged into the original. A change to the directory structure on either side is the hard case, as when a file is moved on one side and changed on the other. Two lines of one storage, described under Copy and branch, are merged the same way.
- </p>
-
- <h2>Property file system</h2>
-
- <p>
- Both kinds of integration are hard because a file has one place in one tree. A file system organized by properties, or tags, would let a file belong to an old structure and a new one at once, and make a directory a query. AccountTree's contract with storage is that an account's home is a directory mounted at <RT·code>/home/<system account></RT·code>, and the mounting is one function in the code. A home provided by a property file system changes that function and nothing else, provided the file system supports idmapped mounts or translates uids itself. At that point the AccountTree project is expected to fork.
- </p>
-
- </RT·article>
- </body>
-</html>