Learn OpenStack · core/identity

Keystone Federation

Federation lets Keystone accept identities asserted by an external Identity Provider (IdP) — such as a corporate SAML2 IdP (Shibboleth, ADFS), an OIDC provider (Keycloak, Okta, Google), or another OpenStack Keystone (K2K). Users authenticate to the IdP; Keystone trusts the assertion and issues its own token.

Concepts

Identity Provider (IdP)

An external system that authenticates users and issues signed assertions about their identity and attributes (e.g., email, group membership). Keystone trusts one or more IdPs.

Service Provider (SP)

In SAML2 federation, the system receiving and consuming assertions. Keystone acts as the SP for incoming federated logins.

Protocol

A protocol is a named link between an IdP and a mapping within Keystone. Each IdP can have multiple protocols (e.g., saml2, oidc).

Mapping

A mapping rule set that translates IdP-supplied attributes (e.g., SAML attributes, OIDC claims) into Keystone local roles, groups, and projects. Mappings define who gets what access based on what the IdP says about them.

Shadow Users

When a federated user logs in for the first time, Keystone auto-provisions a local user record in the shadow_users table. This is the shadow user. Shadow users: - Are linked to the federated IdP and protocol - Have no local password - Are created with domain_id of the federated domain - Can accumulate role assignments over time (e.g., via group mapping) - Are updated on each login to reflect the latest IdP attributes

Shadow user creation is controlled by the mapping's user local rule. If type is ephemeral, no persistent shadow user is created (token-only access, no role persistence).

SAML2 Federation Setup

This walkthrough configures Keystone as a SAML2 Service Provider, trusting an external IdP (e.g., Shibboleth, ADFS, or any SAML2 IdP).

Install and Configure mod_shib

# RHEL/CentOS
dnf install shibboleth

# Ubuntu/Debian
apt install libapache2-mod-shib

Configure Shibboleth SP at /etc/shibboleth/shibboleth2.xml:

<SPConfig xmlns="urn:mace:shibboleth:3.0:native:sp:config" clockSkew="180">
  <ApplicationDefaults entityID="https://keystone.example.com/shibboleth"
                       REMOTE_USER="eppn persistent-id targeted-id">

    <Sessions lifetime="28800" timeout="3600" relayState="ss:mem"
              checkAddress="false" handlerSSL="false" cookieProps="http">
      <SSO entityID="https://idp.acme-corp.com/idp/shibboleth">
        SAML2
      </SSO>
      <Logout>SAML2 Local</Logout>
      <Handler type="MetadataGenerator" Location="/Metadata" signing="false"/>
      <Handler type="Status" Location="/Status"/>
      <Handler type="Session" Location="/Session" showAttributeValues="false"/>
      <Handler type="DiscoveryFeed" Location="/DiscoFeed"/>
    </Sessions>

    <MetadataProvider type="XML" validate="true"
        url="https://idp.acme-corp.com/idp/shibboleth"
        backingFilePath="idp-metadata.xml" maxRefreshDelay="7200">
    </MetadataProvider>

    <AttributeExtractor type="XML" validate="true" reloadChanges="false"
                        path="attribute-map.xml"/>
    <AttributeFilter type="XML" validate="true" path="attribute-policy.xml"/>
    <CredentialResolver type="File" use="signing"
                        key="sp-key.pem" certificate="sp-cert.pem"/>
  </ApplicationDefaults>
</SPConfig>

Configure Apache for Federation

# /etc/httpd/conf.d/keystone-federation.conf
<VirtualHost *:5000>
  ServerName keystone.example.com
  WSGIScriptAlias / /usr/bin/keystone-wsgi-public
  WSGIProcessGroup keystone
  WSGIApplicationGroup %{GLOBAL}

  # SAML2 protected endpoint
  <Location /v3/OS-FEDERATION/identity_providers/acme-idp/protocols/saml2/auth>
    AuthType shibboleth
    ShibRequestSetting requireSession 1
    ShibRequireSession On
    ShibExportAssertion Off
    Require valid-user
  </Location>

  # WebSSO callback (for Horizon)
  <Location /v3/auth/OS-FEDERATION/websso/saml2>
    AuthType shibboleth
    ShibRequestSetting requireSession 1
    Require valid-user
  </Location>
</VirtualHost>

Register the IdP in Keystone

# Create the Identity Provider record
openstack identity provider create acme-idp \
  --remote-id https://idp.acme-corp.com/idp/shibboleth \
  --description "ACME Corp SAML2 IdP"

Create a Mapping

Mappings translate SAML2 attributes into Keystone roles and groups.

