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
72 changes: 72 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,75 @@ jobs:
kubectl -n netbird-e2e logs deployment/mysql --tail=50 || true
echo "=== Events ==="
kubectl -n netbird-e2e get events --sort-by='.lastTimestamp' || true

e2e-oidc-keycloak:
name: E2E — OIDC (Keycloak)
runs-on: ubuntu-latest
needs: lint-and-unit-test
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Helm
uses: azure/setup-helm@v4
with:
version: v4.0.2

- name: Create kind cluster
uses: helm/kind-action@v1
with:
cluster_name: helms-e2e

- name: Run e2e test (oidc-keycloak)
run: ci/scripts/e2e-oidc.sh keycloak

- name: Show debug info on failure
if: failure()
run: |
echo "=== Pod status ==="
kubectl -n netbird-e2e get pods -o wide || true
echo "=== Server logs ==="
kubectl -n netbird-e2e logs deployment/netbird-e2e-server --all-containers --tail=100 || true
echo "=== Dashboard logs ==="
kubectl -n netbird-e2e logs deployment/netbird-e2e-dashboard --tail=100 || true
echo "=== Keycloak logs ==="
kubectl -n netbird-e2e logs deployment/keycloak --tail=100 || true
echo "=== Events ==="
kubectl -n netbird-e2e get events --sort-by='.lastTimestamp' || true

e2e-oidc-zitadel:
name: E2E — OIDC (Zitadel)
runs-on: ubuntu-latest
needs: lint-and-unit-test
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Helm
uses: azure/setup-helm@v4
with:
version: v4.0.2

- name: Create kind cluster
uses: helm/kind-action@v1
with:
cluster_name: helms-e2e

- name: Run e2e test (oidc-zitadel)
run: ci/scripts/e2e-oidc.sh zitadel

- name: Show debug info on failure
if: failure()
run: |
echo "=== Pod status ==="
kubectl -n netbird-e2e get pods -o wide || true
echo "=== Server logs ==="
kubectl -n netbird-e2e logs deployment/netbird-e2e-server --all-containers --tail=100 || true
echo "=== Dashboard logs ==="
kubectl -n netbird-e2e logs deployment/netbird-e2e-dashboard --tail=100 || true
echo "=== Zitadel logs ==="
kubectl -n netbird-e2e logs deployment/zitadel --tail=100 || true
echo "=== PostgreSQL logs ==="
kubectl -n netbird-e2e logs deployment/zitadel-db --tail=50 || true
echo "=== Events ==="
kubectl -n netbird-e2e get events --sort-by='.lastTimestamp' || true
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

### Added

- **OIDC/SSO configuration**: New `oidc.*` values for structured OIDC/SSO
configuration. When `oidc.enabled: true`, the chart renders `http:`,
`deviceAuthFlow:`, `pkceAuthFlow:`, and `idpConfig:` sections in the
server config.yaml. Supports all NetBird-supported IdP managers: keycloak,
auth0, azure, zitadel, okta, authentik, google, jumpcloud, dex, embedded.
- `oidc.audience`, `oidc.userIdClaim`, `oidc.configEndpoint`,
`oidc.authKeysLocation` for HttpServerConfig fields.
- `oidc.deviceAuthFlow.*` for Device Authorization Flow (RFC 8628) — CLI
clients.
- `oidc.pkceAuthFlow.*` for PKCE Authorization Flow (RFC 7636) — dashboard
and web app clients. Supports both plain-text and secret-ref client secrets.
- `oidc.idpManager.*` for IdP Manager configuration (server-side user/group
sync). Provider-specific credentials rendered under the correct YAML key
based on `oidc.idpManager.managerType` (e.g. `keycloakClientCredentials`,
`auth0ClientCredentials`, `azureClientCredentials`).
- OIDC secret values (`IDP_CLIENT_SECRET`, `PKCE_CLIENT_SECRET`) injected
via Kubernetes Secrets using the existing Initium render pipeline.
- Dashboard `AUTH_AUTHORITY` falls back to `server.config.auth.issuer` when
`dashboard.config.authAuthority` is empty.
- E2E test with Keycloak deployed in-cluster: verifies OIDC middleware,
token acquisition via direct grant, and authenticated API access.
- E2E test with Zitadel + PostgreSQL deployed in-cluster: bootstraps
project/apps/service user via Management API, verifies OIDC middleware,
OIDC discovery, and client_credentials token acquisition.
- Unit tests for OIDC config rendering, secret injection, provider
credentials key mapping, and dashboard fallback (190 tests total).

