# Filesystem quotas — the prerequisite for per-user and per-team storage limits **Status: planned, not done. Nothing in the platform reads or writes a quota yet.** This is the host-side half. It makes limits *enforceable*; it does not create any. The platform side — the owner declaring defaults and maxima, and provisioning applying them — is `docs/roles-and-teams.md` → "Quotas", and it is not built either. Do this first, because a policy nothing enforces is worse than no policy: it reads as a limit and is not one. ## Why the kernel and not the platform Members hold `terminal` at `write` and have their own Linux accounts. A quota the platform tracks by counting bytes through the file browser is decorative — a member opens a terminal, or connects over SSH with their own key, and writes past it without the platform seeing the write. Only the filesystem can refuse. So this is a provisioning feature, not an accounting one. ## Where it matters **`edge-pertento`.** It is the first real multi-user deployment and the only host that will hold accounts that are not the owner's. It already has one account, which is the argument for doing this soon rather than carefully later: moving a home directory is easy once and tedious per person. `officer-dev` is a development machine and will not be multi-user. `alpha` is the owner's own. Neither needs this, and neither can do it without the same downtime, so there is no reason to. ## What to check BEFORE booting into rescue Run these on the live machine. They decide whether the procedure below is the right one. ```bash findmnt -no FSTYPE,SOURCE,TARGET / # expect ext4, and note the real device findmnt -no OPTIONS / | tr ',' '\n' | grep -i quota # expect nothing — that is why we are here sudo tune2fs -l "$(findmnt -no SOURCE /)" | grep -iE 'features|Inode size' lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT df -h /home ``` Three things decide the plan: 1. **Is `/home` its own filesystem, or part of `/`?** On a stock Hetzner image it is part of `/`, so the root filesystem is what gains the feature. 2. **Inode size.** Only matters if you use project quotas — they need 256 bytes and cannot be enabled on a 128-byte filesystem without recreating it. The plan below avoids project quotas for exactly this reason. 3. **Is the device under LVM?** `alpha` is; `officer-dev` is not. It changes only the device path. ## The plan: user quotas and group quotas, not project quotas Two mount options cover both cases and neither needs a feature the filesystem might lack: - **`usrquota`** — a limit per Linux user. This is the per-account home directory limit. - **`grpquota`** — a limit per Linux group. **This is how a team's shared folder gets a limit**, because a team is already a Linux group (see `docs/roles-and-teams.md` — the group is what keeps one team out of another's folder, not the permission grant). Project quotas (`prjquota`) would let a *directory* carry a limit independent of ownership. That is more flexible and it is not needed: every case here is "this user" or "this team", both of which are already identities the kernel knows. Skipping it also skips the inode-size problem entirely. **For the group quota to work, the team folder must be `setgid`** (`chmod g+s`), so files created inside it inherit the team's group and are charged to it. Without that, a member's files are charged to the member's own primary group and the team limit counts nothing. This is the single easiest thing to get wrong here. ## Procedure Downtime is roughly ten minutes, most of it reboots. The owner has said downtime is acceptable. **1. Install the tools, on the live machine.** ```bash sudo apt-get update && sudo apt-get install -y quota ``` **2. Take note of the root device**, from the pre-flight above — `/dev/sda1` on a stock image, `/dev/mapper/-` under LVM. Every command below uses it. **3. Boot into Hetzner rescue** from the Cloud console, and do not mount the filesystem. **4. Check it, then add the feature.** `tune2fs` refuses on a filesystem that is not clean, and the check is worth running anyway before changing metadata: ```bash e2fsck -f /dev/sda1 tune2fs -O quota /dev/sda1 tune2fs -l /dev/sda1 | grep -i features # 'quota' must now appear ``` `quota` here is the ext4 *journaled quota* feature. It is what lets quotas come up with the filesystem rather than needing a `quotacheck` scan at every boot, which on 320 GB is not quick. **5. Add the mount options.** Back on the normal system — or by mounting the root filesystem from rescue and editing it there — the `/` line in `/etc/fstab` gains `usrquota,grpquota`: ``` UUID= / ext4 defaults,usrquota,grpquota 0 1 ``` **6. Reboot into the normal system and confirm.** ```bash findmnt -no OPTIONS / | tr ',' '\n' | grep quota # usrquota, grpquota sudo quotaon -p / # both should report "is on" sudo repquota -s / # a table, not an error ``` ## Verify it actually refuses Enabling quotas and setting none is the failure that looks like success. Prove enforcement once, on a throwaway account, before trusting it: ```bash sudo useradd -m quotaprobe sudo setquota -u quotaprobe 10240 10240 0 0 / # 10 MB soft and hard sudo -u quotaprobe dd if=/dev/zero of=/home/quotaprobe/fill bs=1M count=50 # expect: "Disk quota exceeded" and a file of about 10 MB, NOT 50 sudo userdel -r quotaprobe ``` If that writes 50 MB, quotas are not on, whatever the mount options say. ## What this does not do - **It sets no limits.** Every existing account is unlimited until something assigns a quota. That is the correct intermediate state: the machine gains the ability, nobody's behaviour changes. - **It does not touch the platform.** `provision-os.ts` does not call `setquota` yet, and there is nowhere for the owner to declare a default. That is the other half. - **It does not apply to the team folders**, because there are no team folders yet. When they arrive they must be `setgid` or their group quota counts nothing. ## When the platform half is built `POST /users/:id/provision-linux` already exists as a retry path and is the natural home for this: it becomes "re-apply current policy to this account", covering both a new account and a changed limit. **On a host where quotas are not enabled, the platform must say so** — "policy recorded, not enforceable on this host" — rather than storing numbers that do nothing. Same rule as the plugin dependency check: refuse or report, never silently no-op. `quotaon -p` is the check, and it is cheap enough to run per request.