Repository navigation
Add 3 alternative methods of installing Ascender - #234
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate findings affect deployment correctness, connectivity, credential protection, and service startup.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds three non-Kubernetes Ascender installation paths: Docker Compose, Podman/systemd, and direct Rocky/EL host services.
Changes:
- Adds Compose deployment scripts, configuration, and documentation.
- Adds Podman-native container management and systemd integration.
- Adds Ansible roles for PostgreSQL, Valkey, Receptor, builds, configuration, and services.
File summaries
| File | Description |
|---|---|
alternative/services/setup.sh |
Bootstraps Ansible and runs the installation playbook. |
alternative/services/roles/ascender_valkey/tasks/main.yml |
Installs and configures Valkey. |
alternative/services/roles/ascender_valkey/handlers/main.yml |
Defines Valkey restart handling. |
alternative/services/roles/ascender_system/tasks/main.yml |
Installs dependencies and configures system state. |
alternative/services/roles/ascender_system/handlers/main.yml |
Defines system handlers. |
alternative/services/roles/ascender_services/templates/ascender.service.j2 |
Defines the Ascender systemd unit. |
alternative/services/roles/ascender_services/tasks/main.yml |
Runs migrations and starts services. |
alternative/services/roles/ascender_services/handlers/main.yml |
Restarts Ascender and reloads nginx. |
alternative/services/roles/ascender_receptor/tasks/main.yml |
Installs Receptor and initializes Podman. |
alternative/services/roles/ascender_postgresql/tasks/main.yml |
Installs and configures PostgreSQL. |
alternative/services/roles/ascender_postgresql/handlers/main.yml |
Defines PostgreSQL restart handling. |
alternative/services/roles/ascender_defaults/defaults/main.yml |
Defines installation defaults. |
alternative/services/roles/ascender_config/templates/supervisord.conf.j2 |
Configures supervised processes. |
alternative/services/roles/ascender_config/templates/settings.py.j2 |
Defines Ascender settings. |
alternative/services/roles/ascender_config/templates/receptor.conf.j2 |
Configures Receptor. |
alternative/services/roles/ascender_config/templates/postgres.py.j2 |
Configures database credentials. |
alternative/services/roles/ascender_config/templates/nginx.conf.j2 |
Defines host nginx configuration. |
alternative/services/roles/ascender_config/templates/nginx-locations.conf.j2 |
Defines nginx routing. |
alternative/services/roles/ascender_config/templates/environment.j2 |
Defines the service environment. |
alternative/services/roles/ascender_config/templates/channels.py.j2 |
Configures websocket secrets. |
alternative/services/roles/ascender_config/tasks/main.yml |
Generates application configuration and certificates. |
alternative/services/roles/ascender_build/tasks/main.yml |
Checks out and builds Ascender. |
alternative/services/requirements.yml |
Declares Ansible collections. |
alternative/services/README.md |
Documents direct-services installation. |
alternative/services/inventory.example |
Provides example inventory. |
alternative/services/install.yml |
Orchestrates installation roles. |
alternative/services/group_vars/all.yml |
Provides variable examples and overrides. |
alternative/services/ansible.cfg |
Configures Ansible execution. |
alternative/services/.gitignore |
Ignores local inventory. |
alternative/podman/scripts/launch_task.sh |
Starts the Podman task container. |
alternative/podman/scripts/init.sh |
Initializes shared volumes. |
alternative/podman/scripts/bootstrap.sh |
Performs database bootstrap. |
alternative/podman/receptor/launch_awx_receptor.sh |
Starts Receptor and nested Podman. |
alternative/podman/receptor/Dockerfile |
Builds the Receptor sidecar image. |
alternative/podman/receptor/containers.conf |
Configures nested Podman. |
alternative/podman/README.md |
Documents Podman installation and operations. |
alternative/podman/manage-containers.sh |
Manages containers, volumes, and lifecycle. |
alternative/podman/install.sh |
Installs and starts the Podman deployment. |
alternative/podman/config/valkey.conf |
Configures Valkey. |
alternative/podman/config/settings.py |
Defines container settings. |
alternative/podman/config/receptor.conf |
Configures Receptor. |
alternative/podman/config/nginx.conf |
Configures container nginx. |
alternative/podman/config/nginx-locations.conf |
Defines nginx routes. |
alternative/podman/config/nginx-http-redirect.conf |
Defines HTTP redirects. |
alternative/podman/config/nginx-http-insecure.conf |
Configures insecure HTTP mode. |
alternative/podman/ascender.service |
Defines the Podman systemd unit. |
alternative/podman/ascender.conf.example |
Provides Podman configuration defaults. |
alternative/docker-compose/setup.sh |
Generates Compose secrets and certificates. |
alternative/docker-compose/scripts/launch_task.sh |
Starts the Compose task container. |
alternative/docker-compose/scripts/init.sh |
Initializes shared volumes. |
alternative/docker-compose/scripts/bootstrap.sh |
Performs database bootstrap. |
alternative/docker-compose/receptor/launch_awx_receptor.sh |
Starts Receptor and nested Podman. |
alternative/docker-compose/receptor/Dockerfile |
Builds the Receptor sidecar image. |
alternative/docker-compose/receptor/containers.conf |
Configures nested Podman. |
alternative/docker-compose/README.md |
Documents Compose installation and operations. |
alternative/docker-compose/docker-compose.yml |
Defines the Compose deployment and volumes. |
alternative/docker-compose/config/valkey.conf |
Configures Valkey. |
alternative/docker-compose/config/settings.py |
Defines container settings. |
alternative/docker-compose/config/receptor.conf |
Configures Receptor. |
alternative/docker-compose/config/nginx.conf |
Configures container nginx. |
alternative/docker-compose/config/nginx-locations.conf |
Defines nginx routes. |
alternative/docker-compose/config/nginx-http-redirect.conf |
Defines HTTP redirects. |
alternative/docker-compose/config/nginx-http-insecure.conf |
Configures insecure HTTP mode. |
alternative/docker-compose/certs/.gitkeep |
Preserves the certificate directory. |
alternative/docker-compose/.gitignore |
Excludes generated configuration. |
alternative/docker-compose/.env.example |
Provides Compose configuration defaults. |
Review details
Suppressed comments (10)
alternative/docker-compose/docker-compose.yml:181
- The task entrypoint creates
/var/lib/awx/job_status, but this path is not backed by a shared volume and the web service has no mount for it either. Job output written by the task container is therefore unavailable after a task restart and cannot be read by the web container; add a named job-status volume and mount it in init, task, and web.
- projects:/var/lib/awx/projects
- job_private_data:/tmp
- valkey_socket:/var/run/valkey
- receptor_socket:/var/run/awx-receptor
- rsyslog_socket:/var/run/awx-rsyslog
alternative/docker-compose/docker-compose.yml:204
rsyslog_spoolis mounted into the unprivileged rsyslog container, butinitnever mounts or initializes this volume. A fresh named volume is root-owned0755, so uid 1000 cannot create the generated rsyslog configuration/spool files and the rsyslog service will fail on first start; prepare this volume ininitas well.
volumes:
- ./config/settings.py:/etc/tower/settings.py:ro
- ./config/receptor.conf:/etc/receptor/receptor.conf:ro
- valkey_socket:/var/run/valkey
- rsyslog_socket:/var/run/awx-rsyslog
- rsyslog_spool:/var/lib/awx/rsyslog
alternative/docker-compose/docker-compose.yml:144
- The Compose deployment bind-mounts host config, scripts, and certificates with
:robut without an SELinux relabel option. On the Rocky/EL9 hosts this install targets, enforcing SELinux commonly labels the checkout so the containers cannot read these mounts; use:ro,z(or document/handle the SELinux requirement) for the shared bind mounts.
- ./config/settings.py:/etc/tower/settings.py:ro
- ./config/receptor.conf:/etc/receptor/receptor.conf:ro
- ./config/nginx.conf:/etc/nginx/nginx.conf:ro
- ./config/nginx-locations.conf:/etc/nginx/ascender-locations.conf:ro
- ./config/nginx-http-${ASCENDER_HTTP_MODE:-redirect}.conf:/etc/nginx/ascender-http.conf:ro
- ./certs:/etc/tower/certs:ro
alternative/docker-compose/setup.sh:37
- The generated private TLS key is made world-readable (
0644). Any local user on the host can copy it and impersonate the Ascender endpoint; keep the key restricted and provide the container's nginx process access through a secret or an appropriate group/UID mapping instead.
if [ ! -f certs/ascender.crt ] || [ ! -f certs/ascender.key ]; then
openssl req -x509 -nodes -newkey rsa:2048 -days 825 \
-keyout certs/ascender.key -out certs/ascender.crt \
-subj "/CN=${hostname_value}" \
-addext "subjectAltName=DNS:${hostname_value},DNS:localhost,IP:127.0.0.1" 2>/dev/null
# nginx runs as uid 1000 inside the container; it needs to read the key.
chmod 644 certs/ascender.crt certs/ascender.key
alternative/podman/install.sh:80
systemctl enable --nowdoes not restart an already active unit. Re-running this installer therefore replaces the scripts/configuration on disk but leaves the old containers running, contrary to the README's upgrade instructions; explicitly restart the unit after installing the files.
if [[ "${START}" == yes ]]; then
echo "enabling and starting ascender.service (first start pulls images and migrates; follow with: journalctl -fu ascender)"
echo "This will take a while..."
systemctl enable --now ascender.service
alternative/podman/manage-containers.sh:361
- The task entrypoint creates
/var/lib/awx/job_status, but only/tmpis shared here and the web container has no mount for the job-status path. Job output is consequently container-local and is lost on task recreation or inaccessible to the web process; add a shared job-status volume to the task and web containers and initialize it.
alternative/podman/manage-containers.sh:407 VOL_RSYSLOG_SPOOLis mounted into the unprivileged rsyslog container, butrun_initnever mounts or initializes it. A fresh named volume remains root-owned0755, so uid 1000 cannot write the generated rsyslog configuration/spool files and first startup fails; include this volume in the init step.
alternative/services/roles/ascender_build/tasks/main.yml:16- This command has no check-mode handling. With
./setup.sh --check, Ansible skips a command withoutcreates/removes, leaving_commit.stdoutundefined for the build decision below (and a fresh host has no checkout), so the documented check invocation fails instead of validating the install.
alternative/services/roles/ascender_config/tasks/main.yml:114 createschecks only the certificate, while the following task also requires the key. Ifascender.keyis deleted or replaced whileascender.crtremains, OpenSSL is skipped and the play fails on the missing key during a rerun. Gate generation on both files (or otherwise manage the certificate pair atomically).
alternative/services/roles/ascender_config/templates/postgres.py.j2:8- The password is inserted into a single-quoted Python literal without escaping. A documented
ascender_pg_passwordcontaining'or a backslash produces invalidpostgres.py, so the service and migrations fail on the next run; render it as a JSON/Python-escaped string instead.
- Files reviewed: 65/66 changed files
- Comments generated: 22
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate deployment, security, connectivity, and upgrade issues remain.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (18)
Previously missed (2) — in code that hasn't changed since the last review.
alternative/services/roles/ascender_config/tasks/main.yml:128
- The
createsguard only checks for the certificate. If a previous run leavesascender.crtbut loses or replacesascender.key, this task skips generation and the later permission task does not create the missing key, so nginx validation fails instead of repairing the installation. Guard generation on the presence of both files (or stat them and regenerate when either is absent).
alternative/services/roles/ascender_config/templates/postgres.py.j2:8 ascender_pg_passwordis an inventory-configurable value, but it is inserted into a single-quoted Python literal without escaping. A password containing a quote, backslash, or newline will makeconf.d/postgres.pyinvalid and prevent every Ascender process from starting. Serialize the value as a Python/JSON string when rendering this template.
alternative/docker-compose/docker-compose.yml:146
- These host bind mounts omit Docker's SELinux relabel option. On the Rocky/EL hosts this PR targets with SELinux enforcing, the containers can be denied access to the mounted settings, scripts, and certificates; the Podman implementation already uses
:zfor its equivalent mounts. Add,zto every host bind mount in this Compose file (including./scriptsand./certs), not just the files shown here.
- ./config/settings.py:/etc/tower/settings.py:ro
- ./config/receptor.conf:/etc/receptor/receptor.conf:ro
- ./config/nginx.conf:/etc/nginx/nginx.conf:ro
- ./config/nginx-locations.conf:/etc/nginx/ascender-locations.conf:ro
- ./config/nginx-http-${ASCENDER_HTTP_MODE:-redirect}.conf:/etc/nginx/ascender-http.conf:ro
alternative/docker-compose/docker-compose.yml:150
JOBOUTPUT_ROOTis/var/lib/awx/job_statusin the mounted settings, but this service only sharesprojects,valkey_socket, andrsyslog_socket;launch_task.shcreatesjob_statusonly inside the task container. Job output written by the task therefore is not visible to the web container, so the UI cannot reliably serve job logs. Add a shared job-status volume, initialize it with the other shared volumes, and mount it in the web and task containers (and any execution container that writes to it).
- projects:/var/lib/awx/projects
- valkey_socket:/var/run/valkey
- rsyslog_socket:/var/run/awx-rsyslog
alternative/docker-compose/docker-compose.yml:98
- The
rsyslogservice mounts a freshrsyslog_spoolvolume at/var/lib/awx/rsyslog, but this init container never mounts or chmods that volume.launch_awx_rsyslog.shthen runs as uid 1000 and writesrsyslog.confthere, so the first start can fail with permission denied. Include this volume in the init mounts so it receives the same group-writable permissions as the other shared volumes.
- podman_storage:/var/lib/awx/.local/share/containers
alternative/docker-compose/receptor/launch_awx_receptor.sh:26
- This command is documented as required to initialize rootless Podman, but
|| truediscards every failure. If subordinate-ID or storage setup fails, receptor still starts and the install appears healthy until the first job fails. Propagate the failure or distinguish a benign already-initialized result from an actual setup error.
# Required once per storage volume for rootless podman to set up its user
# namespace state; harmless when already done.
podman system migrate >/dev/null 2>&1 || true
alternative/docker-compose/scripts/bootstrap.sh:26
- Every
createsuperuserfailure is treated as proof that the user already exists. Invalid admin input or a transient database error therefore entersupdate_password, which can hide the original failure and leave no usable admin while bootstrap continues. Only use the password-sync fallback after verifying that the user exists; propagate other failures.
awx-manage createsuperuser --noinput --username "${ASCENDER_ADMIN_USER}" --email "${ASCENDER_ADMIN_EMAIL}" 2>/dev/null; then
echo "bootstrap: created admin user"
else
# Already exists: make the password match .env, like the operator does.
awx-manage update_password --username "${ASCENDER_ADMIN_USER}" --password "${ASCENDER_ADMIN_PASSWORD}" >/dev/null
alternative/podman/manage-containers.sh:98
JOBOUTPUT_ROOTis/var/lib/awx/job_status, but the Podman layout defines onlyVOL_JOBDATAfor/tmp; the task entrypoint createsjob_statusin its own container filesystem and the web container never receives it. This loses the shared job output that the UI needs. Add a persistent shared job-status volume and mount/initialize it for the relevant containers.
alternative/podman/manage-containers.sh:262- The
rsyslogcontainer mountsVOL_RSYSLOG_SPOOLat/var/lib/awx/rsyslog, butrun_initdoes not mount that volume, so the init script cannot make it group-writable. The unprivilegedlaunch_awx_rsyslog.shsubsequently createsrsyslog.confin the fresh root-owned volume and fails on the first start. Mount the spool volume in this init command as well.
alternative/podman/receptor/launch_awx_receptor.sh:26 - This command is documented as required to initialize rootless Podman, but
|| truediscards every failure. If subordinate-ID or storage setup fails, receptor still starts and the install appears healthy until the first job fails. Propagate the failure or distinguish a benign already-initialized result from an actual setup error.
alternative/podman/scripts/bootstrap.sh:26 - Every
createsuperuserfailure is treated as proof that the user already exists. Invalid admin input or a transient database error therefore entersupdate_password, which can hide the original failure and leave no usable admin while bootstrap continues. Only use the password-sync fallback after verifying that the user exists; propagate other failures.
alternative/services/install.yml:8 - This new infrastructure-installation playbook does not attach the required
ownerandrepotags/metadata to the resources it creates. Add those tags consistently so installations from this path can be attributed and discovered.
alternative/services/roles/ascender_build/tasks/main.yml:58 - On an upgrade,
ascender.serviceis still running while this task force-reinstalls packages into its live virtualenv; the restart handler is deferred until the end of a later role. Old processes can therefore run while their dependencies are replaced, and migrations can run before those processes are restarted, creating mixed-code requests and migration failures. Stop the service before changing the venv (or switch to a versioned venv atomically) and start it only after the migration/configuration sequence.
alternative/services/roles/ascender_config/templates/channels.py.j2:2 - The websocket secret is also interpolated into a single-quoted Python literal without escaping. Although generated secrets are alphanumeric, the documented variable can be overridden; a quote, backslash, or newline then breaks
channels.pyand prevents Ascender from starting. Use a JSON/Python-escaped rendering here.
alternative/services/roles/ascender_config/templates/receptor.conf.j2:12 - This starts a plaintext Receptor TCP listener on the host by default. The playbook only opens 80/443 in firewalld, but firewalld may be disabled or
ascender_manage_firewalldmay be false, in which case port 27199 is reachable without TLS/authentication. Bind the listener to loopback by default or make it opt-in behind a TLS/peer configuration before exposing it.
alternative/services/roles/ascender_receptor/tasks/main.yml:6 - The downloaded archive contains the executable that will run as the
awxservice user, butget_urldoes not verify it. The v1.6.8 release publisheschecksums.txt; without validating the selected asset's SHA-256, a compromised release path or proxy can replace the binary and gain code execution in the Ascender service.
alternative/services/roles/ascender_services/tasks/main.yml:19 - On an upgrade, the build role notifies
restart ascender, but that handler is not flushed until line 89. Therefore the currently running old Ascender processes remain online while this task applies the new code's migrations, which can expose an incompatible schema to the old workers and cause failed requests or jobs. Stop/restart the service before migrating (while still handling the first-install case) rather than deferring the restart until after the migration.
alternative/services/roles/ascender_system/tasks/main.yml:41 - This Node-based build path installs
npmand documents it as the prerequisite, but the supported package-manager path should usepnpm. Provision/configure pnpm and ensure the UI build invokes it so dependency resolution is consistent with the rest of the project.
- Files reviewed: 65/66 changed files
- Comments generated: 6
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Critical and moderate findings remain unresolved, including rollback-induced outages, sub-ID overlap validation, bootstrap failures, and configuration issues.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (11)
Previously missed (2) — in code that hasn't changed since the last review.
alternative/services/roles/ascender_config/templates/postgres.py.j2:8
- The configured database password is inserted into a single-quoted Python literal without escaping. A password containing a quote, backslash, or newline produces invalid or altered settings and prevents the service from starting. Render a Python/JSON-escaped string instead.
alternative/services/README.md:80 - The PostgreSQL role leaves the default local Unix-socket rule in place and only adds password-authenticated TCP rules. Running
pg_dump -U ascender ascenderas the operator therefore uses peer authentication and normally fails because the OS user is notascender. Document a TCP dump with the stored password or run it as thepostgresOS user.
alternative/docker-compose/docker-compose.yml:18
- This new Compose infrastructure has no
ownerorrepometadata labels. Add the required ownership and repository tags/labels to the managed services (and equivalent metadata to the other alternative-install manifests) so deployed resources can be attributed.
name: ascender
alternative/docker-compose/scripts/bootstrap.sh:26
- This fallback runs for every nonzero
createsuperuserexit, not only the duplicate-user case, while stderr is discarded. Invalid input, a database failure, or another bootstrap error is therefore reported as "user exists" and can be hidden behind a misleadingupdate_passwordfailure. Preserve the original error and only callupdate_passwordwhen the duplicate condition is explicitly detected.
else
# Already exists: make the password match .env, like the operator does.
awx-manage update_password --username "${ASCENDER_ADMIN_USER}" --password "${ASCENDER_ADMIN_PASSWORD}" >/dev/null
alternative/docker-compose/setup.sh:14
- The README's quick-start sequence runs this script with
sudoand then invokesdocker composeas the normal user. When.envis first created by sudo, this mode makes it root-only, so Compose cannot read the generated credentials and the next command fails with missing variables. Preserve the invoking user's ownership/read access (or run the Compose command as root) while still protecting the file.
chmod 0600 .env
alternative/podman/install.sh:71
- On a re-run after an operator replaces either certificate file, this conditional skips the permission repair. A typical root-owned 0600 private key then remains unreadable to the web container (uid 1000/gid 0), so nginx cannot start even though the README says replacing the certs and restarting is supported. Keep the ownership/mode repair outside the generation guard.
if [[ ! -f "${CONFDIR}/certs/ascender.crt" || ! -f "${CONFDIR}/certs/ascender.key" ]]; then
openssl req -x509 -nodes -newkey rsa:2048 -days 825 \
-keyout "${CONFDIR}/certs/ascender.key" -out "${CONFDIR}/certs/ascender.crt" \
-subj "/CN=${hostname_value}" \
-addext "subjectAltName=DNS:${hostname_value},DNS:localhost,IP:127.0.0.1" 2>/dev/null
# nginx runs as uid 1000, gid 0 inside the container: give the group (0)
# read access and keep the private key out of reach of other local users.
chmod 0644 "${CONFDIR}/certs/ascender.crt"
chown root:0 "${CONFDIR}/certs/ascender.key"
chmod 0640 "${CONFDIR}/certs/ascender.key"
alternative/podman/scripts/bootstrap.sh:26
- This fallback runs for every nonzero
createsuperuserexit, not only the duplicate-user case, while stderr is discarded. Invalid input, a database failure, or another bootstrap error is therefore reported as "user exists" and can be hidden behind a misleadingupdate_passwordfailure. Preserve the original error and only callupdate_passwordwhen the duplicate condition is explicitly detected.
alternative/services/README.md:87 - The documented
supervisorctlcommand will not resolve on a fresh install:supervisoris installed only inside{{ ascender_venv }}and is not symlinked into/usr/bin, whilesudotypically resets PATH. Invoke the virtualenv'ssupervisorctl(or set PATH) so this operations command works.
alternative/services/roles/ascender_config/templates/receptor.conf.j2:12 - This enables a listener on the direct-services host's network interfaces, but the default configuration has no TLS or peer authentication. Because firewalld is optional, port 27199 can be reachable by untrusted hosts and accept unauthenticated receptor peers. Bind it locally or make the listener opt-in only after TLS/authentication is configured.
alternative/services/roles/ascender_receptor/tasks/main.yml:6 - The receptor executable is downloaded and installed as root without any integrity check. The pinned GitHub release publishes checksums, so a replaced or corrupted asset would be trusted and run by the system service; verify the version/architecture asset against its release checksum before unpacking it.
alternative/services/roles/ascender_system/tasks/main.yml:41 - This installation path explicitly installs
npmand the README advertises npm as a build prerequisite, but the repository's Node tooling requirement is to use pnpm. Replace the npm dependency/build path with pnpm (including whatever the Ascender UI build invokes) so this alternative does not introduce npm.
- Files reviewed: 66/67 changed files
- Comments generated: 2
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
One or more issues must be addressed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (26)
Previously missed (4) — in code that hasn't changed since the last review.
alternative/docker-compose/docker-compose.yml:98
- The
rsyslog_spoolnamed volume is mounted into the rsyslog container later, but it is not mounted intoinit, andinit.shnever prepares/var/lib/awx/rsyslog. On a fresh install this leaves the volume root-owned while rsyslog runs as UID 1000, so its spool/config writes can fail. Mount this volume in the init service and add the path to the ownership loop (and mirror the fix in the Podman installer).
alternative/podman/manage-containers.sh:262 VOL_RSYSLOG_SPOOLis mounted intoascender-rsyslog, butrun_initdoes not mount that volume andinit.shdoes not prepare/var/lib/awx/rsyslog. A new install therefore leaves this volume root-owned while rsyslog runs as UID 1000, which can prevent spool/config writes. Include the volume in init and initialize its ownership.
alternative/services/roles/ascender_config/tasks/main.yml:128createschecks only the certificate. If an install is interrupted after the certificate is written but before the key is created (or the key is later removed), this task is skipped and nginx is rendered with a missing key, so the installation cannot start. Check that both files exist before skipping generation, or regenerate when either is absent.
alternative/services/roles/ascender_config/templates/postgres.py.j2:8- The password is inserted into a single-quoted Python literal without escaping. A user-supplied
ascender_pg_passwordcontaining an apostrophe, backslash, or newline produces invalid settings and prevents every management/service command from starting. Serialize the value as a Python/JSON string rather than interpolating it into quotes.
alternative/docker-compose/config/nginx-locations.conf:29
$server_portis the container's listener (8052/8053), not the publishedASCENDER_HTTP(S)_PORTon the host. Becausesettings.pyenablesUSE_X_FORWARDED_PORT, Django receives 8052/8053 and can generate absolute URLs or redirects to an unreachable internal port, especially when the host uses a non-default port. Render the external port into this config or pass a correctly trusted forwarded-port value.
uwsgi_param HTTP_X_FORWARDED_PORT $server_port;
alternative/docker-compose/docker-compose.yml:75
- These Docker bind mounts omit the
:z/:ZSELinux relabel option. On the Rocky/EL9 and Fedora hosts described by this install, an enforcing SELinux policy can leave the unprivileged containers unable to read the mounted configs, scripts, and certificates, causing startup to fail. Add the appropriate shared relabel suffix to every host bind mount in this Compose file.
- ./config/valkey.conf:/usr/local/etc/valkey/valkey.conf:ro
alternative/docker-compose/receptor/containers.conf:6
userns="host"makes the rootless job containers use the sidecar's user namespace, so container root is the unprivilegedawxUID rather than root in a subordinate namespace. Execution environments normally run Ansible as root; with this default, jobs that need root-owned paths or package installation will run without the expected privileges. Let rootless Podman create its normal user namespace instead; sharing UID 1000 files does not require this override.
userns="host"
alternative/docker-compose/scripts/bootstrap.sh:26
- The admin password is passed in
--password, so despite the surrounding redirection and the generated env file, it remains visible in the container process command line whileawx-manageruns. Use a supported stdin, file, or environment-based secret input instead of placing it in argv.
awx-manage update_password --username "${ASCENDER_ADMIN_USER}" --password "${ASCENDER_ADMIN_PASSWORD}" >/dev/null
alternative/docker-compose/scripts/bootstrap.sh:10
- The migration container is configured to run as
1000:0, so this fallback cannot append to root-owned/etc/passwdwhen the UID has no entry. Sinceset -eis enabled only after the block, the failure is silently ignored and the bootstrap may then fail when Python resolves the current user. Establish the passwd entry in the image or from a root init step before running this script.
if [ "$(id -u)" -ge 500 ]; then
echo "awx:x:$(id -u):$(id -g):,,,:/var/lib/awx:/bin/bash" >> /etc/passwd
fi
alternative/docker-compose/scripts/launch_task.sh:9
- This container is configured to run as
1000:0, so when that UID is absent from/etc/passwdthe redirection is attempted by an unprivileged process and fails. Becauseset -eis enabled only afterward, the failure is ignored and the script continues without the account entry that Python/user lookups may require. Create the entry in the image or from a root init step before dropping privileges instead of appending it here.
if [ "$(id -u)" -ge 500 ]; then
echo "awx:x:$(id -u):$(id -g):,,,:/var/lib/awx:/bin/bash" >> /etc/passwd
fi
alternative/docker-compose/setup.sh:61
- This rewrites
config/nginx-http-redirect.conf, which is a tracked file in the install directory, just to encode the host port. Running setup from a checkout leaves a persistent Git modification and can carry a stale generated port into another install. Generate a runtime config or interpolate the port without mutating the repository file.
sed -i -E "s#^(return 301 https://\\\$host)(:[0-9]+)?(\\\$request_uri;)#\\1${https_suffix}\\3#" \
config/nginx-http-redirect.conf
alternative/podman/config/nginx-locations.conf:29
$server_portis the container's listener (8052/8053), not the publishedASCENDER_HTTP(S)_PORTon the host. Becausesettings.pyenablesUSE_X_FORWARDED_PORT, Django receives 8052/8053 and can generate absolute URLs or redirects to an unreachable internal port, especially when the host uses a non-default port. Render the external port into this config or pass a correctly trusted forwarded-port value.
uwsgi_param HTTP_X_FORWARDED_PORT $server_port;
alternative/podman/install.sh:66
- This guard also controls the certificate permission fix below. If an operator replaces an existing certificate/key pair as documented, neither file is missing and the key can remain root-owned
0600, which uid 1000/gid 0 in the web container cannot read. Keep generation conditional, but apply the ownership and mode normalization on every install.
alternative/podman/manage-containers.sh:475 - The existing containers are removed before image pulls and the receptor build run. If a registry outage or build failure occurs during an upgrade, the EXIT trap only stops the partially created stack; it cannot restore the old containers, leaving the service down. Pull/build the new images before tearing down the currently running stack, or retain a rollback path.
alternative/podman/scripts/bootstrap.sh:26 - The admin password is passed in
--password, so despite the generated env file it remains visible in the container process command line whileawx-manageruns. Use a supported stdin, file, or environment-based secret input instead of placing it in argv.
alternative/podman/scripts/bootstrap.sh:10 - The migration container is configured to run as
1000:0, so this fallback cannot append to root-owned/etc/passwdwhen the UID has no entry. Sinceset -eis enabled only after the block, the failure is silently ignored and the bootstrap may then fail when Python resolves the current user. Establish the passwd entry in the image or from a root init step before running this script.
alternative/podman/scripts/launch_task.sh:9 - This container is configured to run as
1000:0, so when that UID is absent from/etc/passwdthe redirection is attempted by an unprivileged process and fails. Becauseset -eis enabled only afterward, the failure is ignored and the script continues without the account entry that Python/user lookups may require. Create the entry in the image or from a root init step before dropping privileges instead of appending it here.
alternative/services/README.md:80 - The PostgreSQL role leaves the default local
peerrule inpg_hba.confand adds password authentication only for TCP. Running this documented command as the usual root shell therefore attempts local peer authentication asroot, not database userascender, and fails before producing a backup. Use a command that runs as the postgres OS user or explicitly supplies the TCP/password authentication path.
alternative/services/README.md:87 - The
supervisorpackage is installed only inside{{ ascender_venv }}by the build role, and nosupervisorctlexecutable is linked into a system PATH. Therefore this documented command returnscommand not foundon a normal host; invoke the venv'ssupervisorctl(or install/link it system-wide).
alternative/services/README.md:47 - The documented
./setup.sh --checkdoes not work on a clean host: check mode does not create the generated secret files, butascender_configimmediatelyslurps them and later roles also assume the checked-out source/configuration exists. Either make the playbook check-mode safe by guarding those reads and dependent tasks, or remove this claim and document that check mode is only useful after an installation.
alternative/services/roles/ascender_config/templates/channels.py.j2:2 - The websocket secret is inserted into a single-quoted Python literal without escaping. Although generated values are simple, an explicitly configured secret containing Python string characters can make
channels.pyinvalid and stop the application. Render it as a serialized string instead of embedding it inside quotes.
alternative/services/roles/ascender_config/templates/receptor.conf.j2:12 - This enables a plain TCP listener on the host's default bind address, while the playbook only opens 80/443 in firewalld. If firewalld is disabled or another network path permits access, port 27199 is exposed despite the comment and has no TLS or peer restriction. Keep the listener disabled/loopback-bound by default and require an explicit TLS configuration before enabling remote peering.
alternative/services/roles/ascender_receptor/tasks/main.yml:6 - This downloads and installs an executable release artifact without a checksum. A compromised or replaced GitHub asset would be accepted silently and then run as the Ascender service user. Pin a release checksum (or verify a signed checksum manifest) before unpacking the binary.
alternative/services/roles/ascender_services/tasks/main.yml:33 no_loghides this task from Ansible output, but the effective admin password is still placed in--passwordand is observable in the process command line whileawx-manageruns. Use a supported stdin, file, or environment-based secret input instead of passing the password in argv.
alternative/services/roles/ascender_system/tasks/main.yml:41- The UI build is Node-based, but this installs
npm; the repository toolchain requires pnpm instead. Install/configure pnpm and ensure themakebuild path invokes it so the direct-services installation uses the supported package manager.
alternative/services/roles/ascender_system/tasks/subids.yml:53 ascender_subid_startis written verbatim, while it is only coerced with| intfor overlap calculations;_subid_counthas the same gap. Values such asascender_subid_start=abcorascender_subid_count=0can pass the overlap assertion and produce an invalid/etc/subuid//etc/subgidentry, with rootless Podman failing later. Validate that both values are positive decimal ranges before selecting and writing them.
- Files reviewed: 67/68 changed files
- Comments generated: 7
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical startup failures and configuration issues must be addressed.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (17)
alternative/docker-compose/config/nginx-locations.conf:29
$server_portis the port nginx listens on inside the container (8052/8053), not the published host port. WithUSE_X_FORWARDED_PORT = Trueand the documented 80/443 or custom mappings, Django will therefore see the wrong port when building absolute URLs and redirects. Pass the configured external port instead of the internal$server_port.
uwsgi_param HTTP_X_FORWARDED_PORT $server_port;
alternative/docker-compose/receptor/Dockerfile:30
- These edits assume uncommented storage.conf keys, but the EL9 defaults use
# mount_program,# mountopt, and a commented# additionalimagestores = [array. The first substitution misses the space, the mountopt substitution misses the comment, and the next expression appends an uncommented array element without uncommenting the array; the resulting config either lacks the required overlay settings or is invalid TOML, so the validation below aborts the receptor image build. Rewrite the relevant[storage.options]/[storage.options.overlay]entries explicitly for the supported base image.
RUN sed -i -e 's|^#mount_program|mount_program|g' \
-e '/additionalimage.*/a "/var/lib/shared",' \
-e 's|^mountopt[[:space:]]*=.*$|mountopt = "nodev,fsync=0"|g' \
alternative/docker-compose/scripts/bootstrap.sh:26
createsuperusercan fail for reasons other than an existing username, but this branch suppresses stderr and treats every nonzero exit as “already exists.” A bad email, database failure, or other bootstrap error is then hidden and reported as an unrelated password-sync failure. Only performupdate_passwordfor the specific already-exists result; otherwise preserve the original failure.
if DJANGO_SUPERUSER_PASSWORD="${ASCENDER_ADMIN_PASSWORD}" \
awx-manage createsuperuser --noinput --username "${ASCENDER_ADMIN_USER}" --email "${ASCENDER_ADMIN_EMAIL}" 2>/dev/null; then
echo "bootstrap: created admin user"
else
# Already exists: make the password match .env, like the operator does.
awx-manage update_password --username "${ASCENDER_ADMIN_USER}" --password "${ASCENDER_ADMIN_PASSWORD}" >/dev/null
alternative/docker-compose/setup.sh:61
- The services path rewrites the redirect file only when
setup.shruns. If a user later changes the documentedASCENDER_HTTPS_PORTin.envand runsdocker compose up, the port mapping changes but this mounted config still redirects to the old port. Generate the redirect from the current settings on each start, or make the configuration workflow require rerunning this setup step.
sed -i -E "s#^(return 301 https://\\\$host)(:[0-9]+)?(\\\$request_uri;)#\\1${https_suffix}\\3#" \
config/nginx-http-redirect.conf
alternative/podman/config/nginx-locations.conf:29
- This has the same port-forwarding problem as the Compose configuration:
$server_portis 8052/8053 inside the container, whileUSE_X_FORWARDED_PORTneeds the externally published port. The default HTTPS mapping alone is enough to make Django receive 8053 instead of 443, and custom mappings are also documented. Pass the host port into nginx rather than forwarding this internal value.
uwsgi_param HTTP_X_FORWARDED_PORT $server_port;
alternative/podman/install.sh:66
- Certificate permissions are fixed only inside the self-signed-generation branch. The Podman README tells operators to replace these files with their own certificate and restart the service; a normal root-owned
0600private key then remains unreadable to nginx running as uid 1000/gid 0, causing the web container to fail. Apply the ownership and mode normalization on every install, not only when generating a new certificate.
alternative/podman/receptor/Dockerfile:30 - These edits assume uncommented storage.conf keys, but the EL9 defaults use
# mount_program,# mountopt, and a commented# additionalimagestores = [array. The first substitution misses the space, the mountopt substitution misses the comment, and the next expression appends an uncommented array element without uncommenting the array; the resulting config either lacks the required overlay settings or is invalid TOML, so the validation below aborts the receptor image build. Rewrite the relevant[storage.options]/[storage.options.overlay]entries explicitly for the supported base image.
alternative/podman/scripts/bootstrap.sh:26 - This Podman bootstrap duplicates the Compose error-handling bug: every
createsuperuserfailure is assumed to mean that the user already exists, and stderr is discarded. Configuration or database failures can therefore be masked as a misleadingupdate_passworderror. Restrict the fallback to the specific already-exists case and propagate other failures.
alternative/services/.gitignore:1 setup.shexplicitly acceptsinventory.yml, but onlyinventoryis ignored. Users following that supported path can put host variables or passwords ininventory.ymland accidentally commit them. Ignore both inventory filenames (or use a matching pattern).
alternative/services/README.md:84- The documented upgrade backup command uses the local Unix socket, but the role leaves local connections on peer authentication and only adds password authentication for 127.0.0.1. A normal operator therefore cannot run this command as OS user
ascender; use the postgres OS account or specify the TCP host and password.
alternative/services/roles/ascender_build/tasks/main.yml:32 - The setup wrapper explicitly advertises
./setup.sh --check, but in Ansible check mode thecommandtask that populates_commitis skipped, so_commit.stdoutis absent here. This set-fact therefore fails while evaluating the build decision (and the services role has similar assumptions about skipped command results), making the documented check mode unusable. Guard skipped results or implement check-mode handling before advertising this option.
alternative/services/roles/ascender_config/templates/receptor.conf.j2:12 - This creates a TCP Receptor listener without TLS/authentication or an explicit bind address. The system role only opens HTTP/HTTPS when an already-running firewalld is present and does not install one, so a host without that firewall can expose port 27199 to the network and accept unauthenticated peer traffic. Bind the listener to loopback by default, or require an explicitly configured authenticated TLS listener and firewall rule before enabling remote peering.
alternative/services/roles/ascender_receptor/tasks/main.yml:37 loginctl enable-lingeronly arranges for a user manager/runtime directory at boot; it does not guarantee/run/user/{{ ascender_uid }}exists during this play or order the later system service behind it. The next role invokespodman system migratewith thatXDG_RUNTIME_DIR, andascender.serviceexports the same path, so a fresh install or boot can fail rootless Podman with a missing runtime directory. Create/manage the runtime directory and add the required ordering before starting Podman.
alternative/services/roles/ascender_receptor/tasks/main.yml:5- This downloads an executable archive that is then installed as
/usr/local/bin/receptorwithout verifying its contents. A replaced release asset or compromised download path would be accepted and executed as the Ascender service user. Pin and verify the release's published SHA-256 checksum for each supported version/architecture before unarchiving.
alternative/services/roles/ascender_system/tasks/main.yml:85 - The README advertises
./setup.sh --check, but on a fresh host theusertask is only simulated in check mode and_awx_user.uidis not guaranteed to exist. Thisset_factthen fails while evaluating the missing attribute, so the advertised dry run cannot complete. Handle check mode for this fact and the later build/command tasks, or remove the--checkclaim.
alternative/services/roles/ascender_system/tasks/main.yml:41 - This direct install explicitly provisions
npmfor the UI build. Node-based code and workflows in this project are required to use pnpm; provision a pinned pnpm toolchain and ensure the source build invokes pnpm instead of npm.
alternative/services/setup.sh:79 - The documented inventory runs the playbook as
root, but this bootstrap path invokessudounconditionally when Ansible is missing. On a minimal Rocky host wheresudois not installed, running./setup.shas root cannot installansible-coreand fails before the playbook can install its prerequisites. Use an empty sudo prefix for root and invokesudoonly for non-root callers.
- Files reviewed: 67/68 changed files
- Comments generated: 5
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate installation, runtime, and security issues remain.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (12)
Previously missed (3) — in code that hasn't changed since the last review.
alternative/podman/install.sh:83
- Re-running
install.shoverwrites the manager/configuration files and then callssystemctl enable --nowon a unit that is usually already active.--nowdoes not re-execute an active oneshot service, so the running containers keep the old image/config despite the README promising that reruns upgrade them. Stop/start or restart the unit after installing the files.
alternative/services/roles/ascender_system/tasks/subids.yml:51 - When both files already contain an
awxentry, this logic always prefers/etc/subuidand later writes that range into/etc/subgid. If the existing UID and GID mappings differ, the play silently changes the user's GID range and can orphan rootless Podman storage, contradicting the claim that an existing range is kept. Detect inconsistent existing mappings and fail with repair instructions instead of choosing the UID entry.
alternative/services/setup.sh:44 - This preflight only checks whether the collection plugin names are present, not whether their versions satisfy
requirements.yml. A host with an oldercommunity.postgresqlorcommunity.generalcollection therefore skips installation and can run incompatible modules despite the declared minimums. Validate installed versions or always install/upgrade from the requirements file.
alternative/docker-compose/scripts/bootstrap.sh:15
- The database readiness loop has no deadline. A wrong external host, blocked port, or permanently failed PostgreSQL leaves the migration container retrying forever; Compose reports the container as started while the dependent services never become available. Bound the wait and fail with a diagnostic after a defined timeout.
echo "bootstrap: waiting for PostgreSQL at ${DATABASE_HOST}:${DATABASE_PORT}"
until pg_isready -q -h "${DATABASE_HOST}" -p "${DATABASE_PORT}" -U "${DATABASE_USER}"; do
sleep 2
done
alternative/docker-compose/scripts/bootstrap.sh:25
- Any
createsuperuserfailure is treated as proof that the user already exists because stderr is discarded and theelsebranch unconditionally runsupdate_password. For failures such as invalid input or a database/application error, this can hide the real cause and can complete without creating the requested admin user; only fall back for the known already-exists result and propagate all other errors.
if DJANGO_SUPERUSER_PASSWORD="${ASCENDER_ADMIN_PASSWORD}" \
awx-manage createsuperuser --noinput --username "${ASCENDER_ADMIN_USER}" --email "${ASCENDER_ADMIN_EMAIL}" 2>/dev/null; then
echo "bootstrap: created admin user"
else
# Already exists: make the password match .env, like the operator does.
alternative/podman/install.sh:62
- The certificate permission fix is inside the certificate-generation branch. If an operator follows the README and replaces an existing key, rerunning
install.shleaves a typical root:root 0600 key unreadable to nginx's uid 1000/gid 0, so the service cannot restart. Apply the ownership and mode normalization on every install, not only when generating a certificate.
alternative/podman/scripts/bootstrap.sh:15 - The database readiness loop has no deadline. A wrong external host, blocked port, or permanently failed PostgreSQL leaves
systemctl start ascenderblocked until the unit's one-hour start timeout, instead of failing promptly with a diagnostic. Bound the wait and report the database endpoint after a defined timeout.
alternative/podman/scripts/bootstrap.sh:25 - Any
createsuperuserfailure is treated as proof that the user already exists because stderr is discarded and theelsebranch unconditionally runsupdate_password. For failures such as invalid input or a database/application error, this can hide the real cause and can complete without creating the requested admin user; only fall back for the known already-exists result and propagate all other errors.
alternative/services/requirements.yml:8 - These lower bounds allow
ansible-galaxyto install an untested future collection release, which can require a newer ansible-core or change module behavior and make this installer stop working on the documented Ansible 2.15+ target. The main repository pins the corresponding collections exactly inplaybooks/group_vars/all.yml:343-375; pin the versions tested by this alternative as well.
alternative/services/roles/ascender_config/templates/receptor.conf.j2:12 - This enables a cleartext Receptor TCP listener on the host, while firewalld is optional and a missing or inactive firewall is deliberately tolerated. On an EL host without an active firewall, port 27199 is therefore reachable despite the comment, without TLS protection. Bind the listener locally by default, or require an explicit TLS/firewall configuration before enabling it.
alternative/services/roles/ascender_receptor/tasks/main.yml:5 - This downloads and installs an executable without verifying a checksum or release signature. A compromised release asset or delivery path could therefore replace
receptorand gain code execution as the service user (and access to the host's job runtime). Pin a version-specific checksum or verify the release signature before unpacking.
alternative/services/roles/ascender_system/tasks/main.yml:41 - The direct-service build installs and documents
npmfor the Node-based UI build, but the repository's coding guideline requires pnpm for Node code and workflows. Provision/enable pnpm and make the build invoke it instead of adding npm as the required package.
- Files reviewed: 67/68 changed files
- Comments generated: 8
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Critical installation and credential-handling issues remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (11)
Previously missed (3) — in code that hasn't changed since the last review.
alternative/docker-compose/scripts/bootstrap.sh:28
- Any nonzero
createsuperuserexit is treated as “already exists” because stderr is discarded and theelsebranch is unconditional. Invalid admin input or another database error will therefore be misreported and can be masked by a subsequent password-update attempt; only take that branch when the error explicitly indicates an existing username, and surface other failures.
alternative/podman/install.sh:74 - Certificate permissions are normalized only inside the branch that generates a new certificate. The README tells operators to replace the certificate/key and run
systemctl restart ascender; a replacement key with the usual root:root 0600 permissions then remains unreadable by nginx uid 1000/gid 0, so the service fails. Apply these chmod/chown operations on every install, not only when generating the files.
alternative/podman/scripts/bootstrap.sh:28 - Any nonzero
createsuperuserexit is treated as “already exists” because stderr is discarded and theelsebranch is unconditional. Invalid admin input or another database error will therefore be misreported and can be masked by a subsequent password-update attempt; only take that branch when the error explicitly indicates an existing username, and surface other failures.
alternative/docker-compose/docker-compose.yml:143
- These host bind mounts have no
:zrelabel option. On the Rocky/EL9 SELinux-enforcing hosts this installation targets, the containers can be denied access to the repository files (including the certificate), so the stack may not start. Add a shared SELinux relabel (:z) to every./...bind mount in this Compose file, or explicitly document the required manual relabeling.
- ./config/settings.py:/etc/tower/settings.py:ro
alternative/services/.gitignore:1
setup.shexplicitly prefersinventory.ymlwhen it exists, but this ignore file only excludesinventory. A YAML inventory can contain the same admin/database secrets as the ignored file and would then be easy to commit accidentally; ignore both local inventory filenames.
alternative/services/install.yml:8- This new Ansible infrastructure entry point does not declare the required
ownerandrepotags/metadata. Add the repository's owner/repository attribution to the managed infrastructure (or the play/resource metadata) so deployments remain attributable.
alternative/services/roles/ascender_config/templates/receptor.conf.j2:12 - This starts an unauthenticated Receptor TCP listener on the host's interfaces by default. The firewalld role is optional and tolerates a missing or inactive firewall, so on a host without an active firewall port 27199 becomes remotely reachable even though the comment only warns not to open it; gate the listener behind explicit TLS/mesh configuration or bind it to loopback until it is secured.
alternative/services/roles/ascender_postgresql/tasks/main.yml:69 - The database password is returned by
slurpand copied into a fact withoutno_log; verbose Ansible callbacks can expose both the base64 file content and the effective secret. Mark these secret-handling tasksno_log: true, as the database role task below already does.
alternative/services/roles/ascender_system/tasks/main.yml:8 - The README advertises RHEL 9, but
crbis the Rocky/Alma repository ID; a registered RHEL 9 host exposescodeready-builder-for-rhel-9-<arch>-rpmsinstead. This command exits before package installation on RHEL, so either select the platform-specific repository ID or remove RHEL from the documented support.
alternative/services/roles/ascender_system/tasks/main.yml:41 - This direct-services path installs npm and documents it as the UI build prerequisite, but the repository's Node guidance requires pnpm for Node-based builds. Install/use pnpm instead and update the prerequisite/build invocation so this installation does not introduce a second package-manager path.
alternative/services/setup.sh:21 - This branch installs Ansible on Debian/Ubuntu, but every role in this playbook uses EL-specific
dnfpackages and module commands, so the run is guaranteed to fail after making a partial system change. Reject Debian/Ubuntu here (or provide a separate implementation) instead of advertising them as installable.
- Files reviewed: 67/68 changed files
- Comments generated: 4
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Critical and moderate findings remain in the installation paths, including security and deployment-runtime issues.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (14)
alternative/docker-compose/config/nginx-locations.conf:30
- Because this location uses
uwsgi_pass,proxy_set_headeris not applied to the uWSGI request. Without forwardingHTTP_X_FORWARDED_PROTO, Django can treat HTTPS requests as HTTP, which breaks secure URL/redirect and CSRF behavior behind this TLS terminator. Forward the scheme withuwsgi_param.
uwsgi_param HTTP_X_FORWARDED_PORT $server_port;
proxy_set_header X-Forwarded-Port $server_port;
alternative/docker-compose/docker-compose.yml:151
- The web container handles the external-logging API, whose upstream
SettingLoggingTestreads/var/lib/awx/rsyslog/rsyslog.conf, while the rsyslog container's configurer writes that file on thersyslog_spoolvolume. Without this volume here, the UI validates an image-local/stale configuration instead of the active one. Mountrsyslog_spoolintowebas well.
- rsyslog_socket:/var/run/awx-rsyslog
alternative/docker-compose/docker-compose.yml:99
rsyslog_spoolis mounted by thersyslogservice below, but not by this root init container, so a fresh spool is never prepared. The image's starterrsyslog.confis root-owned whilelaunch_awx_rsyslog.shoverwrites it as UID 1000; the rsyslog container can therefore fail on its first start. Mount this volume ininitand normalize both the directory and starter-file permissions.
- podman_storage:/var/lib/awx/.local/share/containers
alternative/podman/config/nginx-locations.conf:30
- Because this location uses
uwsgi_pass,proxy_set_headeris not applied to the uWSGI request. Without forwardingHTTP_X_FORWARDED_PROTO, Django can treat HTTPS requests as HTTP, which breaks secure URL/redirect and CSRF behavior behind this TLS terminator. Forward the scheme withuwsgi_param.
uwsgi_param HTTP_X_FORWARDED_PORT $server_port;
proxy_set_header X-Forwarded-Port $server_port;
alternative/podman/manage-containers.sh:396
- The web container handles the external-logging API, whose upstream
SettingLoggingTestreads/var/lib/awx/rsyslog/rsyslog.conf, while the rsyslog container's configurer writes that file onVOL_RSYSLOG_SPOOL. Without this mount, the UI validates an image-local/stale configuration instead of the active one. MountVOL_RSYSLOG_SPOOLintoascender-webtoo.
alternative/podman/manage-containers.sh:267 VOL_RSYSLOG_SPOOLis mounted byrun_rsyslog, but not by this root init container, so a fresh spool is never prepared. The image's starterrsyslog.confis root-owned whilelaunch_awx_rsyslog.shoverwrites it as UID 1000; the rsyslog container can therefore fail on its first start. Mount this volume inrun_initand normalize both the directory and starter-file permissions.
alternative/services/README.md:84- This backup command uses the local Unix socket and requests the
ascenderrole. When run as the root/operator user, the default peer authentication compares the OS user and fails instead of using the database password configured by this play. Run the dump as thepostgresOS user or connect to127.0.0.1with the generated password.
alternative/services/roles/ascender_config/tasks/main.yml:128 - The
createsguard checks only the certificate, but the next task requires both the certificate and key. If a partial or custom install leavesascender.crtwithoutascender.key, generation is skipped and the play fails atCertificate permissions, so rerunning cannot repair that state. Gate generation on both files with an explicit existence check.
alternative/services/roles/ascender_config/templates/nginx-locations.conf.j2:27 - Forward the scheme as a uWSGI parameter as well. This TLS-terminating nginx config currently only sets the forwarded port, so Django can treat HTTPS requests as HTTP, breaking secure URL/redirect and CSRF behavior. Add
uwsgi_param HTTP_X_FORWARDED_PROTO $scheme;.
alternative/services/roles/ascender_config/templates/receptor.conf.j2:13 - This listener has neither a bind address nor TLS. Receptor defaults
bindaddrto0.0.0.0, so the direct-services install exposes a plaintext peer endpoint on every interface, while the firewalld tasks only manage ports 80/443. Bind it to loopback by default, or make a TLS-configured listener and explicit firewall/peer setup mandatory before exposing it.
alternative/services/roles/ascender_receptor/tasks/main.yml:6 - The receptor archive is downloaded over HTTPS but never checked against the release checksum. A compromised or replaced artifact would therefore be unpacked and installed as the service's executable; the v1.6.8 release publishes
checksums.txt, so fetch and verify the expected per-architecture SHA-256 (or pin a trusted digest) before unarchiving.
alternative/services/roles/ascender_receptor/tasks/main.yml:6 - The playbook writes the archive under
/usr/local/srcwithout creating that directory. A minimal Rocky 9 host can lack/usr/local/src, so the firstget_urltask fails before Receptor is installed; create the download directory first.
alternative/services/roles/ascender_system/tasks/main.yml:41 - This direct-services path installs npm, but the Node build toolchain for this project is required to use pnpm. Since the build role delegates the UI build to the source Makefile, this package choice makes the installer select the wrong package manager; install pnpm and pass the supported package-manager override through the build instead.
alternative/services/roles/ascender_system/tasks/subids.yml:68 - When
awxalready has entries in both subordinate-id files but the ranges differ, this selection prefers the/etc/subuidrange and the laterlineinfiletask silently rewrites/etc/subgidto match it. That changes an existing user-namespace mapping and can invalidate the user's rootless Podman storage, contrary to the comment that existing ranges are kept; detect and reject mismatched existing ranges (or otherwise handle the migration explicitly) before writing either file.
- Files reviewed: 67/68 changed files
- Comments generated: 5
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate and critical findings affect security and installation reliability.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (13)
Previously missed (2) — in code that hasn't changed since the last review.
alternative/docker-compose/docker-compose.yml:76
- These host bind mounts are not given an SELinux relabel option. On an enforcing EL host, Docker leaves the files with their host labels and the containers can be denied access to the mounted settings, scripts, certificates, and Valkey config, so the documented setup fails. Add
:z(or an equivalent host labeling step) to every host bind mount in this Compose file, not just the named volumes.
alternative/services/setup.sh:105 - This presence-only check ignores the version constraints in
requirements.yml. If an older collection already exposes these four plugins, setup skips installation and the play can still run with an unsupported collection version. Install fromrequirements.yml(or compare the installed versions) unconditionally instead of treating plugin presence as sufficient.
alternative/docker-compose/config/settings.py:32
ALLOWED_HOSTS = ['*']disables Django's production Host-header validation. A client can therefore send an arbitrary Host header, which can make host-derived absolute links or redirects point at an attacker-controlled origin; the comment above explicitly acknowledges this risk. Build the list from the configured hostname and aliases (plus the local health-check addresses) instead of accepting every host.
ALLOWED_HOSTS = ['*']
alternative/docker-compose/docker-compose.yml:50
- This new Compose infrastructure has no
ownerorrepometadata labels on its managed services/volumes, so deployed resources cannot be attributed to an owner or repository. Add the required labels using the repository's canonical values.
services:
postgres:
image: ${POSTGRES_IMAGE:-quay.io/sclorg/postgresql-15-c9s}
alternative/podman/config/settings.py:32
ALLOWED_HOSTS = ['*']disables Django's production Host-header validation. A client can therefore send an arbitrary Host header, which can make host-derived absolute links or redirects point at an attacker-controlled origin. Build the list fromASCENDER_HOSTNAMEand configured aliases (plus the local health-check addresses) instead of accepting every host.
ALLOWED_HOSTS = ['*']
alternative/services/roles/ascender_config/tasks/main.yml:128
- The certificate generation is skipped whenever
ascender.crtexists, even ifascender.keyis missing. In that partial-file state the following permission task cannot recreate the key and nginx will fail to start; gate this task on both certificate files existing (as the Compose and Podman installers do) so a damaged generated certificate can recover.
alternative/services/roles/ascender_config/templates/receptor.conf.j2:12 - With no bind address, Receptor's TCP listener defaults to
0.0.0.0. Becauseascender_manage_firewalldpermits firewalld to be absent or disabled, a host without an effective firewall exposes unauthenticated port 27199 despite the comment saying it is not exposed; bind to loopback by default or make remote peering opt-in with TLS and an explicit firewall rule.
alternative/services/roles/ascender_config/templates/settings.py.j2:21 ALLOWED_HOSTS = ['*']disables Django's production Host-header validation. A client can therefore send an arbitrary Host header, which can make host-derived absolute links or redirects point at an attacker-controlled origin. Build the list fromascender_hostnameandascender_allowed_hosts(plus the local health-check addresses) instead of accepting every host.
alternative/services/roles/ascender_postgresql/tasks/main.yml:5- The PostgreSQL stream is enabled without first resetting any currently enabled stream. On a host that already has another EL9 PostgreSQL stream selected,
dnf module enable postgresql:15fails instead of converging, so the advertised rerun/install path breaks. Reset the module stream before enabling version 15, just as needed for the Node.js stream.
alternative/services/roles/ascender_receptor/tasks/main.yml:5 - This downloads an executable as root and installs it into
/usr/local/binwithout verifying a checksum. If the GitHub asset or the connection is compromised, the playbook will execute the altered binary as theawxservice; pin an official SHA-256 checksum for each supported receptor version and pass it toget_url.
alternative/services/roles/ascender_receptor/tasks/main.yml:37 - This runs
podman system migrateimmediately after enabling linger, but lingering only arranges for systemd to create/run/user/<uid>when the user manager starts (typically at boot). On a fresh host the configuredXDG_RUNTIME_DIRcan therefore be absent and rootless Podman fails during the first play. Create the runtime directory or start the user manager before this command.
alternative/services/roles/ascender_system/tasks/main.yml:41 - The direct-services path provisions
npm, and the subsequentmake sdistuses the source tree's defaultNPM_BIN=npmfor the UI build. This introduces an npm-based Node build into the new installation method; use pnpm for the UI build and install/configure the corresponding tool instead.
alternative/services/roles/ascender_system/tasks/main.yml:21 - Enabling a module stream without resetting the existing Node.js stream fails when the EL9 host already has another stream enabled, which is common on reruns or preconfigured hosts. Reset the stream before enabling
nodejs:22, as the source image build does, so the service installation is repeatable.
- Files reviewed: 67/68 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate findings affect installation, startup, persistence, security, and service initialization.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (14)
alternative/docker-compose/config/settings.py:13
JOBOUTPUT_ROOTis set to/var/lib/awx/job_status, but the task script creates that directory only in the task container and neither the task nor web service mounts a shared volume there. Job output/artifacts therefore remain private to the task container and disappear when it is recreated, while the web process cannot read them. Add a persistent shared job-status volume to the relevant services and initialize its ownership.
STATIC_ROOT = '/var/lib/awx/public/static'
PROJECTS_ROOT = '/var/lib/awx/projects'
JOBOUTPUT_ROOT = '/var/lib/awx/job_status'
alternative/docker-compose/config/settings.py:32
ALLOWED_HOSTS = ['*']disables Django's Host-header validation for the public HTTP/HTTPS endpoints. An attacker can send arbitrary Host headers and influence host-derived absolute URLs; the surrounding comment already acknowledges this forTOWER_URL_BASE. Build this list fromASCENDER_HOSTNAMEand_extra_hostsinstead, or require a trusted proxy to validate Host.
ALLOWED_HOSTS = ['*']
alternative/docker-compose/docker-compose.yml:99
- The
rsyslogservice mountsrsyslog_spoolat/var/lib/awx/rsyslog, but this init container does not mount that volume, so the ownership loop cannot prepare it. A fresh named volume remains root-owned while rsyslog runs as1000:0and cannot create its spool/queue files. Mountrsyslog_spoolhere and add its path toinit.sh's ownership loop.
volumes:
- ./scripts:/opt/ascender:ro
- projects:/var/lib/awx/projects
- job_private_data:/tmp
- receptor_socket:/var/run/awx-receptor
- rsyslog_socket:/var/run/awx-rsyslog
- podman_storage:/var/lib/awx/.local/share/containers
alternative/docker-compose/docker-compose.yml:49
- This Compose IaC declares the deployment without any
ownerorrepolabels on its services, so the resulting containers cannot be attributed to their owner and repository. Add those required labels through the service anchors or each service definition.
services:
postgres:
alternative/podman/config/settings.py:13
JOBOUTPUT_ROOTis set to/var/lib/awx/job_status, butrun_taskandrun_webdo not mount a shared volume at that path;launch_task.shonly creates it in the task container's writable layer. Job output/artifacts are consequently unavailable to the web process and are lost whenever the task container is recreated. Add a persistent shared job-status volume to both containers and initialize it.
STATIC_ROOT = '/var/lib/awx/public/static'
PROJECTS_ROOT = '/var/lib/awx/projects'
JOBOUTPUT_ROOT = '/var/lib/awx/job_status'
alternative/podman/config/settings.py:32
ALLOWED_HOSTS = ['*']disables Django's Host-header validation for the public HTTP/HTTPS endpoints. An attacker can send arbitrary Host headers and influence host-derived absolute URLs. Build this list fromASCENDER_HOSTNAMEand_extra_hostsinstead, or require a trusted proxy to validate Host.
ALLOWED_HOSTS = ['*']
alternative/podman/manage-containers.sh:277
run_rsysloglater mountsVOL_RSYSLOG_SPOOLat/var/lib/awx/rsyslog, butrun_initnever mounts that volume, so the ownership setup cannot make it writable. On a fresh install the volume stays root-owned while rsyslog runs as1000:0and cannot create spool/queue files. Mount this volume inrun_initand add its path to scripts/init.sh.
alternative/services/README.md:90supervisorctlis installed by the build into/var/lib/awx/venv/awx/bin, and this role only exposesawx-manageandawx-pythonin/usr/bin. The documented command therefore normally fails withcommand not found; invoke the venv binary (or install a global wrapper).
alternative/services/README.md:84- The documented backup command uses the local Unix socket, but the role leaves local
pg_hba.confauthentication aspeer; a root/operator shell therefore cannot authenticate as database userascender. Use thepostgresOS account or specify TCP/password (for example,sudo -u postgres pg_dump -d ascender).
alternative/services/roles/ascender_config/templates/receptor.conf.j2:12 - The direct-services play always starts a plaintext TCP listener on port 27199, and the optional firewalld management does not guarantee that this port is blocked when firewalld is disabled or unavailable. A host in that state exposes the Receptor endpoint without TLS; bind it to loopback by default or make the listener/TLS configuration opt-in and only expose it when secured.
alternative/services/roles/ascender_config/templates/settings.py.j2:21 ALLOWED_HOSTS = ['*']disables Django's Host-header validation for the public HTTP/HTTPS endpoints, allowing attacker-controlled Host values to reach host-derived URL generation. Use the configured hostname and aliases as the allowlist instead, rather than accepting arbitrary hosts.
alternative/services/roles/ascender_defaults/defaults/main.yml:10- The fallback maps every architecture other than
aarch64to the x86_64amd64archive. On hosts such asppc64leors390x, the download and extraction can succeed but the installed Receptor binary cannot execute, causing a late service failure; map only supported architectures and fail before downloading when there is no matching archive.
alternative/services/roles/ascender_receptor/tasks/main.yml:5 get_urldoes not create parent directories, and a minimal Rocky 9 host does not reliably contain/usr/local/src; the first run can therefore fail before the receptor tarball is downloaded. Create/usr/local/srcwithansible.builtin.filebefore this task.
alternative/services/roles/ascender_system/tasks/main.yml:41- The service install relies on the Node toolchain for the UI build but installs
npm(and the README documents npm access). This repository's Node build convention requires pnpm; install/configure pnpm and ensure the UI build uses it instead of npm.
- Files reviewed: 67/68 changed files
- Comments generated: 7
- Review effort level: Lite
This adds 3 alternative ways of installing Ascender. While utilizing K8S is the preferred and "supported" route. This is not always optimal for everyone, especially if you just want to try it out or have a small environment. So we have added 3 alternative methods. Each one has a README that explains the method a bit better.
dockeranddocker composeshould already be setup on the server. Then you can just run the shortsetup.shwhich generates the self signed cert and random passwords and then do adocker compose up -ddnf install podmanand then run theinstall.shscript, and in ~8 minutes you have a fully working Ascender install.NOTE: I have only tested these with Rocky 9. Other EL variants may work. I have definitely not tested with Ubuntu and don't expect any of them to work on there, especially the podman or services method. Docker may.