# Write mapping rules to a JSON file
cat > /tmp/acme-saml-mapping.json << 'EOF'
[
  {
    "local": [
      {
        "user": {
          "name": "{0}",
          "email": "{1}"
        }
      },
      {
        "group": {
          "id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"
        }
      }
    ],
    "remote": [
      {
        "type": "MELLON_NAME_ID"
      },
      {
        "type": "MELLON_mail"
      },
      {
        "type": "MELLON_isMemberOf",
        "any_one_of": [
          "cn=openstack-users,ou=Groups,dc=acme-corp,dc=com"
        ]
      }
    ]
  },
  {
    "local": [
      {
        "user": {
          "name": "{0}"
        }
      },
      {
        "group": {
          "id": "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5"
        }
      }
    ],
    "remote": [
      {
        "type": "MELLON_NAME_ID"
      },
      {
        "type": "MELLON_isMemberOf",
        "any_one_of": [
          "cn=openstack-admins,ou=Groups,dc=acme-corp,dc=com"
        ]
      }
    ]
  }
]
EOF

openstack mapping create acme-saml-mapping \
  --rules /tmp/acme-saml-mapping.json

(The group IDs a1b2c3d4... are pre-existing Keystone groups that have role assignments on projects.)

Create the Protocol

openstack federation protocol create saml2 \
  --identity-provider acme-idp \
  --mapping acme-saml-mapping

Test the Federation Flow

# Authenticate via ECP (non-browser SAML2 profile)
openstack token issue \
  --os-auth-type v3samlpassword \
  --os-auth-url http://10.0.0.10:5000/v3 \
  --os-identity-provider acme-idp \
  --os-protocol saml2 \
  --os-identity-provider-url https://idp.acme-corp.com/idp/profile/SAML2/SOAP/ECP \
  --os-username alice@acme-corp.com \
  --os-password s3cur3P@ss \
  --os-project-name engineering \
  --os-project-domain-name Default

OIDC Federation Setup

