Currently, Materialize does not provide a mechanism for password-based authentication in self-hosted environments. Users need a secure way to authenticate with the database to control access to secure their environments and enable role-based access control (RBAC).
A successful solution should accomplish the following:
The solution will not:
The proposed solution is to implement a password authentication mechanism that aligns with PostgreSQL's "password" authentication method. The key components are:
This password authentication will function similarly to PostgreSQL’s "password" method. Users will authenticate with a username and password passed in plain text to Environmentd, which will securely compare the stored values in the system. The "plain text" values may be encrypted at the TLS layer but will be plain text within the pgwire connection.
Passwords hashed with scram-sha-256
will be stored in the configured secret store. A new catalog entry will be created to store sensitive authentication metadata, like a reference to the secret store, and possibly eventually attempt counts or other metadata.
Authentication Flow:
The protocol layer will communicate with the coordinator layer to validate the password. (via the adapter Client) a. The coordinator layer will check the password against the stored hash. If the password is correct, the user will be authenticated. b. If the password is incorrect, the user will be denied access.
Audit and Metrics: Login and failed login requests should be logged. Metrics should be created to monitor potential security events.
Password Rotation or Expiration: While not currently in scope, the design should allow for additions in the future, such as password rotation and expiration.
Users should be able to manage their passwords and set passwords for roles they create. Superusers should be able to alter any non-internal user's password.
Syntax Examples:
CREATE ROLE hunter WITH PASSWORD 'hunter2';
ALTER ROLE hunter SET (PASSWORD 'hunter2');
ALTER ROLE user_name PASSWORD NULL
Password Policies: We will not implement password policies but should enforce a configurable minimum password length.
Login Role Determination: We should display whether a role has a password and can be used as a login role in mz_roles
. See pg_roles.
Secure Hashing: For future compatibility with SCRAM, passwords should be hashed using scram-sha-256
encryption. The RFC for SCRAM dictates an iteration count of at least 4096. However, the RFC for SCRAM-SHA-256 dictates that "the hash iteration-count should be such that a modern machine will take 0.1 seconds to perform the complete algorithm". This also dictates at least 4096 with a recommendation of 15000. We should aim for 15000 if it does not negatively impact connection times or load.
Access Control: Hashed passwords should not be extracted from the secret store and should not be accessible through queryable tables. Passwords must not be logged or output in stack traces.
Password Dating: The creation date of passwords will be recorded along with the hashed and salted password. When the password changes a creation date should be reset. The creation date of passwords should be queryable.
Storage Location Password Hash: The password will be stored in the catalog in its own object, RoleAuth
. This object must record the following:
algorithm / algorithm meta: IE for sha256: algo: sha256, salt: "some-string", iteration_count: some-int Example struct:
RoleAuth {
role_id: ident
password_hash: String
creation_date: Date
}
Iterations must be >= 400,000... this must be configurable with a system variable.
We must use at least 32 bytes of cryptographically random data for salts.
OPTIONAL: Password Versioning: Updates to the password hashing mechanisms may be required.
Passwords for mz_system
(v1) and mz_support
(TBD) roles will be configurable via an Environmentd flag external_login_password_<USER>
, or external_login_password_secret_<user>
only one is required to be implemented, but the emulator must support configuration via environment variables. Orchestratord should provide a set of parameters to set these parameters. Additionally, We should enable login of system users through the external ports when they have external login passwords set. Login through the external port must not be possible unless this flag is set, this logic should not rely on whether the internal user has a password. Our Helm chart and Orchestratord will be adjusted to support these parameters.
Environmentd's HTTP endpoint will have added support for session-based authentication. This includes session creation, storage, and deleting/setting cookies. To handle sessions we should use tower session-store. This should explicitly require secure cookies .with_secure(true)
when running with TLS.
Required new endpoints:
Additionally, we'll need to add a tower middleware layer that validates cookie authentication for all endpoints returning 403 when the user is not authenticated.
Enabling this feature we will require us to provide an alternative method to declare superusers. This is currently done through JWT metadata. In order for this to work with passwords we will need to provide syntax:
ALTER ROLE ... WITH SUPERUSER;
ALTER ROLE ... WITH NO SUPERUSER;
A role's superuser state should be stored as a rolsuper along with the user in the catalog. It may make sense to make this an Option. If the value of rolsuper is not Some(v)
then we defer to metadata if we're using frontegg auth.
The authentication mechanism for an environment must be configurable. A new flag will be created to select the authentication mz_authentication_type
with the options of frontegg
, password
, and disabled
. This will subsume the enable_authentication flag.
Console does currently do not support runtime or startup configuration. Configuration is handled only at build time. To resolve this we should add a config.json
or config.js
file which can be mounted directly into the Nginx container assets. This file should come from a materialize-console config map which must be setup by Orchestratord. We will also need changes to the console to support reading in configuration from this map. The initial config value here should be authentication_type: password
, in cloud we should use authentication_type: frontegg
or authentication_type: jwt
. The console build process can still be used to set default values for this config file.
By default authentication will not enabled in the emulator. This should be configurable with the ability to both turn on authentication and to set a password for the mz_system
user.
A minimal viable prototype (MVP) for this solution will include:
scram-sha-256
.Authd Service: This would involve creating a separate authentication service to handle authentication. This approach is more complex and introduces security concerns, as it would require developing secure endpoints and integrating a user management system that could be prone to additional complexity and security risks.
Password Authentication in Ingress Layer: Using a front-end proxy like PGbouncer or NGINX to handle password authentication before traffic hits Materialize.
Store Passwords in K8s Secrets: It would be viable to store passwords in Kubernetes secrets.
Reasons Not Chosen: This is slightly more complicated and non-compliant with Postgres.
Reasons I still might prefer this: It does have a number of benefits, including slightly better security via enforcement of random strong passwords, and a better rotation story. It's also possible we could have different app password protocols/versions that allow users to manage their passwords entirely through secrets directly or something like Hashicorp vault.
Where exactly are passwords stored: While it makes sense to store passwords in the catalog, I have not yet identified exactly where they should be stored. Should it go alongside mz_roles
, or in a new mz_auth_id
table. The latter makes sense as it could store password metadata, and might let us store multiple passwords down the road.
A: In the catalog
Audit Logging and Brute Force Detection: Do we need to log all failed password attempts and the IPs of those attempts? Do we need to take action to lock an IP or User when an attempt threshold is met? A: yes log attempts and failed attempts along with the password. No mitigation is required, at least not for v1.
Password Strength Validation: Should Materialize include built-in password strength validation, or should it be handled outside the system (e.g., via Kubernetes or other security tools)? A: MZ should not have built-in password strength, down the road we may want to consider something like postgres's passwordcheck.