Variables
pauth.conf
These variables are rendered into /etc/nginx/conf.d/pauth.conf. The model mirrors the Kuiper Perimeter Authentication Configuration tool's own grammar (generator 1.0.2), so a field in that tool maps to a key here one-for-one.
The role's meta/argument_specs.yml is the canonical reference and the validator — required variables have no default and are checked at role entry, before nginx is installed, so a missing one fails immediately rather than part-way through a converge.
Set them in group_vars/app_nginx/ for the application group (see Group Taxonomy) or in host_vars for host-specific overrides.
Shape
Three top-level lists describe three different kinds of nginx server block:
| Variable | Required | Becomes |
|---|---|---|
perimeter_auth_pauth_app_servers |
yes | One server block on 443 per entry, serving one or more Epic environments |
perimeter_auth_pauth_signalr_servers |
yes | One server block on 7327 per entry |
perimeter_auth_pauth_mobile_servers |
no | One server block on 443 per entry, for Haiku/Canto/Rover |
Plus one scalar: perimeter_auth_pauth_root_domain (required) — the root domain by which DMZ resources are accessed, e.g. communityconnect.org, used as the perimeter cookie domain fallback.
App servers
perimeter_auth_pauth_app_servers:
- server_names: # multiple entries support branded URLs
- pauth.hospital.org # on one proxy. 127.0.0.1 is not valid.
- pauth.communityconnect.org
root_redirect_path: HSWeb # optional: the bare host 302s here
environments: # one NGINX pair can front several
- name: PRD # Epic environments
ic_path: Interconnect-PerimeterAuth-PRD
ic_fqdn: ic.hospital.org
hsweb: # exactly one per environment
path: HSWeb
backend_fqdn: hsw.hospital.org
auth_server_url: https://authhsw.hospital.org/HSWeb # optional
slicerdicer: # optional
path: SlicerDicer
backend_fqdn: sd.hospital.org
custom_locations: # optional
- name: Clinical References
path: ClinicalRefs
backend_fqdn: refs.hospital.org
- name: TST
ic_path: Interconnect-PerimeterAuth-TST
ic_fqdn: ic-tst.hospital.org
hsweb:
path: HSWeb_TST
backend_fqdn: hsw-tst.hospital.org
root_redirect_path makes the bare host useful: https://pauth.hospital.org/ answers 302 to /HSWeb/ instead of the 404 Kuiper's configuration gives it, so users can bookmark the plain hostname. A 302 rather than a 301, because the landing app is a routing choice that can change and browsers cache 301s indefinitely. Unset leaves the bare host a 404.
auth_server_url is a different tier, and it is optional
It is the full URL of the load-balanced Authentication Hyperspace Web servers that unauthenticated requests redirect to — not the proxy's own name. Pointing it at the proxy sends users back to the resource they could not reach.
Omitting it leaves $perimeter_server unset, which is what Kuiper does. An
unauthenticated request then builds its redirect from an empty string, so set
it unless you know the environment never serves unregistered clients.
SignalR servers
perimeter_auth_pauth_signalr_servers:
- server_names: [pauth.hospital.org]
apps:
- path: Interconnect-EventWS # the Event Notification instance
backend_fqdn: ic.hospital.org
The Event Notification instance is not the perimeter auth instance
apps[].path is the Event Notification Interconnect instance, which Epic
treats as a separate field from an environment's ic_path. Set both
explicitly even when they happen to match.
Port 7327 is fixed by Epic's Perimeter Authentication URL table and the block always renders; there is no variable for it.
Mobile servers
Requires an Epic environment on August 2026 or newer.
perimeter_auth_pauth_mobile_servers:
- server_names: [mobile.hospital.org]
environments:
- name: PRD
path: Interconnect-Mobile-PRD
backend_fqdn: mobileic.hospital.org
ic_path: Interconnect-PerimeterAuth-PRD
ic_fqdn: ic.hospital.org
Optional settings
| Variable | Type | Default | Description |
|---|---|---|---|
perimeter_auth_pauth_case_insensitive_locations |
bool | true |
Emit case-insensitive regex locations instead of Kuiper's prefix locations. See Case sensitivity. |
perimeter_auth_pauth_slashless_redirects |
bool | true |
Redirect application URLs typed without their trailing slash to the canonical path. See Trailing slashes. |
perimeter_auth_pauth_ire |
bool | false |
This deployment is an Isolated Recovery Environment, so custom locations proxy without the token auth subrequest. See Isolated Recovery Environments. |
perimeter_auth_pauth_upstreams |
list | [] |
Backend FQDNs to build keepalive upstream groups for. Empty derives one per distinct backend, which is what you want. |
perimeter_auth_pauth_resolvers |
list | system resolvers | DNS resolver IPs for the nginx resolver directive. Defaults to ansible_dns.nameservers. |
perimeter_auth_pauth_ssl_client_certificate |
string | "" |
Path to a PEM bundle of CAs for mTLS against nginx directly. Empty disables mTLS. |
perimeter_auth_pauth_known_proxies |
list | [] |
Load balancer IPs permitted to forward client certificates in the X-Forwarded-Client-Certificate header. |
Case sensitivity
nginx prefix locations are case-sensitive, so a user who types https://pauth.hospital.org/hsweb/ gets a 404 from a /HSWeb/ site even though the IIS backend would have served it — nginx is the only case-sensitive hop in the path. Epic's guide notes the behaviour ("by default, locations are case-sensitive") but its generator never applies the case-insensitive form to real locations.
With the default perimeter_auth_pauth_case_insensitive_locations: true, HSWeb, SlicerDicer, custom and SignalR locations render as location ~* ^/HSWeb/ instead.
Ordering changes with it, deliberately
Prefix locations match longest-wins; regex locations match first-wins.
So the role emits locations sorted longest-path-first. Without that, an
environment at HSWeb would swallow traffic meant for HSWeb_TST. The
Molecule scenario asserts the ordering.
The mobile block keeps prefix locations regardless, because its locations nest inside an outer location and mobile URLs come from a generated mobile configuration rather than being typed.
Trailing slashes
Every application location ends in /, and for prefix locations nginx handles a missing trailing slash itself: a location ending in / with proxy_pass inside answers /HSWeb with an implicit 301 to /HSWeb/. Regex locations get no such treatment, so turning on case-insensitive locations alone would silently trade that implicit redirect for a 404 (verified on a real deployment: the Kuiper-faithful prefix config 301s /HSWeb_ODRO, the regex config without this setting 404s it).
With the default perimeter_auth_pauth_slashless_redirects: true, the role emits one exact-match location per HSWeb, SlicerDicer and custom path that returns 301 to the canonical casing with the query string preserved: /hsweb_odro?x=1 → /HSWeb_ODRO/?x=1. That restores the implicit behaviour in regex mode and goes one step further — a wrong-case slashless URL like /hsweb_odro also redirects, where the implicit redirect (and Kuiper's config) 404s it. The match is anchored, so a superstring like /HSWeb_ODROxyz still 404s, and the Location header is relative, so a fronting load balancer's public scheme and host are what the client sees. SignalR and mobile URLs come from software rather than typing and are excluded.
The setting is independent of perimeter_auth_pauth_case_insensitive_locations; in case-sensitive mode it is nearly redundant (nginx's implicit redirect already fires) and exists so behaviour does not shift when the case setting changes.
The auth redirect only happens at an application root
An unauthenticated request to /HSWeb_ODRO/ is answered with a 302 to
auth_server_url, but a deeper path like /HSWeb_ODRO/deep/path gets a bare
401. That is Epic's auth.js behaviour, not the role's: it only redirects
paths ending in /, /Default.aspx, /AuthLaunch.aspx, or page
navigations, so an XHR is never handed a login redirect. Expect 401s, not
redirects, when probing deep paths unauthenticated.
Isolated Recovery Environments
Epic's guide has a section for Perimeter Authentication in an Isolated Recovery Environment: the applications an IRE serves need custom locations, and those locations must proxy without the perimeter token auth subrequest. Customers are told to copy the commented-out Custom Location block in pauth.conf once per application and edit it by hand, leaving the auth_request lines commented.
Set perimeter_auth_pauth_ire: true and the role renders that instead. It is deployment-wide rather than per location, matching how Epic scopes it — a deployment either is an IRE or it is not. Each custom location then includes custom_ire.conf in place of custom.conf, which is the same file without the token auth subrequest:
#### Custom Location — Interconnect OAuth2 (PRD)
location ~* ^/Interconnect\-OAuth2/ {
include conf.d/app_settings/custom_ire.conf;
That is the whole difference. HSWeb and SlicerDicer locations keep their subrequest either way, and the custom locations themselves are still declared as custom_locations on the environment that serves them — the flag changes how they render, not whether they exist:
perimeter_auth_pauth_ire: true
perimeter_auth_pauth_app_servers:
- server_names: [pauth.hospital.org]
environments:
- name: PRD
ic_path: Interconnect-PerimeterAuth-PRD
ic_fqdn: ic.hospital.org
hsweb:
path: HSWeb
backend_fqdn: hsw.hospital.org
custom_locations: # the list Epic gives you for the IRE
- name: Interconnect OAuth2
path: Interconnect-OAuth2
backend_fqdn: ic.hospital.org
Get the location list from Epic
Which applications need a custom location in an IRE is environment-specific
and not published. Ask your Epic Client Systems – Web & Service Servers
representative, mentioning reference SLG 8524651, then declare what they
give you as custom_locations.
Epic's instructions produce a configuration nginx will not load
They say to uncomment every line except auth_request and
auth_request_set. But the next line reads $tokenvalue, a variable that
exists only because auth_request_set creates it, so nginx rejects the
result with [emerg] unknown "tokenvalue" variable. The role's
custom_ire.conf comments out the add_header Set-Cookie line as well.
Recorded as defect 4.
Upstreams
The role derives one keepalive upstream group per distinct backend FQDN and names each group after its FQDN. That naming is load-bearing: nginx matches a variable proxy_pass to an upstream group on name and port, and a group declared upstream <fqdn> { } has port 0, so any other name — or a :443 suffix on the corresponding set $backend_fqdn — silently falls through to a plain resolver lookup with the keepalive pool unused, with no error and no log line.
Kuiper's own tool exposes a free-text "Name" for each upstream that it then discards, and its generated SignalR locations hit exactly this bug.
Backup and rollback
The configure_nginx tag snapshots /etc/nginx before writing to it, and restores that snapshot if the deploy fails validation (nginx -t), the reload, or verification (nginx running, listening on the expected ports, no new [emerg] in the error log). A snapshot is only taken when the configuration actually differs from the newest existing one, so no-op converges do not churn the retention window.
| Variable | Type | Default | Description |
|---|---|---|---|
perimeter_auth_backup_enabled |
bool | true |
Snapshot /etc/nginx before any configuration write. |
perimeter_auth_backup_dir |
string | /var/backups/perimeter_auth |
Where snapshots are kept, one timestamped directory each. |
perimeter_auth_backup_retain |
int | 10 |
Number of snapshots to keep; older ones are pruned. |
perimeter_auth_rollback_on_failure |
bool | true |
Restore the snapshot and reload when a deploy fails. Configuration only — SELinux and firewalld state are untouched. |
perimeter_auth_restore_snapshot |
string | "" |
Snapshot directory to restore under the restore tag. Empty restores the most recent. |
perimeter_auth_expected_ports |
list | [443, 7327] |
Ports nginx must be listening on after a reload. A reload that fails to bind keeps the old configuration serving while systemd and Ansible report success; this check catches it. |
Monitoring
These variables control the enable_monitoring task, which installs nginx-prometheus-exporter and a localhost-only nginx stub_status endpoint for Prometheus scraping.
| Variable | Type | Default | Description |
|---|---|---|---|
perimeter_auth_enable_monitoring |
bool | true |
Set to false to skip the enable_monitoring task entirely. |
perimeter_auth_prometheus_stub_status_port |
int | 8080 |
Port nginx listens on for the stub_status endpoint (localhost only). |
perimeter_auth_prometheus_exporter_port |
int | 9113 |
Port nginx-prometheus-exporter listens on for Prometheus scrapes. |
perimeter_auth_prometheus_exporter_version |
string | 1.5.1 |
Version of nginx-prometheus-exporter to install from GitHub releases. |
perimeter_auth_prometheus_exporter_arch |
string | auto | GitHub release asset architecture suffix. Auto-detected from ansible_architecture. |
Certificates
| Variable | Type | Default | Description |
|---|---|---|---|
perimeter_auth_ssl_certificate |
string | certificate_authority path |
Path to the nginx TLS certificate. |
perimeter_auth_ssl_certificate_key |
string | certificate_authority path |
Path to the nginx TLS certificate key. |
Firewall
Firewall zones are managed by the firewalld role via the perimeter_auth_firewall_zones variable. See Firewall Configuration for zone design details.
Default value:
perimeter_auth_firewall_zones:
public:
target: DROP
services:
- https
- ssh
ports:
- "7327"
monitoring:
sources: []
ports:
- "{{ perimeter_auth_prometheus_exporter_port }}"
Alternative configurations
Standard — monitoring open to Prometheus subnet:
perimeter_auth_firewall_zones:
public:
target: DROP
services:
- https
- ssh
ports:
- "7327"
monitoring:
target: DROP
sources:
- 10.248.5.0/24
services:
- ssh
ports:
- "9113" # nginx-prometheus-exporter
- "9100" # node_exporter
Behind a load balancer — restrict 7327 to LB source IP:
perimeter_auth_firewall_zones:
public:
target: DROP
services:
- https
- ssh
load_balancer:
target: DROP
sources:
- 10.248.1.10/32
ports:
- "7327"
monitoring:
target: DROP
sources:
- 10.248.5.0/24
services:
- ssh
ports:
- "9113"
- "9100"
Fully restricted — SSH public only, HTTPS and pauth behind a load balancer:
perimeter_auth_firewall_zones:
public:
target: DROP
services:
- ssh
load_balancer:
target: DROP
sources:
- 10.248.1.10/32
services:
- https
ports:
- "7327"
monitoring:
target: DROP
sources:
- 10.248.5.0/24
services:
- ssh
ports:
- "9113"
- "9100"
Monitoring disabled — no exporter ports needed: