# Add to an existing installation
Add the advanced SSO stack to a Materialize deployment you already run with the Terraform modules.
If Materialize is already running, you can add the advanced SSO stack
without redeploying it. This guide assumes you manage Materialize with the
[Materialize Terraform modules](https://github.com/MaterializeInc/materialize-terraform-self-managed),
including the `materialize-instance` module.

Materialize is switched over in a separate apply at the end, so you can check
the stack before any user signs in through it:

1. Create the Ory databases.
2. Add the `ory-stack` module and check that it's healthy. Materialize keeps
   using its current authentication.
3. Point Materialize at Hydra.

## Before you begin

Complete the [prerequisites](/self-managed-deployments/sso/advanced/prerequisites/).

## Step 1: Create the Ory databases

Kratos and Hydra each need their own PostgreSQL database, and Polis needs one
too if you plan to enable SAML. They can share a PostgreSQL server, either your
existing one or a dedicated instance, as long as the Kubernetes cluster can
reach it. The enterprise examples use a managed instance (Cloud SQL, Azure
Database for PostgreSQL flexible server, or RDS) in the same network as the
cluster.

1. Create a user and the databases. Each component runs its own schema
   migrations on startup, so the user must own its databases:

   ```sql
   CREATE USER oryadmin WITH PASSWORD '<password>';
   CREATE DATABASE kratos OWNER oryadmin;
   CREATE DATABASE hydra OWNER oryadmin;
   -- Only if you plan to enable SAML:
   CREATE DATABASE polis OWNER oryadmin;
   ```

1. Build a connection string for each database. URL-encode the password, for
   example with Terraform's `urlencode()`:

   ```hcl
   locals {
     ory_kratos_dsn = "postgres://oryadmin:${urlencode(var.ory_db_password)}@<host>:5432/kratos?sslmode=require"
     ory_hydra_dsn  = "postgres://oryadmin:${urlencode(var.ory_db_password)}@<host>:5432/hydra?sslmode=require"
     # uselibpqcompat=true keeps sslmode=require at libpq semantics (encrypt,
     # don't verify), which Polis's driver needs against managed servers.
     ory_polis_dsn  = "postgres://oryadmin:${urlencode(var.ory_db_password)}@<host>:5432/polis?sslmode=require&uselibpqcompat=true"
   }
   ```

## Step 2: Add the Ory stack

1. Add the `ory-stack` module next to your existing modules, substituting
   your own hostnames and `ClusterIssuer`:

   ```hcl
   module "ory" {
     source = "github.com/MaterializeInc/materialize-terraform-self-managed//kubernetes/modules/ory-stack?ref=<RELEASE_TAG>"

     namespace = "ory"

     hydra_fqdn  = "hydra.example.com"
     kratos_fqdn = "kratos.example.com"
     ui_fqdn     = "auth.example.com"

     kratos_dsn = local.ory_kratos_dsn
     hydra_dsn  = local.ory_hydra_dsn

     # Use the ory_oel_image_tag default from the enterprise example at the same release.
     oel_image_tag   = "<ORY_IMAGE_TAG>"
     license_key_jwt = var.license_key

     cert_issuer_ref = {
       name = "letsencrypt-prod"
       kind = "ClusterIssuer"
     }
     # true for an in-cluster self-signed issuer, false for a public ACME issuer
     cert_issuer_signs_cluster_local = false

     # Registers the Materialize Console as an OAuth2 client in Hydra.
     materialize_namespace    = "materialize-environment"
     materialize_console_fqdn = "console.example.com"
   }

   output "ory_lb_addresses" {
     value = module.ory.lb_addresses
   }
   ```

   The load balancer settings differ by cloud. Copy `lb_annotations`, and on
   AWS `lb_load_balancer_class` and `lb_external_traffic_policy`, from the
   `module "ory"` block in the enterprise example for your cloud
   ([AWS](https://github.com/MaterializeInc/materialize-terraform-self-managed/tree/main/aws/examples/enterprise),
   [Azure](https://github.com/MaterializeInc/materialize-terraform-self-managed/tree/main/azure/examples/enterprise),
   [GCP](https://github.com/MaterializeInc/materialize-terraform-self-managed/tree/main/gcp/examples/enterprise)).
   To enable SAML, also set `enable_polis`, `polis_fqdn`, and `polis_dsn = local.ory_polis_dsn`;
   see [Configure identity providers](/self-managed-deployments/sso/advanced/identity-providers/).

1. Apply:

   ```bash
   terraform init -upgrade
   terraform apply
   ```

1. Create DNS records pointing the Hydra, Kratos, and selfservice UI
   hostnames at the addresses in `terraform output ory_lb_addresses`: an A
   record for an IP (Azure, GCP) or a CNAME for a hostname (AWS).

1. Check that Hydra serves its discovery document, with an `issuer` that
   matches `hydra_fqdn`:

   ```bash
   curl -fsSL https://hydra.example.com/.well-known/openid-configuration | jq .issuer
   ```

## Step 3: Point Materialize at Hydra

1. In your existing `materialize-instance` module, switch authentication to
   OIDC and set the OIDC parameters from the `ory-stack` outputs:

   ```hcl
   module "materialize_instance" {
     # ... your existing settings ...

     authenticator_kind = "Oidc"
     # Password for the mz_system admin user, kept as a fallback under OIDC.
     external_login_password_mz_system = var.external_login_password_mz_system

     # With network policies enabled, lets Materialize reach Hydra for its signing keys.
     ory_namespace = "ory"

     system_parameters = {
       oidc_issuer                  = module.ory.hydra_external_url
       oidc_audience                = jsonencode([module.ory.oauth2_client_id])
       oidc_authentication_claim    = "email"
       console_oidc_client_id       = module.ory.oauth2_client_id
       console_oidc_scopes          = "openid email"
       # Optional: grant roles from IdP groups (see Enable role mapping).
       oidc_group_role_sync_enabled = "true"
     }

     # Set a new UUID so environmentd restarts with the new parameters.
     force_rollout = "<NEW_UUID>"
   }
   ```

   Users keep their existing SQL roles as long as the `email` claim matches
   the role names. If your users currently sign in with passwords, see
   [Migrate to SSO](/security/self-managed/sso-migration/).

1. Apply:

   ```bash
   terraform apply
   ```

## Step 4: Verify sign-in

Smoke-test each browser-facing endpoint. These commands assume a publicly trusted issuer (`cert_issuer_ref` set). With the default self-signed issuer, fetch its CA first and pass `--cacert ca.crt` to each `curl`:

```bash
kubectl -n cert-manager get secret <name_prefix>-root-ca -o jsonpath='{.data.ca\.crt}' | base64 -d > ca.crt
```


```bash
# Hydra OIDC discovery (issuer should match ory_hydra_fqdn)
curl -fsSL https://hydra.example.com/.well-known/openid-configuration | jq .issuer

# Kratos health
curl -fsSL https://kratos.example.com/health/ready

# Selfservice UI health
curl -fsSL https://auth.example.com/health/alive

# Polis health (only when enable_polis = true)
curl -fsSL https://polis.example.com/api/health

# Materialize console (expect HTTP 200)
curl -fsSL -o /dev/null -w "%{http_code}\n" https://console.example.com
```

Then sign in end to end, which is what proves SSO works:

1. Open `https://console.example.com`. You are redirected to the selfservice UI at `auth.example.com`, with one button per `upstream_identity_providers` entry and per `saml_providers` entry. Each button's text comes from that entry's `label`; see [Configure identity providers](/self-managed-deployments/sso/advanced/identity-providers/).
2. Sign in through one of them. You should land back in the Console as that user.
3. Run `SELECT current_user;`. It should return the user's email.

## Next steps

- [Configure identity providers](/self-managed-deployments/sso/advanced/identity-providers/)
- [Enable role mapping](/self-managed-deployments/sso/advanced/role-mapping/)
