Security & Access
Access Controls
Protect public URLs with per-endpoint policies. All rules are enforced at the edge before traffic reaches your service.
Configure through the dashboard or the API under Endpoint Settings → Authentication. Every endpoint has one of three auth modes:
- None (Public) — No authentication required. Useful for public sites or webhooks.
- Basic — callers sign in as one of the endpoint's Access Control clients, with a username and password (bcrypt-hashed) and/or a bearer token of their own, sent in the
Authorization: BearerorX-Tunnel-Tokenheader — ideal for API access. You choose which of the two methods the endpoint accepts. Failed password sign-ins are limited per username and IP — see Sign-in protection. - SSO (Unified RBAC) — OAuth/OIDC providers with Role-Based Access Control. See SSO section below.
An endpoint configured before with another mode (the removed token mode, or header, cookie or jwt) refuses every request until you pick one of these three.
Separate from the auth mode, and usable with any of them:
- Mutual TLS (mTLS) — Require clients to present a certificate during the TLS handshake. See Mutual TLS section below.
- JWT validation — check the JWTs your callers already carry with a jwt-validation traffic policy rule. It is a rule, not an auth mode.
Additional edge-level protections:
- IP Allowlist — Restrict access to specific CIDR ranges (IPv4 and IPv6)
- Rate Limiting — Per-IP or per-endpoint request rate limits
# Basic auth via API: set the mode and methods...
curl -X PUT https://api.ngris.com/v1/endpoints/{uuid}/settings \
-H "Authorization: Bearer $TOKEN" \
-d '{"mode":"basic","enforce_https":true,"enabled_auth_methods":{"username_password":true,"bearer_token":false,"oauth_sso":false}}'
# ...then add a user (an Access Control client)
curl -X POST https://api.ngris.com/v1/endpoints/{uuid}/rbac/clients \
-H "Authorization: Bearer $TOKEN" \
-d '{"identifier":"admin","username":"admin","password":"s3cret-pass"}'
# Create WAF policy with IP restriction (policy + conditions saved together)
curl -X POST https://api.ngris.com/v1/firewall/policies \
-H "Authorization: Bearer $TOKEN" \
-d '{"name":"block-ips","action":"block","conditions":[{"type":"source_ip","operator":"in","value":"203.0.113.0/24"}]}'
Unified RBAC
Unified Role-Based Access Control manages users, roles, and permissions centrally across all your endpoints.
Clients & Users
A "Client" represents a user or service account. Clients can authenticate via:
- Username/Password — For direct login
- OAuth/SSO — Linked to an identity provider
- Tokens — For programmatic access
Roles & Permissions
Roles define what a client can do. Restrict access based on:
- Allowed Paths — Whitelist URL paths (e.g.,
/api/*) - Denied Paths — Blacklist sensitive paths (e.g.,
/admin/**) - Allowed Methods — Restrict HTTP methods (e.g., GET only)
Team Members & Audit
An account can be shared with other people. The person who created the account is its owner; everyone else joins as a member. Owners and members differ only in permissions — resources belong to the account, not to any one person.
Inviting members
Invite teammates by email from Account Settings → Members. They receive an invitation link, accept it, and join the account with the role you assign.
- Owner — full control of the account, including billing, plan changes, and member management. The owner is always exempt from the member gate.
- Member — access governed by their assigned role / RBAC permissions. Non-owner members may only access the account when the plan grants the
account_membersentitlement.
Seat cap
The number of members an account may hold is capped by the plan's max_members limit (plan_limits.max_members on GET /v1/account/plan: active members plus pending invitations, the owner included; -1 means unlimited). Invitations beyond the cap are rejected. Downgrading to a plan without the account_members entitlement suspends existing non-owner memberships until the account upgrades again.
Account audit log
Every resource action on the account — endpoint, domain, certificate, firewall, API-key, billing, and organisation changes — is recorded in the account-scoped audit log, showing who acted, how they authenticated, and from which device. (Personal sign-in / password / 2FA events live on the per-user security surfaces, not here.)
account_members entitlement and the audit log requires the account_audit_log entitlement. See GET /v1/account/entitlements to check what the active account has.SSO / OIDC
Require SSO for protected endpoints. Users authenticate via OAuth, and Ngris matches their subject ID to a client record with RBAC applied automatically. This is endpoint access control — distinct from Account SSO, which signs your team into the ngris dashboard.
Configure per-endpoint through Endpoint Settings → Authentication by enabling OAuth / SSO sign-in (over the API: PUT /v1/endpoints/{uuid}/settings with "mode": "sso", which is stored as basic with the oauth_sso method on), then add the providers on the OAuth / SSO tab. Supported providers:
- Google — Google Workspace / Gmail
- GitHub — GitHub accounts and organizations
- Azure AD — Microsoft Entra ID
- Okta / Auth0 — Enterprise identity providers
- Custom OIDC — Keycloak, Authentik, Zitadel, and any OIDC-compliant provider
Setup Steps
- Configure OAuth 2.0 / OIDC credentials in your identity provider (Web Application, Authorization Code flow)
- Set the redirect URL to:
https://{your-subdomain}.ngris.io/_tunnel_auth/oauth/callback - Obtain OAuth subject identifiers (
subclaim for Google, username for GitHub,oidfor Azure) - Create endpoint clients in the dashboard: Users tab → Create Client → Enable OAuth/SSO → Enter Subject & Provider
- Assign roles via the Memberships tab
Mutual TLS (mTLS)
Require clients to present a certificate signed by a trusted Certificate Authority during the TLS handshake. Ngris manages the server-side certificates for every endpoint automatically — you only upload the client CAs you trust.
Enforcement happens at the TLS layer, before any HTTP request is parsed. Rejected connections never reach your origin.
1. Upload a Client CA
Go to Security → mTLS and click Upload CA. Paste a PEM-encoded CA certificate.
- One CA per upload — bundles (multiple certificates in one PEM) are rejected.
- Certificate must have
BasicConstraints: CA = true. Leaf certs are rejected. - The CA must be valid now (not expired, not dated in the future) and allowed to sign certificates.
- Duplicates are detected by SHA-256 fingerprint per account: uploading a CA your account already has returns the existing one.
- On upload, Ngris parses and stores Subject CN, Issuer CN, fingerprint, and validity window for display.
2. Enable mTLS on an Endpoint
Open the endpoint, go to Settings → Mutual TLS, pick a mode and select one or more CAs:
- Disabled — No client certificate is requested. Default.
- Optional — Request a certificate; accept the connection whether or not one is presented. Traffic-policy rules can then match on the presented cert.
- Required — Reject the TLS handshake unless the client presents a valid certificate signed by one of the selected CAs.
3. Attribute-Level Filtering (optional)
Once an endpoint accepts client certs, you can layer extra checks with a Require Client Certificate traffic-policy rule in the on_http_request phase. Match on:
- Subject CN — regex against the leaf certificate's Subject Common Name.
- SAN — regex matched against any DNS, email, or URI SAN.
- Issuer CN — regex against the Issuer Common Name.
All provided matchers are AND-ed together. Empty matchers are skipped. Use this to route, for example, only CN=admin certs to /admin/*.
Let ngris issue the certificates
No CA of your own? ngris can create one for your account and issue a certificate for each device. ngris keeps the CA's private key, and it can't be exported. You can download the CA certificate to trust it elsewhere too.
- Create the CA. In Security → mTLS, click Create certificate authority and give it a name, for example Company laptops. It is listed with your other CAs, marked Managed by ngris.
- Issue a certificate for each device. On the Client certificates tab, click New certificate. Enter the device's name (letters, digits, dots, dashes or underscores) and a lifetime (90 days unless you choose another, up to 397). With Download for a browser or phone you get a certificate file and its password: the password is shown once, so save it, then open the file on the device and enter the password. With I have a signing request (CSR), paste the device's signing request and download the certificate. The device keeps its own key.
- Trust the CA on your endpoints in each endpoint's Mutual TLS settings, as in step 2. Every certificate the CA issues is then accepted there. Endpoints trust CAs, not single certificates: to let different devices into different endpoints, create one CA for each group of devices.
From the CLI, run this on the device: ngris client-cert request --ca "Company laptops" --name laptop-anna. It makes the device's key on the device and writes laptop-anna.crt, laptop-anna.key and ca.pem. ngris client-cert list shows your certificates, and ngris client-cert revoke <certificate_uuid> revokes one. The API and the MCP server issue certificates from a signing request too.
Renew a device by issuing a new certificate with the same name (with the CLI, into a new folder: --out-dir). The old one keeps working until it expires or you revoke it.
Revoke the certificate of a lost or stolen device from its menu on the Client certificates tab, with an optional reason. Every endpoint refuses it from the device's next request or connection. A WebSocket or other stream that is already open stays open until it closes. Revoking can't be undone: issue a new certificate instead.
Your plan needs managed client certificates and mTLS to create a CA, and it sets how many managed CAs and active certificates the account can have. If your plan no longer includes managed client certificates, endpoints stop accepting the certificates of those CAs at once. Nothing is deleted: certificates that haven't expired or been revoked work again when the feature is back. You can still list and revoke them from the CLI or the API.
Which device is calling
For every request with a client certificate the edge verified, your app receives these headers. The edge removes any copies a client sends, on every endpoint, with mTLS or without.
| Header | Value |
|---|---|
| X-Ngris-Client-Cert-Name | The device name, for example laptop-anna (certificates from a CA managed by ngris) |
| X-Ngris-Client-Cert-Subject | The certificate's subject, for example CN=laptop-anna |
| X-Ngris-Client-Cert-Serial | The certificate's serial number, lowercase hex |
| X-Ngris-Client-Cert-Fingerprint | The certificate's SHA-256 fingerprint, lowercase hex |
The same details come signed in X-Ngris-Signature. Verify it as shown in Verify the signed identity. With edge login, the user's token carries a client_cert claim. Without edge login, the token names the certificate: sub is client-cert:<fingerprint>, auth_method is client_cert, and the client_cert claim holds name, subject, serial and fingerprint.
aud and the certificate, never on host. Check that aud is your endpoint's UUID, then authorise on sub or the client_cert claim, or on the headers above. Don't decide anything on the token's host: it is the host the client asked for, and on HTTP/1.1 the client chooses it.Troubleshooting
- Clients see TLS handshake failure on Required mode — they are not presenting a client certificate, or the cert's issuer chain is not signed by any selected CA. Use
openssl s_client -connect host:443 -cert c.crt -key c.keyto inspect. - Expired CA — a CA's expiry is shown on the mTLS page; expired CAs are kept in the library but no longer validate chains. Upload the new CA and reselect on affected endpoints.
- Cannot delete a CA — detach it from every endpoint first (endpoint's Mutual TLS tab). Ngris refuses to delete in-use CAs.
- Every connection refused after a plan change — if your plan no longer includes client certificates, endpoints that require them refuse all connections until client certificates are back on your plan or you turn mTLS off for the endpoint.
# Upload a client CA
curl -X POST https://api.ngris.com/v1/client-cas \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Corporate Device CA","ca_pem":"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"}'
# Enable mTLS on an endpoint (mode + trusted CA IDs)
curl -X PUT https://api.ngris.com/v1/endpoints/{uuid}/mtls \
-H "Authorization: Bearer $TOKEN" \
-d '{"mode":"required","ca_uuids":["<client_ca_uuid>"]}'
# Test with a client certificate
curl --cert client.crt --key client.key https://your-endpoint.ngris.io/
# Client without a cert, on a required endpoint — handshake rejected:
# curl: (56) OpenSSL SSL_read: error, errno 0
# Create a CA that ngris manages
curl -X POST https://api.ngris.com/v1/client-cas/managed \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Company laptops"}'
# On the device: make its key and certificate
ngris client-cert request --ca "Company laptops" --name laptop-anna
curl --cert laptop-anna.crt --key laptop-anna.key https://your-endpoint.ngris.io/
# A lost device: find its certificate, then revoke it
ngris client-cert list --ca "Company laptops"
ngris client-cert revoke <certificate_uuid>
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(
new URL('https://api.ngris.com/.well-known/ngris-edge-jwks.json'));
export async function ngrisDevice(req, allowedFingerprints) {
const { payload } = await jwtVerify(
req.headers['x-ngris-signature'], JWKS, {
issuer: 'https://api.ngris.com/edge-identity',
audience: process.env.NGRIS_ENDPOINT_UUID,
algorithms: ['ES256'],
});
const device = payload.client_cert; // name, subject, serial, fingerprint
if (!device || !allowedFingerprints.has(device.fingerprint)) {
throw new Error('device not allowed');
}
return device; // never decide on payload.host
}
Endpoint authentication from the agent CLI
You don't have to open the dashboard to put an endpoint behind a login. The agent can attach HTTP Basic auth and/or mutual TLS to its endpoint at connect time straight from command-line flags — enforcement still happens at the edge, so nothing extra runs on your machine. The flags also work in the config file.
Basic auth
Pass --basic-auth user:pass to require a username and password. Repeat the flag for multiple users. The value is split on the first colon, so a password may itself contain colons; passwords must be at least 8 characters.
--basic-authturns on username/password sign-in only. Bearer tokens and OAuth/SSO stay off unless you turn them on in the dashboard; sign-in methods you already chose there are kept, with username/password added.- The agent manages its own set of edge users. Re-running with a new
--basic-authlist replaces the users the agent created — users you added in the dashboard are left untouched. - Restarting without
--basic-authremoves the users the agent created. If no other user is left on the endpoint, the login goes away and the endpoint is public again; users you added in the dashboard keep it behind the login. - Every edge user needs a role. The agent's users get the endpoint's default roles; if it has none, they get a role named
ngris-agentwith full access (no path or method rules), created once and reused. It is not a default role, so users you add in the dashboard don't get it. Edit it, or mark another role as default, to change what the agent's users can reach. - Passwords are stored bcrypt-hashed and verified at the edge; the plaintext is never written to the endpoint's record.
Mutual TLS
Require clients to present a certificate with --mutual-tls <mode>. Like all mTLS it works on HTTP(S) endpoints only (ngris http, ngris webfm), not on ngris tcp or ngris udp tunnels. The modes match the Mutual TLS section above:
- disabled — clear mTLS on the endpoint, including CAs attached in the dashboard. No CA needed.
- optional — request a certificate but accept the connection whether or not one is presented.
- required — reject the TLS handshake unless the client presents a valid certificate.
For optional and required, point --client-ca <file> at a single PEM-encoded CA certificate — the one that signs the client certs you trust. It must be one CA, not a bundle. Enabling mTLS (any mode other than disabled) requires the mTLS (mtls_attachment) entitlement on your plan; without it the agent logs a clear error and keeps serving traffic (the tunnel is never blocked).
Restarting without --mutual-tls detaches the CA the agent uploaded; with no CA left, mTLS is turned off. CAs you attached in the dashboard stay attached and keep mTLS on. The CA itself stays in your Client CAs list. A CA you had uploaded in the dashboard before passing it to --client-ca is not detached — turn mTLS off for the endpoint in the dashboard, or run once with --mutual-tls disabled.
Protect a writable WebFM server
The same flags work on every HTTP tunnel command, including the Web File Manager. Running ngris webfm <path> --rw exposes a world-writable file server — anyone who can reach the endpoint can upload, edit, and delete files. Always pair --rw with --basic-auth (or --mutual-tls); the agent prints a loud warning if you don't.
ngris to change them. The flags describe everything the agent sets: a restart without one removes what it had set. Two agents serving the same endpoint (for example two agents on one machine, which share its endpoint) each apply their own flags, so the one started last wins.--basic-auth, --mutual-tls / --client-ca, --traffic-policy or its shorthand flags, or webfm --rw (also in the config file), the agent logs in with its token; without a token it doesn't start. To remove endpoint auth an earlier run set, run once with the token, or remove it in the dashboard.# One Basic-auth user (password ≥ 8 chars)
ngris http 3000 --basic-auth alice:s3cretpw
# Multiple users — repeat the flag
ngris http 3000 --basic-auth alice:s3cretpw --basic-auth bob:hunter2!
# Require a client certificate signed by your CA (needs the mTLS entitlement)
ngris http 3000 --mutual-tls required --client-ca ./client-ca.pem
# Request a cert but accept clients without one
ngris http 3000 --mutual-tls optional --client-ca ./client-ca.pem
# Protect a read-write Web File Manager
ngris webfm ./site --rw --basic-auth admin:s3cretpw
Request Forwarding
When authentication succeeds, Ngris injects identity information into request headers before forwarding to your backend.
Standard Headers
Enable in Endpoint Settings to receive user context:
| Header | Description |
|---|---|
| X-User-ID | Internal ID of the authenticated client |
| X-User-Email | Email address of the user |
| X-User-Username | Username of the client |
| X-User-DisplayName | Display name of the user |
| X-User-Roles | Comma-separated list of assigned roles |
| X-Auth-Method | Method used (token, basic, session) |
| X-Forwarded-For | Original client IP address |
Variable Substitution
Use {{.VariableName}} in custom headers and cookies to inject dynamic user information:
| Variable | Description |
|---|---|
| {{.UserID}} | Client internal ID |
| {{.UserEmail}} | User email address |
| {{.UserRoles}} | Comma-separated roles |
| {{.ClientIP}} | Client IP address |
| {{.Metadata.KEY}} | Custom metadata field |
Verify the signed identity
Every request the edge forwards after a successful edge login carries X-Ngris-Signature: a short-lived token signed by ngris that names the user. It is sent for agent tunnels and for apps hosted on ngris alike, and also on every request made with a client certificate the edge verified (see Which device is calling). The X-User-* headers above are plain-text conveniences; the token is the one to trust.
- Format: a JWT signed with ES256; the
kidnames the key. - Public keys:
https://api.ngris.com/.well-known/ngris-edge-jwks.json(a standard JWK Set; cache it, your JWT library refreshes it on a newkid). - Check
iss=https://api.ngris.com/edge-identityandaud= your endpoint's UUID, which is in the endpoint's dashboard URL (/account/endpoints/<uuid>) and inGET /v1/endpoints. - Tokens are valid for 5 minutes.
| Claim | Value |
|---|---|
| sub | The user's UUID on this endpoint (with legacy basic auth: the username). For a request known only by its client certificate: client-cert:<fingerprint> |
| The user's email, when known | |
| preferred_username | The username |
| name | The display name |
| roles | The user's roles on this endpoint |
| auth_method | token, basic, session or client_cert |
| client_cert | The client certificate the edge verified, when there is one: name (certificates from a CA managed by ngris), subject, serial and fingerprint |
| host | The host the visitor asked for. Don't authorise on it: on HTTP/1.1 the client chooses it. Check aud instead |
| iat, nbf, exp | Issued, not before, expiry (5 minutes after issue) |
X-User-*, X-Username, X-Auth-Method, X-Metadata-* and custom forwarding headers a client sends before it adds its own, and it removes X-Ngris-Signature and every X-Ngris-Client-Cert-* header from every incoming request. The edge's session cookie is not passed on unless you choose to forward it.
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(
new URL('https://api.ngris.com/.well-known/ngris-edge-jwks.json'));
export async function ngrisUser(req) {
const token = req.headers['x-ngris-signature'];
if (!token) return null;
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://api.ngris.com/edge-identity',
audience: process.env.NGRIS_ENDPOINT_UUID,
algorithms: ['ES256'],
});
return payload; // sub, email, preferred_username, name, roles
}
import os, jwt
jwks = jwt.PyJWKClient(
"https://api.ngris.com/.well-known/ngris-edge-jwks.json")
def ngris_user(headers):
token = headers["X-Ngris-Signature"]
key = jwks.get_signing_key_from_jwt(token).key
return jwt.decode(token, key, algorithms=["ES256"],
issuer="https://api.ngris.com/edge-identity",
audience=os.environ["NGRIS_ENDPOINT_UUID"])
jwks, err := keyfunc.NewDefault([]string{
"https://api.ngris.com/.well-known/ngris-edge-jwks.json"})
if err != nil { return err }
tok, err := jwt.Parse(r.Header.Get("X-Ngris-Signature"), jwks.Keyfunc,
jwt.WithValidMethods([]string{"ES256"}),
jwt.WithIssuer("https://api.ngris.com/edge-identity"),
jwt.WithAudience(os.Getenv("NGRIS_ENDPOINT_UUID")))
if err != nil || !tok.Valid { /* not from ngris: refuse */ }
Webhook signatures
Every webhook ngris sends is signed the same way: metric-alert webhook channels (including the Test button), Service Desk webhooks and support-desk webhooks. The signature is in one header:
X-Ngris-Signature: sha256=<hex>, the HMAC-SHA256 of the raw request body, keyed by the webhook's secret.
- Sign the body exactly as received, before any JSON parsing: re-serialised JSON gives a different digest.
- Compare in constant time, and refuse a request with a missing or different signature.
- A webhook with no secret is sent without the header.
Test your code with the What arrives example: signed with the secret whsec_0123456789abcdef, that body gives exactly that signature.
The same header name carries the signed identity on requests the edge forwards after edge login. There it holds a JWT, not an HMAC.
POST /hooks/ngris HTTP/1.1
Content-Type: application/json
X-Ngris-Signature: sha256=cac8722329e12029331d705744900f1e1a878be9aeaaad498467e599757d51af
{"event":"ticket.created","id":42}
import crypto from 'node:crypto';
// body: the raw request body (a Buffer),
// before any JSON parsing.
function fromNgris(body, sig, secret) {
const want = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
const got = Buffer.from(sig ?? '');
return got.length === want.length &&
crypto.timingSafeEqual(
got, Buffer.from(want));
}
fromNgris(rawBody,
req.headers['x-ngris-signature'],
process.env.WEBHOOK_SECRET);
import hashlib, hmac, os
# body: the raw request body (bytes),
# before any JSON parsing.
def from_ngris(body, sig, secret):
want = "sha256=" + hmac.new(
secret.encode(), body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(
sig or "", want)
from_ngris(request.get_data(),
request.headers.get(
"X-Ngris-Signature"),
os.environ["WEBHOOK_SECRET"])
// body: the raw request body.
func fromNgris(body []byte,
sig, secret string) bool {
m := hmac.New(sha256.New,
[]byte(secret))
m.Write(body)
want := "sha256=" +
hex.EncodeToString(m.Sum(nil))
return hmac.Equal(
[]byte(sig), []byte(want))
}
body, _ := io.ReadAll(r.Body)
ok := fromNgris(body,
r.Header.Get("X-Ngris-Signature"),
os.Getenv("WEBHOOK_SECRET"))
Static Egress IP
Ngris does not offer static egress IPs. There is no egress setting on endpoints and no egress add-on, dedicated or shared.
With a tunnel you usually don't need one: requests your service makes to a partner leave from your own network, not through Ngris, so the partner allowlists your network's address as usual. Ngris carries only the inbound side, and the agent connects out to the edge, so nothing inbound is opened on your network. Backend, full-stack, function and WordPress apps you deploy on Ngris have no outbound internet access by default. On a paid plan such an app can turn on outbound HTTP/HTTPS (see Outbound network access), and those requests come from Ngris infrastructure, with no fixed source IP.