From a0e8f4bba1da42838ac44eec8aa375a97fdab64f Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Mon, 1 Dec 2025 19:00:19 +0100 Subject: [PATCH] chore: Improve documentation (routing) --- doc/src/SUMMARY.md | 37 ++++----- .../adr/0001-record-architecture-decisions.md | 6 +- doc/src/adr/0002-open-policy-agent.md | 14 ++-- doc/src/adr/0004-v4-api.md | 4 +- doc/src/adr/0005-auth-passkey.md | 8 +- doc/src/adr/0007-federation-mapping.md | 8 +- doc/src/adr/0008-federation-workload.md | 20 ++--- doc/src/adr/0009-auth-token-revoke.md | 23 +++--- .../0010-pci-dss-failed-auth-protection.md | 9 ++- ...1-pci-dss-inactive-account-deactivation.md | 10 +-- .../0012-pci-dss-account-password-expiry.md | 8 +- doc/src/architecture.md | 23 +++--- doc/src/federation.md | 27 ++++--- doc/src/install.md | 81 ++++++++++++++++--- doc/src/intro.md | 60 +++++++------- doc/src/jwt.md | 34 ++++---- doc/src/oidc.md | 18 ++--- doc/src/passkey.md | 10 +-- doc/src/policy.md | 64 +++++++-------- 19 files changed, 264 insertions(+), 200 deletions(-) diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index c2201eb1..f9228922 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -1,29 +1,30 @@ # OpenStack Keystone -[Introduction](./intro.md) +[Introduction](./intro.md) + [Installation](./install.md) --- # Keystone internals -* [Architecture](./architecture.md) - * [Architecture decision records](adr/index.md) - * [Record architecture decisions](adr/0001-record-architecture-decisions.md) - * [Open Policy Agent](adr/0002-open-policy-agent.md) - * [Sea ORM](adr/0003-sea-orm.md) - * [v4 API](adr/0004-v4-api.md) - * [Passkey Auth](adr/0005-auth-passkey.md) - * [Federation IDP](adr/0006-federation-idp.md) - * [Federation Mapping](adr/0007-federation-mapping.md) - * [Workload Federation](adr/0008-federation-workload.md) - * [Auth token revocation](adr/0009-auth-token-revoke.md) - * [PCI-DSS: Failed Auth Protection](adr/0010-pci-dss-failed-auth-protection.md) - * [PCI-DSS: Inactive Account Deactivation](adr/0011-pci-dss-inactive-account-deactivation.md) - * [PCI-DSS: Account Password Expiration](adr/0012-pci-dss-account-password-expiry.md) -* [Policy enforcement](./policy.md) -* [Fernet token]() - * [Token payloads]() +- [Architecture](./architecture.md) + - [Architecture decision records](adr/index.md) + - [Record architecture decisions](adr/0001-record-architecture-decisions.md) + - [Open Policy Agent](adr/0002-open-policy-agent.md) + - [Sea ORM](adr/0003-sea-orm.md) + - [v4 API](adr/0004-v4-api.md) + - [Passkey Auth](adr/0005-auth-passkey.md) + - [Federation IDP](adr/0006-federation-idp.md) + - [Federation Mapping](adr/0007-federation-mapping.md) + - [Workload Federation](adr/0008-federation-workload.md) + - [Auth token revocation](adr/0009-auth-token-revoke.md) + - [PCI-DSS: Failed Auth Protection](adr/0010-pci-dss-failed-auth-protection.md) + - [PCI-DSS: Inactive Account Deactivation](adr/0011-pci-dss-inactive-account-deactivation.md) + - [PCI-DSS: Account Password Expiration](adr/0012-pci-dss-account-password-expiry.md) +- [Policy enforcement](./policy.md) +- [Fernet token]() + - [Token payloads]() --- diff --git a/doc/src/adr/0001-record-architecture-decisions.md b/doc/src/adr/0001-record-architecture-decisions.md index 16c2a678..96af9e8b 100644 --- a/doc/src/adr/0001-record-architecture-decisions.md +++ b/doc/src/adr/0001-record-architecture-decisions.md @@ -12,8 +12,10 @@ We need to record the architectural decisions made on this project. ## Decision -We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). +We will use Architecture Decision Records, as +[described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). ## Consequences -See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools). +See Michael Nygard's article, linked above. For a lightweight ADR toolset, see +Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools). diff --git a/doc/src/adr/0002-open-policy-agent.md b/doc/src/adr/0002-open-policy-agent.md index 8d136b9e..c815953e 100644 --- a/doc/src/adr/0002-open-policy-agent.md +++ b/doc/src/adr/0002-open-policy-agent.md @@ -9,21 +9,21 @@ Accepted ## Context Use of oslo.policy is not easily possible from Rust. In addition to that during -the OpenStack Summit 2025 it [was -shown](https://www.youtube.com/watch?v=_B4Zsd8RG88&list=PLKqaoAnDyfgr91wN_12nwY321504Ctw1s&index=33) +the OpenStack Summit 2025 it +[was shown](https://www.youtube.com/watch?v=_B4Zsd8RG88&list=PLKqaoAnDyfgr91wN_12nwY321504Ctw1s&index=33) how Open Policy Agent can be used to further improve the policy control in OpenStack. As such the Keystone implement the policy enforcement using the OPA with the following rules: -1. `List` operation MUST receive the all query parameters of the operation in the - target. +1. `List` operation MUST receive the all query parameters of the operation in + the target. 2. For `Show` operation the policy MUST receive the current record as the target (fetch the record and pass it into the policy engine). 3. `Update` operation MUST receive current and new state of the resource (first the current resource is fetched and passed together with the new state - [current, target] to the policy engine). + [current, target] to the policy engine). 4. `Create` operation works similarly as current oslo.policy with the desired state passed to the policy engine. @@ -40,5 +40,5 @@ Engine. - Policy evaluation requires external service (OPA) to be running. -- When covering existing functionality of the python Keystone policies SHOULD -be converted as is and do not introduce a changed flow. +- When covering existing functionality of the python Keystone policies SHOULD be + converted as is and do not introduce a changed flow. diff --git a/doc/src/adr/0004-v4-api.md b/doc/src/adr/0004-v4-api.md index f5a31a3e..c1c7e1c5 100644 --- a/doc/src/adr/0004-v4-api.md +++ b/doc/src/adr/0004-v4-api.md @@ -25,5 +25,5 @@ v4 API version should be introduced. - New extended functionality will not be available in the v3. - Certain necessary changes may be ported to the v3 (including python) to -implement backwards compatibility. Example: acknowledge new token payload issued -with v4 with the python Keystone. + implement backwards compatibility. Example: acknowledge new token payload + issued with v4 with the python Keystone. diff --git a/doc/src/adr/0005-auth-passkey.md b/doc/src/adr/0005-auth-passkey.md index 6997a145..37312a73 100644 --- a/doc/src/adr/0005-auth-passkey.md +++ b/doc/src/adr/0005-auth-passkey.md @@ -29,12 +29,12 @@ authenticators. to the standard. - User should be able to request the desired scope in the authentication -initialization request. In this case a scoped token is returned when user has -the required access. + initialization request. In this case a scoped token is returned when user has + the required access. - To prevent attacks authentication requests for not existing users or users -without registered authenticators MUST return fake (but valid) authentication -state. + without registered authenticators MUST return fake (but valid) authentication + state. ## Consequences diff --git a/doc/src/adr/0007-federation-mapping.md b/doc/src/adr/0007-federation-mapping.md index a828c003..94a44565 100644 --- a/doc/src/adr/0007-federation-mapping.md +++ b/doc/src/adr/0007-federation-mapping.md @@ -19,9 +19,9 @@ for Keystone. ## Decision -"Mapping" (attribute mapping) MUST describe how the information from OIDC -claims need to be translated into the Keystone data model. It MUST also describe -user defined bounds to allow use restriction. +"Mapping" (attribute mapping) MUST describe how the information from OIDC claims +need to be translated into the Keystone data model. It MUST also describe user +defined bounds to allow use restriction. When `domain_id` is not being set on the IdP level it MUST be defined either on the mapping entry, or the mapping MUST define `domain_id_claim` to extract the @@ -37,4 +37,4 @@ used. ## Consequences - Mappings MUST be configured carefully to prevent login of users across the -domain borders. `bound_xxx` should be used extensively to guard this. + domain borders. `bound_xxx` should be used extensively to guard this. diff --git a/doc/src/adr/0008-federation-workload.md b/doc/src/adr/0008-federation-workload.md index 3125d83b..f0e97740 100644 --- a/doc/src/adr/0008-federation-workload.md +++ b/doc/src/adr/0008-federation-workload.md @@ -13,22 +13,22 @@ workflow, Zuul job, etc). Usually such services provide a JWT issued by the platform which the service provider can trust. This is very similar (and technically relates) to the OIDC standard. -In the JWT flow the "user" is exchanging a JWT token issued by the trusted -IdP for a Keystone token. This authentication response includes a token and a +In the JWT flow the "user" is exchanging a JWT token issued by the trusted IdP +for a Keystone token. This authentication response includes a token and a service catalog to provide a known OpenStack usage scenario. ## Decision -OIDC mappings MUST specify a `type` which is `oidc` or `jwt` to specify the -flow they define. A `jwt` type mapping can be only used in the JWT flow. +OIDC mappings MUST specify a `type` which is `oidc` or `jwt` to specify the flow +they define. A `jwt` type mapping can be only used in the JWT flow. The new authentication API includes the IdP ID. The authentication request does -not support the Json request body and uses a generic `authorization: bearer -` header and `openstack-mapping-name: ` to request the -information. Depending on the mapping configuration the desired authorization -scope is returned. The flow does not support explicitly requesting the scope -beyond what is described by the mapping. - +not support the Json request body and uses a generic +`authorization: bearer ` header and +`openstack-mapping-name: ` to request the information. Depending +on the mapping configuration the desired authorization scope is returned. The +flow does not support explicitly requesting the scope beyond what is described +by the mapping. ## Consequences diff --git a/doc/src/adr/0009-auth-token-revoke.md b/doc/src/adr/0009-auth-token-revoke.md index dbb89d81..57198b7b 100644 --- a/doc/src/adr/0009-auth-token-revoke.md +++ b/doc/src/adr/0009-auth-token-revoke.md @@ -65,9 +65,12 @@ Following conditions are combined with the AND condition: database record. When this list is empty an error is being returned. - `token.project_id` is compared against the database record when present. - `token.user_id` is compared against the database record when present. -- `token.trustor_id` is compared against the database record `user_id` when present. -- `token.trustee_id` is compared against the database record `user_id` when present. -- `token.trust_id` is compared against the database record `trust_id` when present. +- `token.trustor_id` is compared against the database record `user_id` when + present. +- `token.trustee_id` is compared against the database record `user_id` when + present. +- `token.trust_id` is compared against the database record `trust_id` when + present. - `token.issued_at` is compared against the database record with `revocation_event.issued_before >= token.issued_at`. @@ -75,9 +78,9 @@ Python version of the Keystone applies additional match verification for the selected data on the server side and not in the database query. - When `revocation_event.domain_id` is set it is compared against -`token.domain_id` and `token.identity_domain_id`. + `token.domain_id` and `token.identity_domain_id`. - When `revocation_event.role_id` is present it is compared against every of the -`token.roles`. + `token.roles`. After the first non matching result further evaluation is being stopped. Logically there does not seem to be a reason for such handling and it looks to @@ -89,7 +92,6 @@ While following checks allow much higher details of the revocation events in the context of the usual fernet token revocation it is only going to match on the `audit_id` and `issued_before`. - ### Revocation table purge In the python Keystone there is no automatic cleanup handling. Due to that @@ -105,9 +107,10 @@ try to delete expired records errors can occur. However, if only rust version is validating the tokens python version will not perform any backups. Additionally no errors were reported yet in installations with multiple Keystone instances. Therefore it is necessary for the rust implementation to do periodic cleanup. It -should be exexcuted with the following query filter: `revoked_at < (now - -(expiration + expiration_buffer))`. Such implementation must be made optional -with possibility to disable this behavior using the config file. +should be exexcuted with the following query filter: +`revoked_at < (now - (expiration + expiration_buffer))`. Such implementation +must be made optional with possibility to disable this behavior using the config +file. ## Consequences @@ -116,4 +119,4 @@ with possibility to disable this behavior using the config file. - Token validation processing time is increased with the database lookup. - Expired revocation records are optionally periodically cleaned by the rust -implementation. + implementation. diff --git a/doc/src/adr/0010-pci-dss-failed-auth-protection.md b/doc/src/adr/0010-pci-dss-failed-auth-protection.md index c7814e93..1badadd7 100644 --- a/doc/src/adr/0010-pci-dss-failed-auth-protection.md +++ b/doc/src/adr/0010-pci-dss-failed-auth-protection.md @@ -68,12 +68,13 @@ must be applied part of the locked account verification: - When `user_options.IGNORE_LOCKOUT_ATTEMPT` is set user account is NOT locked -- When `user.failed_auth_count >= conf.security_compliance.lockout_failure_attempts` +- When + `user.failed_auth_count >= conf.security_compliance.lockout_failure_attempts` the account is locked. -- When `user.failed_auth_at + conf.security_compliance.lockout_duration > -now()` account is locked. When the time is `< now()` - reset the counters - in the database. +- When `user.failed_auth_at + conf.security_compliance.lockout_duration > now()` + account is locked. When the time is `< now()` - reset the counters in the + database. - Otherwise the account is NOT locked. diff --git a/doc/src/adr/0011-pci-dss-inactive-account-deactivation.md b/doc/src/adr/0011-pci-dss-inactive-account-deactivation.md index 8177bc64..57897138 100644 --- a/doc/src/adr/0011-pci-dss-inactive-account-deactivation.md +++ b/doc/src/adr/0011-pci-dss-inactive-account-deactivation.md @@ -61,9 +61,9 @@ authentication request must be rejected with `http.Unauthorized`. Additional background process must be implemented to deactivate inactive accounts. For this when `conf.security_compliance.disable_user_account_days_inactive` is set a process -should loop over all user accounts. When the `user.last_active_at + -disable_user_account_days_inactive < now()` presence of the -`user.options.IGNORE_USER_INACTIVITY_OPT` should be checked. When absent the +should loop over all user accounts. When the +`user.last_active_at + disable_user_account_days_inactive < now()` presence of +the `user.options.IGNORE_USER_INACTIVITY_OPT` should be checked. When absent the account must be updated setting `user.enabled` to `false`. Since it is technically possible that the background process is not running for @@ -82,8 +82,8 @@ workflow the `user.last_active_at` should be set to the current date time. - Authentication with methods other than username password are not updating the `lst_active_at`. Due to that the account that used i.e. application - credentials for the activation for more than X days would become disabled. This - requires account to perform periodic login using the password. + credentials for the activation for more than X days would become disabled. + This requires account to perform periodic login using the password. - It should be considered to update application credentials workflow to update the `user.last_active_at` attribute after successful authentication. diff --git a/doc/src/adr/0012-pci-dss-account-password-expiry.md b/doc/src/adr/0012-pci-dss-account-password-expiry.md index 13006688..f54b3e71 100644 --- a/doc/src/adr/0012-pci-dss-account-password-expiry.md +++ b/doc/src/adr/0012-pci-dss-account-password-expiry.md @@ -12,9 +12,11 @@ PCI-DSS contains the following requirement to the IAM system: If passwords/passphrases are used as the only authentication factor for user access (i.e., in any single-factor authentication implementation) then either: -• Passwords/passphrases are changed at least once every 90 days, OR -• The security posture of accounts is dynamically analyzed, and real-time -access to resources is automatically determined accordingly. + +- Passwords/passphrases are changed at least once every 90 days, OR + +- The security posture of accounts is dynamically analyzed, and real-time + access to resources is automatically determined accordingly. Python Keystone implements this requirement with the help of the `conf.security_compliance.password_expires_days` and `password.expires_at` diff --git a/doc/src/architecture.md b/doc/src/architecture.md index 4b6c8b2f..7cf7c5da 100644 --- a/doc/src/architecture.md +++ b/doc/src/architecture.md @@ -32,8 +32,8 @@ between sqlalchemy and sea-orm. The later suggests doing database schema first. In the next step object types are created out of the database. That means that the database migration must be written first and cannot be automatically generated from the code (easily, but there is a way). Current migrations do not -create database schema that is managed by the py-keystone. Therefore in order -to get a fully populated database schema it is necessary to apply +create database schema that is managed by the py-keystone. Therefore in order to +get a fully populated database schema it is necessary to apply `keystone-manage db_sync` and `keystone-db up` independently. Target of the keystone-ng is to be deployed in pair with the python keystone of @@ -41,17 +41,16 @@ Target of the keystone-ng is to be deployed in pair with the python keystone of database, nor to apply any changes to the schema manaaged by the py-keystone. A federation rework assumes model change. To keep it working with the python-keystone artificial table entries may be created (in the example when a -new identity provider is being created automatically sanitized entries are -being added for the legacy identity provider and necessary protocols) A -federation rework assumes model change. To keep it working with the -python-keystone artificial table entries may be created (in the example when a -new identity provider is being created automatically sanitized entries are -being added for the legacy identity provider together with necessary idp -protocols). +new identity provider is being created automatically sanitized entries are being +added for the legacy identity provider and necessary protocols) A federation +rework assumes model change. To keep it working with the python-keystone +artificial table entries may be created (in the example when a new identity +provider is being created automatically sanitized entries are being added for +the legacy identity provider together with necessary idp protocols). ## Fernet keystone-ng uses the same mechanism for tokens to provide compatibility. The -fernet-keys repository must be provided in the runtime (i.e. by mounting them -as a volume into the container). There is no tooling to create or rotate keys -as the py-keystone does. +fernet-keys repository must be provided in the runtime (i.e. by mounting them as +a volume into the container). There is no tooling to create or rotate keys as +the py-keystone does. diff --git a/doc/src/federation.md b/doc/src/federation.md index b94636d3..59b9d5bf 100644 --- a/doc/src/federation.md +++ b/doc/src/federation.md @@ -2,8 +2,8 @@ Python Keystone is not implementing the Federation natively (neither SAML2, nor OIDC). It relies on the proxy server for the authentication protocol specifics -and tries to map resulting users into the local database. This leads to a -pretty big number of limitations (not limited to): +and tries to map resulting users into the local database. This leads to a pretty +big number of limitations (not limited to): - Identity Provider can be only configured by cloud administrators only @@ -13,19 +13,20 @@ pretty big number of limitations (not limited to): initiated logout) - Forces deployment of the proxy service in front of Keystone relying on the - modules for SAML2 and/or OIDC implementation (such modules may be abandoned - or removed). + modules for SAML2 and/or OIDC implementation (such modules may be abandoned or + removed). - Client authentication right now is complex and error prone (every public - provider has implementation specifics that are often even not cross-compatible) + provider has implementation specifics that are often even not + cross-compatible) In order to address those challenges a complete reimplementation is being done with a different design. This allows implementing features not technically possible in the py-keystone: - Federation is controlled on the domain level by the domain managers. This - means that the domain manager is responsible for the configuration of how users - should be federated from external IdPs. + means that the domain manager is responsible for the configuration of how + users should be federated from external IdPs. - Identity providers and/or attribute mappings can be reused by different domains allowing implementing social logins. @@ -34,21 +35,22 @@ possible in the py-keystone: decreases amount of different flows to the minimum making client applications much simpler and more reliable. - ## API changes A series of brand new API endpoints have been added to the Keystone API. - **/v4/federation/identity_providers** (manage the identity providers) -- **/v4/federation/mappings** (manage the mappings tied to the identity provider) +- **/v4/federation/mappings** (manage the mappings tied to the identity + provider) - **/v4/federation/auth** (initiate the authentication and get the IdP url) -- **/v4/federation/oidc/callback** (exchange the authorization code for the Keystone token) +- **/v4/federation/oidc/callback** (exchange the authorization code for the + Keystone token) -- **/v4/federation/identity_providers/{idp_id}/jwt** - (exchange the JWT token issued by the referred IdP for the Keystone token) +- **/v4/federation/identity_providers/{idp_id}/jwt** (exchange the JWT token + issued by the referred IdP for the Keystone token) ## DB changes @@ -72,7 +74,6 @@ Following tables are added: {{#include ../../src/db/entity/federated_auth_state.rs:8:16}} ``` - ## Compatibility notes Since the federation is implemented very differently to how it was done before diff --git a/doc/src/install.md b/doc/src/install.md index 5092ea64..0e159345 100644 --- a/doc/src/install.md +++ b/doc/src/install.md @@ -1,8 +1,8 @@ # Installation The easiest way to get started with the keystone-ng is using the container -image. It is also possible to use the compiled version. It can be either -compiled locally or downloaded from the project artifacts. +image. It is also possible to use the compiled binary. It can be either compiled +locally or downloaded from the project artifacts. ## Using pre-compiled binaries @@ -13,8 +13,7 @@ the pre-compiled binaries for a variety of platforms. ## Compiling In order to compile the keystone-ng it is necessary to have the rust compiler -available. It may be installed from the system packages or using the -`rustup.rs` +available. It may be installed from the system packages or using the `rustup.rs` ```console curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh @@ -35,9 +34,9 @@ It produces 2 binaries: - target/release/keystone-db (the database management tool) -Currently keystone depends on the openssl (through one of the dependencies). -Depending on the environment it may be a statically linked or dynamically. -There are signals that that may be not necessary anymore once all dependencies +Currently keystone depends on the openssl (as a transitive dependency). +Depending on the environment it may be a statically linked or dynamically. There +are signals that that may be not necessary anymore once all dependencies transition to the use of rustls. ## Using containers @@ -66,16 +65,16 @@ docker run -v /etc/keystone/:/etc/keystone -p 8080:8080 ghcr.io/openstack-experi Rust Keystone is using different ORM and implements migration that co-exist together with alembic migrations of the python Keystone. It also ONLY manages -the database schema additions and does NOT include the original database -schema. Therefore it is necessary to apply both migrations. +the database schema additions and does NOT include the original database schema. +Therefore it is necessary to apply both migrations. ```console -keystone-db -u +keystone-db --config /etc/keystone/keystone.conf ``` -It is important to also understand that the DB_URL may differ between python -and rust due to the optional presence of the preferred database driver in the -url. keystone-ng will ignore the the driver in the application itself, but the +It is important to also understand that the DB_URL may differ between python and +rust due to the optional presence of the preferred database driver in the url. +keystone-ng will ignore the the driver in the application itself, but the migration may require user to manually remove it since it is being processed by the ORM itself and not by the keystone-ng code. @@ -94,3 +93,59 @@ opa run -s policies **NOTE:** by default OPA process listens on the localhost only what lead to unavailability to expose it between containers. Please use `-a 0.0.0.0:8181` to start listening on all interfaces. + +## Parallel installation with the python Keystone + +Since Keystone-NG is only an addition and is not a drop-in replacement for the +python Keystone it is necessary to deploy both versions together. + +With the python Keystone no changes on the deployment strategy should be +necessary. Whichever WSGI method is used to run the Keystone it stays this way +and continues listening on the expected port. + +The rust Keystone is deployed in parallel to it (usually on the same hardware) +and by default it listens on the port 8080. + +Next required step is to alter the http proxy server configuration. In the +devstack this is usually the Apache webserver. Some operators may run nginx or +haproxy in front of the default webserver with the Keystone. Depending on the +preferred functionality (i.e. whether the token validation should be performed +by the python or the rust implementation) redirects can be implemented. This way +it is possible to decide for every single API call individually whether it +should be served by python or rust implementation. + +### Nginx sample configuration + +```config + +server { + listen 443 ssl; + server_name devstack.v6.rocks; + + ... + + # by default in devstack services are exposed with the url path style. + location /identity/v4 { + server http://localhost:8080; + } + proxy_pass http://<192.168.1.1>; +} +``` + +### Apache sample configuration + +```config + + + ServerName devstack..v6.rocks + + ... + # Rust /v4 API + ProxyPass "/identity/v4" http://localhost:8080/v4 retry=0 + # Python /v3 APIs are served by the uwsgi app + ProxyPass "/identity/v3" "unix:/var/run/uwsgi/keystone-api.socket|uwsgi://uwsgi-uds-keystone-api/v3" retry=0 + # We want discovery URL to be served by Rust. The same way any /v3/ API can + # be forwarded to rust version (where supported). + ProxyPass "/identity" http://localhost:8080 retry=0 + +``` diff --git a/doc/src/intro.md b/doc/src/intro.md index bae750cd..24fd4b47 100644 --- a/doc/src/intro.md +++ b/doc/src/intro.md @@ -7,15 +7,15 @@ project/tenant management, and federation services across thousands of deployments. However, as we embarked on adding next-generation identity features—such as native WebAuthn (“passkeys”), modern federation flows, direct OIDC support, JWT login, workload authorization, restricted tokens and -service-accounts—it became clear that certain design and performance -limitations of the Python codebase would hamper efficient implementation of -these new features. +service-accounts—it became clear that certain design and performance limitations +of the Python codebase would hamper efficient implementation of these new +features. Consequently, we initiated a project termed “Keystone-NG”: a Rust-based component that augments rather than fully replaces the existing Keystone -service. The original plan was to implement only the new feature-set in Rust -and route those new API paths to the Rust component, while keeping the core -Python Keystone service in place for existing users and workflows. +service. The original plan was to implement only the new feature-set in Rust and +route those new API paths to the Rust component, while keeping the core Python +Keystone service in place for existing users and workflows. As development progressed, however, the breadth of new functionality (and the opportunity to revisit some of the existing limitations) led to a partial @@ -32,54 +32,52 @@ In practice, this architecture means: user workflows, catalogs, policies and plugins. - The Rust “Keystone-NG” component handles new functionality, specifically: - - Native WebAuthN (passkeys) support for passwordless / phishing-resistant MFA - A reworked federation service, enabling modern identity brokering and advanced federation semantics OIDC (OpenID Connect) Direct in Keystone, - enabling Keystone to act as an OIDC Provider or integrate with external - OIDC identity providers natively JWT login flows, enabling stateless, - compact tokens suitable for new micro-services, CLI, SDK, and - workload-to-workload scenarios + enabling Keystone to act as an OIDC Provider or integrate with external OIDC + identity providers natively JWT login flows, enabling stateless, compact + tokens suitable for new micro-services, CLI, SDK, and workload-to-workload + scenarios - Workload Authorization, designed for service-to-service authorization in cloud native contexts (not just human users) - Restricted Tokens and Service Accounts, which allow fine-grained, - limited‐scope credentials for automation, agents, and service accounts, - with explicit constraints and expiry + limited‐scope credentials for automation, agents, and service accounts, with + explicit constraints and expiry By routing only the new flows through the Rust component we preserve the stability and ecosystem compatibility of Keystone, while enabling a -forward-looking identity architecture. Over time, additional identity flows -may be migrated or refactored into the Rust component as needed, but our -current objective is to retain the existing Keystone Python implementation as -the trusted, mature baseline and incrementally build the “Keystone-NG” Rust -service as the complement. - -We believe this approach allows the best of both worlds: the trusted maturity -of Keystone’s Python code-base, combined with the modern, high-safety, +forward-looking identity architecture. Over time, additional identity flows may +be migrated or refactored into the Rust component as needed, but our current +objective is to retain the existing Keystone Python implementation as the +trusted, mature baseline and incrementally build the “Keystone-NG” Rust service +as the complement. + +We believe this approach allows the best of both worlds: the trusted maturity of +Keystone’s Python code-base, combined with the modern, high-safety, high-performance capabilities of Rust where they matter most. ## Compatibility Highest priority is to ensure that this implementation is compatible with the original python Keystone: authentication issued by Rust implementation is -accepted by the Python Keystone and vice versa. At the same time it is -expected, that the new implementation may implement new features not supported -by the Python implementation. In this case, it is still expected that such -features do not break authentication flows. It must be possible to deploy -Python and Rust implementation in parallel and do request routing on the web -server level. +accepted by the Python Keystone and vice versa. At the same time it is expected, +that the new implementation may implement new features not supported by the +Python implementation. In this case, it is still expected that such features do +not break authentication flows. It must be possible to deploy Python and Rust +implementation in parallel and do request routing on the web server level. ## Database Adding new features most certainly require having database changes. It is not -expected that such changes interfere with the Python implementation to ensure -it is working correctly. +expected that such changes interfere with the Python implementation to ensure it +is working correctly. ## API Also here it is expected that new API resources are going to be added. As above -it is not expected that such changes interfere with the Python implementation -to ensure it is still working correctly and existing clients will not break. +it is not expected that such changes interfere with the Python implementation to +ensure it is still working correctly and existing clients will not break. diff --git a/doc/src/jwt.md b/doc/src/jwt.md index 2d3331d2..9d549c89 100644 --- a/doc/src/jwt.md +++ b/doc/src/jwt.md @@ -4,11 +4,11 @@ It is possible to authenticate with the JWT token issued by the federated IdP. More precisely it is possible to exchange a valid JWT for the Keystone token. There are few different use scenarios that are covered. -Since the JWT was issued without any knowledge of the Keystone scopes it -becomes hard to control scope. In the case of real human login the Keystone may -issue unscoped token allowing user to further rescope it. In the case of the -workflow federation that introduces a potential security vulnerability. As such -in this scenario the attribute mapping is responsible to fix the scope. +Since the JWT was issued without any knowledge of the Keystone scopes it becomes +hard to control scope. In the case of real human login the Keystone may issue +unscoped token allowing user to further rescope it. In the case of the workflow +federation that introduces a potential security vulnerability. As such in this +scenario the attribute mapping is responsible to fix the scope. Login request looks following: @@ -20,8 +20,8 @@ Login request looks following: ## Regular user obtains JWT (ID token) at the IdP and presents it to Keystone In this scenario a real user (human) is obtaining the valid JWT from the IDP -using any available method without any communication with Keystone. This may -use authorization code grant, password grant, device grant or any other enabled +using any available method without any communication with Keystone. This may use +authorization code grant, password grant, device grant or any other enabled method. This JWT is then presented to the Keystone and an explicitly requested attribute mapping converts the JWT claims to the Keystone internal representation after verifying the JWT signature, expiration and further @@ -30,13 +30,13 @@ restricted bound claims. ## Workload federation Automated workflows (Zuul job, GitHub workflows, GitLab CI, etc) are typical -workloads not being bound to any specific user and are more regularly -considered being triggered by certain services. Such workflows are usually in -possession of a JWT token issued by the service owned IdP. Keystone allows -exchange of such tokens to the regular Keystone token after validating token -issuer signature, expiration and applying the configured attribute mapping. -Since in such case there is no real human the mapping also need to be -configured slightly different. +workloads not being bound to any specific user and are more regularly considered +being triggered by certain services. Such workflows are usually in possession of +a JWT token issued by the service owned IdP. Keystone allows exchange of such +tokens to the regular Keystone token after validating token issuer signature, +expiration and applying the configured attribute mapping. Since in such case +there is no real human the mapping also need to be configured slightly +different. - It is strongly advised the attribute mapping must fill `token_user_id`, `token_project_id` (and soon `token_role_ids`). This allows strong control of @@ -82,9 +82,11 @@ Mapping: } ``` -TODO: add more claims according to [docs](https://docs.github.com/en/actions/reference/security/oidc#oidc-token-claims) +TODO: add more claims according to +[docs](https://docs.github.com/en/actions/reference/security/oidc#oidc-token-claims) -A way for the workflow to obtain the JWT [is described here](https://docs.github.com/en/actions/reference/security/oidc#methods-for-requesting-the-oidc-token). +A way for the workflow to obtain the JWT +[is described here](https://docs.github.com/en/actions/reference/security/oidc#methods-for-requesting-the-oidc-token). ```yaml ... diff --git a/doc/src/oidc.md b/doc/src/oidc.md index 987b0b28..7e4a6034 100644 --- a/doc/src/oidc.md +++ b/doc/src/oidc.md @@ -62,18 +62,19 @@ Authentication with the claim missing is going to be rejected. ## Using Okta as the Identity provider for a single domain Okta/Auth0 as an managed Identity provider can be easily integrated as a source -of the users and groups for the customer dedicated domain. [A dedicated -application](https://developer.okta.com/docs/guides/implement-grant-type/authcode/main/#set-up-your-app) +of the users and groups for the customer dedicated domain. +[A dedicated application](https://developer.okta.com/docs/guides/implement-grant-type/authcode/main/#set-up-your-app) need to be established on Okta (i.e. OpenStack) for the authentication delegation. There are many configuration options that can be used on the Okta side and will influence the interaction. It is not possible to describe every single one precisely, therefore only the basic setting are described here: - grant type: authorization code -- sign in redirect uris (enable the cli login): [`http://localhost:8050/oidc/callback`]. +- sign in redirect uris (enable the cli login): + [`http://localhost:8050/oidc/callback`]. -Group memberships are not exposed by default and require [additional -changes](https://developer.okta.com/docs/guides/customize-tokens-groups-claim/main/#add-a-groups-claim-for-a-custom-authorization-server) +Group memberships are not exposed by default and require +[additional changes](https://developer.okta.com/docs/guides/customize-tokens-groups-claim/main/#add-a-groups-claim-for-a-custom-authorization-server) On the Keystone side the following must be implemented: @@ -98,13 +99,14 @@ On the Keystone side the following must be implemented: Afterwards `osc` can be used by users to authenticate. clouds.yaml + ```yaml clouds: devstack-oidc-okta: auth_type: v4federation auth: - auth_url: - identity_provider: + auth_url: + identity_provider: ``` ```console @@ -124,7 +126,6 @@ A dedicated client must be created with following settings: - valid_redirect_uris: [`http://localhost:8050/oidc/callback`] - On the Keystone side the following must be implemented: - register an identity provider with the data obtained from Okta app @@ -170,7 +171,6 @@ A dedicated client must be created with following settings: A dedicated (and default) client scope must be created with the user attribute claim mapper populating "domain_id" scope with the corresponding value. - On the Keystone side the following must be implemented: - register an identity provider with the data obtained from Okta app diff --git a/doc/src/passkey.md b/doc/src/passkey.md index 07267cef..f6f69802 100644 --- a/doc/src/passkey.md +++ b/doc/src/passkey.md @@ -6,8 +6,8 @@ added to allow authenticating the user more securely. Important thing to be mentioned is that Operating System Passkeys (Apple keychain passkey, Google passkey, Microsoft ???) require browser to be running. This makes them unsuitable for the remote access. It is possible to implement -client authentication similar to the OIDC login which also requires browser, -but it is not implemented now. Therefore only authentication with bare security +client authentication similar to the OIDC login which also requires browser, but +it is not implemented now. Therefore only authentication with bare security device (Yubikey or similar) is implemented. ## Authenticate with Security Device @@ -37,12 +37,11 @@ Few dedicated API resources are added controlling the necessary aspects: - /users/{user_id}/passkeys/register_finish (complete the security key registration) -- /users/{user_id}/passkeys/login_start (initialize login of the security - device of the user) +- /users/{user_id}/passkeys/login_start (initialize login of the security device + of the user) - /users/{user_id}/passkeys/login_finish (complete the security key login) - ## DB changes Following DB tables are added: @@ -52,6 +51,7 @@ Following DB tables are added: ```rust {{#include ../../src/db/entity/webauthn_credential.rs:9:17}} ``` + - webauthn_state ```rust diff --git a/doc/src/policy.md b/doc/src/policy.md index edd108ad..96cd5537 100644 --- a/doc/src/policy.md +++ b/doc/src/policy.md @@ -1,41 +1,40 @@ # API policy enforcement -API policy is implemented using the [Open Policy Agent -(OPA)](https://openpolicyagent.org). It is a very powerful tool and allows -implementing policies much more complex than what the `oslo.policy` would ever -allow. The `policy` folder contain default policies. They can be overloaded by -the deployment. +API policy is implemented using the +[Open Policy Agent (OPA)](https://openpolicyagent.org). It is a very powerful +tool and allows implementing policies much more complex than what the +`oslo.policy` would ever allow. The `policy` folder contain default policies. +They can be overloaded by the deployment. OPA can be integrated into Keystone in 2 ways: - HTTP. This is a default and recommended way of integrating applications with -the OPA. Usually the OPA process is started as a side car container to keep -network latencies as low as possible. Policies themselves are bundled into the -container which OPA process is capable of downloading and even periodically -refreshing. It can be started as `opa run -s --log-level debug -tools/opa-config.yaml`. Alternatively the OPA process can itself run in the -container in which case the configuration file should be mounted as a volume -and referred from the entrypoint. + the OPA. Usually the OPA process is started as a side car container to keep + network latencies as low as possible. Policies themselves are bundled into the + container which OPA process is capable of downloading and even periodically + refreshing. It can be started as + `opa run -s --log-level debug tools/opa-config.yaml`. Alternatively the OPA + process can itself run in the container in which case the configuration file + should be mounted as a volume and referred from the entrypoint. - WASM. Policies can be built into a WASM binary module. This method does not -support feeding additional data and dynamic policy reload as of now. -Unfortunately there is also a memory access violation error in the `wasmtime` -crate happening for the big policy files. The investigation is in progress, so -it is preferred not to rely on this method anyway. While running OPA as a WASM -eliminates any networking communication, it heavily reduces feature set. In -particular hot policy reload, decision logging, external calls done by the -policies themselves are not possible by design. Using this way of policy -enforcement requires `wasm` feature enabled. + support feeding additional data and dynamic policy reload as of now. + Unfortunately there is also a memory access violation error in the `wasmtime` + crate happening for the big policy files. The investigation is in progress, so + it is preferred not to rely on this method anyway. While running OPA as a WASM + eliminates any networking communication, it heavily reduces feature set. In + particular hot policy reload, decision logging, external calls done by the + policies themselves are not possible by design. Using this way of policy + enforcement requires `wasm` feature enabled. All the policies currently are using the same policy names and definitions as the original Keystone to keep the deviation as less as possible. For the newly added APIs this is not anymore the case. -With the Open Policy Agent it is not only possible to define a decision -(allowed or forbidden), but also to produce additional information describing -i.e. reason of the request refusal. This is currently being used by the -policies by defining an array of "violation" objects explaining missing -permissions. +With the Open Policy Agent it is not only possible to define a decision (allowed +or forbidden), but also to produce additional information describing i.e. reason +of the request refusal. This is currently being used by the policies by defining +an array of "violation" objects explaining missing permissions. Sample policy for updating the federated IDP mapping: @@ -77,18 +76,19 @@ violation contains {"field": "role", "msg": "updating mapping requires `manager` ``` As can be guessed such policy would permit the API request when `admin` role is -present in the current credentials roles or the mapping in scope is owned by -the domain the user is currently scoped to with the `manager` role.` +present in the current credentials roles or the mapping in scope is owned by the +domain the user is currently scoped to with the `manager` role.` ## List operation All query parameters are passed into the policy engine to be provide capability -of making decision based on the parameters passed. For example an admin user -may specify `domain_id` parameter when the current authentication scope is not +of making decision based on the parameters passed. For example an admin user may +specify `domain_id` parameter when the current authentication scope is not matching the given `domain_id` or a user with the `manager` role being able to list shared federated identity providers. -Policy is being evaluated before the real data is being fetched from the backend. +Policy is being evaluated before the real data is being fetched from the +backend. ## Show operation @@ -111,5 +111,5 @@ certain conditions (i.e. when tag "locked" is added). ## Delete operation -Resource deletion also passes the current resource state in the context to -allow comprehensive logic. +Resource deletion also passes the current resource state in the context to allow +comprehensive logic.