Skip to content

Rootless linger - #3

Open
pablomh wants to merge 31 commits into
masterfrom
rootless_linger
Open

Rootless linger#3
pablomh wants to merge 31 commits into
masterfrom
rootless_linger

Conversation

@pablomh

@pablomh pablomh commented Feb 22, 2026

Copy link
Copy Markdown
Owner

No description provided.

pablomh and others added 11 commits February 22, 2026 00:30
Enables Foreman deployment using rootless Podman containers, improving
security by eliminating root privileges for container operations while
maintaining full functionality of the Foreman/Katello stack.

What this achieves:

- Rootless deployment: All containers run without root privileges
- Single service user: Entire stack (foreman, candlepin, postgresql, redis,
  pulp) runs under dedicated foremanctl user
- Migration support: Automated playbook converts existing rootful deployments
- Namespace isolation: Container volumes properly mapped to user namespaces
- Port access: Unprivileged ports configured for HTTP/HTTPS access

New capabilities:

- Fresh deployments use rootless mode by default
- Existing rootful deployments can migrate with automated playbook
- All certificates, data, and configuration preserved during migration
- User lingering enabled for service persistence across reboots

This change modernizes the deployment model to follow container security
best practices while maintaining backward compatibility through the migration
playbook.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…s/systemd/users/$UID

Changes:
- Remove XDG_RUNTIME_DIR management (systemd manages it automatically)
- Keep linger support (enable-linger and linger checks)
- Move quadlet files from ~/.config/containers/systemd to /etc/containers/systemd/users/$UID
- Remove XDG_RUNTIME_DIR from all environment blocks and sudo commands
- Update playbooks to map only foremanctl_quadlet_dir variable
- Simplify rootless_user role to not track or verify XDG_RUNTIME_DIR

Benefits:
- Simpler deployment without manual XDG_RUNTIME_DIR management
- Quadlet files in /etc are easier to manage (no become_user needed)
- Systemd automatically manages runtime directories with linger enabled

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The containers.podman.podman_container module needs to know where to
write quadlet files. Without the quadlet_dir parameter, it defaults
to writing to ~/.config/containers/systemd/, but we want files in
/etc/containers/systemd/users/$UID/.

This fixes errors like:
  src file does not exist: /etc/containers/systemd/users/996/pulp-worker@.container

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Applied architecture:
1. Quadlet management: Root (writes to /etc/containers/systemd/users/$UID/)
2. Image/secret management: User (via become_user)
3. Systemd operations: User (via become_user with scope: user)

Changes:
- postgresql: Split into user block (images/secrets), root task (quadlet), user block (systemd)
- redis: Split into user task (image), root task (quadlet), user block (systemd)
- candlepin: Split into user block (secrets/artemis/image), root task (quadlet), user block (systemd)
- pulp: Split into user block (images/secrets), root tasks (quadlets), user block (systemd/migration)

Note: foreman and foreman_proxy roles still need similar refactoring

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Applied the same architecture to foreman and foreman_proxy roles:
1. Quadlet management: Root (writes to /etc/containers/systemd/users/$UID/)
2. Image/secret management: User (via become_user)
3. Systemd operations: User (via become_user with scope: user)

Foreman role changes:
- User block: Pull image and create all secrets
- Root tasks: Deploy foreman quadlet, dynflow template, create symlinks
- Root task (conditional): Deploy recurring task quadlets
- User block: Render timers, daemon-reload, migrate DB, start services
- No change: Wait for services and configure proxy

Foreman_proxy role changes:
- User task: Pull image
- Includes: configs.yaml and certs.yaml (self-contained with own become_user)
- Root task: Deploy quadlet
- User block: daemon-reload, start service
- No change: Register proxy to Foreman

All roles now consistently separate:
- Root-managed quadlet files in /etc/containers/systemd/users/$UID/
- User-managed images/secrets/services in user's podman storage

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
When tasks run as root (without become_user), files/directories/symlinks
are created with root:root ownership by default. Explicitly setting
owner: root and group: root is redundant.

Removed from:
- foreman: Dynflow symlinks
- pulp: Worker symlinks
- rootless_user: Quadlet directory

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Changed all documentation references from:
  systemctl --user <command>

To:
  systemctl --machine={{ foremanctl_user }}@ --user <command>

And for journalctl:
  journalctl --user --machine={{ foremanctl_user }}@ <command>

This is necessary because without XDG_RUNTIME_DIR being set, systemctl
cannot connect to the user's systemd instance. Using --machine allows
root to interact with a specific user's systemd instance without
needing to set environment variables or use sudo -u.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Systemd module needs XDG_RUNTIME_DIR to connect to the user's systemd
instance, even when using become_user. Without it, we get:
  "Could not find the requested service postgresql: host"

Changes:
- Map foremanctl_xdg_runtime_dir in deploy playbooks
- Add environment: XDG_RUNTIME_DIR to postgresql systemd operations
- Add environment: XDG_RUNTIME_DIR to postgresql handler

