Skip to content

Commit 44b1705

Browse files
Add optional token-based kubeconfig generation for CAPI cluster (#890)
* Add playbook for creating CAPI service account and token-based kubeconfig * Add comments to variables * Fix linting issues * Fix linting issues * Apply suggestions from code review * Fix linting * Fix linting * fix lint * Standardise capi-mgmt service account resource names * Address comments on PR * Fix changes according to reviews --------- Co-authored-by: Scott Davidson <[email protected]>
1 parent 1904642 commit 44b1705

File tree

3 files changed

+160
-1
lines changed

3 files changed

+160
-1
lines changed

roles/capi_cluster/defaults/main.yml

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
---
2-
32
# The chart to use
43
capi_cluster_chart_repo: https://azimuth-cloud.github.io/capi-helm-charts
54
capi_cluster_chart_name: openstack-cluster
@@ -581,6 +580,33 @@ capi_cluster_release_values: >-
581580
# The name of the file into which the kubeconfig of the cluster should be output
582581
capi_cluster_kubeconfig_path: "{{ ansible_env.HOME }}/kubeconfig"
583582

583+
# Optional configuration for the creation of service account, secret and a clusterrole
584+
# to generate a long-lived, token-based kubeconfig.
585+
# The default certificate-based kubeconfig generated by CAPI has a lifetime of one
586+
# year by default. The version of this file which is stored inside the k3s cluster
587+
# as a k8s secret gets periodically refreshed by CAPI so never expires but if the
588+
# kubeconfig is extracted and provided to an external service (e.g. the magnum-capi-helm
589+
# driver) then a non-expiring token-based kubeconfig is required.
590+
capi_cluster_service_account_enabled: false
591+
capi_cluster_service_account_name: capi-mgmt
592+
capi_cluster_service_account_namespace: capi-mgmt
593+
capi_cluster_service_account_secret_name: capi-mgmt-service-account
594+
capi_cluster_clusterrolebinding_name: capi-mgmt
595+
596+
# Name of an existing cluster role which the service account should be bound to
597+
# Note: This defaults to the cluster-admin role since capi-helm-charts needs permission to
598+
# create a role and role binding anyway (for the CAPI autoscaler deployment). Therefore,
599+
# restricting to anything less privileged than admin still allows trivial privilege escalation via
600+
# the creation of new roles and role bindings.
601+
capi_cluster_service_account_role_name: cluster-admin
602+
603+
# Optional flag to delete the existing service account and generate
604+
# a new service account and token instead. This should only be used
605+
# if the existing token is leaked needs to be rotated.
606+
# The token rotation method is to set this variable at run time with
607+
# `ansible-playbook -e capi_cluster_service_account_rotate_secret=true ...`
608+
capi_cluster_service_account_rotate_secret: false
609+
584610
# The volumes policy for the cluster (keep or delete)
585611
# Determines what happens to Cinder volumes created for PVCs when the cluster is deleted
586612
capi_cluster_volumes_policy: keep
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
- name: Create namespace for a new service account
2+
ansible.builtin.command: kubectl create namespace {{ capi_cluster_service_account_namespace }}
3+
register: magnum_create_namespace
4+
changed_when: magnum_create_namespace.rc == 0
5+
failed_when: >-
6+
magnum_create_namespace.rc != 0 and
7+
'AlreadyExists' not in magnum_create_namespace.stderr
8+
9+
- name: Delete service account
10+
ansible.builtin.command: >-
11+
kubectl delete serviceaccount
12+
-n {{ capi_cluster_service_account_namespace }}
13+
{{ capi_cluster_service_account_name }}
14+
register: magnum_delete_service_account
15+
changed_when: magnum_delete_service_account.rc == 0
16+
failed_when: >-
17+
magnum_delete_service_account.rc != 0 and
18+
'NotFound' not in magnum_delete_service_account.stderr
19+
when: capi_cluster_service_account_rotate_secret
20+
21+
- name: Create service account
22+
ansible.builtin.command: kubectl apply -f -
23+
args:
24+
stdin: "{{ sa_definition | to_nice_yaml }}"
25+
vars:
26+
sa_definition:
27+
apiVersion: v1
28+
kind: ServiceAccount
29+
metadata:
30+
name: "{{ capi_cluster_service_account_name }}"
31+
namespace: "{{ capi_cluster_service_account_namespace }}"
32+
changed_when: false
33+
34+
- name: Create token secret for service account
35+
ansible.builtin.command: kubectl apply -f -
36+
args:
37+
stdin: "{{ sa_secret_definition | to_nice_yaml }}"
38+
vars:
39+
sa_secret_definition:
40+
apiVersion: v1
41+
kind: Secret
42+
type: kubernetes.io/service-account-token
43+
metadata:
44+
name: "{{ capi_cluster_service_account_secret_name }}"
45+
namespace: "{{ capi_cluster_service_account_namespace }}"
46+
annotations:
47+
kubernetes.io/service-account.name: "{{ capi_cluster_service_account_name }}"
48+
register: token_based_secret_applied
49+
until: token_based_secret_applied is succeeded
50+
retries: 30
51+
delay: 10
52+
changed_when: false
53+
54+
# Grant cluster-admin permissions via ClusterRoleBinding to allow generation of all Helm releases.
55+
# Strict access control isn’t enforced here, as the user can escalate privileges using ServiceAccounts, Roles, and RoleBindings anyway.
56+
# which is necessary for the management cluster to operate on resources.
57+
- name: Create cluster role binding of cluster-admin configuration for kubeconfig service account
58+
ansible.builtin.command: kubectl apply -f -
59+
args:
60+
stdin: "{{ sa_clusterrolebinding | to_nice_yaml }}"
61+
vars:
62+
sa_clusterrolebinding:
63+
apiVersion: rbac.authorization.k8s.io/v1
64+
kind: ClusterRoleBinding
65+
metadata:
66+
name: "{{ capi_cluster_clusterrolebinding_name }}"
67+
subjects:
68+
- kind: ServiceAccount
69+
name: "{{ capi_cluster_service_account_name }}"
70+
namespace: "{{ capi_cluster_service_account_namespace }}"
71+
roleRef:
72+
kind: ClusterRole
73+
name: "{{ capi_cluster_service_account_role_name }}"
74+
apiGroup: rbac.authorization.k8s.io
75+
changed_when: false
76+
77+
- name: Extract token for the service account
78+
no_log: true
79+
ansible.builtin.command: >-
80+
kubectl get secret
81+
-n {{ capi_cluster_service_account_namespace }}
82+
{{ capi_cluster_service_account_secret_name }}
83+
-o jsonpath='{.data.token}'
84+
register: sa_token_encoded
85+
changed_when: false
86+
87+
- name: Decode token for service account
88+
no_log: true
89+
ansible.builtin.set_fact:
90+
sa_token: "{{ sa_token_encoded.stdout | b64decode }}"
91+
92+
- name: Read saved kubeconfig file
93+
no_log: true
94+
ansible.builtin.slurp:
95+
src: "{{ capi_cluster_kubeconfig_path }}"
96+
register: kubeconfig_file_raw
97+
98+
- name: Decode kubeconfig content from base64
99+
no_log: true
100+
ansible.builtin.set_fact:
101+
kubeconfig_content: "{{ kubeconfig_file_raw.content | b64decode | from_yaml }}"
102+
103+
- name: Strip client cert/key and add token to create token-based kubeconfig
104+
no_log: true
105+
ansible.builtin.set_fact:
106+
modified_kubeconfig: >-
107+
{{
108+
kubeconfig_content | combine({
109+
"users": [
110+
{
111+
"name": kubeconfig_content.users[0].name,
112+
"user": {
113+
"token": sa_token
114+
}
115+
}
116+
]
117+
}, recursive=True)
118+
}}
119+
120+
- name: Save token-based kubeconfig to capi cluster kubeconfig path
121+
ansible.builtin.copy:
122+
content: "{{ modified_kubeconfig | to_nice_yaml }}"
123+
dest: "{{ capi_cluster_kubeconfig_path }}"
124+
mode: '0600'

roles/capi_cluster/tasks/main.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,15 @@
104104
dest: "{{ capi_cluster_kubeconfig_path }}"
105105
mode: u=rw,g=,o=
106106

107+
- name: Including token-based kubeconfig tasks to generate token-based kubeconfig
108+
when: capi_cluster_service_account_enabled
109+
environment:
110+
KUBECONFIG: "{{ capi_cluster_kubeconfig_path }}"
111+
block:
112+
- name: Include token-based kubeconfig task file
113+
ansible.builtin.include_tasks: capi-cluster-service-account.yml
114+
115+
107116
- name: Delete CAPI cluster
108117
when: capi_cluster_release_state == 'absent'
109118
block:

0 commit comments

Comments
 (0)