- **PAT seeding**: Optional Personal Access Token seeding via `pat.*` values.
When `pat.enabled: true`, a service user account and PAT are seeded into
the database using Initium's `seed` command. The seed waits for the server
Expand Down
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: lint unittest e2e e2e-sqlite e2e-postgres e2e-mysql e2e-setup e2e-teardown test
.PHONY: lint unittest e2e e2e-sqlite e2e-postgres e2e-mysql e2e-oidc-keycloak e2e-oidc-zitadel e2e-setup e2e-teardown test

CHARTS := $(wildcard charts/*)

Expand Down Expand Up @@ -35,10 +35,18 @@ e2e-postgres: e2e-setup
e2e-mysql: e2e-setup
ci/scripts/e2e.sh mysql

e2e-oidc-keycloak: e2e-setup
ci/scripts/e2e-oidc.sh keycloak

e2e-oidc-zitadel: e2e-setup
ci/scripts/e2e-oidc.sh zitadel

e2e: e2e-setup
ci/scripts/e2e.sh sqlite
ci/scripts/e2e.sh postgres
ci/scripts/e2e.sh mysql
ci/scripts/e2e-oidc.sh keycloak
ci/scripts/e2e-oidc.sh zitadel

e2e-teardown:
kind delete cluster --name $(E2E_CLUSTER) 2>/dev/null || true
Expand Down
190 changes: 190 additions & 0 deletions charts/netbird/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,152 @@ In both cases, the seed:
curl -H "Authorization: Token nbp_..." https://netbird.example.com/api/groups
```

## OIDC / SSO Configuration

The chart supports structured OIDC configuration for integrating with
external identity providers. When `oidc.enabled: true`, the chart renders
`http:`, `deviceAuthFlow:`, `pkceAuthFlow:`, and `idpConfig:` sections into
the server config.yaml.

### Keycloak Example

```yaml
server:
config:
auth:
issuer: "https://keycloak.example.com/realms/netbird"

oidc:
enabled: true
audience: "netbird"
userIdClaim: "sub"
configEndpoint: "https://keycloak.example.com/realms/netbird/.well-known/openid-configuration"

deviceAuthFlow:
enabled: true
provider: "keycloak"
providerConfig:
clientId: "netbird-client"
domain: "keycloak.example.com"
tokenEndpoint: "https://keycloak.example.com/realms/netbird/protocol/openid-connect/token"
deviceAuthEndpoint: "https://keycloak.example.com/realms/netbird/protocol/openid-connect/auth/device"
scope: "openid profile email"

pkceAuthFlow:
enabled: true
providerConfig:
clientId: "netbird-dashboard"
authorizationEndpoint: "https://keycloak.example.com/realms/netbird/protocol/openid-connect/auth"
tokenEndpoint: "https://keycloak.example.com/realms/netbird/protocol/openid-connect/token"
scope: "openid profile email groups offline_access"
redirectUrls:
- "https://netbird.example.com/nb-auth"
- "https://netbird.example.com/nb-silent-auth"

idpManager:
enabled: true
managerType: "keycloak"
clientConfig:
issuer: "https://keycloak.example.com/realms/netbird"
tokenEndpoint: "https://keycloak.example.com/realms/netbird/protocol/openid-connect/token"
clientId: "netbird-backend"
clientSecret:
secretName: keycloak-client-secret
secretKey: clientSecret
grantType: "client_credentials"
```

### Auth0 Example

```yaml
oidc:
enabled: true
audience: "netbird-api"
deviceAuthFlow:
enabled: true
provider: "auth0"
providerConfig:
clientId: "<spa-client-id>"
domain: "<tenant>.auth0.com"
audience: "netbird-api"
idpManager:
enabled: true
managerType: "auth0"
clientConfig:
issuer: "https://<tenant>.auth0.com/"
tokenEndpoint: "https://<tenant>.auth0.com/oauth/token"
clientId: "<m2m-client-id>"
clientSecret:
secretName: auth0-client-secret
secretKey: clientSecret
grantType: "client_credentials"
providerConfig:
Audience: "https://<tenant>.auth0.com/api/v2/"
AuthIssuer: "https://<tenant>.auth0.com/"
```

### Azure Entra ID Example

```yaml
oidc:
enabled: true
audience: "api://<application-id>"
userIdClaim: "oid"
deviceAuthFlow:
enabled: true
provider: "azure"
providerConfig:
clientId: "<client-id>"
domain: "login.microsoftonline.com"
audience: "api://<application-id>"
idpManager:
enabled: true
managerType: "azure"
clientConfig:
issuer: "https://login.microsoftonline.com/<tenant-id>/v2.0"
tokenEndpoint: "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token"
clientId: "<client-id>"
clientSecret:
secretName: azure-client-secret
secretKey: clientSecret
grantType: "client_credentials"
providerConfig:
ObjectID: "<service-principal-object-id>"
GraphAPIEndpoint: "https://graph.microsoft.com"
```

### Secret Injection

OIDC client secrets are injected via Kubernetes Secrets and never stored in
ConfigMaps. Create a Secret for your IdP manager client:

```bash
kubectl create secret generic keycloak-client-secret \
--from-literal=clientSecret='your-client-secret' \
-n netbird
```

The chart injects the secret as an environment variable (`IDP_CLIENT_SECRET`
or `PKCE_CLIENT_SECRET`) in the config-init container, and references it in
the config template as `${IDP_CLIENT_SECRET}` / `${PKCE_CLIENT_SECRET}`.

### Dashboard Auto-Derivation

When `dashboard.config.authAuthority` is empty, the dashboard automatically
uses `server.config.auth.issuer` as the OIDC authority. You should still set
`dashboard.config.authClientId` and `dashboard.config.authAudience` explicitly.

### Manual Testing for SaaS Providers

Providers that cannot be deployed in-cluster (Azure Entra ID, Auth0, Okta,
ADFS) can be tested manually:

1. Configure the provider with appropriate app registrations
2. Create Kubernetes Secrets with the client credentials
3. Install the chart with provider-specific OIDC values
4. Verify: `kubectl logs` shows the server connecting to the IdP,
`curl -H "Authorization: Bearer <token>" .../api/users` returns 200

## Values Reference

### Global
Expand All @@ -263,6 +409,50 @@ curl -H "Authorization: Token nbp_..." https://netbird.example.com/api/groups
| `database.passwordSecret.secretKey` | string | `"password"` | Key in the Secret |
| `database.sslMode` | string | `"disable"` | SSL mode for PostgreSQL (ignored for mysql/sqlite) |

### OIDC / SSO

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `oidc.enabled` | bool | `false` | Enable OIDC configuration |
| `oidc.audience` | string | `""` | JWT audience claim (HttpServerConfig.AuthAudience) |
| `oidc.userIdClaim` | string | `""` | JWT user ID claim (default: "sub") |
| `oidc.configEndpoint` | string | `""` | OIDC discovery endpoint URL |
| `oidc.authKeysLocation` | string | `""` | JWT keys location URL (JWKS) |
| `oidc.deviceAuthFlow.enabled` | bool | `false` | Enable device authorization flow (CLI) |
| `oidc.deviceAuthFlow.provider` | string | `"hosted"` | Device auth provider name |
| `oidc.deviceAuthFlow.providerConfig.clientId` | string | `""` | Client ID for CLI app |
| `oidc.deviceAuthFlow.providerConfig.clientSecret` | string | `""` | Client secret (usually empty for public) |
| `oidc.deviceAuthFlow.providerConfig.domain` | string | `""` | Provider domain |
| `oidc.deviceAuthFlow.providerConfig.audience` | string | `""` | Audience for token validation |
| `oidc.deviceAuthFlow.providerConfig.tokenEndpoint` | string | `""` | Token endpoint override |
| `oidc.deviceAuthFlow.providerConfig.deviceAuthEndpoint` | string | `""` | Device auth endpoint override |
| `oidc.deviceAuthFlow.providerConfig.scope` | string | `"openid"` | OAuth2 scopes |
| `oidc.deviceAuthFlow.providerConfig.useIdToken` | bool | `false` | Use ID token instead of access token |
| `oidc.pkceAuthFlow.enabled` | bool | `false` | Enable PKCE authorization flow (dashboard) |
| `oidc.pkceAuthFlow.providerConfig.clientId` | string | `""` | Client ID for dashboard app |
| `oidc.pkceAuthFlow.providerConfig.clientSecret.value` | string | `""` | Plain-text client secret |
| `oidc.pkceAuthFlow.providerConfig.clientSecret.secretName` | string | `""` | Secret name for client secret |
| `oidc.pkceAuthFlow.providerConfig.clientSecret.secretKey` | string | `"clientSecret"` | Key in Secret |
| `oidc.pkceAuthFlow.providerConfig.domain` | string | `""` | Provider domain |
| `oidc.pkceAuthFlow.providerConfig.audience` | string | `""` | Audience |
| `oidc.pkceAuthFlow.providerConfig.authorizationEndpoint` | string | `""` | Authorization endpoint override |
| `oidc.pkceAuthFlow.providerConfig.tokenEndpoint` | string | `""` | Token endpoint override |
| `oidc.pkceAuthFlow.providerConfig.scope` | string | `"openid profile email"` | OAuth2 scopes |
| `oidc.pkceAuthFlow.providerConfig.redirectUrls` | list | `[]` | Allowed redirect URLs |
| `oidc.pkceAuthFlow.providerConfig.useIdToken` | bool | `false` | Use ID token |
| `oidc.pkceAuthFlow.providerConfig.disablePromptLogin` | bool | `false` | Disable login prompt |
| `oidc.pkceAuthFlow.providerConfig.loginFlag` | int | `0` | Login flag value |
| `oidc.idpManager.enabled` | bool | `false` | Enable IdP manager for user sync |
| `oidc.idpManager.managerType` | string | `""` | Manager type (keycloak, auth0, azure, zitadel, okta, etc.) |
| `oidc.idpManager.clientConfig.issuer` | string | `""` | OIDC issuer for management API |
| `oidc.idpManager.clientConfig.tokenEndpoint` | string | `""` | Token endpoint |
| `oidc.idpManager.clientConfig.clientId` | string | `""` | Client ID |
| `oidc.idpManager.clientConfig.clientSecret.secretName` | string | `""` | Secret name for client secret |
| `oidc.idpManager.clientConfig.clientSecret.secretKey` | string | `"clientSecret"` | Key in Secret |
| `oidc.idpManager.clientConfig.grantType` | string | `"client_credentials"` | OAuth2 grant type |
| `oidc.idpManager.extraConfig` | object | `{}` | Provider-specific extra config |
| `oidc.idpManager.providerConfig` | object | `{}` | Provider-specific credentials |

### PAT (Personal Access Token)

| Key | Type | Default | Description |
Expand Down
Loading