Skip to content

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:

perimeter_auth_firewall_zones:
  public:
    target: DROP
    services:
      - https
      - ssh
    ports:
      - "7327"