Skip to content

Commit 5531f5f

Browse files
committed
orchestratord: Add SASL/SCRAM-SHA-256 authentication
1 parent e461509 commit 5531f5f

File tree

5 files changed

+125
-26
lines changed

5 files changed

+125
-26
lines changed

doc/user/content/security/self-managed/authentication.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,73 @@ users:
8282
[^1]: The `mz_system` user is also used by the Materialize Operator for upgrades
8383
and maintenance tasks.
8484

85+
## Configuring SASL/SCRAM authentication
86+
87+
{{< public-preview >}}This feature{{</ public-preview >}}
88+
89+
{{< note >}}
90+
SASL/SCRAM-SHA-256 authentication requires Materialize v0.147.16 or later.
91+
{{</ note >}}
92+
93+
SASL/SCRAM-SHA-256 authentication is a challenge-response authentication mechanism
94+
that provides security for PostgreSQL wire protocol connections. It is
95+
compatible with PostgreSQL clients that support SCRAM-SHA-256.
96+
97+
To configure Self-Managed Materialize for SASL/SCRAM authentication:
98+
99+
| Configuration | Description
100+
|---------------| ------------
101+
|`spec.authenticatorKind` | Set to `Sasl` to enable SASL/SCRAM-SHA-256 authentication for PostgreSQL connections.
102+
|`external_login_password_mz_system` | To the Kubernetes Secret referenced by `spec.backendSecretName`, add the secret key `external_login_password_mz_system`. This is the password for the `mz_system` user [^1], who is the only user initially available when SASL authentication is enabled.
103+
104+
For example, if using Kind, in the `sample-materialize.yaml` file:
105+
106+
```
107+
apiVersion: v1
108+
kind: Namespace
109+
metadata:
110+
name: materialize-environment
111+
---
112+
apiVersion: v1
113+
kind: Secret
114+
metadata:
115+
name: materialize-backend
116+
namespace: materialize-environment
117+
stringData:
118+
metadata_backend_url: "..."
119+
persist_backend_url: "..."
120+
external_login_password_mz_system: "enter_mz_system_password"
121+
---
122+
apiVersion: materialize.cloud/v1alpha1
123+
kind: Materialize
124+
metadata:
125+
name: 12345678-1234-1234-1234-123456789012
126+
namespace: materialize-environment
127+
spec:
128+
environmentdImageRef: materialize/environmentd:v0.147.2
129+
backendSecretName: materialize-backend
130+
authenticatorKind: Sasl
131+
```
132+
133+
### Logging in and creating users
134+
135+
The process is the same as for [password authentication](#logging-in-and-creating-users):
136+
137+
1. Login as the `mz_system` user using the `external_login_password_mz_system` password
138+
2. Use [`CREATE ROLE ... WITH LOGIN PASSWORD ...`](/sql/create-role) to create new users
139+
140+
User passwords are automatically stored in SCRAM-SHA-256 format in the database.
141+
142+
### Authentication behavior
143+
144+
When SASL authentication is enabled:
145+
146+
- **PostgreSQL connections** (e.g., `psql`, client libraries) use SCRAM-SHA-256 authentication
147+
- **HTTP/Web Console connections** use standard password authentication
148+
149+
This hybrid approach provides maximum security for SQL connections while maintaining
150+
compatibility with web-based tools.
151+
85152
## Enabling RBAC
86153

87154
{{< include-md file="shared-content/enable-rbac.md" >}}

doc/user/data/self_managed/authentication_setting.yml

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,28 @@ columns:
33
- column: "Description"
44

55
rows:
6-
- "authenticatorKind Value": "**None**"
7-
"Description": |
8-
Disables authentication. All users are trusted based on their claimed
9-
identity **without** any verification. **Default**
10-
- "authenticatorKind Value": "**Password**"
11-
"Description": |
12-
Enables [password authentication](#configuring-password-authentication) for
13-
users. When enabled, users must authenticate with their password.
6+
- "authenticatorKind Value": "**None**"
7+
"Description": |
8+
Disables authentication. All users are trusted based on their claimed
9+
identity **without** any verification. **Default**
10+
- "authenticatorKind Value": "**Password**"
11+
"Description": |
12+
Enables [password authentication](#configuring-password-authentication) for
13+
users. When enabled, users must authenticate with their password.
1414
15-
{{< tip >}}
16-
When enabled, you must also set the `mz_system` user password in
17-
`external_login_password_mz_system`. See [Configuring password
18-
authentication](#configuring-password-authentication) for details.
19-
{{</ tip >}}
15+
{{< tip >}}
16+
When enabled, you must also set the `mz_system` user password in
17+
`external_login_password_mz_system`. See [Configuring password
18+
authentication](#configuring-password-authentication) for details.
19+
{{</ tip >}}
20+
- "authenticatorKind Value": "**Sasl**"
21+
"Description": |
22+
Enables [SASL/SCRAM-SHA-256 authentication](#configuring-saslscram-authentication) for
23+
PostgreSQL wire protocol connections. This is a challenge-response authentication
24+
mechanism that provides enhanced security compared to simple password authentication.
25+
26+
{{< tip >}}
27+
When enabled, you must also set the `mz_system` user password in
28+
`external_login_password_mz_system`. See [Configuring SASL/SCRAM
29+
authentication](#configuring-saslscram-authentication) for details.
30+
{{</ tip >}}

src/cloud-resources/src/crd/materialize.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,11 @@ pub mod v1alpha1 {
175175
pub rollout_strategy: MaterializeRolloutStrategy,
176176
// The name of a secret containing metadata_backend_url and persist_backend_url.
177177
// It may also contain external_login_password_mz_system, which will be used as
178-
// the password for the mz_system user if authenticator_kind is Password.
178+
// the password for the mz_system user if authenticator_kind is Password or Sasl.
179179
pub backend_secret_name: String,
180-
// How to authenticate with Materialize. Valid options are Password and None.
181-
// If set to Password, the backend secret must contain external_login_password_mz_system.
180+
// How to authenticate with Materialize. Valid options are Password, Sasl, and None.
181+
// If set to Password or Sasl, the backend secret must contain external_login_password_mz_system.
182+
// Sasl enables SCRAM-SHA-256 authentication for PostgreSQL wire protocol connections.
182183
#[serde(default)]
183184
pub authenticator_kind: AuthenticatorKind,
184185
// Whether to enable role based access control. Defaults to false.

src/orchestratord/src/controller/materialize/environmentd.rs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1394,7 +1394,10 @@ fn create_environmentd_statefulset_object(
13941394
..Default::default()
13951395
});
13961396
args.push("--listeners-config-path=/listeners/listeners.json".to_owned());
1397-
if mz.spec.authenticator_kind == AuthenticatorKind::Password {
1397+
if matches!(
1398+
mz.spec.authenticator_kind,
1399+
AuthenticatorKind::Password | AuthenticatorKind::Sasl
1400+
) {
13981401
args.push("--system-parameter-default=enable_password_auth=true".into());
13991402
env.push(EnvVar {
14001403
name: "MZ_EXTERNAL_LOGIN_PASSWORD_MZ_SYSTEM".to_string(),
@@ -1628,12 +1631,21 @@ fn create_connection_info(
16281631
&config.default_certificate_specs.internal,
16291632
&mz.spec.internal_certificate_spec,
16301633
);
1631-
let authenticator_kind = mz.spec.authenticator_kind;
1634+
let auth_kind = mz.spec.authenticator_kind;
1635+
1636+
// SASL authentication is only supported for SQL (PostgreSQL wire protocol).
1637+
// HTTP listeners must use Password authentication when SASL is enabled.
1638+
// This is validated at environmentd startup via ListenerConfig::validate().
1639+
let (sql_auth, http_auth) = match auth_kind {
1640+
AuthenticatorKind::Sasl => (AuthenticatorKind::Sasl, AuthenticatorKind::Password),
1641+
other => (other, other),
1642+
};
1643+
16321644
let mut listeners_config = ListenersConfig {
16331645
sql: btreemap! {
16341646
"external".to_owned() => SqlListenerConfig{
16351647
addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0,0,0,0)), config.environmentd_sql_port),
1636-
authenticator_kind,
1648+
authenticator_kind: sql_auth,
16371649
allowed_roles: AllowedRoles::Normal,
16381650
enable_tls: external_enable_tls,
16391651
},
@@ -1649,7 +1661,7 @@ fn create_connection_info(
16491661
"external".to_owned() => HttpListenerConfig{
16501662
base: BaseListenerConfig {
16511663
addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0,0,0,0)), config.environmentd_http_port),
1652-
authenticator_kind,
1664+
authenticator_kind: http_auth,
16531665
allowed_roles: AllowedRoles::Normal,
16541666
enable_tls: external_enable_tls,
16551667
},
@@ -1679,7 +1691,11 @@ fn create_connection_info(
16791691
},
16801692
},
16811693
};
1682-
if authenticator_kind == AuthenticatorKind::Password {
1694+
1695+
if matches!(
1696+
auth_kind,
1697+
AuthenticatorKind::Password | AuthenticatorKind::Sasl
1698+
) {
16831699
listeners_config.sql.remove("internal");
16841700
listeners_config.http.remove("internal");
16851701

@@ -1732,8 +1748,8 @@ fn create_connection_info(
17321748
},
17331749
};
17341750

1735-
let (scheme, leader_api_port, mz_system_secret_name) = match mz.spec.authenticator_kind {
1736-
AuthenticatorKind::Password => {
1751+
let (scheme, leader_api_port, mz_system_secret_name) = match auth_kind {
1752+
AuthenticatorKind::Password | AuthenticatorKind::Sasl => {
17371753
let scheme = if external_enable_tls { "https" } else { "http" };
17381754
(
17391755
scheme,

test/orchestratord/mzcompose.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,7 +1030,7 @@ def default(cls) -> Any:
10301030

10311031
def modify(self, definition: dict[str, Any]) -> None:
10321032
definition["materialize"]["spec"]["authenticatorKind"] = self.value
1033-
if self.value == "Password":
1033+
if self.value == "Password" or self.value == "Sasl":
10341034
definition["secret"]["stringData"][
10351035
"external_login_password_mz_system"
10361036
] = "superpassword"
@@ -1046,9 +1046,13 @@ def validate(self, mods: dict[type[Modification], Any]) -> None:
10461046
if self.value == "Password" and version <= MzVersion.parse_mz("v0.147.6"):
10471047
return
10481048

1049+
if self.value == "Sasl" and version < MzVersion.parse_mz("v0.147.16"):
1050+
return
1051+
10491052
port = (
10501053
6875
1051-
if version >= MzVersion.parse_mz("v0.147.0") and self.value == "Password"
1054+
if (version >= MzVersion.parse_mz("v0.147.0") and self.value == "Password")
1055+
or (version >= MzVersion.parse_mz("v0.147.16") and self.value == "Sasl")
10521056
else 6877
10531057
)
10541058
for i in range(120):
@@ -1098,7 +1102,7 @@ def validate(self, mods: dict[type[Modification], Any]) -> None:
10981102
# psycopg.connect(
10991103
# host="127.0.0.1",
11001104
# user="mz_system",
1101-
# password="superpassword" if self.value == "Password" else None,
1105+
# password="superpassword" if (self.value == "Password" or self.value == "Sasl") else None,
11021106
# dbname="materialize",
11031107
# port=port,
11041108
# )

0 commit comments

Comments
 (0)