Testing with postgresql first before applying to all roles.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added DEBUG tasks to investigate why systemd can't find the service:
1. Check if quadlet .container file was created
2. Verify file permissions and ownership
3. Test if foremanctl user can read the file
4. Check quadlet directory permissions
5. List systemd units after daemon-reload
6. Check for generated .service file location
7. Display journal logs

This will help identify if the issue is:
- File not created
- Permission denied
- Generator not running
- Service not being generated from quadlet

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Set all quadlet files to mode 0644 to ensure the foremanctl user's
systemd instance can read them when they're stored in
/etc/containers/systemd/users/$UID/.

Modified roles:
- postgresql: Make quadlet file readable (0644)
- redis: Make quadlet file readable (0644)
- candlepin: Make quadlet file readable (0644)
- pulp: Make all three quadlet files readable (0644)
- foreman: Make foreman and dynflow quadlet files readable (0644)
- foreman_proxy: Make quadlet file readable (0644)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@pablomh
pablomh force-pushed the rootless_linger branch 6 times, most recently from db6ecc0 to 43271a8 Compare February 23, 2026 17:41
Testing if running daemon-reload as root via systemctl --machine avoids the need to become_user and set XDG_RUNTIME_DIR. Also commented out chmod 0644 on quadlet files to test if it's still needed.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
pablomh and others added 11 commits February 23, 2026 19:19
The systemd module with scope: user doesn't work when running as root without become_user. Use systemctl --machine command instead.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Confirmed that quadlet files must be world-readable even when using systemctl --machine. The systemd-generator runs as the user and needs to read the quadlet files to generate service files.

Without 0644 permissions, the generator silently fails and no service file is created.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Changes applied to all service roles:

1. SELinux labels (container_var_lib_t):
   - PostgreSQL: data directory
   - Redis: data directory
   - Pulp: storage directories and secret files
   - Candlepin: log directories

2. systemctl --machine pattern:
   - Use 'systemctl --machine=${user}@ --user' for all systemd operations
   - Run as root instead of become_user
   - Eliminates need for XDG_RUNTIME_DIR environment variable
   - Applies to daemon-reload, start, restart, enable operations

3. Quadlet files:
   - Keep chmod 0644 (confirmed necessary for systemd-generator)

4. Cleanup:
   - Remove debug tasks from postgresql
   - Remove flush_handlers where not needed
   - Simplify pulp service start (no async needed)

Modified roles:
- postgresql: SELinux label, systemctl pattern, cleanup
- redis: SELinux label, systemctl pattern
- candlepin: SELinux label, systemctl pattern
- pulp: SELinux labels, systemctl pattern, refactor service management
- foreman: systemctl pattern, refactor service management
- foreman_proxy: systemctl pattern

All handlers updated to use systemctl --machine pattern.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Remove commented-out task block that was used during testing.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Changes:
- Fix role paths in dev playbooks (use 'rootless_user' instead of relative path)
- Update variable mapping to use foremanctl_quadlet_dir (was foremanctl_xdg_runtime_dir)
- Remove obsolete fapolicyd rule for /var/lib/foremanctl/.ansible/tmp/

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Changes:
- Add UserService class in conftest.py to check user systemd services
- Add user_service fixture that uses systemctl --machine to check user services
- Add foremanctl_user and foremanctl_uid fixtures
- Update all service tests to use user_service instead of server.service():
  * foreman_test.py: foreman, dynflow instances, recurring timers
  * postgresql_test.py: postgresql service
  * redis_test.py: redis service
  * candlepin_test.py: candlepin service
  * pulp_test.py: pulp-api, pulp-content, pulp-worker services and target
  * foreman_proxy_test.py: foreman-proxy service
  * foreman_target_test.py: foreman.target
- Update quadlet file location tests to check /etc/containers/systemd/users/$UID/

All services now run in the foremanctl user's systemd instance, not the
system systemd, so tests must check the user systemd via systemctl --machine.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add mode: '0755' to rootless_user_systemd_user_dir (~/.config/systemd/user)
to follow standard systemd conventions.

The directory needs to be:
- Readable by systemd (755)
- Owned by the user
- Standard location for user unit files (.timer, .target, etc.)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Removed 5 debug tasks that were leftover from development:
- rootless_user_home display
- rootless_user_quadlet_dir display
- rootless_user_systemd_user_dir display
- .config/containers path display
- rootless_user_linger_result display

These are no longer needed in production.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Moved become: true from play level to task level for 'Install podman'.

Why this change:
- With play-level become: true, Ansible stages modules as root in /var/tmp/
- When tasks use become_user: foremanctl, this creates cross-user execution
- fapolicyd blocks: 'foremanctl trying to execute root-owned files from /var/tmp'

