diff --git a/Chart.yaml b/Chart.yaml index 6fb3f6f..64f90a4 100644 --- a/Chart.yaml +++ b/Chart.yaml @@ -15,12 +15,12 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 2.2.1 +version: 2.3.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "2.3.0" +appVersion: "2.4.0-rc.18" icon: "https://authorizer.dev/images/logo.png" diff --git a/README.md b/README.md index e6a5f55..0532ad5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Helm chart for [Authorizer](https://authorizer.dev) — an open-source, self-hos This chart deploys the Authorizer binary as a Kubernetes `Deployment`, wires up a `Service` (HTTP + optional gRPC port), optional metrics infrastructure, and exposes all server flags as `values.yaml` keys. -Chart version: **2.2.1** | App version: **2.3.0** +Chart version: **2.3.0** | App version: **2.4.0-rc.18** ## Getting Started @@ -186,6 +186,7 @@ The metrics port is never added to the main `Service` used for Ingress. Use `met | ---- | ----------- | -------- | ------- | | `authorizer.jwt_type` | JWT signing algorithm (e.g. `HS256`, `RS256`, `ES256`) | false | — | | `authorizer.jwt_secret` | Secret for HMAC-based JWT signing | false | — | +| `authorizer.encryption_key` | Encrypts TOTP secrets and OTP digests at rest. **Required when `jwt_type` is `RS*`/`ES*`** — there is no `jwt_secret` to fall back to and the server refuses to start. Generate once with `openssl rand -hex 32` and keep it stable; changing it makes existing TOTP enrolments undecryptable | false | — | | `authorizer.jwt_private_key` | Private key for RSA/EC-based JWT signing | false | — | | `authorizer.jwt_public_key` | Public key for RSA/EC-based JWT verification | false | — | | `authorizer.jwt_role_claim` | Custom claim name for roles in the JWT | false | — | diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl index 7fc608d..7d9415d 100644 --- a/templates/_helpers.tpl +++ b/templates/_helpers.tpl @@ -60,3 +60,23 @@ Create the name of the service account to use {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} + +{{/* +Render a boolean value, honouring an explicit `false`. + +`{{ .Values.x | default true }}` is WRONG for booleans: Go template `default` +substitutes whenever the value is *empty*, and `false` is empty — so a user +setting `false` silently gets `true` and has no way to turn the flag off. That +made 12 flags unsettable, including enable_playground, +enable_graphql_introspection, enable_grpc_reflection and enable_signup, i.e. +exactly the ones an operator hardens a production deployment with. + +Only a genuinely absent value (nil / "invalid" kind) falls back to the default. + +Usage: {{ include "authorizer.bool" (list .Values.authorizer.enable_signup true) }} +*/}} +{{- define "authorizer.bool" -}} +{{- $value := index . 0 -}} +{{- $default := index . 1 -}} +{{- if kindIs "invalid" $value }}{{ $default }}{{ else }}{{ $value }}{{ end -}} +{{- end -}} diff --git a/templates/deployment.yaml b/templates/deployment.yaml index b4aba5b..7772a17 100644 --- a/templates/deployment.yaml +++ b/templates/deployment.yaml @@ -1,3 +1,18 @@ +{{/* +2.4.0 makes `--enable-email-verification` with no working SMTP a FATAL boot +error, not a warning: every recovery route terminates at the same mailbox, so +without a mail path a user is created unverified and can never recover. + +Caught at render time rather than letting it through, because the failure mode +in-cluster is a CrashLoopBackOff whose cause is one line in the container log. +The condition mirrors the server's IsEmailServiceEnabled exactly — host set, +port > 0, sender email set — so this never rejects a config the server accepts. +*/}} +{{- if .Values.authorizer.enable_email_verification }} + {{- if not (and .Values.authorizer.smtp_host (gt (int .Values.authorizer.smtp_port) 0) .Values.authorizer.smtp_sender_email) }} + {{- fail "authorizer.enable_email_verification is true but SMTP is incomplete. Authorizer 2.4.0 exits at boot in this configuration (users would be created unverified with no way to ever verify), so the pod would CrashLoopBackOff. Set authorizer.smtp_host, authorizer.smtp_port and authorizer.smtp_sender_email, or set enable_email_verification: false." }} + {{- end }} +{{- end }} apiVersion: apps/v1 kind: Deployment metadata: @@ -49,12 +64,16 @@ spec: --database-cert-key="${DATABASE_CERT_KEY}" \ --fga-store="${FGA_STORE}" \ --fga-store-url="${FGA_STORE_URL}" \ + --fga-allow-unconstrained-agents="${FGA_ALLOW_UNCONSTRAINED_AGENTS:-false}" \ --client-id="${CLIENT_ID}" \ --client-secret="${CLIENT_SECRET}" \ --admin-secret="${ADMIN_SECRET}" \ --redis-url="${REDIS_URL}" \ --jwt-type="${JWT_TYPE}" \ --jwt-secret="${JWT_SECRET}" \ + {{- if .Values.authorizer.encryption_key }} + --encryption-key="${ENCRYPTION_KEY}" \ + {{- end }} --jwt-private-key="${JWT_PRIVATE_KEY}" \ --jwt-public-key="${JWT_PUBLIC_KEY}" \ --jwt-role-claim="${JWT_ROLE_CLAIM}" \ @@ -69,6 +88,7 @@ spec: --allowed-origins="${ALLOWED_ORIGINS}" \ --default-authorize-response-type="${DEFAULT_AUTHORIZE_RESPONSE_TYPE}" \ --default-authorize-response-mode="${DEFAULT_AUTHORIZE_RESPONSE_MODE}" \ + --oauth2-1-strict="${OAUTH2_1_STRICT:-false}" \ --organization-name="${ORGANIZATION_NAME}" \ --organization-logo="${ORGANIZATION_LOGO}" \ --smtp-host="${SMTP_HOST}" \ @@ -81,6 +101,7 @@ spec: --smtp-skip-tls-verification="${SMTP_SKIP_TLS_VERIFICATION:-false}" \ --reset-password-url="${RESET_PASSWORD_URL}" \ --backchannel-logout-uri="${BACKCHANNEL_LOGOUT_URI}" \ + --url="${AUTHORIZER_URL}" \ --env="${ENV}" \ --host="${HOST:-0.0.0.0}" \ --http-port="${PORT:-8080}" \ @@ -104,6 +125,7 @@ spec: --graphql-max-body-bytes="${GRAPHQL_MAX_BODY_BYTES:-1048576}" \ --log-level="${LOG_LEVEL:-info}" \ --enable-login-page="${ENABLE_LOGIN_PAGE:-true}" \ + --enable-org-discovery="${ENABLE_ORG_DISCOVERY:-false}" \ --enable-playground="${ENABLE_PLAYGROUND:-true}" \ --disable-admin-header-auth="${DISABLE_ADMIN_HEADER_AUTH:-true}" \ --enable-graphql-introspection="${ENABLE_GRAPHQL_INTROSPECTION:-true}" \ @@ -116,11 +138,12 @@ spec: --enable-mobile-basic-authentication="${ENABLE_MOBILE_BASIC_AUTHENTICATION:-true}" \ --enable-phone-verification="${ENABLE_PHONE_VERIFICATION:-false}" \ --enable-magic-link-login="${ENABLE_MAGIC_LINK_LOGIN:-false}" \ - --enable-totp-login="${ENABLE_TOTP_LOGIN:-false}" \ - --enforce-mfa="${ENFORCE_MFA:-true}" \ - --enable-mfa="${ENABLE_MFA:-false}" \ - --enable-email-otp="${ENABLE_EMAIL_OTP:-false}" \ - --enable-sms-otp="${ENABLE_SMS_OTP:-false}" \ + --enforce-mfa="${ENFORCE_MFA:-false}" \ + --disable-totp-login="${DISABLE_TOTP_LOGIN:-false}" \ + --disable-webauthn-mfa="${DISABLE_WEBAUTHN_MFA:-false}" \ + --disable-email-otp="${DISABLE_EMAIL_OTP:-false}" \ + --disable-sms-otp="${DISABLE_SMS_OTP:-false}" \ + --disable-mfa="${DISABLE_MFA:-false}" \ --enable-signup="${ENABLE_SIGNUP:-true}" \ --twilio-account-sid="${TWILIO_ACCOUNT_SID}" \ --twilio-api-key="${TWILIO_API_KEY}" \ @@ -144,7 +167,9 @@ spec: --microsoft-client-id="${MICROSOFT_CLIENT_ID}" \ --microsoft-client-secret="${MICROSOFT_CLIENT_SECRET}" \ --microsoft-tenant-id="${MICROSOFT_TENANT_ID}" \ + --microsoft-allowed-tenants="${MICROSOFT_ALLOWED_TENANTS}" \ --microsoft-scopes="${MICROSOFT_SCOPES}" \ + --oauth-allow-unverified-provider-email="${OAUTH_ALLOW_UNVERIFIED_PROVIDER_EMAIL:-false}" \ --apple-client-id="${APPLE_CLIENT_ID}" \ --apple-client-secret="${APPLE_CLIENT_SECRET}" \ --apple-scopes="${APPLE_SCOPES}" \ @@ -255,6 +280,13 @@ spec: name: jwt-secret key: jwt_secret {{- end }} + {{- if .Values.authorizer.encryption_key }} + - name: "ENCRYPTION_KEY" + valueFrom: + secretKeyRef: + name: encryption-key + key: encryption_key + {{- end }} {{- if .Values.authorizer.jwt_private_key }} - name: "JWT_PRIVATE_KEY" valueFrom: @@ -304,7 +336,7 @@ spec: value: "{{ .Values.authorizer.couchbase_bucket }}" {{- end }} {{- if .Values.authorizer.couchbase_bucket_ram_quota }} - - name: "COUCHBASE_BUCKET_RAM_QUOTA" + - name: "COUCHBASE_RAM_QUOTA" value: "{{ .Values.authorizer.couchbase_bucket_ram_quota }}" {{- end }} {{- if .Values.authorizer.couchbase_scope }} @@ -315,12 +347,20 @@ spec: - name: "AUTHORIZER_URL" value: "{{ .Values.authorizer.authorizer_url }}" {{- end }} + - name: "HOST" + value: {{ .Values.authorizer.host | default "0.0.0.0" | quote }} - name: "PORT" value: "{{ .Values.authorizer.http_port | default 8080 }}" - name: "METRICS_PORT" value: "{{ .Values.authorizer.metrics_port | default 8081 }}" - name: "METRICS_HOST" value: {{ .Values.authorizer.metrics_host | default "0.0.0.0" | quote }} + - name: "LOG_LEVEL" + value: {{ .Values.authorizer.log_level | default "info" | quote }} + {{- if .Values.authorizer.env }} + - name: "ENV" + value: {{ .Values.authorizer.env | quote }} + {{- end }} - name: "RATE_LIMIT_RPS" value: {{ .Values.authorizer.rate_limit_rps | default 30 | toString | quote }} - name: "RATE_LIMIT_BURST" @@ -331,7 +371,7 @@ spec: - name: "GRPC_PORT" value: {{ .Values.authorizer.grpc_port | default 9091 | toString | quote }} - name: "ENABLE_GRPC_REFLECTION" - value: {{ .Values.authorizer.enable_grpc_reflection | default true | toString | quote }} + value: {{ include "authorizer.bool" (list .Values.authorizer.enable_grpc_reflection true) | quote }} - name: "GRPC_INSECURE" value: {{ .Values.authorizer.grpc_insecure | default false | toString | quote }} {{- if .Values.authorizer.grpc_tls_cert }} @@ -420,6 +460,53 @@ spec: value: {{ .Values.authorizer.graphql_max_aliases | default 30 | toString | quote }} - name: "GRAPHQL_MAX_BODY_BYTES" value: {{ int64 (default 1048576 .Values.authorizer.graphql_max_body_bytes) | toString | quote }} + - name: "OAUTH2_1_STRICT" + value: {{ .Values.authorizer.oauth2_1_strict | default false | toString | quote }} + + # HTTP routes / cookies / auth feature toggles. See values.yaml + # for the operational notes attached to each one. + - name: "ENABLE_LOGIN_PAGE" + value: {{ include "authorizer.bool" (list .Values.authorizer.enable_login_page true) | quote }} + - name: "ENABLE_ORG_DISCOVERY" + value: {{ .Values.authorizer.enable_org_discovery | default false | toString | quote }} + - name: "ENABLE_PLAYGROUND" + value: {{ include "authorizer.bool" (list .Values.authorizer.enable_playground true) | quote }} + - name: "ENABLE_GRAPHQL_INTROSPECTION" + value: {{ include "authorizer.bool" (list .Values.authorizer.enable_graphql_introspection true) | quote }} + - name: "DISABLE_ADMIN_HEADER_AUTH" + value: {{ include "authorizer.bool" (list .Values.authorizer.disable_admin_header_auth true) | quote }} + - name: "APP_COOKIE_SECURE" + value: {{ include "authorizer.bool" (list .Values.authorizer.app_cookie_secure true) | quote }} + - name: "APP_COOKIE_SAME_SITE" + value: {{ .Values.authorizer.app_cookie_same_site | default "none" | quote }} + - name: "ADMIN_COOKIE_SECURE" + value: {{ include "authorizer.bool" (list .Values.authorizer.admin_cookie_secure true) | quote }} + - name: "ENABLE_STRONG_PASSWORD" + value: {{ include "authorizer.bool" (list .Values.authorizer.enable_strong_password true) | quote }} + - name: "ENABLE_BASIC_AUTHENTICATION" + value: {{ include "authorizer.bool" (list .Values.authorizer.enable_basic_authentication true) | quote }} + - name: "ENABLE_EMAIL_VERIFICATION" + value: {{ .Values.authorizer.enable_email_verification | default false | toString | quote }} + - name: "ENABLE_MOBILE_BASIC_AUTHENTICATION" + value: {{ include "authorizer.bool" (list .Values.authorizer.enable_mobile_basic_authentication true) | quote }} + - name: "ENABLE_PHONE_VERIFICATION" + value: {{ .Values.authorizer.enable_phone_verification | default false | toString | quote }} + - name: "ENABLE_MAGIC_LINK_LOGIN" + value: {{ .Values.authorizer.enable_magic_link_login | default false | toString | quote }} + - name: "ENABLE_SIGNUP" + value: {{ include "authorizer.bool" (list .Values.authorizer.enable_signup true) | quote }} + - name: "ENFORCE_MFA" + value: {{ include "authorizer.bool" (list .Values.authorizer.enforce_mfa false) | quote }} + - name: "DISABLE_TOTP_LOGIN" + value: {{ .Values.authorizer.disable_totp_login | default false | toString | quote }} + - name: "DISABLE_WEBAUTHN_MFA" + value: {{ .Values.authorizer.disable_webauthn_mfa | default false | toString | quote }} + - name: "DISABLE_EMAIL_OTP" + value: {{ .Values.authorizer.disable_email_otp | default false | toString | quote }} + - name: "DISABLE_SMS_OTP" + value: {{ .Values.authorizer.disable_sms_otp | default false | toString | quote }} + - name: "DISABLE_MFA" + value: {{ .Values.authorizer.disable_mfa | default false | toString | quote }} # FGA (OpenFGA) integration {{- if .Values.authorizer.fga_store }} @@ -436,6 +523,11 @@ spec: name: fga-store-url key: fga_store_url {{- end }} + # 2.4.0: a delegated (agent-acting-for-user) FGA check against a model + # with no `type agent` now DENIES rather than silently authorizing as + # the user alone. Set true only while migrating a model. + - name: "FGA_ALLOW_UNCONSTRAINED_AGENTS" + value: {{ include "authorizer.bool" (list .Values.authorizer.fga_allow_unconstrained_agents false) | quote }} # JWT additional config {{- if .Values.authorizer.jwt_role_claim }} @@ -605,10 +697,19 @@ spec: - name: "MICROSOFT_TENANT_ID" value: "{{ .Values.authorizer.microsoft_tenant_id }}" {{- end }} + {{- if .Values.authorizer.microsoft_allowed_tenants }} + - name: "MICROSOFT_ALLOWED_TENANTS" + value: "{{ .Values.authorizer.microsoft_allowed_tenants }}" + {{- end }} {{- if .Values.authorizer.microsoft_scopes }} - name: "MICROSOFT_SCOPES" value: "{{ .Values.authorizer.microsoft_scopes }}" {{- end }} + # 2.4.0 nOAuth defense: a social login whose provider did not attest + # the email no longer links to an existing account. This is the + # compatibility escape hatch; the server warns on every boot when set. + - name: "OAUTH_ALLOW_UNVERIFIED_PROVIDER_EMAIL" + value: {{ include "authorizer.bool" (list .Values.authorizer.oauth_allow_unverified_provider_email false) | quote }} {{- if .Values.authorizer.apple_client_id }} - name: "APPLE_CLIENT_ID" valueFrom: diff --git a/templates/secrets.yaml b/templates/secrets.yaml index b24c164..fda5b28 100644 --- a/templates/secrets.yaml +++ b/templates/secrets.yaml @@ -118,6 +118,16 @@ type: Opaque data: jwt_secret: "{{ .Values.authorizer.jwt_secret | b64enc }}" {{- end }} +{{- if .Values.authorizer.encryption_key }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: encryption-key +type: Opaque +data: + encryption_key: "{{ .Values.authorizer.encryption_key | b64enc }}" +{{- end }} {{- if .Values.authorizer.jwt_private_key }} --- apiVersion: v1 diff --git a/values.yaml b/values.yaml index 0cbeb5e..adffaf6 100644 --- a/values.yaml +++ b/values.yaml @@ -92,6 +92,9 @@ redis: storage: null authorizer: + # Host address to listen on (--host). + host: "0.0.0.0" + # Main HTTP listen port (--http-port). Must differ from metrics_port. http_port: 8080 @@ -101,6 +104,12 @@ authorizer: # Bind address for /metrics (--metrics-host). Use 0.0.0.0 for in-cluster scraping. metrics_host: "0.0.0.0" + # Log level (--log-level): debug, info, warn, error. + log_level: "info" + + # Environment label for this instance (--env), e.g. "production", "staging". + env: null + # gRPC server (--grpc-port, --enable-grpc-reflection, --grpc-insecure, # --grpc-tls-cert, --grpc-tls-key). The same public API is served over gRPC; # the REST gateway (/v1/*) wraps it in-process. Must differ from http_port @@ -200,6 +209,21 @@ authorizer: # JWT secret (for HMAC-based JWT) jwt_secret: null + # Key used to encrypt secrets AT REST (TOTP seeds) and to HMAC the OTP + # digests used by email/SMS verification and password reset. + # + # Falls back to jwt_secret when unset, so HMAC (HS*) installs need not set + # it. RSA/ECDSA (RS*/ES*) installs have no jwt_secret to fall back to, and + # the server REFUSES TO START without this — an empty key would derive a + # publicly computable constant, leaving those secrets effectively + # unprotected. + # + # Set it explicitly even on HMAC installs: while this and jwt_secret are the + # same value, rotating jwt_secret silently locks out every enrolled TOTP + # user, because the at-rest key changes with it and there is no + # re-encryption path. + encryption_key: null + # JWT private key (for RSA/EC-based JWT) jwt_private_key: null @@ -272,9 +296,58 @@ authorizer: graphql_max_aliases: 30 graphql_max_body_bytes: 1048576 + # ---------------------------------------------------------------------- + # HTTP routes / cookies / auth feature toggles + # ---------------------------------------------------------------------- + + enable_login_page: true + # Public organization (home-realm) discovery endpoint + /app email-first + # SSO routing step (--enable-org-discovery). Opt-in; off keeps the login + # page unchanged. + enable_org_discovery: false + enable_playground: true + enable_graphql_introspection: true + + # Disable admin authentication via the X-Authorizer-Admin-Secret header + # (--disable-admin-header-auth). true (default here) requires session/ + # cookie-based admin auth instead of the static header secret. + disable_admin_header_auth: true + + app_cookie_secure: true + # SameSite attribute for session cookies (lax, strict, none). + app_cookie_same_site: "none" + admin_cookie_secure: true + + enable_strong_password: true + enable_basic_authentication: true + enable_email_verification: false + enable_mobile_basic_authentication: true + enable_phone_verification: false + enable_magic_link_login: false + enable_signup: true + + # MFA (--enforce-mfa, --disable-*). MFA methods are enabled by default and + # opted out via disable_*; enforce_mfa forces enrollment for all users. + # Matches the server default since 2.4.0, which flipped this from true to + # false: MFA is offered and skippable rather than mandatory. The chart used + # to hardcode the old `true`, silently forcing every Helm deployment into + # mandatory enrollment with no way to decline. + enforce_mfa: false + disable_totp_login: false + disable_webauthn_mfa: false + disable_email_otp: false + disable_sms_otp: false + disable_mfa: false + # FGA (OpenFGA) integration fga_store: null fga_store_url: null + # 2.4.0: a delegated (agent-acting-for-user) check against an authorization + # model with no `type agent` now DENIES instead of authorizing as the + # delegating user alone — that silently dropped the agent half of the + # permission intersection. Set true only while migrating a model; the server + # logs every use. Prefer adding `type agent` to the model. + fga_allow_unconstrained_agents: false # JWT additional config jwt_role_claim: null @@ -296,6 +369,11 @@ authorizer: default_authorize_response_type: null default_authorize_response_mode: null + # Enforce OAuth 2.1 restrictions (--oauth2-1-strict): reject the + # implicit/hybrid-with-token response types and PKCE plain (require S256). + # Breaking; opt-in. + oauth2_1_strict: false + # Branding organization_name: null organization_logo: null @@ -324,8 +402,21 @@ authorizer: microsoft_client_id: null microsoft_client_secret: null microsoft_tenant_id: null + # Entra tenant IDs allowed to sign in when microsoft_tenant_id is a + # multi-tenant alias (common/organizations/consumers). Comma-separated. + # Empty allows any tenant, but an untrusted tenant's email will not link to + # an existing account — Entra's `email` is mutable and unattested there. + microsoft_allowed_tenants: null microsoft_scopes: null + # 2.4.0 nOAuth defense: a social login whose provider did not attest the + # email address no longer signs into an existing account. This is the + # compatibility escape hatch — it still cannot cross into an account another + # credential owns, but it leaves same-provider collisions open (two Entra + # tenants asserting one address). The server warns on every boot when set. + # Prefer pinning microsoft_tenant_id or enabling the xms_edov claim. + oauth_allow_unverified_provider_email: false + apple_client_id: null apple_client_secret: null apple_scopes: null