Skip to content

Networks rootless - #7

Open
pablomh wants to merge 98 commits into
masterfrom
networks_rootless
Open

Networks rootless#7
pablomh wants to merge 98 commits into
masterfrom
networks_rootless

Conversation

@pablomh

@pablomh pablomh commented Mar 9, 2026

Copy link
Copy Markdown
Owner

No description provided.

pablomh and others added 5 commits March 9, 2026 10:35
Introduces the deploy_network role, which wraps
containers.podman.podman_network to create a named bridge network with
configurable properties: internal (no external routing), isolate (no
cross-network packet forwarding), IPv6, subnet, gateway, and DNS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace host networking with four named Podman bridge networks:

- foreman-db (internal, isolated): PostgreSQL and its clients
- foreman-cache (internal, isolated): Redis and its clients
- foreman-app: application layer — Foreman, Candlepin, Pulp, httpd
- foreman-proxy-net: Foreman and the smart proxy

Services reach each other by container DNS name instead of localhost.
Per-service notes:

- postgresql: also accessible via a Unix socket bind-mounted at
  /var/run/postgresql, used by Ansible's community.postgresql modules
  during deployment so no TCP port needs to be published to the host.
- redis: publishes no ports; clients reach it by DNS name on
  foreman-cache.
- candlepin: Tomcat binds to 0.0.0.0 so that other containers on the
  bridge network can reach it via its DNS name.
- migration containers (foreman db:migrate, pulp migrations): attached
  to foreman-db when the database is internal; switched to foreman-app
  when the database is external, because foreman-db uses internal: true
  which removes the default gateway, making it impossible for containers
  on that network to reach an external database host.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The development setup runs the Foreman Rails process directly on the
host rather than in a container, so it cannot resolve container bridge
DNS names such as postgresql or candlepin. Keeping host networking for
all services in the devel environment means every service remains
reachable on localhost as before, without requiring per-service URL
overrides for the Rails process.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
foreman-certs generates certificates only for localhost and the server
FQDN. Now that Foreman connects to Candlepin via the bridge DNS name
"candlepin", TLS hostname validation fails against those certificates.
A dedicated certificate with SAN=candlepin, signed by the installer
CA, is generated using the openssl CLI (consistent with the rest of
the codebase).

The certificate validity is set to 7300 days (20 years), matching the
default used by puppet-certs (theforeman/puppet-certs manifests/init.pp
$expiration parameter). The signing step runs on every deployment to
ensure the certificate is always freshly dated, consistent with how
foreman-certs handles the other installer certificates.

The localhost certificate previously used by Candlepin's Tomcat is now
unused and removed:

- "localhost" dropped from certificates_hostnames — the certificates
  role no longer generates the cert.
- localhost_key / localhost_certificate removed from
  default_certificates.yml and installer_certificates.yml.

The healthcheck is updated to validate the new certificate instead of
skipping verification with --insecure. --resolve candlepin:23443:127.0.0.1
forces the connection to the loopback so it works in both bridge
networking (where other containers reach Candlepin by DNS name) and
host networking (where "candlepin" would not resolve via container DNS).

Tests are updated to route connectivity checks through the foreman
container (which shares the same bridge network as candlepin), to verify
against the new certificate, and to check candlepin_certificate expiry
instead of the now-removed localhost_certificate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds docs/deployment.md with a description of the four bridge
networks introduced by this series, their properties (internal,
isolate), which containers are attached to each, and the rationale
for the port publishing decisions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@pablomh
pablomh force-pushed the networks_rootless branch from fb87658 to 8ccd08a Compare March 9, 2026 14:35
Creates a dedicated service user (foremanctl) for running Podman
containers in rootless mode. The role handles all prerequisites:
- acl package (required for Ansible become_user on CentOS 10)
- User and group creation with a non-login shell
- subuid/subgid ranges for user namespace mapping
- Unprivileged port binding via sysctl
- loginctl enable-linger so the user session survives across boots
- XDG_RUNTIME_DIR discovery (exposed as rootless_user_xdg_runtime_dir)
- Quadlet and systemd user config directories

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@pablomh
pablomh force-pushed the networks_rootless branch 3 times, most recently from 77eb19e to 101ae2a Compare March 9, 2026 15:52
pablomh and others added 17 commits March 9, 2026 16:58
…plays

