fix(control-panel/sftp): create shared fpm group before useradd

Provisioning failed on a stock host because `useradd -g <fpm_gid>` (82, the
alpine www-data gid) requires a group at that gid, which no host baseline
creates. Add ensure_fpm_group(): create a `heleos-web` group at fpm_gid if
absent, before the SFTP user is created. Idempotent; resolves the open item
noted in site-templates/README.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Bart Van Geyt 2026-08-09 04:35:32 +02:00
parent a90d07875d
commit 638fb24593

View file

@ -19,6 +19,11 @@ from .runner import Runner
SSHD_DROPIN = "/etc/ssh/sshd_config.d/sftp-{customer}.conf" SSHD_DROPIN = "/etc/ssh/sshd_config.d/sftp-{customer}.conf"
# Host group (at fpm_gid) shared by php-fpm and every customer's SFTP user. The
# alpine images run www-data as uid/gid 82, but a stock host has no group at that
# gid, so useradd -g <fpm_gid> would fail. Created once per host.
FPM_GROUP_NAME = "heleos-web"
def _user_exists(runner: Runner, user: str) -> bool: def _user_exists(runner: Runner, user: str) -> bool:
if runner.dry_run: if runner.dry_run:
@ -27,6 +32,20 @@ def _user_exists(runner: Runner, user: str) -> bool:
return out.strip().isdigit() return out.strip().isdigit()
def _group_exists(runner: Runner, gid: int) -> bool:
if runner.dry_run:
return False
out = runner.run(["getent", "group", str(gid)], check=False, capture=True)
return bool(out.strip())
def ensure_fpm_group(runner: Runner, cfg: Config) -> None:
"""Ensure a group with gid == fpm_gid exists so the SFTP user's primary group
(and the setgid web root) can share it with php-fpm. Idempotent."""
if not _group_exists(runner, cfg.fpm_gid):
runner.run(["groupadd", "-g", str(cfg.fpm_gid), FPM_GROUP_NAME])
def set_web_ownership(runner: Runner, cfg: Config, site: Site) -> None: def set_web_ownership(runner: Runner, cfg: Config, site: Site) -> None:
web = context.webroot(cfg, site) web = context.webroot(cfg, site)
runner.run(["chown", "-R", f"{cfg.fpm_uid}:{cfg.fpm_gid}", web]) runner.run(["chown", "-R", f"{cfg.fpm_uid}:{cfg.fpm_gid}", web])
@ -45,6 +64,7 @@ def ensure_customer_account(runner: Runner, cfg: Config, customer: str,
home = f"{cfg.customers_root}/{customer}" home = f"{cfg.customers_root}/{customer}"
created = not _user_exists(runner, user) created = not _user_exists(runner, user)
if created: if created:
ensure_fpm_group(runner, cfg) # useradd -g <fpm_gid> needs this group
runner.run([ runner.run([
"useradd", "-M", "-N", "useradd", "-M", "-N",
"-g", str(cfg.fpm_gid), # primary group = fpm group "-g", str(cfg.fpm_gid), # primary group = fpm group