This walkthrough configures Keystone as an OIDC Relying Party, trusting an OIDC IdP (e.g., Keycloak at https://keycloak.example.com).

Install mod_auth_openidc

# RHEL/CentOS
dnf install mod_auth_openidc

# Ubuntu/Debian
apt install libapache2-mod-auth-openidc

Configure mod_auth_openidc

# /etc/httpd/conf.d/keystone-oidc.conf
OIDCProviderMetadataURL https://keycloak.example.com/realms/openstack/.well-known/openid-configuration
OIDCClientID keystone-sp
OIDCClientSecret oidc_client_secret_here
OIDCRedirectURI http://keystone.example.com:5000/v3/OS-FEDERATION/identity_providers/keycloak-idp/protocols/oidc/auth
OIDCCryptoPassphrase random_passphrase_here
OIDCResponseType code
OIDCScope "openid email profile groups"
OIDCPassClaimsAs environment
OIDCClaimPrefix OIDC_CLAIM_

<Location /v3/OS-FEDERATION/identity_providers/keycloak-idp/protocols/oidc/auth>
  AuthType openid-connect
  Require valid-user
</Location>

<Location /v3/auth/OS-FEDERATION/websso/oidc>
  AuthType openid-connect
  Require valid-user
</Location>

Register the IdP in Keystone

openstack identity provider create keycloak-idp \
  --remote-id https://keycloak.example.com/realms/openstack \
  --description "Keycloak OIDC IdP"

Create an OIDC Mapping

cat > /tmp/keycloak-oidc-mapping.json << 'EOF'
[
  {
    "local": [
      {
        "user": {
          "name": "{0}",
          "email": "{1}",
          "domain": {
            "name": "federated"
          }
        }
      },
      {
        "group": {
          "id": "c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6"
        }
      }
    ],
    "remote": [
      {
        "type": "OIDC_CLAIM_preferred_username"
      },
      {
        "type": "OIDC_CLAIM_email"
      },
      {
        "type": "OIDC_CLAIM_groups",
        "any_one_of": ["openstack-users"]
      }
    ]
  },
  {
    "local": [
      {
        "user": {
          "name": "{0}"
        }
      },
      {
        "group": {
          "id": "d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1"
        }
      }
    ],
    "remote": [
      {
        "type": "OIDC_CLAIM_preferred_username"
      },
      {
        "type": "OIDC_CLAIM_groups",
        "any_one_of": ["openstack-admins"]
      }
    ]
  }
]
EOF

openstack mapping create keycloak-oidc-mapping \
  --rules /tmp/keycloak-oidc-mapping.json

Create the OIDC Protocol

openstack federation protocol create oidc \
  --identity-provider keycloak-idp \
  --mapping keycloak-oidc-mapping

Test the OIDC Flow

# Get an unscoped federated token
openstack token issue \
  --os-auth-type v3oidcpassword \
  --os-auth-url http://10.0.0.10:5000/v3 \
  --os-identity-provider keycloak-idp \
  --os-protocol oidc \
  --os-discovery-endpoint https://keycloak.example.com/realms/openstack/.well-known/openid-configuration \
  --os-client-id keystone-sp \
  --os-client-secret oidc_client_secret_here \
  --os-username alice \
  --os-password s3cur3P@ss

# Exchange unscoped token for a scoped token
openstack token issue \
  --os-auth-type v3token \
  --os-token <unscoped-token-from-above> \
  --os-project-name engineering \
  --os-project-domain-name Default

Keystone-to-Keystone (K2K) Federation

K2K allows one OpenStack Keystone (the IdP Keystone, cloud-a) to issue tokens that a second Keystone (the SP Keystone, cloud-b) accepts. Users authenticate to cloud-a and receive tokens that grant access to resources in cloud-b.

Architecture

  Cloud A (IdP Keystone)                  Cloud B (SP Keystone)
  ─────────────────────                   ──────────────────────
  User authenticates                      Receives SAML2 assertion
  Keystone issues token          ────→    Validates assertion
  Keystone generates SAML2               Issues scoped token
  assertion for cloud-b                   User accesses resources

Configure Cloud A (IdP Keystone)

# /etc/keystone/keystone.conf on cloud-a
[saml]
# SP entity ID (our Keystone's identity)
idp_entity_id = https://keystone-a.example.com/v3/OS-FEDERATION/saml2/idp
idp_sso_endpoint = https://keystone-a.example.com/v3/OS-FEDERATION/saml2/auth
# Signing certificate for SAML2 assertions
certfile = /etc/keystone/ssl/certs/signing_cert.pem
keyfile = /etc/keystone/ssl/private/signing_key.pem
# Contact info
idp_contact_company = Example Corp
idp_contact_name = OpenStack Admin
idp_contact_email = admin@example.com
idp_metadata_path = /etc/keystone/saml2_idp_metadata.xml

Generate IdP metadata:

keystone-manage saml_idp_metadata > /etc/keystone/saml2_idp_metadata.xml

Register Cloud B as a Service Provider in Cloud A

# On cloud-a
openstack service provider create cloud-b \
  --auth-url https://keystone-b.example.com/v3/OS-FEDERATION/identity_providers/cloud-a/protocols/saml2/auth \
  --service-provider-url https://keystone-b.example.com/Shibboleth.sso/SAML2/ECP \
  --description "Cloud B Service Provider"

Configure Cloud B (SP Keystone)

Cloud B uses the same Shibboleth/mod_shib setup as a regular SAML2 SP, but the IdP is cloud-a's Keystone:

# On cloud-b
openstack identity provider create cloud-a-idp \
  --remote-id https://keystone-a.example.com/v3/OS-FEDERATION/saml2/idp \
  --description "Cloud A Keystone IdP"

Create mapping for cloud-a users on cloud-b:

cat > /tmp/k2k-mapping.json << 'EOF'
[
  {
    "local": [
      {
        "user": {
          "name": "{0}",
          "domain": {
            "name": "cloud-a-federated"
          }
        }
      },
      {
        "group": {
          "id": "e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
        }
      }
    ],
    "remote": [
      {
        "type": "openstack_user"
      },
      {
        "type": "openstack_roles",
        "any_one_of": ["member", "admin"]
      }
    ]
  }
]
EOF

openstack mapping create k2k-mapping --rules /tmp/k2k-mapping.json

openstack federation protocol create saml2 \
  --identity-provider cloud-a-idp \
  --mapping k2k-mapping

Authenticate K2K (Python keystoneauth1 example)

from keystoneauth1 import session
from keystoneauth1.identity.v3 import Password
from keystoneauth1.identity.v3 import Keystone2Keystone

# Step 1: Authenticate to cloud-a
auth_a = Password(
    auth_url="https://keystone-a.example.com/v3",
    username="alice",
    password="s3cur3P@ss",
    user_domain_name="Default",
    project_name="engineering",
    project_domain_name="Default",
)
session_a = session.Session(auth=auth_a)

# Step 2: Use cloud-a token to get cloud-b token
auth_b = Keystone2Keystone(
    base_plugin=auth_a,
    service_provider="cloud-b",
    project_name="cross-cloud-project",
    project_domain_name="Default",
)
session_b = session.Session(auth=auth_b)
token_b = session_b.get_token()

Mapping Rules

Mapping rules are JSON arrays. Each rule has a local and remote section.

Rule Structure

[
  {
    "local": [
      { "user": { "name": "{0}" } },
      { "group": { "id": "..." } },
      { "projects": [ { "name": "{1}", "roles": [ { "name": "member" } ] } ] }
    ],
    "remote": [
      { "type": "ATTRIBUTE_NAME" },
      { "type": "ATTRIBUTE_NAME", "any_one_of": ["value1", "value2"] },
      { "type": "ATTRIBUTE_NAME", "not_any_of": ["denied_value"] },
      { "type": "ATTRIBUTE_NAME", "regex": true, "any_one_of": ["pattern.*"] }
    ]
  }
]

remote Conditions

Condition Meaning
any_one_of Attribute must contain at least one listed value
not_any_of Attribute must NOT contain any listed value
blacklist Exclude these values (deprecated — use not_any_of)
whitelist Include only these values (deprecated — use any_one_of)
regex: true Treat any_one_of values as Python regex patterns

local Directives

Directive Effect
user.name Set the federated user's local name
user.email Set the user's email
user.domain.name Assign the user to a specific domain
user.type local (persistent shadow user) or ephemeral (no DB record)
group.id Add user to an existing Keystone group (by UUID)
group.name + group.domain Add user to a group by name+domain
projects Directly assign the user to projects with specific roles

Auto-Provision Projects (Direct Project Mapping)

{
  "local": [
    {
      "user": { "name": "{0}" },
      "projects": [
        {
          "name": "federated-sandbox",
          "roles": [ { "name": "member" } ]
        }
      ]
    }
  ],
  "remote": [
    { "type": "OIDC_CLAIM_preferred_username" }
  ]
}

Manage Identity Providers

Create an Identity Provider

openstack identity provider create acme-idp \
  --remote-id https://idp.acme-corp.com/idp/shibboleth \
  --description "ACME Corp corporate IdP"

Multiple --remote-id values can be specified (useful for IdPs with multiple entity IDs).

List Identity Providers

openstack identity provider list

Show an Identity Provider

openstack identity provider show acme-idp

Update an Identity Provider

openstack identity provider set acme-idp \
  --remote-id https://idp.acme-corp.com/idp/shibboleth \
  --remote-id https://idp2.acme-corp.com/idp/shibboleth

Delete an Identity Provider

openstack identity provider delete acme-idp

Manage Federation Protocols

Create a Protocol

openstack federation protocol create saml2 \
  --identity-provider acme-idp \
  --mapping acme-saml-mapping

List Protocols for an IdP

openstack federation protocol list --identity-provider acme-idp

Show a Protocol

openstack federation protocol show saml2 --identity-provider acme-idp

Update a Protocol's Mapping

openstack federation protocol set saml2 \
  --identity-provider acme-idp \
  --mapping acme-saml-mapping-v2

Delete a Protocol

openstack federation protocol delete saml2 --identity-provider acme-idp

Manage Mappings

Create a Mapping

openstack mapping create acme-saml-mapping \
  --rules /tmp/acme-saml-mapping.json

List Mappings

openstack mapping list

Show a Mapping

openstack mapping show acme-saml-mapping

Update a Mapping

openstack mapping set acme-saml-mapping \
  --rules /tmp/acme-saml-mapping-v2.json

Delete a Mapping

openstack mapping delete acme-saml-mapping

WebSSO (Horizon Integration)

To allow Horizon users to log in via federated IdPs, configure WebSSO.

Keystone Config

[federation]
trusted_dashboard = http://203.0.113.10/dashboard/auth/websso/
sso_callback_template = /etc/keystone/sso_callback_template.html

Horizon Config

# /etc/openstack-dashboard/local_settings.py
WEBSSO_ENABLED = True
WEBSSO_INITIAL_CHOICE = "credentials"
WEBSSO_CHOICES = (
    ("credentials", _("Keystone Credentials")),
    ("acme-idp_saml2", _("ACME Corp SSO (SAML2)")),
    ("keycloak-idp_oidc", _("Keycloak OIDC")),
)
# Format: "<identity_provider_name>_<protocol>"

The Horizon login page will show a dropdown with the configured IdP choices.

Troubleshoot Federation

SAML2 Attribute Mapping Issues

# Check what attributes Shibboleth is passing
# Add to Apache config temporarily:
# ShibUseHeaders On  (exposes SHIB_ headers for debugging)

# View active Shibboleth session (user must be logged in)
curl http://keystone.example.com/Shibboleth.sso/Session

# Check Shibboleth log
tail -f /var/log/shibboleth/shibd.log

OIDC Claims Debug

# Add to Apache (development only):
# OIDCHTMLErrorTemplate /tmp/oidc_error.html
# LogLevel debug

# Verify OIDC_CLAIM_ environment variables are passed:
# Add a debug handler that prints env vars to a test endpoint

Keystone Mapping Evaluation

# Test mapping evaluation against a set of remote attributes
openstack mapping purge --all
# Check keystone.log for mapping evaluation details
# Enable debug logging temporarily:
tail -f /var/log/keystone/keystone.log | grep -i mapping

Shadow User Issues

# List shadow users for a specific IdP
openstack user list --domain federated

# Show a federated user (shadow user)
openstack user show alice --domain federated

# Force re-evaluation: delete the shadow user (they will be re-created on next login)
openstack user delete alice --domain federated