After this change:
- Ansible stages modules as SSH user (vagrant) in ~/.ansible/tmp/
- 'Install podman' task: becomes root to install package
- Image pull tasks: become foremanctl_user to pull images
- No cross-user execution from /var/tmp/ -> fapolicyd happy

The fapolicyd rule for /home/vagrant/.ansible/tmp/ already exists in CI.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add skip_list for command-instead-of-module rule in .ansible-lint config.

Why we skip this rule:
We intentionally use 'systemctl --machine=user@ --user' commands instead
of the ansible.builtin.systemd module for managing user systemd instances.

Reasons:
- systemctl --machine is the recommended systemd approach for cross-session
  management (managing user systemd from root context)
- Avoids need for XDG_RUNTIME_DIR environment variable setup
- More reliable than systemd module with become_user
- Avoids D-Bus security policy issues

The ansible.builtin.systemd module with become_user requires environment
variable management and can hit D-Bus permission issues, making systemctl
--machine the cleaner and more reliable solution for rootless deployments.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
In rootless Podman deployment, secrets are stored per-user. Running
'podman secret inspect' as root cannot access the foremanctl user's
secrets. Updated assert_secret_content helper to run podman commands
as the foremanctl user using sudo -u.
- Update postgresql tests to use 'sudo -u foremanctl podman exec' for
  internal database mode (rootless containers)
- Update pulp test to access containers in foremanctl user's storage
- Fix upgrade workflow to stop foreman.target using systemctl --machine
  pattern instead of system systemctl

In rootless Podman, containers run in the user's storage namespace and
cannot be accessed by root's podman commands. Tests now check database
mode and use appropriate command prefix.
- Rename migrate.yaml to migrate-to-rootless.yaml to match directory name
- Add metadata.obsah.yaml with help text for obsah/foremanctl CLI

This fixes test_filename_matches_directory and test_is_documented test
failures.
Add -H flag to 'sudo -u foremanctl' commands to set HOME to the target
user's home directory. Without this, sudo tries to preserve the current
working directory (/home/vagrant) which the foremanctl user cannot
access, causing 'cannot chdir to /home/vagrant: Permission denied'
errors.

The -H flag makes sudo change to the target user's home directory,
avoiding permission issues when running podman commands as foremanctl.
The -H flag only sets HOME but doesn't change the working directory.
The -i flag simulates an initial login, which:
- Sets HOME to the target user's home directory
- Changes to that home directory
- Processes the user's shell profile

This fully avoids the 'cannot chdir to /home/vagrant: Permission denied'
error when running podman commands as the foremanctl user.
The foremanctl user has shell=/sbin/nologin and cannot access
/home/vagrant. Following the pattern used in playbooks, change to /tmp
(which is world-accessible) before running podman commands:

  cd /tmp && sudo -u foremanctl podman ...

Since testinfra's server.run() already wraps commands in
'sudo /bin/sh -c', there's no need for an additional sh -c wrapper.

This avoids both:
- 'This account is currently not available' (from sudo -i with nologin)
- 'cannot chdir to /home/vagrant: Permission denied' (from sudo -u)

The /tmp directory is accessible to all users, so foremanctl can use it
as a working directory.
The certificates fixture was hardcoding certificates_ca_directory to
'/var/lib/foremanctl/certificates' in the Jinja2 context, which
overrode the template's own definition during variable substitution.

For installer certificates, this caused all paths to use
/var/lib/foremanctl/certificates instead of /root/ssl-build, resulting
in 'No such file or directory' errors.

Use two-pass rendering:
1. First pass: render template to extract certificates_ca_directory value
2. Second pass: render with that value in context for proper Jinja2
   variable substitution in path definitions

This allows each certificate source template to define its own
certificates_ca_directory while still supporting Jinja2 templating.
Unified service checking for:
- User services (systemctl --machine=user@ --user)
- Podman containers (podman inspect)

Replaced separate service checking implementations with a single
GenericService class that takes an optional 'user' parameter:
- If user is set: checks user systemd service
- If user is None: checks podman container

This simplifies the code and makes it easier to support both internal
(user service) and external (container) database modes.
External database setup (remote-database.yaml) also configures rootless
user environment and runs PostgreSQL as a user service, not as a rootful
container.

The playbook runs:
1. rootless_user role - creates foremanctl user
2. postgresql role - runs PostgreSQL as user service

This is identical to internal database setup, just on a separate host.

Updated:
- database_user_service: Always use user=foremanctl_user for both modes
- PostgreSQL tests: Remove database_mode branching, use same pattern
  for both internal and external (cd /tmp && sudo -u foremanctl podman)

This fixes 'no such object: postgresql' errors in external database mode
where GenericService was incorrectly checking root's podman instead of
foremanctl user's podman.
@pablomh
pablomh force-pushed the rootless_linger branch 2 times, most recently from 49c3dab to d8938ca Compare February 25, 2026 00:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant