Service Tokens¶
When OpenStack services communicate with each other on behalf of a user - such as the Compute service (Nova) calling the Block Storage service (Cinder) or Networking service (Neutron) - operations can fail if the user’s authentication token expires before the multi-step operation finishes.
To solve this problem, OpenStack supports service tokens. A service token
is a secondary token issued to a dedicated service account and transmitted via
the X-Service-Token HTTP header alongside the primary user’s
X-Auth-Token header.
When the receiving service (such as Cinder) receives both tokens, its keystonemiddleware pipeline validates the service token. If the service token is valid, the operation is permitted to proceed even if the user’s token has expired, provided the operation was initiated while the user’s token was valid. Service tokens also allow target services to verify the identity of the calling service and authorize service-specific roles.
For a detailed explanation of service users and tokens in OpenStack, see the Keystone documentation on managing service users.
Using service tokens with openstacksdk¶
openstacksdk relies on keystoneauth for HTTP communication and session
management. To attach a service token to requests made by an SDK
Connection, you configure a
keystoneauth1.service_token.ServiceTokenAuthWrapper.
The wrapper combines two distinct authentication plugins:
User Authentication Plugin (
user_auth): Provides credentials for the initiating user or the calling service’s standard user context (sent in theX-Auth-Tokenheader).Service Authentication Plugin (
service_auth): Provides credentials for the service user account (sent in theX-Service-Tokenheader).
Once wrapped, the composite auth plugin is passed to a
keystoneauth1.session.Session, which is then supplied to the SDK
Connection constructor:
from keystoneauth1 import service_token
from keystoneauth1 import session as ks_session
import openstack.connection
auth = service_token.ServiceTokenAuthWrapper(
user_auth=user_auth,
service_auth=service_auth,
)
sess = ks_session.Session(auth=auth)
conn = openstack.connection.Connection(session=sess, region_name=region)
Configuration¶
There are two primary mechanisms available for configuring service tokens.
Service user auth with a service token (oslo.config)¶
In an OpenStack service operational context (such as Nova communicating with Cinder), configuration is typically managed via oslo.config.
The service configuration file defines credentials for both the target service
client group (e.g. [cinder]) and the service user group
([service_user]):
[cinder]
auth_type = password
auth_url = https://identity.example.com/identity/v3
project_domain_name = Default
project_name = service
user_domain_name = Default
username = nova
password = secret-nova-password
os_region_name = RegionOne
[service_user]
send_service_user_token = true
auth_type = password
auth_url = https://identity.example.com/identity/v3
project_domain_name = Default
project_name = service
user_domain_name = Default
username = nova
password = secret-nova-password
Both plugins are loaded using keystoneauth1.loading and wrapped
together:
from keystoneauth1 import loading as ks_loading
from keystoneauth1 import service_token
from keystoneauth1 import session as ks_session
import openstack.connection
from oslo_config import cfg
CONF = cfg.CONF
# Load target service auth and service token auth
cinder_auth = ks_loading.load_auth_from_conf_options(CONF, 'cinder')
service_auth = ks_loading.load_auth_from_conf_options(CONF, 'service_user')
# Wrap them so both tokens are sent
auth = service_token.ServiceTokenAuthWrapper(
user_auth=cinder_auth,
service_auth=service_auth,
)
sess = ks_session.Session(auth=auth)
conn = openstack.connection.Connection(
session=sess,
region_name=CONF.cinder.os_region_name,
)
End-user credentials with a service token¶
When an operation is initiated by an end user whose credentials are loaded from
a standard clouds.yaml configuration (or an incoming request context), but
the application must include a service token from its configuration file, you
can combine the two sources:
import os
from keystoneauth1 import loading as ks_loading
from keystoneauth1 import service_token
from keystoneauth1 import session as ks_session
import openstack.config
import openstack.connection
from oslo_config import cfg
CONF = cfg.CONF
# User credentials from clouds.yaml
cloud_config = openstack.config.OpenStackConfig().get_one(
cloud=os.environ['OS_CLOUD']
)
user_auth = cloud_config.get_auth()
# Service token credentials from service configuration
service_auth = ks_loading.load_auth_from_conf_options(CONF, 'service_user')
auth = service_token.ServiceTokenAuthWrapper(
user_auth=user_auth,
service_auth=service_auth,
)
sess = ks_session.Session(auth=auth)
conn = openstack.connection.Connection(
session=sess,
region_name=CONF.cinder.os_region_name,
)
Example¶
A full runnable script demonstrating this pattern against Cinder’s
list-volume-types endpoint is available in the repository at
examples/advanced/service_tokens.py:
#!/usr/bin/env python3
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Connect to an OpenStack cloud using service tokens.
Reads an oslo.config configuration file (defaulting to /tmp/service.conf, or
specified via --config-file) containing [cinder] and [service_user] sections.
Demonstrates sending a service token alongside the user token:
X-Auth-Token: <cinder auth token>
X-Service-Token: <nova service token>
Run with mitmweb/mitmproxy to inspect the outgoing headers::
export HTTP_PROXY=http://127.0.0.1:8080
python examples/advanced/service_tokens.py
"""
import sys
from keystoneauth1 import loading as ks_loading
from keystoneauth1 import service_token
from keystoneauth1 import session as ks_session
from oslo_config import cfg
import openstack.connection
CONF = cfg.ConfigOpts()
def _register_opts() -> None:
# Register options for the target service client group ([cinder])
cinder_group = cfg.OptGroup('cinder', title='Cinder Options')
CONF.register_group(cinder_group)
CONF.register_opts(
[cfg.StrOpt('os_region_name', default='RegionOne')],
group='cinder',
)
ks_loading.register_session_conf_options(CONF, 'cinder')
ks_loading.register_auth_conf_options(CONF, 'cinder')
# Register options for the service user token group ([service_user])
service_user_group = cfg.OptGroup(
'service_user', title='Service User Options'
)
CONF.register_group(service_user_group)
CONF.register_opts(
[cfg.BoolOpt('send_service_user_token', default=False)],
group='service_user',
)
ks_loading.register_session_conf_options(CONF, 'service_user')
ks_loading.register_auth_conf_options(CONF, 'service_user')
def main() -> None:
_register_opts()
CONF(sys.argv[1:], default_config_files=['/tmp/service.conf'])
# Load auth plugins from configuration
cinder_auth = ks_loading.load_auth_from_conf_options(CONF, 'cinder')
service_auth = ks_loading.load_auth_from_conf_options(CONF, 'service_user')
# Wrap auth plugins so both X-Auth-Token and X-Service-Token are sent
auth = service_token.ServiceTokenAuthWrapper(
user_auth=cinder_auth, service_auth=service_auth
)
sess = ks_session.Session(auth=auth)
conn = openstack.connection.Connection(
session=sess, region_name=CONF.cinder.os_region_name
)
print('Listing volume types...')
try:
vtypes = list(conn.block_storage.types())
print(f'Found {len(vtypes)} volume type(s):')
for vt in vtypes:
print(f' - {vt.name} ({vt.id})')
except Exception as exc:
print(f'ERROR: {exc}')
if __name__ == '__main__':
main()
To test this locally with an HTTP proxy (such as mitmproxy) to inspect the
outgoing X-Auth-Token and X-Service-Token headers:
export HTTP_PROXY=http://127.0.0.1:8080
python examples/advanced/service_tokens.py