base.yaml gains foremanctl_user/group/home/quadlet_dir/systemd_user_dir
vars. It also introduces foremanctl_become_method and foremanctl_runtime_env
as the single place to control how all rootless become blocks operate:
- foremanctl_become_method: swap between 'sudo' (default) or
  'community.general.machinectl' (or any future method) without touching
  individual roles
- foremanctl_runtime_env: holds XDG_RUNTIME_DIR for sudo; set to {} when
  the become method provides the session environment automatically

deploy.yaml is split into two plays so the rootless_user role can expose
foremanctl_xdg_runtime_dir as a fact before the service roles run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move the default certificate directory from /root/certificates to
/var/lib/foremanctl/certificates so that the rootless containers running
in foremanctl's user namespace can read them without elevated privileges.

The path is hardcoded rather than using foremanctl_home because
default_certificates.yml is loaded before base.yaml and base.yaml itself
references certificate path vars.

ca.yml: add base directory creation; set owner/group to
foremanctl_user/foremanctl_group on all directories and generated files.

issue.yml: set owner/group to foremanctl_user/foremanctl_group on all
generated keys, CSRs and certificates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Directories created owned by foremanctl_user/group so the rootless
  user can manage them
- podman_unshare become method used to chown the data directory to the
  postgres container UID/GID (26:26) inside the user namespace, avoiding
  the need for a shell-based 'sudo -u foremanctl podman unshare chown'
- Container runs inside a foremanctl_become_method block with
  foremanctl_runtime_env, so the become method and XDG handling are
  controlled by a single pair of variables in base.yaml
- Socket dir moved from /var/run/postgresql to
  /var/lib/foremanctl/postgresql-socket (mode 0777) and mounted inside
  the container at /tmp/socket to avoid the sclorg entrypoint chowning
  /var/run/postgresql to postgres:postgres with mode 0700
- community.postgresql.* tasks remain socket-based: root bypasses DAC
  on the 0777 socket dir and the socket file itself is world-accessible
- unix_socket_directories configured to include /tmp/socket so the
  existing socket path is reachable from within other containers
- All systemd calls use scope: user (tasks and handler)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Data directory created owned by foremanctl_user/group
- podman_unshare become method used to chown /var/lib/redis to the
  redis container UID/GID (1001:1001) inside the user namespace
- Container runs inside a foremanctl_become_method/foremanctl_runtime_env
  block for consistent become abstraction across all roles
- All systemd calls use scope: user
- REGISTRY_AUTH_FILE removed from image pull (handled by podman's user
  credential store in rootless mode)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Log directories created owned by foremanctl_user/group (containers
  run as uid 0 in user namespace = foremanctl on the host)
- All podman secret, image and container tasks moved inside a
  foremanctl_become_method/foremanctl_runtime_env block
- All systemd calls use scope: user (tasks and handler)
- REGISTRY_AUTH_FILE removed from image pull

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Storage directories created owned by foremanctl_user/group with
  appropriate SELinux labels
- Secret key and symmetric key files generated as foremanctl_user/group
- podman_unshare become method used to chown /var/lib/pulp to pulp
  container UID/GID (700:700) inside the user namespace
- All podman secret, image and container tasks moved inside a
  foremanctl_become_method/foremanctl_runtime_env block
- pulp-worker.target moved from /etc/systemd/system/ to
  foremanctl_systemd_user_dir (user scope)
- Pulp worker symlinks updated to use foremanctl_quadlet_dir instead
  of /etc/containers/systemd/
- Old pulp-worker cleanup also uses foremanctl_quadlet_dir
- All systemd calls use scope: user (tasks and handlers)
- REGISTRY_AUTH_FILE removed from image pulls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- All podman secret, image and container tasks moved inside a
  foremanctl_become_method/foremanctl_runtime_env block
- Dynflow symlinks updated to use foremanctl_quadlet_dir instead of
  /etc/containers/systemd/
- Recurring task timer files written to foremanctl_systemd_user_dir
  instead of /etc/systemd/system/
- All systemd calls use scope: user (tasks and handlers)
- REGISTRY_AUTH_FILE removed from image pull

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- All podman secret, image and container tasks moved inside a
  foremanctl_become_method/foremanctl_runtime_env block
- All systemd calls use scope: user (tasks and handler)
- REGISTRY_AUTH_FILE removed from image pull
- smart_proxy registration task kept outside the become block as it
  communicates with Foreman over HTTPS, not with Podman

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- foreman.target moved from /etc/systemd/system/ to
  foremanctl_systemd_user_dir so it lives in the user systemd scope
