Skip to content
NebulaCtrldocs

Set up sign-in and recover access

Register NebulaCtrl as an OpenID Connect client, apply the settings, change identity provider, and regain access after a lockout.

NebulaCtrl has no local passwords, no sign-up form and no second factor of its own. One external OpenID Connect (OIDC) provider authenticates everyone, and each installation is configured with exactly one issuer. This runbook sets that provider up, changes it, and recovers when nobody can sign in.

Before you start

  • Admin access to your identity provider, to create a client and read a user's sub claim.
  • The final public URL of the control plane. The redirect URI contains it.
  • A copy of .env. The installer also keeps .env.bak-TIMESTAMP beside it whenever it changes the file.
  • The rollback. Every change below edits values in .env. To undo one, put the previous values back (the copy or the .bak file) and run the same command again.

1. Create the client at your provider

SettingValue
Client typeWeb application with a client secret (a confidential client).
FlowAuthorization code with PKCE (S256). NebulaCtrl also sends state and nonce.
Redirect URIBASE_URL/api/v1/auth/callback, exactly.
Scopesopenid profile email.
IssuerAn https:// URL whose /.well-known/openid-configuration answers, with a certificate the control plane host trusts.

The ID token must carry these claims:

ClaimRequirement
subRequired. Identity is the issuer plus sub, never the email, so a recycled email address cannot inherit an account.
emailRequired.
email_verifiedfalse refuses the sign-in. It must be true for an invitation or a SCIM grant to bind to the person.
name, pictureOptional.
amrOptional. Needed for Require two-factor for everyone.

If the provider's discovery document lists an end_session_endpoint, Sign out also ends the session at the provider.

A plain http:// issuer is refused unless NEBULA_OIDC_INSECURE_HTTP is true, which the installer sets for you. Use that for development only.

2. Give NebulaCtrl the client

The installer takes the issuer, client ID and client secret, and writes them to .env. Pass the secret in the environment, because a flag is visible to every process on the host:

curl -fsSL https://get.nebulactrl.dev | NEBULA_OIDC_CLIENT_SECRET=CLIENT_SECRET sh -s -- --dir /opt/nebula --oidc-issuer ISSUER --oidc-client-id CLIENT_ID --yes

CLIENT_SECRET, ISSUER and CLIENT_ID come from your provider. The installer warns when the issuer does not answer discovery, rewrites .env, restarts the container and waits for it to become healthy. Add --oidc-provider-name NAME to change the label on the sign-in button.

You can also edit /opt/nebula/.env and restart the container:

docker compose -p nebula -f /opt/nebula/deploy/compose.prod.yaml --env-file /opt/nebula/.env --profile local-db up -d nebula

Omit --profile local-db when you use an external database. The settings are NEBULA_OIDC_ISSUER, NEBULA_OIDC_CLIENT_ID, NEBULA_OIDC_CLIENT_SECRET, NEBULA_OIDC_PROVIDER_NAME, NEBULA_OIDC_SCOPES and NEBULA_OIDC_INSECURE_HTTP.

The standard Compose file does not pass --oidc-acr-values to the container. That flag sends an acr_values parameter on every sign-in, for a provider that reports a second factor only when it is asked for a stronger context. To use it, install with a Compose file of your own that sets NEBULA_OIDC_ACR_VALUES, using --compose-file. Console updates then replace your file, so update that install with the installer.

3. Sign in

Open the public URL and select Continue with the provider's name. On a new installation, the first person to sign in becomes the owner of an organization named Default. Everyone after that needs an invitation or a SCIM grant, or they see "signed in successfully, but has no invitation to an organization on this installation". See Manage members and teams.

Verify

  • The sign-in page shows Continue with and your provider's name.
  • After you sign in, you land in the console with the owner role.
  • The control plane log has no discover OIDC issuer error:
docker compose -p nebula -f /opt/nebula/deploy/compose.prod.yaml --env-file /opt/nebula/.env --profile local-db logs --tail 40 nebula

Change identity provider

Identity is the issuer plus sub. The database stores it in app_user.subject as the issuer without a trailing slash, a pipe, and the sub: https://id.example.com|248289761001. A new issuer produces different subjects, so on first sign-in your existing people would look like strangers and be refused for having no invitation. Rewrite their subjects before they sign in through the new provider.

  1. Back up the database. See Back up and restore the control plane.

  2. Create the client at the new provider and apply it as in step 2.

  3. If the new issuer gives the same sub values (the same provider at a new address), swap the prefix for everyone:

    docker compose -p nebula -f /opt/nebula/deploy/compose.prod.yaml --env-file /opt/nebula/.env --profile local-db exec -T postgres psql -U nebula -d nebula -c "UPDATE app_user SET subject = 'NEW_ISSUER|' || substr(subject, length('OLD_ISSUER|') + 1) WHERE starts_with(subject, 'OLD_ISSUER|')"

    Write both issuers without a trailing slash.

  4. If the sub values differ, set each person's new subject. Read a person's sub from your provider's user record or from their decoded ID token:

    docker compose -p nebula -f /opt/nebula/deploy/compose.prod.yaml --env-file /opt/nebula/.env --profile local-db exec -T postgres psql -U nebula -d nebula -c "UPDATE app_user SET subject = 'NEW_ISSUER|NEW_SUB' WHERE email = '[email protected]'"

    Anyone you do not update needs a new invitation.

  5. Sign in through the new provider and confirm you are still the owner.

Browser sessions survive an issuer change and last up to 30 days from sign-in. To end all of them, run DELETE FROM session the same way.

Direct database changes leave no audit event

The statements on this page change the database without going through the control plane, so the audit log records nothing for them. Note what you changed and when.

If it goes wrong

The control plane does not start and logs discover OIDC issuer. The issuer URL, its TLS certificate or its discovery document is wrong or unreachable from the host. Check curl -fsS ISSUER/.well-known/openid-configuration on the host, correct .env, and start the container again.

The callback answers exchange OIDC authorization code or verify ID token. The provider rejected the code exchange, or the ID token failed verification. Check the redirect URI at the provider against BASE_URL/api/v1/auth/callback, the client ID and secret in .env, and that the token's audience is the client ID.

The callback answers the sign-in link expired or its state did not match; start again. The sign-in must finish within 10 minutes, in the same browser that started it. Start again from the sign-in page.

"identity provider reports the email is not verified" or "identity provider returned no email or subject". Configure the provider to return email and email_verified: true.

A person you invited still sees "has no invitation". They signed in with a different address than the one you invited, or the provider did not report the address as verified. Invite the address they sign in with.

Everyone is refused after you turned on "Require two-factor for everyone". The provider does not report a second factor in amr. Turn the requirement off in the database:

docker compose -p nebula -f /opt/nebula/deploy/compose.prod.yaml --env-file /opt/nebula/.env --profile local-db exec -T postgres psql -U nebula -d nebula -c "UPDATE organization SET require_mfa = false WHERE slug = 'default'"

Use the organization's slug if it is not default. The change takes effect on the next request.

The only owner can no longer sign in. Make another member the owner:

docker compose -p nebula -f /opt/nebula/deploy/compose.prod.yaml --env-file /opt/nebula/.env --profile local-db exec -T postgres psql -U nebula -d nebula -c "UPDATE membership SET role = 'owner' WHERE user_id = (SELECT id FROM app_user WHERE email = '[email protected]') AND organization_id = (SELECT id FROM organization WHERE slug = 'default')"

On this page