Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions examples/kerberos-auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Kerberos / NTLM (Integrated Windows Authentication) example

This example shows how to monitor an internal site that is protected by
**Negotiate (SPNEGO/Kerberos)** or **NTLM** using an Elastic Synthetics
browser monitor.

Elastic Synthetics drives Chromium through Playwright, and Chromium
already has first-class support for Integrated Windows Authentication.
All we need to do is pass it the usual command-line flags through
`playwrightOptions.args`:

```ts
playwrightOptions: {
args: [
'--auth-server-allowlist=*.corp.local,corp.local',
'--auth-negotiate-delegate-allowlist=*.corp.local',
],
}
```

The Synthetics runner forwards `playwrightOptions.args` verbatim into
`chromium.launch({ args })`, so the flags land on the real Chromium
process.

## Requirements

This workaround works with **Private Locations only**. It will not work
from Elastic's managed global locations.

On the host running the Private Location agent:

1. **Kerberos credentials must be available to the agent process.**
- Linux: a keytab for the service account plus a `kinit`'d ticket
cache (`KRB5CCNAME`). Keep it fresh with a cron job or
`systemd` timer (e.g. `kinit -R` every few hours, `kinit -kt` on
failure).
- Windows: run the agent on a domain-joined host as a domain user;
the OS will supply tickets automatically.
2. **`/etc/krb5.conf`** (Linux) must be configured for your realm.
3. **The SPN** (e.g. `HTTP/intranet.corp.local@CORP.LOCAL`) must be
registered against the service account that fronts the protected URL.
4. **The target hostname** must match an entry in
`--auth-server-allowlist`. The matcher is hostname-only and supports
shell-style wildcards — `*.corp.local` will NOT match the bare
`corp.local`.

## Files

| File | Purpose |
|---|---|
| `synthetics.config.ts` | Enables the Chromium auth flags and pins the monitor to a Private Location. |
| `protected-site.journey.ts` | Example journey that navigates to the protected URL and asserts a successful authenticated response. |

## Running

```sh
npm install
npx @elastic/synthetics . --params '{"url":"https://intranet.corp.local/"}'
```

### Verifying the flags are applied

If you want to confirm Chromium actually received the flags, run any
journey in non-headless mode and inspect the process from another shell:

```sh
ps -ef | grep -E 'chrome|headless_shell' | grep -- '--auth-server-allowlist'
```

You should see both `--auth-server-allowlist=...` and
`--auth-negotiate-delegate-allowlist=...` in the command line of the main
browser process.

### Troubleshooting

| Symptom | Likely cause |
|---|---|
| 401 on every request, no `Authorization` header sent | Host does not match the allowlist pattern, or the agent process has no Kerberos ticket. Check with `klist` as the agent user. |
| 401 with `Authorization: Negotiate ...` sent but still rejected | SPN mismatch, clock skew > 5 min, or the ticket cache is for the wrong principal. |
| Works interactively but fails under the agent | The agent service is running as a different user than your shell. Set `KRB5CCNAME` in the service unit or mount a shared ccache. |
| Delegation errors on downstream hops | Add the downstream host to `--auth-negotiate-delegate-allowlist` and make sure its SPN is marked *Trusted for Delegation* in AD. |

## Limitations

- **Lightweight HTTP monitors are not supported.** Heartbeat's Go HTTP
client has no native Negotiate/NTLM transport, so this approach cannot
be extended to `http` monitors today. Use a browser monitor for
Kerberos-protected endpoints until native support lands.
- **Managed/global locations are not supported.** The flags are only
useful if the host executing Chromium has access to the realm.
12 changes: 12 additions & 0 deletions examples/kerberos-auth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "synthetics-kerberos-auth-example",
"version": "1.0.0",
"private": true,
"description": "Example: Elastic Synthetics browser monitor against a Kerberos/NTLM protected site",
"scripts": {
"test": "npx @elastic/synthetics ."
},
"dependencies": {
"@elastic/synthetics": "*"
}
}
32 changes: 32 additions & 0 deletions examples/kerberos-auth/protected-site.journey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { journey, step, expect } from '@elastic/synthetics';