- All tasks run inside a foremanctl_become_method/foremanctl_runtime_env
  block
- systemd daemon_reload and service start both use scope: user

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces three building blocks for testing rootless Podman deployments:

run_as(server, user, cmd)
  Runs a command on the remote host as a specific user.  The underlying
  mechanism is controlled by the FOREMANCTL_RUN_AS_METHOD environment
  variable so different approaches can be tried without touching test
  files:
  - runuser (default): propagates exit codes correctly
  - machinectl: full login session, XDG_RUNTIME_DIR auto-set, but
    always exits 0 (use stdout/stderr assertions, not result.succeeded)
  - systemd-run: runs in the user systemd scope

foremanctl_run(server, cmd)
  Convenience shortcut for run_as(server, 'foremanctl', cmd).

UserService
  Testinfra-compatible class for user-scope systemd units, exposing
  is_running, is_enabled and exists properties backed by
  'systemctl --user' commands run via run_as().

user_service fixture
  Module-scoped factory: user_service('redis') returns a UserService
  for the foremanctl user, so test assertions read identically to the
  existing server.service() ones.

Also fixes the certificates fixture to use the rootless certificate
directory (/var/lib/foremanctl/certificates) instead of /root/certificates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Service checks use user_service() fixture (user-scope systemd)
- Socket path updated to /var/lib/foremanctl/postgresql-socket/
- podman exec calls wrapped with foremanctl_run() so they run in
  the foremanctl user namespace regardless of RUN_AS_METHOD

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Service check uses user_service() fixture (user-scope systemd)
- podman exec wrapped with foremanctl_run()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Service check uses user_service() fixture (user-scope systemd)
- podman exec and podman secret calls wrapped with foremanctl_run()
- openssl certificate read on the host (no container exec) unchanged

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Service checks use user_service() fixture (user-scope systemd)
- pulp-worker list query uses 'systemctl --user' via foremanctl_run()
- pulp-worker.target check uses user_service()
- podman exec call wrapped with foremanctl_run() and -ti flag dropped
  (non-interactive in CI)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Service checks use user_service() fixture (user-scope systemd)
- Dynflow symlink path updated from /etc/containers/systemd/ to
  /var/lib/foremanctl/.config/containers/systemd/
- Dynflow service instance checks use user_service()
- Recurring timer/service checks use user_service()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both service checks now use user_service() (user-scope systemd).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When fapolicyd is enabled, Ansible become_user: foremanctl fails because
fapolicyd blocks Python module execution from /var/tmp/ (the temp path
Ansible uses when becoming an unprivileged user). Add the missing rule.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@pablomh
pablomh force-pushed the networks_rootless branch from 101ae2a to 8894813 Compare March 9, 2026 16:00
pablomh and others added 2 commits March 10, 2026 11:38
When deploying rootlessly, networks must be created in the foremanctl
user's Podman namespace. Previously the task ran as root, making networks
invisible to rootless containers running as foremanctl.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The remote-database playbook was missing the rootless user setup play
and base.yaml vars, causing foremanctl_user/foremanctl_group/foremanctl_runtime_env
to be undefined when the postgresql role ran on the database host.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Revert back to the original working implementation that uses runuser -l
flag with XDG_RUNTIME_DIR export and single-quote escaping (escaped), as
it was in commit a2c086d. XDG_RUNTIME_DIR is required so systemctl
--user can locate the D-Bus session bus socket.

Also revert postgresql and pulp tests to their original commands from
a2c086d, avoiding the multiple-shell-layer escaping issues introduced
by subsequent "fix" commits. The original psql -c style commands work
correctly through testinfra's sudo wrapper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@pablomh
pablomh force-pushed the networks_rootless branch from 16560be to 5c91c63 Compare March 14, 2026 00:28
Add AddHost={{ ansible_facts['fqdn'] }}:host-gateway to dynflow-sidekiq
containers so they can resolve the server FQDN to reach the HTTPD
reverse proxy and Pulp API. Without this, Katello background workers
fail with PulpRpmClient::ApiError: Connection failed when processing
repository tasks. The Foreman web container already had this fix
(added in 975e3d8), but the dynflow workers were missed.

