forked from onesteinbv/Project_Single_Sign_On
-
Notifications
You must be signed in to change notification settings - Fork 0
/
install-bitwarden-part-2-saml-sso.yml
372 lines (317 loc) · 14.6 KB
/
install-bitwarden-part-2-saml-sso.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
---
- name: Install BitWarden SSO via SAML op server
hosts: grpbitwarden
remote_user: root
# Execute like the following line;
# ansible-playbook install-bitwarden.yml -i hosts -e client_id="organization. \
# <the organization uuid as created in bitwarden>" --vault-password-file .vault-password
# Playbook to install bitwarden password server package on host.
vars:
root_ca_path: /usr/local/share/ca-certificates/mkcert_development_CA_62268663181785622328732999788222374785.crt
bitwarden_server_url: https://password.{{ domain }}
# bitwarden_client_id: "client-bitwarden-{{ ansible_fqdn }}"
bitwarden_client_id: "{{ bitwarden_server_url }}/sso/saml2"
bitwarden_client_name: "client-bitwarden-{{ ansible_fqdn }}"
keycloak_server_url: https://ad.{{ domain }}
tasks:
- name: Test connection to host
ansible.builtin.ping:
- name: Read global vars
ansible.builtin.include_vars: global-vars.yml
- name: Read encrypted content
ansible.builtin.include_vars: encrypted-vars.yml
- name: TIP
ansible.builtin.debug:
msg: Do yourself a favour; export ANSIBLE_STDOUT_CALLBACK=debug
# We need the uuid of the organization, so ask this from the command line
- name: Fail if no uuid provided on command line
ansible.builtin.fail:
msg: |
==========================================================================
Fatal: We need the client_id from the organization API key!
Read the last part of the install-bitwarden-sso.yml playbook
and rerun while adding the correct argument as shown next
"-e client_id=organization.3f0c3962-29d3-11ed-bc12-07773c74e67b"
You can find this id as follows
Login to BitWarden, open your organization.
Go to "Settings" -> "My Organization" and click on the
"View API Key" button. Copy the information shown under "client_id"
==========================================================================
when: client_id is not defined
- name: We need to get the uuid part of the client_id
ansible.builtin.set_fact:
# Slide from right so we always have the UUID, even when string is not starting with "organization."
organization_uuid: "{{ client_id[-36:] }}"
############################################################################################
# Also this needs to be fixed.
# The installation of the Bitwarden Directory Connector to sync users from ldap to bitwarden
# DO NOT EVER USE THE PIECE OF CRAP Bitwarden Directory Connector to sync users from ldap
############################################################################################
# SAML SSO: https://bitwarden.com/help/configure-sso-saml/
# Install all needed software in one go.
- name: Install all needed packages
ansible.builtin.apt:
name: "{{ item }}"
loop:
- python3-lxml
- libxml2-utils
- xmlstarlet
- openssl
# Generate a key on the Bitwarden server
- name: Generate key on Bitwarden server
ansible.builtin.command:
cmd: /usr/bin/openssl req -x509 -sha256 -newkey rsa:2048 -keyout sp.key -out sp.crt -days 3650 -nodes -subj '/CN={{ domain }}'
chdir: /tmp
register: my_output # <- Registers the command output.
changed_when: my_output.rc != 0 # <- Uses the return code to define when the task has changed.
# Start getting auth token
# Retrieve token url needed. Returns JSON payload with var token-service.
- name: Retrieve token url from server
ansible.builtin.uri:
url: "{{ keycloak_server_url }}/realms/master"
validate_certs: false
register: tokenurl
- name: Show token-service
ansible.builtin.debug:
var: tokenurl.json["token-service"]
- name: Store url for easier retrieval
ansible.builtin.set_fact:
token_url: "{{ tokenurl.json[\"token-service\"] }}"
# Call info endpoint
- name: "Retrieve endpoint info for our realm {{ realm }}"
ansible.builtin.uri:
url: "{{ keycloak_server_url }}/realms/{{ realm }}/.well-known/openid-configuration"
validate_certs: false
register: endpointinfo
- name: Show endpopint info
ansible.builtin.debug:
var: endpointinfo
- name: Store authorization_endpoint for faster retrieval
ansible.builtin.set_fact:
authorization_endpoint: "{{ endpointinfo.json[\"authorization_endpoint\"] }}"
# "authorization_endpoint": "https://ad.{{ domain }}/realms/ONESTEIN.LAN/protocol/openid-connect/auth",
- name: Show authorization endpoint
ansible.builtin.debug:
var: authorization_endpoint
- name: Store token_endpoint for faster retrieval
ansible.builtin.set_fact:
token_endpoint: "{{ endpointinfo.json[\"token_endpoint\"] }}"
- name: Show token endpoint
ansible.builtin.debug:
var: token_endpoint
- name: Store userinfo_endpoint for faster retrieval
ansible.builtin.set_fact:
userinfo_endpoint: "{{ endpointinfo.json[\"userinfo_endpoint\"] }}"
- name: Show userinfo endpoint
ansible.builtin.debug:
var: userinfo_endpoint
# Get authentication token from token-service url
- name: Retrieve authentication token from token-service url
ansible.builtin.uri:
url: "{{ tokenurl.json[\"token-service\"] }}/token"
method: POST
body_format: form-urlencoded
validate_certs: false
body:
realm: master
client_id: admin-cli
username: "{{ kc_adminid }}"
password: "{{ kc_adminpw }}"
grant_type: password
register: authtoken
- name: Show authtoken
ansible.builtin.debug:
var: authtoken
- name: Show access_token
ansible.builtin.debug:
var: authtoken.json["access_token"]
- name: Store access token into variable for easier retrieval
ansible.builtin.set_fact:
auth_token: "{{ authtoken.json[\"access_token\"] }}"
- name: Show auth_token
ansible.builtin.debug:
var: auth_token
# Retrieve IDP metadata descriptor and copy the 509 formatted certificate
# https://ad.{{ domain }}/realms/ONESTEIN.LAN/protocol/saml/descriptor
- name: Retrieve IDP metadata descriptor to use the 509 formatted certificate
ansible.builtin.uri:
url: "{{ keycloak_server_url }}/realms/{{ realm }}/protocol/saml/descriptor"
# headers:
# Accept: "application/json"
# Authorization: "Bearer {{ auth_token }}"
method: get
validate_certs: false
return_content: true
register: idp_metadata
- name: Show idp metadata
ansible.builtin.debug:
var: idp_metadata
# Please do not go down this road and stay sane...
# - xml:
# xmlstring: "{{ idp_metadata.content }}"
# xpath: /listResponse/instance
# xpath: /md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor/ds:KeyInfo/ds:X509Data/ds:X509Certificate
# content: attribute
# register: instance_attributes
# We are going to save the XML metadate for shell processing
- name: Save IDP XML metadata to file for processing
ansible.builtin.copy:
content: "{{ idp_metadata.content }}"
dest: /tmp/idp.xml
mode: '0600'
# We are going to use a shell command to retrieve the value for X509Certificate
- name: Run xmlstarlet to retrieve X509Certificate
ansible.builtin.command:
cmd: /usr/bin/xmlstarlet sel -t -v //ds:X509Certificate /tmp/idp.xml
register: xmlstarlet
changed_when: xmlstartlet.rc != 0 # <- Uses the return code to define when the task has changed.
- name: Show xmlstartlet
ansible.builtin.debug:
var: xmlstarlet
- name: Store output in certificate variable
ansible.builtin.set_fact:
idp_crt: "{{ xmlstarlet.stdout }}"
- name: Remove first line from tmp files
ansible.builtin.lineinfile:
path: "/tmp/{{ item }}"
regexp: '^-----(BEGIN|END).*-----$'
state: absent
loop:
- sp.crt
- sp.key
- name: Retrieve remote ssl cert
ansible.builtin.command:
cmd: cat /tmp/sp.crt | tr -d '\n'
register: sp_crt
changed_when: sp_crt.rc != 0 # <- Uses the return code to define when the task has changed.
- name: Show sp crt
ansible.builtin.debug:
var: sp_crt
- name: Retrieve remote ssl key
ansible.builtin.command:
cmd: cat /tmp/sp.key | tr -d '\n'
register: sp_key
changed_when: sp_key.rc != 0 # <- Uses the return code to define when the task has changed.
- name: Show sp key
ansible.builtin.debug:
var: sp_key
# Retrieve current list of clients of our type
# So, this works in curl:
# curl -k -v -H "Accept: application/json" -H "Authorization: Bearer ${access_token}" "https://ad.{{ domain }}:8443/admin/realms/master/clients" | jq .
- name: Retrieve current list of clients and search for already existing "{{ bitwarden_client_id }} "
ansible.builtin.uri:
url: "{{ keycloak_server_url }}/admin/realms/{{ realm }}/clients?clientId={{ bitwarden_client_id }}"
headers:
Accept: "application/json"
Authorization: "Bearer {{ auth_token }}"
method: get
validate_certs: false
register: existingclient
- name: Returned json
ansible.builtin.debug:
var: existingclient
# - name: Copy currently existing client definition to backup file in JSON format.
# copy:
# content: "{{ existingclient }}"
# dest: /tmp/bitwarden-original-client-backup.json
- name: Find ID in returned json
ansible.builtin.debug:
var: existingclient.json[0].id
- name: If it already exists then delete client with id "{{ bitwarden_client_id }}".
# Example: "id": "ba973624-2d00-488f-8d18-154224c63f8f"
ansible.builtin.uri:
url: "{{ keycloak_server_url }}/admin/realms/{{ realm }}/clients/{{ existingclient.json[0].id }}"
headers:
Accept: "application/json"
Authorization: "Bearer {{ auth_token }}"
method: DELETE
validate_certs: false
status_code: 204
when: existingclient.json[0].id is defined
register: deleteclient
- name: Show deleteclient
ansible.builtin.debug:
var: deleteclient
- name: Convert Ninja template to variable
ansible.builtin.set_fact:
jsonbody: "{{ lookup('template', 'bitwarden-keycloak-saml-sso.json.j2') }}"
- name: Show json body
ansible.builtin.debug:
var: jsonbody
- name: For debugging store json var in local file
ansible.builtin.copy:
content: "{{ jsonbody }}"
dest: /tmp/bitwarden-new-client-backup.json
mode: '0600'
# Generate the json payload to upload
- name: Upload JSON template file to create new Client ID on Keycloak server
ansible.builtin.uri:
url: "{{ keycloak_server_url }}/admin/realms/{{ realm }}/clients"
headers:
Accept: "application/json"
Authorization: "Bearer {{ auth_token }}" # .json[\"access_token\"] }}"
method: POST
validate_certs: false
body_format: json
body: "{{ jsonbody }}"
status_code: 201
register: createclientresult
- name: If all went well we now have a locaton of the newly created Client ID
ansible.builtin.debug:
var: createclientresult.location
# # You can also search the client id using this piece of code
# - name: Search id of newly created client
# uri:
# url: "{{ keycloak_server_url }}/admin/realms/{{ realm }}/clients?clientId={{ bitwarden_client_id }}"
# headers:
# Accept: "application/json"
# Authorization: "Bearer {{ auth_token }}"
# method: get
# validate_certs: false
# register: client1
# # Example: "id": "ba973624-2d00-488f-8d18-154224c63f8f"
- name: Show post install message
ansible.builtin.debug:
msg: |
****************************************************************************************
* Here some stuff you need to do yourself
* - Login to your BitWarden server and open your Organization.
*
* - Open the Settings tab and enter the following unique Identifier
* for your organization "{{ organization_id }}"
*
* Once you have your Organization Identifier, you can proceed to
* enabling and configuring your integration. To enable Login with SSO
* From the Organization Vault, navigate to the Manage tab and
* select Single Sign-On from the left-hand menu.
*
* - On the Single Sign-On Screen, check the Allow SSO Authentication checkbox.
* - From the Type dropdown menu, select the "SAML 2.0" option.
*
* == SAML Service Provider Configuration ==
* - SP Entity ID should read "https://password.{{ domain }}/sso/saml2"
* - SAML 2.0 Metadata URL should start with "https://password.{{ domain }}/sso/saml2/"
* - Assertion Consumer Service (ACS) URL should start with "https://password.{{ domain }}/sso/saml2"
* - Set Name ID Format to "Email Address"
* - Outbound Signing Algorithm should end with "rsa-sha256"
* - Signing Behaviour should be "If IdP Wants Authn Requests Signed"
* - Minimum Incoming Signing Algorithm should also end with "rsa-sha256"
* - Uncheck checkbox "Expect signed assertions"
* - Uncheck checkbox "Validate certificates"
*
* == SAML Identity Provider Configuration ==
* - Set Entity ID to "{{ keycloak_server_url }}/realms/{{ realm }}" .
* - Set Binding Type to "HTTP POST".
* - Set Single Sign On Service Url to "{{ keycloak_server_url }}/realms/{{ realm }}/protocol/saml".
* - BitWarden does not yet support "Single Log Out Service URL".
* - Set the X509 Public Certificate to "{{ idp_crt }}"
* - Outbound Signing Algorithm should end with "rsa-sha256"
* - Uncheck checkbox "Allow outbound logout requests".
* - Uncheck checkbox "Sign authentication requests".
*
* Go to top menu 'Vaults' -> "{{ organization_name }}".
* Click the 3 dots to the right of the organization name and click on 'Link SSO'.
*
* Notes; BitWarden allows per-organization logins based on valid redirect URLs containing
* a specific organization uuid. This playbook allows one redirect URLs.
********************************************************************************************