/**
* Journey that exercises a Kerberos/NTLM-protected URL.
*
* At runtime Chromium receives a 401 with
* WWW-Authenticate: Negotiate
* (or `NTLM`). Because the host is on `--auth-server-allowlist`, Chromium
* asks its platform GSSAPI / SSPI layer for a token built from the
* ambient Kerberos ticket and replays the request with
* Authorization: Negotiate <base64-token>
*
* If the handshake succeeds the page loads and the assertions below pass.
* If it fails (no ticket, wrong SPN, host not allowlisted, delegation
* denied) the step will fail with a 401 and a screenshot.
*/
journey('intranet (Kerberos/NTLM protected)', ({ page, params }) => {
step('navigate to protected URL', async () => {
const response = await page.goto(params.url, {
waitUntil: 'domcontentloaded',
});
// A successful Negotiate handshake yields a non-401 response.
expect(response?.status(), 'expected Kerberos auth to succeed').toBeLessThan(
400
);
});

step('assert authenticated content is rendered', async () => {
// Replace with a selector that is only visible to authenticated users.
await expect(page.locator('body')).toBeVisible();
});
});
65 changes: 65 additions & 0 deletions examples/kerberos-auth/synthetics.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { SyntheticsConfig } from '@elastic/synthetics';

/**
* Example configuration that enables Chromium's built-in Negotiate (SPNEGO/
* Kerberos) and NTLM authentication for browser monitors.
*
* Chromium natively supports Integrated Windows Authentication; it just needs
* to be told which servers are allowed to initiate the handshake. Those
* switches are ordinary Chromium command-line flags, so we forward them via
* `playwrightOptions.args`, which the Synthetics runner spreads directly into
* `chromium.launch({ args })`.
*
* Prerequisites on the host running the Private Location agent:
*
* 1. The agent host must be able to obtain a Kerberos ticket for the
* service principal(s) you want to monitor. On Linux this typically
* means a keytab plus a kinit'd credential cache that is kept fresh
* (cron / systemd timer). On a domain-joined Windows host the OS handles
* this automatically.
*
* 2. The user that runs the Private Location agent must have KRB5CCNAME
* (or the default file ccache) populated with a valid TGT at the time
* journeys execute.
*
* 3. `--auth-server-allowlist` matches on hostname only (not full URLs) and
* supports shell-style wildcards. `*.corp.local` matches `a.corp.local`
* but NOT the bare `corp.local` — list both or use `*corp.local` when
* needed.
*
* This example will NOT work from Elastic's managed global locations; it
* requires a Private Location on a host that is part of, or can
* authenticate against, your Kerberos/AD realm.
*/
export default () => {
const config: SyntheticsConfig = {
params: {
// Override with the real internal URL of the protected resource.
url: 'https://intranet.corp.local/',
},
playwrightOptions: {
ignoreHTTPSErrors: false,
args: [
// Hosts allowed to perform Integrated Auth. Comma-separated, supports
// shell-style wildcards. Tighten this to only the domains you trust.
'--auth-server-allowlist=*.corp.local,corp.local',

// Hosts allowed to receive a forwardable (delegated) Kerberos ticket.
// Only include services that genuinely need delegation.
'--auth-negotiate-delegate-allowlist=*.corp.local',

// Optional: disable the canonical-name DNS lookup that Chromium
// performs to build the SPN. Turn this off if your SPNs are
// registered under the short hostname rather than the FQDN returned
// by reverse DNS.
// '--disable-auth-negotiate-cname-lookup',
],
},
monitor: {
schedule: 10,
// Integrated auth only works from Private Locations.
privateLocations: ['my-private-location'],
},
};
return config;
};
Loading