Add OpenID Connect Token Auth for k8s

This patch adds openid token auth support when calling k8s APIs.

Openid token auth of k8s relies on an external openid provider,
and Keycloak acts as the openid provider in this implementation.

Implements: blueprint support-openid-k8s-vim
Change-Id: Ie5e080a20cba3ba0ed514ede7955eb16729d797c
changes/01/851701/20
Qibin Yao 6 months ago committed by Ayumu Ueha
parent dbca617b98
commit 57902730d6

@ -568,6 +568,19 @@
kubernetes_vim_rsc_wait_timeout: 800
tox_envlist: dsvm-functional-sol-kubernetes-v2
- job:
name: tacker-functional-devstack-kubernetes-oidc-auth
parent: tacker-functional-devstack-multinode-sol-kubernetes-v2
description: |
Multinodes job for Kubernetes OIDC Auth tests
host-vars:
controller-tacker:
tox_envlist: dsvm-functional-sol_kubernetes_oidc_auth
vars:
keycloak_host: "{{ hostvars['controller-k8s']['nodepool']['private_ipv4'] }}"
keycloak_http_port: 8080
keycloak_https_port: 8443
- job:
name: tacker-compliance-devstack-multinode-sol
parent: tacker-functional-devstack-multinode-legacy
@ -597,4 +610,5 @@
- tacker-functional-devstack-multinode-sol-kubernetes-v2
- tacker-functional-devstack-multinode-sol-multi-tenant
- tacker-functional-devstack-multinode-sol-kubernetes-multi-tenant
- tacker-functional-devstack-kubernetes-oidc-auth
- tacker-compliance-devstack-multinode-sol

@ -26,3 +26,4 @@ Reference
block_storage_usage_guide.rst
reservation_policy_usage_guide.rst
maintenance_usage_guide.rst
kubernetes_openid_token_auth_usage_guide.rst

