Configuring applications

An application gets behind the ISG login by an entry in apps.yaml on every broker, or — with a dynamic apps domain — by an administrator on the web UI. This page is both routes, the three ways an application learns who the user is, and what the customer's side has to provide. The full key list is in the configuration reference; apps.example.yaml in the source tree carries every key with a comment.

A missing apps.yaml means Web Access is off, and the daemon starts as before. An apps.yaml that is there but does not load stops the daemon from starting, and the error names the file and the field; isg system check runs the same loader (the apps.yaml row). Web Access needs the web UI's password login against an identity source: without one the daemon does not start with this file.

With the shipped Ansible

The switch is isg_cfg_apps in the environment's variables: one entry per application, with the fields of apps.example.yaml. Left empty, Web Access is off and no apps.yaml is written. Every entry needs an id of the form APP-2026-000001, unique and never handed to another application: approvals, sessions, locks, tickets and the audit log (app_id) all refer to the application by it, while the name stays the technical name (the JWT audience and the OpenBao path). Set, a run does this:

A change to an application is a configuration change: config-src.yml writes the new apps.yaml, and the rolling upgrade carries it to the brokers one at a time. Replacing the binary alone does not.

The listener and its certificate

Key What it is
listen The TLS listener's address, for every application
tls_cert_file, tls_key_file The listener's chain and key (key mode 0600 or 0400); the leaf has to cover every host
webui_url The web UI's address: where the proxy sends the browser to sign in, and back to on sign-out
idle_timeout_seconds, max_duration_seconds The application session's idle limit (1800 s) and absolute limit (28800 s), independent of the web UI's
reserved_usernames Names that never reach an application (admin, root, administrator by default)
credential_kv_prefix Where the shared accounts live in OpenBao (isg/apps by default)
dynamic_apps_domain The domain of the applications added on the web UI; empty means none

What ISG does not do for you: the DNS. Every app host name has to resolve to the brokers' shared address, and has to be on the same site as the web UI — the web UI's session cookie is SameSite=Strict. The client trusts the apps' certificate the same way as the web UI's.

One application

- id: APP-2026-000002
  name: prometheus
  display_name: Prometheus
  host: prometheus.apps.example.internal
  upstream: http://monitor1.example.internal:9090
  identity: none
  allow_groups: [sysadmins]

What the application learns about the user

identity: none

ISG gates and audits; the application learns nothing. For a console with no user notion of its own (Prometheus, a mail catcher), or one that is fine with a single shared view.

identity: jwt

Every request carries a fresh assertion in assertion_header (X-ISG-Assertion by default), signed with ES256 (assertion_key_file, an ECDSA P-256 key): sub, email, name, groups, aud (audience, by default the name), iss (assertion_issuer, isg by default), and the role from role_map (the first matching group wins), else default_role. It is valid for assertion_ttl_seconds (60 by default, 10–300). The public key is on every application host at /.well-known/isg/jwks.json. The application maps sub to its local account of the same name — that is why the reserved names exist: measured on Grafana, sub: admin became the built-in super-admin.

identity: credential

ISG puts a shared account's secret into the request; the user never sees it, and their own name stays in the ISG audit and transcript (account). credentials is a list: the first entry whose groups meet the user's groups is used, and with none the answer is 403 — there is no silent default account. The secret is read from OpenBao at <kv_mount>/data/<prefix>/<app>/<name>, where <prefix> is credential_kv_prefix, so vault.yaml is mandatory next to such an application. When it cannot be read, the request does not go up (503).

kind What goes up OpenBao fields
basic Authorization: Basic username, password
header the header named on the entry (e.g. X-Api-Key) value
form the upstream session cookies of an account ISG signed in with itself username, password, optional totp_secret

The browser's own Authorization header and the named headers are removed from every request, and the upstream's WWW-Authenticate response header too, so the browser never opens its own password dialog.

A console that signs in with a form (kind: form)

The account's form recipe is the console's own sign-in, step by step: steps (a GET only extracts tokens; at least one POST; encoding: form or json; field values are literals or {{username}}, {{password}}, {{totp}}, {{token.<name>}}), success (all of status, cookie, redirect_prefix that are given must hold on the last step's answer) and logged_out (when an answer means the upstream session has ended: status, or a 3xx whose Location starts with redirect_prefix).

credentials:
  - name: admin
    groups: [sysadmins]
    kind: form
    form:
      steps:
        - method: POST
          path: /login
          encoding: json
          fields: {user: '{{username}}', password: '{{password}}'}
      success: {status: [200], cookie: grafana_session}
      logged_out:
        status: [401]
        redirect_prefix: /login
        unless_body_contains: [session.token.rotate]

Applications added on the web UI

With dynamic_apps_domain set in apps.yaml, an administrator adds, changes and deletes applications on the web UI's Manage apps page (/apps/manage) next to the ones in the file; every broker serves the change within about 5 s, with no restart. Each such application's host is one label under that domain, so two things are a one-time setup on the customer's side: the listener certificate needs a wildcard SAN for the domain (star-dot-domain; checked when apps.yaml loads, a missing one refuses the start), and a wildcard DNS record for the domain has to point at the brokers' shared address. The applications in the file stay as they are and cannot be edited on the web UI. With Ansible the domain is isg_cfg_apps_dynamic_domain; when the role issues the application certificate, it adds the wildcard SAN itself. ISG gives each such application its id from the record store's sequence; deleting one and adding it again under the same name is a new application, so none of the old one's approvals carry over. A dynamic application cannot switch on record_body.