Skip test_postgresql_service when database_mode=external: in that
configuration postgresql runs on a separate database VM, not on
quadlet. The companion test_postgresql_missing_with_external already
asserts the correct behaviour for external mode.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@pablomh
pablomh force-pushed the networks_rootless branch from f1f3926 to a641bdc Compare March 14, 2026 11:11
pablomh and others added 2 commits March 14, 2026 14:06
- Remove || true from upgrade stop step; the stop should succeed
- Group installer certificate pre_tasks in a single block with one
  when condition; drop redundant become: true (inherited from play)
- Remove noisy comment from candlepin handler
- Fix hardcoded /var/lib/foremanctl in certs_secrets.yml comments
- Add blank line before Create Candlepin configuration task
- Add foremanctl_registry_auth_file to base.yaml and include
  REGISTRY_AUTH_FILE in foremanctl_runtime_env so all podman pull
  tasks in foremanctl blocks get registry authentication automatically

Flush handlers after service start is intentional: restarting before
start would fail on first deploy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
runuser requires root and misparses --user as its own flag. Replace with
sudo -u foremanctl and set XDG_RUNTIME_DIR explicitly so systemctl --user
can reach the D-Bus session bus.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@pablomh
pablomh force-pushed the networks_rootless branch 6 times, most recently from e97e9f7 to a087be1 Compare March 14, 2026 22:21
Tests run last (filename sorts after redis_test.py) since stopping the
target would break any subsequent service checks. This also validates
the stop command used in the upgrade workflow.

Use server.run with systemctl --machine=foremanctl@ --user for all
systemctl operations, consistent with how non-podman commands are run
in other tests. After each lifecycle operation, poll the Foreman HTTPS
frontend (https://server_fqdn/api/v2/ping with CA cert) matching the
pattern used in test_https_foreman_ping. On timeout, dump user service
states for diagnosis.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@pablomh
pablomh force-pushed the networks_rootless branch 5 times, most recently from 248bedd to 23920dc Compare March 15, 2026 03:52
Tests run last (filename sorts after redis_test.py) since stopping the
target would break any subsequent service checks. This also validates
the stop command used in the upgrade workflow.

Use server.run with systemctl --machine=foremanctl@ --user for all
systemctl operations. After each lifecycle operation, poll the Foreman
HTTPS frontend (https://server_fqdn/api/v2/ping with CA cert) matching
the pattern used in test_https_foreman_ping. On timeout, dump service
states, foreman.service status, container states and foreman logs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@pablomh
pablomh force-pushed the networks_rootless branch 4 times, most recently from 2102bca to 69f50bc Compare March 15, 2026 13:02
Tests run last (filename sorts after redis_test.py) since stopping the
target would break any subsequent service checks. This also validates
the stop command used in the upgrade workflow.

Use server.run with systemctl --machine=foremanctl@ --user for all
systemctl operations. After each lifecycle operation, poll the Foreman
HTTPS frontend (https://server_fqdn/api/v2/ping with CA cert) matching
the pattern used in test_https_foreman_ping.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@pablomh
pablomh force-pushed the networks_rootless branch from 69f50bc to e1eb1a7 Compare March 15, 2026 18:32
pablomh and others added 5 commits March 18, 2026 01:06
Wrap the systemd restart in block/rescue to collect service status
and container logs when the restart fails, then re-raise so CI
still marks the run as failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Foreman depends on Candlepin being fully initialised before it can
serve requests reliably. Without this ordering, foreman.service starts
simultaneously with candlepin.service, competing for CPU and disk I/O
during the JVM startup. This pushes Rails loading time close to the
default TimeoutStartSec (90s) and causes intermittent failures on
restart, particularly when the system is under load.

Because candlepin.service already declares:
  After=redis.service postgresql.service

the transitive ordering becomes:

  postgresql --+
               +--> candlepin --> foreman
  redis -------+

redis.service and postgresql.service are kept as explicit dependencies
to preserve correct ordering in future non-Katello deployments that
may not include candlepin.service.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extends foreman_target_test.py with lifecycle tests intended to run
at the end of the test suite. Tests that foreman.target can be cleanly
stopped and restarted, and that Foreman becomes available again
afterwards via the HTTPS endpoint (same curl pattern as
test_https_foreman_ping in httpd_test.py).
Incorporates upstream commits:
- Use candlepin.service in foreman startup ordering (f2e162d)
- Add foreman.target lifecycle tests (5a2eb8b)

Conflicts resolved keeping rootless implementations; remote-database
playbook updated to include both SSL cert vars and postgresql_networks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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