@ -0,0 +1,322 @@
============================================
Kubernetes VIM OpenID Token Auth Usage Guide
============================================
Overview
--------
Kubernetes has multiple authentication strategies. This document describes
how Tacker use `OpenID token`_ to authenticate with Kubernetes.
The OpenID token authentication of Kubernetes relies on an external OpenID
provider, and `Keycloak`_ acts as the OpenID provider in this document.
Preparation
-----------
Prerequisites
~~~~~~~~~~~~~
The following packages should be installed:
* Kubernetes
* Docker
Start Keycloak
~~~~~~~~~~~~~~
Create a CSR config file ``csr.cnf``:
.. code-block:: cfg
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
IP.1 = 127.0.0.1
IP.2 = 192.168.2.33 # Host IP
Generate SSL certificate for Keycloak:
.. code-block:: shell
#!/bin/bash
req_conf=csr.cnf
ssl_dir=/etc/keycloak/ssl
key_file=$ssl_dir/keycloak.key
csr_file=$ssl_dir/keycloak.csr
crt_file=$ssl_dir/keycloak.crt
k8s_ssl_dir=/etc/kubernetes/pki
k8s_ca_crt=$k8s_ssl_dir/ca.crt
k8s_ca_key=$k8s_ssl_dir/ca.key
# make a directory for storing certificate
mkdir -p $ssl_dir
# generate private key
openssl genrsa -out $key_file 2048
# generate certificate signing request
openssl req -new -key $key_file -out $csr_file -subj "/CN=Keycloak" \
-config $req_conf
# use Kubernetes's CA for issuing certificate
openssl x509 -req -in $csr_file -CA $k8s_ca_crt -CAkey $k8s_ca_key \
-CAcreateserial -out $crt_file -days 365 -extensions v3_req \
-extfile $req_conf
# add executeable permission to key file
chmod 755 $key_file
Starts a Keycloak container with docker:
.. code-block:: console
$ docker run -d \
--net=host \
-e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \
-e KC_HTTP_PORT=8080 -e KC_HTTPS_PORT=8443 \
-e KC_HTTPS_CERTIFICATE_FILE=/opt/keycloak/conf/keycloak.crt \
-e KC_HTTPS_CERTIFICATE_KEY_FILE=/opt/keycloak/conf/keycloak.key \
-v /etc/keycloak/ssl:/opt/keycloak/conf \
quay.io/keycloak/keycloak:18.0.2 \
start-dev
Setup Keycloak
~~~~~~~~~~~~~~
Login to the admin console
* Open the URL https://192.168.2.33:8443/admin with a browser
* Fill in the form with the following values:
+ :guilabel:`Username`: **admin**
+ :guilabel:`Password`: **admin**
Create a realm
* Click :guilabel:`Add realm`
* Fill in the :guilabel:`Name` with **oidc**
* Click :guilabel:`Create`
Create a user
* Click :guilabel:`Users` in the menu
* Click :guilabel:`Add user`
* Fill in the :guilabel:`Username` with **end-user**
* Click :guilabel:`Save`
Set user credentials
* Click :guilabel:`Credentials`
* Fill in the form with the following values:
+ :guilabel:`Password` : **end-user**
+ :guilabel:`Password Confirmation` : **end-user**
* Turn off Temporary
* Click :guilabel:`Save`
Set user attributes
* Click :guilabel:`Attributes`
* Fill in the form with the following values:
+ :guilabel:`Key` : **name**
+ :guilabel:`Value` : **end-user**
* Click :guilabel:`Add`
* Click :guilabel:`Save`
Create a client
* Click :guilabel:`Clients` in the menu
* Click :guilabel:`Create`
* Fill in the :guilabel:`Client ID` with **tacker**
* Click :guilabel:`Save`
Set client type and valid redirect URIs
* Click :guilabel:`Settings`
* Select **confidential** as the :guilabel:`Access Type`
* Fill in the :guilabel:`Valid Redirect URIs` with **http://***
* Click :guilabel:`Save`
Set client mappers
* Click :guilabel:`Mappers`
* Click :guilabel:`Create`
* Select **User Attribute** as the :guilabel:`Mapper Type`
* Fill in the form with the following values:
+ :guilabel:`Name` : **name**
+ :guilabel:`User Attribute` : **name**
+ :guilabel:`Token Claim Name` : **name**
* Select **String** as the :guilabel:`Claim JSON Type`
* Click :guilabel:`Save`
View client secret
* Click :guilabel:`Credentials` and view the secret.
Setup Kubernetes
~~~~~~~~~~~~~~~~
Add oidc related startup parameters to kube-apiserver manifest file.
.. code-block:: console
$ cat /etc/kubernetes/manifests/kube-apiserver.yaml
spec:
containers:
- command:
- kube-apiserver
- --oidc-issuer-url=https://192.168.2.33:8443/realms/oidc
- --oidc-client-id=tacker
- --oidc-username-claim=name
- --oidc-username-prefix=-
- --oidc-ca-file=/etc/kubernetes/ssl/ca.crt
.. note::
After modifying kube-apiserver manifest file, the kube-apiserver will be restarted.
Create a cluster role binding to grant end users permissions to manipulate
Kubernetes resources.
* Create a cluster role binding file ``cluster_role_binding.yaml``:
.. code-block:: yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: oidc-cluster-admin-binding
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: User
name: end-user
* Create cluster role binding:
.. code-block:: console
$ kubectl create -f cluster_role_binding.yaml
Verify
~~~~~~
Get token endpoint from Keycloak:
.. code-block:: console
$ curl -ks -X GET https://192.168.2.33:8443/realms/oidc/.well-known/openid-configuration | jq -r .token_endpoint
Result:
.. code-block:: console
https://192.168.2.33:8443/realms/oidc/protocol/openid-connect/token
Get a OpenID token from Keycloak:
.. code-block:: console
$ ID_TOKEN=$(curl -ks -X POST https://192.168.2.33:8443/realms/oidc/protocol/openid-connect/token \
-d grant_type=password -d scope=openid -d username=end-user -d password=end-user \
-d client_id=tacker -d client_secret=A93HfOUpySm6BjPug9PJdJumjEGUJMhc | jq -r .id_token)
$ echo $ID_TOKEN
Result:
.. code-block:: console
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIxbC1RMy1KanQ1eHVrNzhYbUVkZDU2Mko4YXRRVF95MU1zS0JDUTBBcklnIn0.eyJleHAiOjE2NjAxMjExNTUsImlhdCI6MTY2MDEyMDg1NSwiYXV0aF90aW1lIjowLCJqdGkiOiIwZjdkNDE2My05Njk1LTQ3MGMtYmE1OC02MWI4NDM4YTU4MzQiLCJpc3MiOiJodHRwczovLzE5Mi4xNjguMi4zMzo4NDQzL3JlYWxtcy9vaWRjIiwiYXVkIjoidGFja2VyIiwic3ViIjoiOGZlZDVhYzctZDY4OS00NWM1LWE5NmQtYTlmN2M3Y2QxZTJjIiwidHlwIjoiSUQiLCJhenAiOiJ0YWNrZXIiLCJzZXNzaW9uX3N0YXRlIjoiNzhiYzhmNDEtMjc4NC00YTU5LWJjOTUtNjNkZDM5YTQ5NjNiIiwiYXRfaGFzaCI6Ik9WczJ3Q29VclU0QWxZaml0dGNQLXciLCJhY3IiOiIxIiwic2lkIjoiNzhiYzhmNDEtMjc4NC00YTU5LWJjOTUtNjNkZDM5YTQ5NjNiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJlbmQtdXNlciJ9.SAncMlpRCLY8JlRUZ7YwirmYs5Qc-5qrvJYmyCZSiRgHyn-7cuqNan3vyVGO46Iv9Da51_Im3L5HaVJTcReeCZ2fhuzgei3yOquPugcfaqKKZEujA042Cc0pFTLS_dPl1xX3XINEcN4nGYGhGtLi8CBH0iANi-IY_VEdxogTyc9MlKgjP9Ca8eYNUPhop49GwLC-ph5vMShS9O834ywtQargb51zokQsoXAYrGBJMTWr37uMxP7UWXpYAQa82OyX3fElpueurd5WGEzGT1AhN1Ad4uIAxgD6dxFsiQYOHRSH-sByV0IwMdZoqIm4GFS6NHLj5usr6PSA5U9QpgCI7Q
Get all namespaces with OpenID token from Kubernetes:
.. code-block:: console
curl -ks -H "Authorization: Bearer $ID_TOKEN" https://192.168.2.33:6443/api/v1/namespaces
Result:
.. code-block:: console
{
"kind": "NamespaceList",
"apiVersion": "v1",
...omit...
View certificates
~~~~~~~~~~~~~~~~~
View Kubernetes CA certificate:
.. code-block:: console
$ cat /etc/kubernetes/pki/ca.crt
-----BEGIN CERTIFICATE-----
MIICwjCCAaqgAwIBAgIBADANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDEwdrdWJl
LWNhMB4XDTIwMDgyNjA5MzIzMVoXDTMwMDgyNDA5MzIzMVowEjEQMA4GA1UEAxMH
a3ViZS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALxkeE16lPAd
pfJj5GJMvZJFcX/CD6EB/LUoKwGmqVoOUQPd3b/NGy+qm+3bO9EU73epUPsVaWk2
Lr+Z1ua7u+iib/OMsfsSXMZ5OEPgd8ilrTGhXOH8jDkif9w1NtooJxYSRcHEwxVo
+aXdIJhqKdw16NVP/elS9KODFdRZDfQ6vU5oHSg3gO49kgv7CaxFdkF7QEHbchsJ
0S1nWMPAlUhA5b8IAx0+ecPlMYUGyGQIQgjgtHgeawJebH3PWy32UqfPhkLPzxsy
TSxk6akiXJTg6mYelscuxPLSe9UqNvHRIUoad3VnkF3+0CJ1z0qvfWIrzX3w92/p
YsDBZiP6vi8CAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB/wQFMAMB
Af8wDQYJKoZIhvcNAQELBQADggEBAIbv2ulEcQi019jKz4REy7ZyH8+ExIUBBuIz
InAkfxNNxV83GkdyA9amk+LDoF/IFLMltAMM4b033ZKO5RPrHoDKO+xCA0yegYqU
BViaUiEXIvi/CcDpT9uh2aNO8wX5T/B0WCLfWFyiK+rr9qcosFYxWSdU0kFeg+Ln
YAaeFY65ZWpCCyljGpr2Vv11MAq1Tws8rEs3rg601SdKhBmkgcTAcCzHWBXR1P8K
rfzd6h01HhIomWzM9xrP2/2KlYRvExDLpp9qwOdMSanrszPDuMs52okXgfWnEqlB
2ZrqgOcTmyFzFh9h2dj1DJWvCvExybRmzWK1e8JMzTb40MEApyY=
-----END CERTIFICATE-----
View Keycloak certificate:
.. code-block:: console
$ cat /etc/keycloak/ssl/keycloak.crt
-----BEGIN CERTIFICATE-----
MIIC7TCCAdWgAwIBAgIUQK2k5uNvlRLx43LI/t3a2/A/3iQwDQYJKoZIhvcNAQEL
BQAwFTETMBEGA1UEAxMKa3ViZXJuZXRlczAeFw0yMjA4MDQwNjIwNTFaFw0yMzA4
MDQwNjIwNTFaMBMxETAPBgNVBAMMCEtleWNsb2FrMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAni7HWLn2IpUImGO1sbBf/XuqATkXSeIIRuQuFymwYPoX
BP7RowzrbfF9KUwdIKlz9IXjqb1hplumiqNy1Sc7MmrTY9Fj87MNAMlnCIvyWkjE
XVXWxGef49mqc85P2K1iuAsr2R7sDrv7SC0ch+lHclOjGDmCjKOk8qF3kD1LATWg
zf42aXb4nNF9kyIOPEbI+jX4PWhAQpEz5nIG+xIRjTHGfacjpeg0+XOK21wLAuQB
fqebJ6GxX4OzB37ZtLLgrKyBYWaWuYkWbexVRM3wEvQu8ENkvhV017iPuPHSxNWx
Y8z072XMs9j8XRQD65EVqObXyizotPRJF4slEJ9qMQIDAQABozcwNTAJBgNVHRME
AjAAMAsGA1UdDwQEAwIF4DAbBgNVHREEFDAShwR/AAABhwTAqAIhhwQKCgCMMA0G
CSqGSIb3DQEBCwUAA4IBAQBebjmNHd8sJXjvPQc3uY/3KSDpk9AYfYzhUZvcvLNg
z0llFqXHaFlMqHTsz1tOH4Ns4PDKKoRT0JIKC1FkvjzqgL+X2jWFS0NRoNyd3W3B
yHLEL7MdQqDR+tZX02EGfaGXjuy8GHIU4J2hXhohmpn6ntfiRONfY8jaEjIecPFS
IwZWXNhsDESa1zuDe0PatES/Ati8bAUpN2rb/7rsE/AeM5GXpQfOKV0XxdIeBZ82
Vf5cUDWPipvq2Q9KS+yrTvEObGtA6gKhQ4bpz3MieU3N8AtQpEKtROH7mJWMHyl2
roD1k8KeJlfvR/XcVTGFcgIdNLfKIdd99Xfi4gSaIKuw
-----END CERTIFICATE-----
Deploy CNF with OpenID token
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Please refer to `CNF usage guide`_ to deploy CNF with OpenID token.
.. _OpenID token : https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens
.. _Keycloak: https://www.keycloak.org
.. _CNF usage guide: https://docs.openstack.org/tacker/latest/user/etsi_containerized_vnf_usage_guide.html

@ -166,10 +166,12 @@ Cert Verify
Kubernetes
----------
You configure Kubernetes VIM with parameters retrieved from ``kubectl`` command
as described in
You configure Kubernetes VIM with parameters retrieved
from ``kubectl`` command as described in
:doc:`/install/kubernetes_vim_installation`.
Here is an example of Kubernetes VIM configuration.
1. This is an example of Kubernetes VIM configuration with
Service Account Token.
.. code-block:: yaml
@ -195,6 +197,56 @@ Here is an example of Kubernetes VIM configuration.
-----END CERTIFICATE-----"
type: "kubernetes"
2. Another example of Kubernetes VIM configuration with
OpenID Connect Token. The OpenID Connect related parameters are described in
:doc:`kubernetes_openid_token_auth_usage_guide`.
.. code-block:: yaml
auth_url: "https://192.168.33.100:6443"
project_name: "default"
oidc_token_url: "https://192.168.33.100:8443/realms/oidc/protocol/openid-connect/token"
client_id: "tacker"
client_secret: "A93HfOUpySm6BjPug9PJdJumjEGUJMhc"
username: "end-user"
password: "end-user"
ssl_ca_cert: "-----BEGIN CERTIFICATE-----
MIICwjCCAaqgAwIBAgIBADANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDEwdrdWJl
LWNhMB4XDTIwMDgyNjA5MzIzMVoXDTMwMDgyNDA5MzIzMVowEjEQMA4GA1UEAxMH
a3ViZS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALxkeE16lPAd
pfJj5GJMvZJFcX/CD6EB/LUoKwGmqVoOUQPd3b/NGy+qm+3bO9EU73epUPsVaWk2
Lr+Z1ua7u+iib/OMsfsSXMZ5OEPgd8ilrTGhXOH8jDkif9w1NtooJxYSRcHEwxVo
+aXdIJhqKdw16NVP/elS9KODFdRZDfQ6vU5oHSg3gO49kgv7CaxFdkF7QEHbchsJ
0S1nWMPAlUhA5b8IAx0+ecPlMYUGyGQIQgjgtHgeawJebH3PWy32UqfPhkLPzxsy
TSxk6akiXJTg6mYelscuxPLSe9UqNvHRIUoad3VnkF3+0CJ1z0qvfWIrzX3w92/p
YsDBZiP6vi8CAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB/wQFMAMB
Af8wDQYJKoZIhvcNAQELBQADggEBAIbv2ulEcQi019jKz4REy7ZyH8+ExIUBBuIz
InAkfxNNxV83GkdyA9amk+LDoF/IFLMltAMM4b033ZKO5RPrHoDKO+xCA0yegYqU
BViaUiEXIvi/CcDpT9uh2aNO8wX5T/B0WCLfWFyiK+rr9qcosFYxWSdU0kFeg+Ln
YAaeFY65ZWpCCyljGpr2Vv11MAq1Tws8rEs3rg601SdKhBmkgcTAcCzHWBXR1P8K
rfzd6h01HhIomWzM9xrP2/2KlYRvExDLpp9qwOdMSanrszPDuMs52okXgfWnEqlB
2ZrqgOcTmyFzFh9h2dj1DJWvCvExybRmzWK1e8JMzTb40MEApyY=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIC7TCCAdWgAwIBAgIUQK2k5uNvlRLx43LI/t3a2/A/3iQwDQYJKoZIhvcNAQEL
BQAwFTETMBEGA1UEAxMKa3ViZXJuZXRlczAeFw0yMjA4MDQwNjIwNTFaFw0yMzA4
MDQwNjIwNTFaMBMxETAPBgNVBAMMCEtleWNsb2FrMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAni7HWLn2IpUImGO1sbBf/XuqATkXSeIIRuQuFymwYPoX
BP7RowzrbfF9KUwdIKlz9IXjqb1hplumiqNy1Sc7MmrTY9Fj87MNAMlnCIvyWkjE
XVXWxGef49mqc85P2K1iuAsr2R7sDrv7SC0ch+lHclOjGDmCjKOk8qF3kD1LATWg
zf42aXb4nNF9kyIOPEbI+jX4PWhAQpEz5nIG+xIRjTHGfacjpeg0+XOK21wLAuQB
fqebJ6GxX4OzB37ZtLLgrKyBYWaWuYkWbexVRM3wEvQu8ENkvhV017iPuPHSxNWx
Y8z072XMs9j8XRQD65EVqObXyizotPRJF4slEJ9qMQIDAQABozcwNTAJBgNVHRME
AjAAMAsGA1UdDwQEAwIF4DAbBgNVHREEFDAShwR/AAABhwTAqAIhhwQKCgCMMA0G
CSqGSIb3DQEBCwUAA4IBAQBebjmNHd8sJXjvPQc3uY/3KSDpk9AYfYzhUZvcvLNg
z0llFqXHaFlMqHTsz1tOH4Ns4PDKKoRT0JIKC1FkvjzqgL+X2jWFS0NRoNyd3W3B
yHLEL7MdQqDR+tZX02EGfaGXjuy8GHIU4J2hXhohmpn6ntfiRONfY8jaEjIecPFS
IwZWXNhsDESa1zuDe0PatES/Ati8bAUpN2rb/7rsE/AeM5GXpQfOKV0XxdIeBZ82
Vf5cUDWPipvq2Q9KS+yrTvEObGtA6gKhQ4bpz3MieU3N8AtQpEKtROH7mJWMHyl2
roD1k8KeJlfvR/XcVTGFcgIdNLfKIdd99Xfi4gSaIKuw
-----END CERTIFICATE-----"
type: "kubernetes"
Auth URL
~~~~~~~~
@ -214,8 +266,35 @@ Use SSL CA Cert
~~~~~~~~~~~~~~~
The value of SSL CA Cert for X.509 client authentication. It can be ``None``.
The SSL certificates of Kubernetes and OpenID provider should be concatenated
with a newline if both are needed.
Type
~~~~
Type of VIM to specify it explicitly as ``kubernetes``.
OpenID Token URL
~~~~~~~~~~~~~~~~
Token Endpoint URL of OpenID provider.
Client ID
~~~~~~~~~
The name of Relying Party(client).
Client Secret
~~~~~~~~~~~~~
The secret of Relying Party(client).
Username
~~~~~~~~
The name of End-user.
Password
~~~~~~~~
The password of End-user.

@ -57,7 +57,7 @@ please refer to [#first]_.
auth_url: "https://192.168.33.100:6443"
project_name: "default"
bearer_token: "eyJhbGciOiJSUzI1NiIsImtpZCI6IlBRVDgxQkV5VDNVR1M1WGEwUFYxSXFkZFhJWDYzNklvMEp2WklLMnNFdk0ifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhZG1pbi10b2tlbi12cnpoaiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJhZG1pbiIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImNhY2VmMzEzLTMzYjYtNDQ5MS1iMWUyLTg0NmQ2N2E0OTdkNSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDprdWJlLXN5c3RlbTphZG1pbiJ9.R76VIWVZnQxa9NG02HIqux1xTJG4i7dkXsp52T4UU8bvNfsfi18kW_p3ZvaNTxw0yABBcmkYZoOBe4MNP5cTP6TtR_ERZoA5QCViasW_u36rSTBT0-MHRPbkXjJYetzYaFYUO-DlJd3194yOtVHtrxUd8D31qw0f1FlP8BHxblDjZkYlgYSjHCxcwEdwlnYaa0SiH2kl6_oCBRFg8cUfXDeTOmH9XEfdrJ6ubJ4OyqG6YjfiKDDiEHgIehy7s7vZGVwVIPy6EhT1YSOIhY5aF-G9nQSg-GK1V9LIq7petFoW_MIEt0yfNQVXy2D1tBhdJEa1bgtVsLmdlrNVf-m3uA"
ssl_ca_cert: "-----BEGIN CERTIFICATE-----
ssl_ca_cert: "-----BEGIN CERTIFICATE-----nID
MIICwjCCAaqgAwIBAgIBADANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDEwdrdWJl
LWNhMB4XDTIwMDgyNjA5MzIzMVoXDTMwMDgyNDA5MzIzMVowEjEQMA4GA1UEAxMH
a3ViZS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALxkeE16lPAd
@ -76,6 +76,66 @@ please refer to [#first]_.
-----END CERTIFICATE-----"
type: "kubernetes"
In addition to using ``bearer_token`` to authenticate with Kubernetes ,
OpenID token [#sixth]_ is also supported. The following sample specifies
``oidc_token_url``, ``client_id``, ``client_secret``, ``username``, ``password``
instead of ``bearer_token`` for OpenID token authentication.
Before using OpenID token authentication, additional settings are required.
Please refer to [#seventh]_, and how to get the values of the ``oidc_token_url``,
``client_id``, ``client_secret``, ``username``, ``password`` and ``ssl_ca_cert``
parameters is documented.
The SSL certificates of Kubernetes and OpenID provider are concatenated
in ``ssl_ca_cert``.
.. code-block:: console
$ cat vim-k8s.yaml
auth_url: "https://192.168.33.100:6443"
project_name: "default"
oidc_token_url: "https://192.168.33.100:8443/realms/oidc/protocol/openid-connect/token"
client_id: "tacker"
client_secret: "A93HfOUpySm6BjPug9PJdJumjEGUJMhc"
username: "end-user"
password: "end-user"
ssl_ca_cert: "-----BEGIN CERTIFICATE-----
MIICwjCCAaqgAwIBAgIBADANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDEwdrdWJl
LWNhMB4XDTIwMDgyNjA5MzIzMVoXDTMwMDgyNDA5MzIzMVowEjEQMA4GA1UEAxMH
a3ViZS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALxkeE16lPAd
pfJj5GJMvZJFcX/CD6EB/LUoKwGmqVoOUQPd3b/NGy+qm+3bO9EU73epUPsVaWk2
Lr+Z1ua7u+iib/OMsfsSXMZ5OEPgd8ilrTGhXOH8jDkif9w1NtooJxYSRcHEwxVo
+aXdIJhqKdw16NVP/elS9KODFdRZDfQ6vU5oHSg3gO49kgv7CaxFdkF7QEHbchsJ
0S1nWMPAlUhA5b8IAx0+ecPlMYUGyGQIQgjgtHgeawJebH3PWy32UqfPhkLPzxsy
TSxk6akiXJTg6mYelscuxPLSe9UqNvHRIUoad3VnkF3+0CJ1z0qvfWIrzX3w92/p
YsDBZiP6vi8CAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB/wQFMAMB
Af8wDQYJKoZIhvcNAQELBQADggEBAIbv2ulEcQi019jKz4REy7ZyH8+ExIUBBuIz
InAkfxNNxV83GkdyA9amk+LDoF/IFLMltAMM4b033ZKO5RPrHoDKO+xCA0yegYqU
BViaUiEXIvi/CcDpT9uh2aNO8wX5T/B0WCLfWFyiK+rr9qcosFYxWSdU0kFeg+Ln
YAaeFY65ZWpCCyljGpr2Vv11MAq1Tws8rEs3rg601SdKhBmkgcTAcCzHWBXR1P8K
rfzd6h01HhIomWzM9xrP2/2KlYRvExDLpp9qwOdMSanrszPDuMs52okXgfWnEqlB
2ZrqgOcTmyFzFh9h2dj1DJWvCvExybRmzWK1e8JMzTb40MEApyY=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIC7TCCAdWgAwIBAgIUQK2k5uNvlRLx43LI/t3a2/A/3iQwDQYJKoZIhvcNAQEL
BQAwFTETMBEGA1UEAxMKa3ViZXJuZXRlczAeFw0yMjA4MDQwNjIwNTFaFw0yMzA4
MDQwNjIwNTFaMBMxETAPBgNVBAMMCEtleWNsb2FrMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAni7HWLn2IpUImGO1sbBf/XuqATkXSeIIRuQuFymwYPoX
BP7RowzrbfF9KUwdIKlz9IXjqb1hplumiqNy1Sc7MmrTY9Fj87MNAMlnCIvyWkjE
XVXWxGef49mqc85P2K1iuAsr2R7sDrv7SC0ch+lHclOjGDmCjKOk8qF3kD1LATWg
zf42aXb4nNF9kyIOPEbI+jX4PWhAQpEz5nIG+xIRjTHGfacjpeg0+XOK21wLAuQB
fqebJ6GxX4OzB37ZtLLgrKyBYWaWuYkWbexVRM3wEvQu8ENkvhV017iPuPHSxNWx
Y8z072XMs9j8XRQD65EVqObXyizotPRJF4slEJ9qMQIDAQABozcwNTAJBgNVHRME
AjAAMAsGA1UdDwQEAwIF4DAbBgNVHREEFDAShwR/AAABhwTAqAIhhwQKCgCMMA0G
CSqGSIb3DQEBCwUAA4IBAQBebjmNHd8sJXjvPQc3uY/3KSDpk9AYfYzhUZvcvLNg
z0llFqXHaFlMqHTsz1tOH4Ns4PDKKoRT0JIKC1FkvjzqgL+X2jWFS0NRoNyd3W3B
yHLEL7MdQqDR+tZX02EGfaGXjuy8GHIU4J2hXhohmpn6ntfiRONfY8jaEjIecPFS
IwZWXNhsDESa1zuDe0PatES/Ati8bAUpN2rb/7rsE/AeM5GXpQfOKV0XxdIeBZ82
Vf5cUDWPipvq2Q9KS+yrTvEObGtA6gKhQ4bpz3MieU3N8AtQpEKtROH7mJWMHyl2
roD1k8KeJlfvR/XcVTGFcgIdNLfKIdd99Xfi4gSaIKuw
-----END CERTIFICATE-----"
type: "kubernetes"
2. Register Kubernetes VIM
~~~~~~~~~~~~~~~~~~~~~~~~~~
We could register Kubernetes VIM to tacker by running the following command:
@ -857,3 +917,5 @@ References
.. [#third] https://specs.openstack.org/openstack/tacker-specs/specs/victoria/container-network-function.html#kubernetes-resource-kind-support
.. [#fourth] https://docs.openstack.org/tacker/latest/user/vnfd-sol001.html
.. [#fifth] https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names
.. [#sixth] https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens
.. [#seventh] https://docs.openstack.org/tacker/latest/reference/kubernetes_openid_token_auth_usage_guide.html

@ -3,6 +3,7 @@
- ensure-db-cli-installed
- orchestrate-devstack
- modify-heat-policy
- setup-k8s-oidc
- setup-default-vim
- setup-helm
- role: setup-multi-tenant-vim

@ -0,0 +1,7 @@
---
features:
- |
Adds "openid token" authentication strategy for Kubernetes VIM. Users can
specify openid authentication parameters in the `VIM Register Request`
or the `VNF Instantiate Request` to indicate tacker to use openid token
for authentication.

@ -97,7 +97,7 @@
become: yes
become_user: stack
- name: Fetch k8s's CA Certificate
- name: Fetch k8s CA certificate
fetch:
src: "/etc/kubernetes/pki/ca.crt"
dest: "/tmp/"
@ -105,6 +105,14 @@
when:
- k8s_ssl_verify
- name: Fetch keycloak server certificate
fetch:
src: "/etc/keycloak/ssl/keycloak.crt"
dest: "/tmp/"
flat: true
when:
- keycloak_host is defined
when:
- inventory_hostname == 'controller-k8s'
- kuryr_k8s_api_url is defined
@ -123,6 +131,14 @@
src={{ devstack_base_dir }}/tacker/tacker/tests/etc/samples/local-k8s-vim.yaml
dest={{ zuul_work_dir }}/tacker/tests/etc/samples/local-k8s-vim.yaml
- name: Copy test k8s vim file for oidc
copy:
remote_src=True
src={{ devstack_base_dir }}/tacker/tacker/tests/etc/samples/local-k8s-vim-oidc.yaml
dest={{ zuul_work_dir }}/tacker/tests/etc/samples/local-k8s-vim-oidc.yaml
when:
- keycloak_host is defined
- name: Check if project's tools/test-setup-k8s-vim.sh exists
stat:
path: "{{ zuul_work_dir }}/tools/test-setup-k8s-vim.sh"
@ -169,6 +185,28 @@
when:
- p.stat.exists
- name: Replace k8s auth uri in local-k8s-vim-oidc.yaml
replace:
path: "{{ item }}"
regexp: "https://127.0.0.1:6443"
replace: "{{ kuryr_k8s_api_url }}"
with_items:
- "{{ zuul_work_dir }}/tacker/tests/etc/samples/local-k8s-vim-oidc.yaml"
when:
- p.stat.exists
- keycloak_host is defined
- name: Replace keycloak uri in local-k8s-vim-oidc.yaml
replace:
path: "{{ item }}"
regexp: "https://127.0.0.1:8443"
replace: "https://{{ keycloak_host }}:{{ keycloak_https_port }}"
with_items:
- "{{ zuul_work_dir }}/tacker/tests/etc/samples/local-k8s-vim-oidc.yaml"
when:
- p.stat.exists
- keycloak_host is defined
- name: Replace k8s auth token in local-k8s-vim.yaml
replace:
path: "{{ item }}"
@ -179,7 +217,7 @@
when:
- p.stat.exists
- name: Copy k8s's CA Certificate to tacker
- name: Copy k8s CA certificate to tacker
copy:
src: "/tmp/ca.crt"
dest: "/tmp/"
@ -187,23 +225,41 @@
- p.stat.exists
- k8s_ssl_verify
- name: Get k8s's CA Certificate
command: cat "/tmp/ca.crt"
register: ssl_ca_cert
- name: Copy keycloak server certificate to tacker
copy:
src: "/tmp/keycloak.crt"
dest: "/tmp/"
when:
- p.stat.exists
- k8s_ssl_verify
- keycloak_host is defined
- name: Write k8s CA certificate to a certificates aggregated file
shell: cat /tmp/ca.crt > /tmp/agg.crt
when:
- p.stat.exists
- k8s_ssl_verify
- name: Write keycloak server certificate to a certificates aggregated file
shell: cat /tmp/keycloak.crt >> /tmp/agg.crt
when:
- p.stat.exists
- keycloak_host is defined
- name: Register ssl certificate if exists
shell: test -f /tmp/agg.crt && cat /tmp/agg.crt
register: ssl_ca_cert
- name: Replace k8s CA Certificate in local-k8s-vim.yaml
- name: Replace ssl_ca_cert in local-k8s-vim.yaml and local-k8s-vim-oidc.yaml
replace:
path: "{{ item }}"
regexp: "ssl_ca_cert: .*$"
replace: "ssl_ca_cert: '{{ ssl_ca_cert.stdout }}'"
with_items:
- "{{ zuul_work_dir }}/tacker/tests/etc/samples/local-k8s-vim.yaml"
- "{{ zuul_work_dir }}/tacker/tests/etc/samples/local-k8s-vim-oidc.yaml"
when:
- p.stat.exists
- k8s_ssl_verify
- ssl_ca_cert.rc == 0 and ssl_ca_cert.stdout != ""
- name: Replace the config file path in the test-setup-k8s-vim.sh
replace:

@ -0,0 +1 @@
oidc_work_dir: "/tmp/oidc"

@ -0,0 +1,11 @@
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: oidc-cluster-admin-binding
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: User
name: end-user

@ -0,0 +1,10 @@
#!/bin/bash
docker run -d \
--net=host \
-e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \
-e KC_HTTP_PORT=8080 -e KC_HTTPS_PORT=8443 \
-e KC_HTTPS_CERTIFICATE_FILE=/opt/keycloak/conf/keycloak.crt \
-e KC_HTTPS_CERTIFICATE_KEY_FILE=/opt/keycloak/conf/keycloak.key \
-v /etc/keycloak/ssl:/opt/keycloak/conf quay.io/keycloak/keycloak:18.0.2 \
start-dev

@ -0,0 +1,27 @@
#!/bin/bash
wk_dir=/tmp/oidc
req_conf=$wk_dir/ssl_csr.conf
ssl_dir=/etc/keycloak/ssl
key_file=$ssl_dir/keycloak.key
csr_file=$ssl_dir/keycloak.csr
crt_file=$ssl_dir/keycloak.crt
k8s_ssl_dir=/etc/kubernetes/pki
k8s_ca_crt=$k8s_ssl_dir/ca.crt
k8s_ca_key=$k8s_ssl_dir/ca.key
# make a directory for storing certificate
mkdir -p $ssl_dir
# generate private key
openssl genrsa -out $key_file 2048
# generate certificate signing request
openssl req -new -key $key_file -out $csr_file -subj "/CN=Keycloak" -config $req_conf
# use Kubernetess CA for issuing certificate
openssl x509 -req -in $csr_file -CA $k8s_ca_crt -CAkey $k8s_ca_key -CAcreateserial -out $crt_file -days 365 -extensions v3_req -extfile $req_conf
# add executeable permission to key file
chmod 755 $key_file

@ -0,0 +1,19 @@
#!/bin/bash
KEYCLOAK_BASE_URL=https://127.0.0.1:8443
ADMIN_TOKEN=$(curl -k -sS -X POST "${KEYCLOAK_BASE_URL}/realms/master/protocol/openid-connect/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'username=admin' \
-d 'password=admin' \
-d 'grant_type=password' \
-d 'client_id=admin-cli' | jq -r .access_token)
if [ $? -ne 0 ]
then
exit $?
fi
curl -k -L -X POST "${KEYCLOAK_BASE_URL}/admin/realms" \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d @"oidc_realm.json"

File diff suppressed because it is too large Load Diff

@ -0,0 +1,12 @@
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
IP.1 = 127.0.0.1

@ -0,0 +1,109 @@
- block:
- name: Create a work directory
file:
path: "{{ oidc_work_dir }}"
state: directory
mode: 0755
become: yes
- name: Copy setup files to k8s server
copy:
src: "{{ item }}"
dest: "{{ oidc_work_dir }}"
owner: stack
group: stack
mode: 0644
with_items:
- "ssl_csr.conf"
- "generate_ssl_cert.sh"
- "create_keycloak.sh"
- "oidc_realm.json"
- "import_oidc_realm.sh"
- "cluster_role_binding.yaml"
become: yes
- name: Replace {{ oidc_work_dir }} if not default
replace:
path: "{{ item }}"
regexp: "/tmp/oidc"
replace: "{{ oidc_work_dir }}"
with_items:
- "{{ oidc_work_dir }}/generate_ssl_cert.sh"
when:
- oidc_work_dir != '/tmp/oidc'
become: yes
- name: Add keycloak's ip to CSR conf
lineinfile:
path: "{{ oidc_work_dir }}/ssl_csr.conf"
line: "IP.2 = {{ keycloak_host }}"
become: yes
- name: Generate SSL certificate for keycloak
command: /bin/bash {{ oidc_work_dir }}/generate_ssl_cert.sh
become: yes
- name: Create and start keycloak server
command: /bin/bash {{ oidc_work_dir }}/create_keycloak.sh
become: yes
- name: Wait for keycloak be active
wait_for:
host: "{{ keycloak_host }}"
port: "{{ keycloak_https_port }}"
delay: 120
timeout: 300
- name: Install jq command
package:
name: jq
state: present
become: yes
- name: Replace keycloak host:port to import_oidc_realm.sh
replace:
path: "{{ item }}"
regexp: "https://127.0.0.1:8443"
replace: "https://{{ keycloak_host}}:{{ keycloak_https_port }}"
with_items:
- "{{ oidc_work_dir }}/import_oidc_realm.sh"
become: yes
- name: Import oidc realm
command: /bin/bash import_oidc_realm.sh
args:
chdir: "{{ oidc_work_dir }}"
become: yes
- name: Setup oidc on k8s server
blockinfile:
path: /etc/kubernetes/manifests/kube-apiserver.yaml
insertafter: "- --tls-private-key-file=.*"
block: |2
- --oidc-issuer-url=https://{{ keycloak_host }}:{{ keycloak_https_port }}/realms/oidc
- --oidc-client-id=tacker
- --oidc-username-claim=name
- --oidc-username-prefix=-
- --oidc-ca-file=/etc/kubernetes/pki/ca.crt
become: yes
ignore_errors: yes
- name: Wait for k8s apiserver to restart
wait_for:
host: "{{ hostvars['controller-k8s']['nodepool']['private_ipv4'] }}"
port: 6443
delay: 30
timeout: 180
ignore_errors: yes
- name: Create clusterrolebinding on k8s server
command: >
kubectl create -f {{ oidc_work_dir }}/cluster_role_binding.yaml
become: yes
become_user: stack
ignore_errors: yes
when:
- inventory_hostname == 'controller-k8s'
- keycloak_host is defined

@ -23,6 +23,8 @@ from kubernetes.client import api_client
from oslo_config import cfg
from oslo_log import log as logging
from tacker.common import oidc_utils
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
@ -32,15 +34,33 @@ class KubernetesHTTPAPI(object):
def get_k8s_client(self, auth_plugin):
config = client.Configuration()
config.host = auth_plugin['auth_url']
if ('username' in auth_plugin) and ('password' in auth_plugin)\
and (auth_plugin['password'] is not None):
config.username = auth_plugin['username']
config.password = auth_plugin['password']
basic_token = config.get_basic_auth_token()
config.api_key['authorization'] = basic_token
if 'bearer_token' in auth_plugin:
if 'oidc_token_url' in auth_plugin:
# obtain token from oidc server
if 'id_token' not in auth_plugin:
id_token = oidc_utils.get_id_token_with_password_grant(
auth_plugin.get('oidc_token_url'),
auth_plugin.get('username'),
auth_plugin.get('password'),
auth_plugin.get('client_id'),
client_secret=auth_plugin.get('client_secret'),
ssl_ca_cert=auth_plugin.get('ca_cert_file')
)
auth_plugin['id_token'] = id_token
# set id token to k8s config
config.api_key_prefix['authorization'] = 'Bearer'
config.api_key['authorization'] = auth_plugin['bearer_token']
config.api_key['authorization'] = auth_plugin['id_token']
else:
if ('username' in auth_plugin) and ('password' in auth_plugin)\
and (auth_plugin['password'] is not None):
config.username = auth_plugin['username']
config.password = auth_plugin['password']
basic_token = config.get_basic_auth_token()
config.api_key['authorization'] = basic_token
if 'bearer_token' in auth_plugin:
config.api_key_prefix['authorization'] = 'Bearer'
config.api_key['authorization'] = auth_plugin['bearer_token']
ca_cert_file = auth_plugin.get('ca_cert_file')
if ca_cert_file is not None:
config.ssl_ca_cert = ca_cert_file

@ -0,0 +1,57 @@
# Copyright (C) 2022 Fujitsu
# All Rights Reserved.
#
# 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.
"""Utilities functions for get openid token."""
import requests
from tacker.extensions.vnfm import OIDCAuthFailed
def get_id_token_with_password_grant(
token_endpoint, username, password, client_id,
client_secret=None, ssl_ca_cert=None, timeout=20):
if not token_endpoint or not username or not password or not client_id:
raise OIDCAuthFailed(detail='token_endpoint, username, password,'
' client_id can not be empty.')
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
body = {
'grant_type': 'password',
'scope': 'openid',
'client_id': client_id,
'client_secret': client_secret,
'username': username,
'password': password
}
verify = ssl_ca_cert if ssl_ca_cert else False
try:
resp = requests.post(token_endpoint, headers=headers, data=body,
verify=verify, timeout=timeout)
if (resp.status_code == 200
and resp.headers['Content-Type'] == 'application/json'):
return resp.json()['id_token']
raise OIDCAuthFailed(
detail=f'response code: {resp.status_code}, body: {resp.text}')
except requests.exceptions.RequestException as exc:
raise OIDCAuthFailed(detail=str(exc))

@ -52,7 +52,10 @@ class Vim(model_base.BASE,
class VimAuth(model_base.BASE, models_v1.HasId):
vim_id = sa.Column(types.Uuid, sa.ForeignKey('vims.id'),
nullable=False)
password = sa.Column(sa.String(255), nullable=False)
# NOTE(Yao Qibin): The password is nullable in the actual database, and
# password is not necessary in some cases(eg. use bearer token for auth),
# so change the nullable from False to True.
password = sa.Column(sa.String(255), nullable=True)
auth_url = sa.Column(sa.String(255), nullable=False)
vim_project = sa.Column(types.Json, nullable=False)
auth_cred = sa.Column(types.Json, nullable=False)

@ -52,9 +52,20 @@ class NfvoPluginDb(nfvo.NFVOPluginBase, db_base.CommonDbMixin):
res['auth_url'] = vim_auth_db[0].auth_url
res['vim_project'] = vim_auth_db[0].vim_project
res['auth_cred'] = vim_auth_db[0].auth_cred
res['auth_cred']['password'] = vim_auth_db[0].password
if vim_auth_db[0].password:
res['auth_cred']['password'] = vim_auth_db[0].password
# NOTE(Yao Qibin): Since oidc_token_url contains keyword `token`,
# its value will be masked.
# To prevent its value from being masked, temporarily change its name.
if "oidc_token_url" in res['auth_cred']:
res['auth_cred']['oidc_x_url'] = res['auth_cred'].pop(
'oidc_token_url')
if mask_password:
res['auth_cred'] = strutils.mask_dict_password(res['auth_cred'])
# Revert to oidc_token_url
if "oidc_x_url" in res['auth_cred']:
res['auth_cred']['oidc_token_url'] = res['auth_cred'].pop(
'oidc_x_url')
return self._fields(res, fields)
def _fields(self, resource, fields):
@ -93,7 +104,7 @@ class NfvoPluginDb(nfvo.NFVOPluginBase, db_base.CommonDbMixin):
vim_auth_db = nfvo_db.VimAuth(
id=uuidutils.generate_uuid(),
vim_id=vim.get('id'),
password=vim_cred.pop('password'),
password=vim_cred.pop('password', None),
vim_project=vim.get('vim_project'),
auth_url=vim.get('auth_url'),
auth_cred=vim_cred)
@ -167,7 +178,7 @@ class NfvoPluginDb(nfvo.NFVOPluginBase, db_base.CommonDbMixin):
except orm_exc.NoResultFound:
raise nfvo.VimNotFoundException(vim_id=vim_id)
vim_auth_db.update({'auth_cred': vim_cred, 'password':
vim_cred.pop('password'), 'vim_project':
vim_cred.pop('password', None), 'vim_project':
vim_project})
vim_db.update({'updated_at': timeutils.utcnow()})
self._cos_db_plg.create_event(

@ -257,6 +257,11 @@ class InvalidMaintenanceParameter(exceptions.InvalidInput):
message = _("Could not find the required params for maintenance")
class OIDCAuthFailed(exceptions.InvalidInput):
message = _("OIDC authentication and authorization failed."
" Detail: %(detail)s")
def _validate_service_type_list(data, valid_values=None):
if not isinstance(data, list):
msg = _("Invalid data format for service list: '%s'") % data

@ -139,6 +139,10 @@ class Kubernetes_Driver(abstract_vim_driver.VimAbstractDriver):
self.discover_placement_attr(vim_obj)
self.encode_vim_auth(vim_obj['id'],
vim_obj['auth_cred'])
# NOTE(Yao Qibin): To avoid obtaining token multiple times,
# the id_token is keeped in auth_cred, which will be deleted here.
if 'id_token' in vim_obj['auth_cred']:
vim_obj['auth_cred'].pop('id_token')
LOG.debug('VIM registration completed for %s', vim_obj)
@log.log
@ -197,6 +201,10 @@ class Kubernetes_Driver(abstract_vim_driver.VimAbstractDriver):
encoded_auth = fernet_obj.encrypt(
auth['ssl_ca_cert'].encode('utf-8'))
auth['ssl_ca_cert'] = encoded_auth
if 'client_secret' in auth:
encoded_auth = fernet_obj.encrypt(
auth["client_secret"].encode("utf-8"))
auth["client_secret"] = encoded_auth
if CONF.k8s_vim.use_barbican:
try:

@ -161,7 +161,21 @@ class NfvoPlugin(nfvo_db_plugin.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin,
# is not updatable. so no need to consider it
if 'auth_cred' in update_args:
auth_cred = update_args['auth_cred']
if ('username' in auth_cred) and ('password' in auth_cred)\
if 'oidc_token_url' in auth_cred:
# update oidc info, and remove bearer_token if exists
vim_obj['auth_cred']['oidc_token_url'] = auth_cred.get(
'oidc_token_url')
vim_obj['auth_cred']['username'] = auth_cred.get(
'username')
vim_obj['auth_cred']['password'] = auth_cred.get(
'password')
vim_obj['auth_cred']['client_id'] = auth_cred.get(
'client_id')
vim_obj['auth_cred']['client_secret'] = auth_cred.get(
'client_secret')
if 'bearer_token' in vim_obj['auth_cred']:
vim_obj['auth_cred'].pop('bearer_token')
elif ('username' in auth_cred) and ('password' in auth_cred)\
and (auth_cred['password'] is not None):
# update new username and password, remove bearer_token
# if it exists in the old vim
@ -169,15 +183,27 @@ class NfvoPlugin(nfvo_db_plugin.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin,
vim_obj['auth_cred']['password'] = auth_cred['password']
if 'bearer_token' in vim_obj['auth_cred']:
vim_obj['auth_cred'].pop('bearer_token')
if 'oidc_token_url' in vim_obj['auth_cred']:
vim_obj['auth_cred'].pop('oidc_token_url')
if 'client_id' in vim_obj['auth_cred']:
vim_obj['auth_cred'].pop('client_id')
if 'client_secret' in vim_obj['auth_cred']:
vim_obj['auth_cred'].pop('client_secret')
elif 'bearer_token' in auth_cred:
# update bearer_token, remove username and password
# if they exist in the old vim
vim_obj['auth_cred']['bearer_token'] =\
auth_cred['bearer_token']
if ('username' in vim_obj['auth_cred']) and\
('password' in vim_obj['auth_cred']):
if 'username' in vim_obj['auth_cred']:
vim_obj['auth_cred'].pop('username')
if 'password' in vim_obj['auth_cred']:
vim_obj['auth_cred'].pop('password')
if 'oidc_token_url' in vim_obj['auth_cred']:
vim_obj['auth_cred'].pop('oidc_token_url')
if 'client_id' in vim_obj['auth_cred']:
vim_obj['auth_cred'].pop('client_id')
if 'client_secret' in vim_obj['auth_cred']:
vim_obj['auth_cred'].pop('client_secret')
if 'ssl_ca_cert' in auth_cred:
# update new ssl_ca_cert
vim_obj['auth_cred']['ssl_ca_cert'] =\

@ -356,3 +356,8 @@ class K8sResourceNotFound(SolHttpError404):
class K8sInvalidManifestFound(SolHttpError400):
message = _("Invalid manifest found.")
class OIDCAuthFailed(SolHttpError400):
message = _("OIDC authentication and authorization failed."
" Detail: %(detail)s")

@ -0,0 +1,57 @@
# Copyright (C) 2022 Fujitsu
# All Rights Reserved.
#
# 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.
"""Utilities functions for get openid token."""
import requests
from tacker.sol_refactored.common.exceptions import OIDCAuthFailed
def get_id_token_with_password_grant(
token_endpoint, username, password, client_id,
client_secret=None, ssl_ca_cert=None, timeout=20):
if not token_endpoint or not username or not password or not client_id:
raise OIDCAuthFailed(detail='token_endpoint, username, password,'
' client_id can not be empty.')
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
body = {
'grant_type': 'password',
'scope': 'openid',
'client_id': client_id,
'client_secret': client_secret,
'username': username,
'password': password
}
verify = ssl_ca_cert if ssl_ca_cert else False
try:
resp = requests.post(token_endpoint, headers=headers, data=body,
verify=verify, timeout=timeout)
if (resp.status_code == 200
and resp.headers['Content-Type'] == 'application/json'):
return resp.json()['id_token']
raise OIDCAuthFailed(
detail=f'response code: {resp.status_code}, body: {resp.text}')
except requests.exceptions.RequestException as exc:
raise OIDCAuthFailed(detail=str(exc))

@ -76,7 +76,15 @@ def vim_to_conn_info(vim):
accessInfo=access_info
)
if vim['vim_type'] == "kubernetes": # k8s
if vim_auth['username'] and vim_auth['password']:
if 'oidc_token_url' in vim_auth:
access_info = {
'oidc_token_url': vim_auth.get('oidc_token_url'),
'username': vim_auth.get('username'),
'password': vim_auth.get('password'),
'client_id': vim_auth.get('client_id'),
'client_secret': vim_auth.get('client_secret')
}
elif vim_auth.get('username') and vim_auth.get('password'):
access_info = {
'username': vim_auth['username'],
'password': vim_auth['password']

@ -24,6 +24,7 @@ from oslo_log import log as logging
import yaml
from tacker.sol_refactored.common import exceptions as sol_ex
from tacker.sol_refactored.common import oidc_utils
from tacker.sol_refactored.infra_drivers.kubernetes import kubernetes_resource
@ -84,6 +85,7 @@ def is_match_pod_naming_rule(rsc_kind, rsc_name, pod_name):
def get_k8s_reses_from_json_files(target_k8s_files, vnfd, k8s_api_client,
namespace):
k8s_resources = []
for target_k8s_file in target_k8s_files:
@ -156,21 +158,36 @@ class AuthContextManager:
k8s_config = client.Configuration()
k8s_config.host = self.vim_info.interfaceInfo['endpoint']
if ('username' in self.vim_info.accessInfo and
self.vim_info.accessInfo.get('password') is not None):
k8s_config.username = self.vim_info.accessInfo['username']
k8s_config.password = self.vim_info.accessInfo['password']
basic_token = k8s_config.get_basic_auth_token()
k8s_config.api_key['authorization'] = basic_token
if 'bearer_token' in self.vim_info.accessInfo:
k8s_config.api_key_prefix['authorization'] = 'Bearer'
k8s_config.api_key['authorization'] = self.vim_info.accessInfo[
'bearer_token']
if 'ssl_ca_cert' in self.vim_info.interfaceInfo:
self._create_ca_cert_file(
self.vim_info.interfaceInfo['ssl_ca_cert'])
if 'oidc_token_url' in self.vim_info.accessInfo:
# Obtain a openid token from openid provider
id_token = oidc_utils.get_id_token_with_password_grant(
self.vim_info.accessInfo.get('oidc_token_url'),
self.vim_info.accessInfo.get('username'),
self.vim_info.accessInfo.get('password'),
self.vim_info.accessInfo.get('client_id'),
client_secret=self.vim_info.accessInfo.get('client_secret'),
ssl_ca_cert=self.ca_cert_file
)
k8s_config.api_key_prefix['authorization'] = 'Bearer'
k8s_config.api_key['authorization'] = id_token
else:
if ('username' in self.vim_info.accessInfo and
self.vim_info.accessInfo.get('password') is not None):
k8s_config.username = self.vim_info.accessInfo['username']
k8s_config.password = self.vim_info.accessInfo['password']
basic_token = k8s_config.get_basic_auth_token()
k8s_config.api_key['authorization'] = basic_token
if 'bearer_token' in self.vim_info.accessInfo:
k8s_config.api_key_prefix['authorization'] = 'Bearer'
k8s_config.api_key['authorization'] = self.vim_info.accessInfo[
'bearer_token']
if self.ca_cert_file:
k8s_config.ssl_ca_cert = self.ca_cert_file
k8s_config.verify_ssl = True
else:

@ -0,0 +1,9 @@
auth_url: "https://127.0.0.1:6443"
project_name: "default"
oidc_token_url: "https://127.0.0.1:8443/realms/oidc/protocol/openid-connect/token"
username: "end-user"
password: "end-user"
client_id: "tacker"
client_secret: "K0Zp5dvdOFhZ7W9PVNZn14omW9NmCQvQ"
ssl_ca_cert: None
type: "kubernetes"

@ -0,0 +1,147 @@
# Copyright (C) 2022 FUJITSU
# All Rights Reserved.
#
# 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.
import yaml
from tackerclient.common import exceptions
from tacker.tests.functional import base
from tacker.tests.utils import read_file
SECRET_PASSWORD = '***'
class VimTest(base.BaseTackerTest):
@classmethod
def generate_vim_info_oidc_for_creation(cls):
data = yaml.safe_load(read_file('local-k8s-vim-oidc.yaml'))
auth_cred = {'oidc_token_url': data['oidc_token_url'],
'username': data['username'],
'password': data['password'],
'client_id': data['client_id'],
'client_secret': data['client_secret']}
if 'ssl_ca_cert' in data:
auth_cred['ssl_ca_cert'] = data['ssl_ca_cert']
return {'vim': {'name': 'VIM-OIDC-AUTH',
'description': 'Kubernetes VIM with oidc auth',
'type': 'kubernetes',
'auth_url': data['auth_url'],
'auth_cred': auth_cred,
'vim_project': {'name': 'default'},
'is_default': False}}
@classmethod
def generate_vim_info_oidc_for_update(cls):
data = yaml.safe_load(read_file('local-k8s-vim-oidc.yaml'))
auth_cred = {'oidc_token_url': data['oidc_token_url'],
'username': data['username'],
'password': data['password'],
'client_id': data['client_id'],
'client_secret': data['client_secret']}
if 'ssl_ca_cert' in data:
auth_cred['ssl_ca_cert'] = data['ssl_ca_cert']
return {'vim': {'name': 'VIM-OIDC-AUTH',
'description': 'Kubernetes VIM with oidc auth',
'auth_cred': auth_cred}}
@classmethod
def generate_vim_info_token_for_update(cls):
data = yaml.safe_load(read_file('local-k8s-vim.yaml'))
auth_cred = {'bearer_token': data['bearer_token']}
if 'ssl_ca_cert' in data:
auth_cred['ssl_ca_cert'] = data['ssl_ca_cert']
return {'vim': {'name': 'VIM-BEARER-TOKEN',
'description': 'Kubernetes VIM with bearer token',
'auth_cred': auth_cred}}
def assert_vim_auth_oidc(self, vim_auth_req, vim_auth_res):
unexpected_attrs = {'bearer_token'}
# check only specified attributes exist
self.assertNotIn(unexpected_attrs, vim_auth_res)
self.assertEqual(vim_auth_req['oidc_token_url'],
vim_auth_res['oidc_token_url'])
self.assertEqual(vim_auth_req['username'],
vim_auth_res['username'])
self.assertEqual(SECRET_PASSWORD,
vim_auth_res['password'])
self.assertEqual(vim_auth_req['client_id'],
vim_auth_res['client_id'])
self.assertEqual(SECRET_PASSWORD,
vim_auth_res['client_secret'])
def assert_vim_auth_token(self, vim_auth_res):
unexpected_attrs = {'oidc_token_url', 'username', 'password',
'client_id', 'client_secret'}
# check only specified attributes exist
self.assertNotIn(unexpected_attrs, vim_auth_res)
self.assertEqual(SECRET_PASSWORD,
vim_auth_res['bearer_token'])
def test_vim_creation_update_with_oidc_auth(self):
vim_oidc_create = self.generate_vim_info_oidc_for_creation()
vim_oidc_update = self.generate_vim_info_oidc_for_update()
vim_token_update = self.generate_vim_info_token_for_update()
# Register vim
vim_res = self.client.create_vim(vim_oidc_create)
vim_id = vim_res['vim']['id']
self.assert_vim_auth_oidc(vim_oidc_create['vim']['auth_cred'],
vim_res['vim']['auth_cred'])
# Read vim
vim_show_res = self.client.show_vim(vim_id)
self.assert_vim_auth_oidc(vim_oidc_create['vim']['auth_cred'],
vim_show_res['vim']['auth_cred'])