Perimeter Auth Role Usage Guide
Requirements
| Requirement | Details |
|---|---|
| Internet access | The target host must be able to reach nginx.org to download the nginx signing key and packages from the official nginx repository. |
Tags
A tag must be specified when running this role. Running without --tags or with --tags all is not supported and will result in an error.
| Tag | Includes | Description |
|---|---|---|
install |
prepare_filesystem, configure_selinux, install_nginx, configure_nginx, tune_kernel_parameters, configure_log_rotation, enable_monitoring, configure_firewall |
Runs all tasks required to install and configure Perimeter Auth. |
prepare_filesystem |
Creates required directories and sets permissions. | |
install_nginx |
Installs nginx and the njs module from the official nginx repository. Enables and starts the nginx service. | |
configure_nginx |
Deploys auth.js, nginx.conf, pauth.conf, and the conf.d/app_settings/*.conf includes from the role templates. Snapshots /etc/nginx first, validates with nginx -t before reloading, verifies nginx adopted the new configuration after, and rolls back to the snapshot on any failure. See Variables for the backup settings. |
|
tune_kernel_parameters |
Creates a systemd service override to raise the nginx open file descriptor limit to 65535 (soft and hard). Verifies the limit is applied to the running process. | |
configure_log_rotation |
Installs logrotate, configures hourly log rotation for nginx under /etc/logrotate.d/hourly/nginx, and creates /etc/cron.hourly/logrotate to trigger it. Sends SIGUSR1 to the nginx master process after each rotation to reopen log files without restarting. |
|
configure_selinux |
Configures SELinux to enforcing mode with the targeted policy. RHEL only — skipped on Ubuntu. | |
configure_firewall |
Configures firewalld zones declaratively via the firewalld role. Zones are defined by perimeter_auth_firewall_zones. See Firewall Configuration for details. |
|
enable_monitoring |
Installs nginx-prometheus-exporter as a systemd service and deploys a localhost stub_status endpoint for Prometheus scraping. Skipped entirely when perimeter_auth_enable_monitoring: false. See Variables for configuration options. |
|
https_test |
Deploys a Hello World HTTPS vhost to verify nginx and the firewall are working. Not included in install. |
|
restore |
Restores /etc/nginx from a snapshot taken by configure_nginx, validates it, and reloads. Not included in install. Restores the most recent snapshot unless perimeter_auth_restore_snapshot names a specific one. Configuration only — SELinux and firewalld state belong to their own tags and are not rolled back. |
Usage
Initial installation
Follow these steps in order for a fresh host.
1. Set the variables
The Epic topology this proxy fronts is environment-specific, so it belongs in the environment-scoped group — group_vars/<env>_<cloud>_app_nginx/vars.yml (e.g. group_vars/ire_copier_azure_app_nginx/), which every nginx host in that environment shares. Only values that hold for nginx hosts everywhere go in group_vars/app_nginx/, and genuinely per-host values (a differing certificate SAN list) go in host_vars/<inventory-name>.yml. See Application & Environment Groups for the tier split and Variables for required and optional values.
2. Create SSL directories
The prepare_filesystem task creates the directories that the TLS certificate will be written into. Run this before issuing the certificate.
ansible-playbook --limit=app_nginx playbooks/deploy-perimeter-auth.yml --tags prepare_filesystem --become
3. Install the TLS certificate
A public-facing proxy needs a publicly-trusted certificate
Any environment serving external clients must present a public certificate on the NGINX host, not one from the internal CA. The load balancer in front of the proxy passes SignalR through without terminating TLS, so browsers and the Epic mobile apps validate the certificate NGINX itself presents — an internally-issued certificate fails on every external client.
Obtain that certificate from a public CA with every public hostname the proxy answers on as a SAN (the server_names across all of perimeter_auth_pauth_app_servers, ..._signalr_servers, and ..._mobile_servers), install the certificate and key on the host, and point perimeter_auth_ssl_certificate / perimeter_auth_ssl_certificate_key at them. Skip the internal-CA step below. group_vars/ire_copier_azure_app_nginx/ is a worked example.
For an internal-only proxy, run the certificate-authority-issue playbook to generate the key, sign the certificate, and write both to the host. The paths passed here must match perimeter_auth_ssl_certificate_key and perimeter_auth_ssl_certificate (defaults shown below).
ansible-playbook playbooks/certificate-authority-issue.yml --limit=app_nginx -e @extra_vars/certificate_authority.yml -e certificate_authority_linux_key_path=/etc/ssl/private/nginx-epic.key -e certificate_authority_linux_cert_path=/etc/ssl/certs/nginx-epic.crt --tags generate_key_linux,generate_csr_linux,sign_certificate,write_certificate_linux --become
4. Run the full install
5. Install the node exporter for monitoring
This installs node_exporter with server-side TLS only (encrypted, no client certificate required) — the default linux_exporter_tls_server_config. For plain HTTP or full mTLS instead, and the full variable/tag reference, see the Linux Exporter Usage Guide.
#create user and prepare filesystem
ansible-playbook --limit=app_nginx playbooks/deploy-node_exporter.yml --tags create_user,prepare_filesystem --become
#issue the certificate
ansible-playbook playbooks/certificate-authority-issue.yml \
--limit=app_nginx \
-e @extra_vars/certificate_authority.yml \
-e certificate_authority_linux_key_path=/etc/node_exporter/tls.priv \
-e certificate_authority_linux_cert_path=/etc/node_exporter/tls.crt \
-e certificate_authority_linux_key_owner=node-exp \
-e certificate_authority_linux_key_group=node-exp \
-e certificate_authority_linux_cert_owner=node-exp \
-e certificate_authority_linux_cert_group=node-exp \
-e 'certificate_authority_cert_sans=[]' \
-e certificate_authority_cert_include_default_names=true \
-e certificate_authority_cert_include_ip_san=true \
-e certificate_authority_cert_validity_days=5475 \
--tags generate_key_linux,generate_csr_linux,sign_certificate,write_certificate_linux \
--become
#install node exporter service and configure firewall
ansible-playbook --limit=app_nginx playbooks/deploy-node_exporter.yml --tags install --become
Note:
deploy-node_exporter.ymlopens port 9100 viaansible.posix.firewalld. If you later re-run theconfigure_firewalltag, the declarativefirewalldrole will replace all zone files and remove that rule unless port 9100 is included inperimeter_auth_firewall_zones. See Firewall Configuration for the recommended zone override.
Where the Epic configuration comes from
Epic ships a Perimeter Authentication configuration generator, available two ways: as downloadable templates on EEDS, and — from Kuiper August 2026 — as the Perimeter Authentication Configuration tool on Kuiper's Tools page. The role uses the Kuiper tool, because its output is what customers will be comparing against.
Most files are generated, not templated
The tool reports FilesToFill: ["pauth.conf"], and generating two very different
configurations confirms it: auth.js, the five conf.d/app_settings/*.conf
includes and nginx.conf come back byte-identical. They take no user values.
So the role commits those verbatim as versioned assets — nginx.conf excepted
only for its two certificate paths — and templates only pauth.conf. Do not
hand-edit the generated files; regenerate them.
Adopting a new Epic template version
ansible-playbook roles/perimeter_auth/tools/refresh-from-kuiper.yml -e kuiper_host=<kuiper> -e pauth_template_version=<version>
That uploads the reference configuration, downloads the generated bundle, and rewrites the committed static assets. Then:
- Read
git diff— that is exactly what Epic changed in the static files. - Diff
tools/.refresh/<version>/conf.d/pauth.conf(Kuiper's own output for the reference configuration) against a render oftemplates/pauth.conf.j2with the same inputs. That shows whether Epic changed the vhost, and whether any inlinedeviationcomment has become obsolete — if Epic fixes the missing semicolon, for instance, the role should stop adding one. - Run Molecule. It is the only place
nginx -texecutes, so it is the only automated check on the rendered configuration.
There is no golden-reference test
An earlier revision committed Kuiper's output as a fixture and failed CI on any undeclared difference. It was removed: it compared the template against whatever fixture happened to be committed, so it stayed green while a refresh bug quietly replaced that fixture with a weaker one — and it pinned case-insensitive locations off, meaning the mode actually deployed was never the one validated.
Deliberate differences from Kuiper are recorded as inline deviation comments
in the template, which cannot drift from the code they annotate.
None of this happens at deploy time
The role has no runtime dependency on Kuiper. The perimeter proxy sits in the DMZ and has to stay rebuildable when Kuiper is unavailable, which is precisely when you are most likely to need it.
Known generator defects
Generating configurations and reading the output turned up three defects in the tool itself, one of which produces nginx configuration that will not load, plus a fourth in the setup instructions for Isolated Recovery Environments. They are written up with reproduction steps in Kuiper Generator Defects, in enough detail to raise with Epic.
Never deploy generator output unvalidated.
Other deliberate differences
The full list is the inline deviation comments in templates/pauth.conf.j2. The ones worth knowing:
- Case-insensitive locations so users typing
/hsweb/reach a/HSWeb/site. See Variables. zoneandresolveon upstream servers, which the generator drops. Withoutresolve, nginx resolves each backend once at worker start and never again.$root_domain/X-Root-Domain, which generator 1.0.2 stopped emitting even though theauth.jsin the same bundle still reads it as the perimeter cookie domain fallback. Do not remove these to match Kuiper.- SlicerDicer optional, where Kuiper requires exactly one per environment and so forces a placeholder FQDN on sites that do not use it.