Skip to content

Commit 00e3d0d

Browse files
committed
add PKCE option to kubelogin POC
Add device-code flow with PKCE to Kubelogin POC. Signed-off-by: Tuomo Tanskanen <[email protected]>
1 parent 86eb4a8 commit 00e3d0d

File tree

6 files changed

+524
-96
lines changed

6 files changed

+524
-96
lines changed

security/kubelogin-poc/README.md

Lines changed: 166 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,32 @@
1-
# kuberos/kubelogin poc
1+
# int128/kubelogin POC
22

3-
<!-- markdownlint-disable MD013 -->
4-
5-
## Kubelogin, or kubectl-oidc_login
6-
7-
Docs:
3+
Upstream documentation:
84

95
- <https://github.com/int128/kubelogin>
106
- <https://github.com/int128/kubelogin/blob/master/docs/usage.md>
7+
- <https://dexidp.io/docs/connectors/oidc/>
8+
- <https://openid.net/specs/openid-connect-core-1_0.html#Claims>
9+
- <https://dexidp.io/docs/custom-scopes-claims-clients/>
1110

12-
### Kubelogin installation
11+
## Kubelogin installation
1312

1413
Get the binary from
1514
[releases](https://github.com/int128/kubelogin/releases/tag/v1.28.0), and
1615
install as `kubectl-oidc_login` in `/usr/local/bin` or elsewhere in `$PATH`.
1716

18-
## Grant types
17+
## Cluster POC
1918

20-
Kubelogin and Dex support following grant types:
21-
22-
- auto (automatic selection)
23-
- authcode (token passthru via web browser callback)
24-
- authcode-keyboard (generates code you need to paste yourself)
25-
- device-code (like authcode, but for a device of certain code)
26-
- password (supply username and password on the command line, not enabled by default)
27-
28-
Grant type is specified with `--grant-type=<type>` on command line during setup.
29-
30-
### Password
31-
32-
Password flow is enabled only, if Dex config is set to allow it by setting one
33-
of the connectors as `passwordConnector`:
34-
35-
```yaml
36-
oauth2:
37-
passwordConnector: ldap
38-
```
39-
40-
If this config is not present, password grant type is rejected as
41-
`not supported`. Most of the connectors do not support username/password.
42-
`local` and `ldap` do.
43-
44-
## Putting it all together
45-
46-
Now that we know we can do `kubelogin` -> `Dex` -> `openLDAP`, we need to
47-
setup the final chain, where Dex is integrated with k8s apiserver, and it
48-
actually authenticates `kubectl` commands via Dex.
19+
POC sets up `kubelogin` -> `Dex` -> `openLDAP` auth chain, where Dex is
20+
integrated with k8s apiserver as OIDC provider, and it actually authenticates
21+
`kubectl` commands via Dex.
4922

5023
TL;DR: `./run.sh` creates Kind cluster and will setup everything for you.
5124
Read [the script](./run.sh) before running it, or do the steps one by one
5225
following the steps below.
5326

27+
- `./run.sh password` to create cluster with password grant flow
28+
- `./run.sh device-code` to create cluster with device-code flow
29+
5430
### Kind setup
5531

5632
In case you don't have a k8s cluster you want to test this in, you can setup
@@ -64,14 +40,14 @@ users won't have any rights.
6440

6541
For Kind setup, we also need to mount a directory holding a Dex generated CA
6642
cert, so apiserver will trust the Dex when connecting. Using [Dex's certificate
67-
generation script](gencert.sh), you get a CA cert, which should be mounted via
43+
generation script](./gencert.sh), you get a CA cert, which should be mounted via
6844
directory `/etc/ssl/certs/dex-test` in the apiserver.
6945

7046
### Dex setup
7147

7248
We setup Dex to run in nodeport `32000` so it is reachable by the apiserver and
73-
the user's kubectl client. Alternatively, Dex could be configued with service
74-
andpoint, and apiserver could be configured to connect to `dex.dex` for example.
49+
the user's kubectl client. Alternatively, Dex could be configured with service
50+
endpoint, and apiserver could be configured to connect to `dex.dex` for example.
7551
OIDC provider is often external to the cluster, and hence the nodeport simulates
7652
reality better than in-cluster service.
7753

@@ -84,13 +60,150 @@ the generated CA certificate in previous step. Using expired or non-trusted
8460
certificates causes apiserver not trust Dex, and login attempts will fail even
8561
if the Dex token kubectl gained is valid.
8662

87-
See [dex.yaml](k8s/dex.yaml).
88-
8963
We also need to add a line to `/etc/hosts` to create fake Dex domain:
90-
`127.0.0.1 dex.example.com` or simply change all occurences of
64+
`127.0.0.1 dex.example.com` or simply change all occurrences of
9165
`dex.example.com` with `127.0.0.1`. Here we use the `dex.example.com` in order
9266
to make a difference between Dex host and other services.
9367

68+
Depending on the connector and grant type you want to configure, choose the
69+
appropriate configs below.
70+
71+
#### Password connector
72+
73+
Password flow is enabled only if Dex config is set to allow it by setting one
74+
of the connectors as `passwordConnector`:
75+
76+
```yaml
77+
oauth2:
78+
passwordConnector: ldap
79+
```
80+
81+
If this config is not present, password grant type is rejected as
82+
`not supported`. Most of the connectors do not support username/password.
83+
`local` and `ldap` do.
84+
85+
#### Grant types
86+
87+
Kubelogin and Dex support the following grant types:
88+
89+
- auto (automatic selection)
90+
- authcode (token passthrough via web browser callback)
91+
- authcode-keyboard (generates code you need to paste yourself)
92+
- device-code (like authcode, but can be completed on remote machine, for a session
93+
identified by a device-code)
94+
- password (supply username and password on the command line, not enabled by default)
95+
96+
Grant type is specified with `--grant-type=<type>` on command line during setup.
97+
98+
##### Grant type: password
99+
100+
<https://oauth.net/2/grant-types/password/>
101+
102+
If we want to achieve headless CLI login, we need to use `password` grant type.
103+
It should be noted that password grants are removed in OAuth 2.1 spec, and kind
104+
of exist outside the OAuth2 spec. Password grants skip the auth code flow, and
105+
exchange credentials directly to tokens. For this reason, a client secret must
106+
be passed to user to be included in their kubeconfig since PKCE is not available.
107+
108+
We configure the client as a `staticClient` in
109+
[Dex configuration](./k8s/dex-password.yaml):
110+
111+
```yaml
112+
staticClients:
113+
- id: kubelogin-test
114+
name: Kubelogin
115+
secret: kubelogin-test-secret
116+
```
117+
118+
and the [kubelogin config](./kubeconfig.password.example) in kubeconfig needs to
119+
look like:
120+
121+
```yaml
122+
- name: oidc
123+
user:
124+
exec:
125+
apiVersion: client.authentication.k8s.io/v1beta1
126+
args:
127+
- oidc-login
128+
- get-token
129+
- --oidc-issuer-url=https://dex.example.com:32000
130+
- --oidc-client-id=kubelogin-test
131+
- --oidc-client-secret=kubelogin-test-secret
132+
- --oidc-extra-scope=email
133+
- --oidc-extra-scope=profile
134+
- --oidc-extra-scope=groups
135+
- --grant-type=password
136+
- --insecure-skip-tls-verify
137+
```
138+
139+
NOTE: password grant is legacy, and often
140+
[recommended](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth-ropc)
141+
not to be used outside high-trust environments. LDAP auth does not support MFA
142+
and involves giving out your username and password to service admins (versus
143+
identity provider only).
144+
145+
Read more:
146+
147+
- <https://datatracker.ietf.org/doc/html/rfc6749>
148+
149+
##### Grant type: device-code
150+
151+
If we do not want to pass client-secret to the user (it has security
152+
implications), we lose the full headless CLI login experience, as we cannot use
153+
password grant anymore. Instead, we need to use `device-code` flow. This allows
154+
the user to login on another machine that is browser capable, and tie that login to
155+
one's own kubectl via the device-code.
156+
157+
We need to adjust the `oauth2` configuration to have specific response types:
158+
159+
```yaml
160+
oauth2:
161+
passwordConnector: ldap
162+
skipApprovalScreen: true
163+
responseTypes: ["code", "token", "id_token"]
164+
```
165+
166+
and the `staticClients` part will look like:
167+
168+
```yaml
169+
staticClients:
170+
- id: kubelogin-test
171+
name: Kubelogin
172+
# public client must be true or secret must be given to user
173+
public: true
174+
# redirect uris are needed for device code flow
175+
redirectURIs:
176+
- 'urn:ietf:wg:oauth:2.0:oob'
177+
- '/device/callback'
178+
```
179+
180+
and the [kubelogin config](./kubeconfig.device-code.example) in kubeconfig needs
181+
to look like:
182+
183+
```yaml
184+
- name: oidc
185+
user:
186+
exec:
187+
apiVersion: client.authentication.k8s.io/v1beta1
188+
args:
189+
- oidc-login
190+
- get-token
191+
- --oidc-issuer-url=https://dex.example.com:32000
192+
- --oidc-client-id=kubelogin-test
193+
- --oidc-pkce-method=S256
194+
- --oidc-extra-scope=email
195+
- --oidc-extra-scope=profile
196+
- --oidc-extra-scope=groups
197+
- --insecure-skip-tls-verify
198+
- --use-device-code
199+
```
200+
201+
Read more about PKCE:
202+
203+
- <https://datatracker.ietf.org/doc/html/rfc7636>
204+
- <https://oauth.net/2/pkce/>
205+
- <https://github.com/dexidp/dex/issues/2244>
206+
94207
### OpenLDAP k8s setup
95208

96209
We need to set up OpenLDAP so it is reachable by Dex. In this setup, we run it
@@ -121,58 +234,22 @@ Kubernetes Apiserver. Client can be configured to skip
121234
--exec-arg=get-token \
122235
--exec-arg=--oidc-issuer-url=https://dex.example.com:32000 \
123236
--exec-arg=--oidc-client-id=kubelogin-test \
124-
--exec-arg=--oidc-client-secret=kubelogin-test-secret \
125237
--exec-arg=--oidc-extra-scope=email \
126238
--exec-arg=--oidc-extra-scope=profile \
127239
--exec-arg=--oidc-extra-scope=groups \
240+
--exec-arg=--insecure-skip-tls-verify \
241+
# for password grant you need these
242+
--exec-arg=--oidc-client-secret=kubelogin-test-secret \
128243
--exec-arg=--grant-type=password \
129-
--exec-arg=--insecure-skip-tls-verify
130-
```
131-
132-
and kubeconfig ends up looking like [this](kubeconfig.example).
244+
# for device-code you need these
245+
--exec-arg=--oidc-pkce-method=S256 \
246+
--exec-arg=--grant-type=device-code \
247+
# if you need to debug
248+
--exec-arg=-v1
133249
134-
```yaml
135-
apiVersion: v1
136-
# regular cluster configuration here
137-
clusters:
138-
- cluster:
139-
certificate-authority-data: <cluster ca redacted>
140-
server: https://<apiserver ip>:<port>
141-
name: kind
142-
contexts:
143-
- context:
144-
cluster: kind
145-
user: oidc
146-
name: kind-oidc
147-
current-context: kind-oidc
148-
kind: Config
149-
preferences: {}
150-
users:
151-
# in user section, use whatever user name you want, but the oidc-login setup
152-
# needs to match the OIDC config passed to the apiserver
153-
# insecure-skip-tls-verify is only here because of the test setup
154-
# issuer url needs to be accessible and match the Dex issuer config
155-
- name: oidc
156-
user:
157-
exec:
158-
apiVersion: client.authentication.k8s.io/v1beta1
159-
args:
160-
- oidc-login
161-
- get-token
162-
- --oidc-issuer-url=https://dex.example.com:32000
163-
- --oidc-client-id=kubelogin-test
164-
- --oidc-client-secret=kubelogin-test-secret
165-
- --oidc-extra-scope=email
166-
- --oidc-extra-scope=profile
167-
- --oidc-extra-scope=groups
168-
- --grant-type=password
169-
- --insecure-skip-tls-verify
170-
command: kubectl
171-
env: null
172-
provideClusterInfo: false
173250
```
174251

175-
#### Cluster roles
252+
### Cluster roles
176253

177254
Cluster role needs to be assigned based on the username or the groups passed via
178255
the `groups` scope. These roles need to be configured by the pre-existing
@@ -195,8 +272,4 @@ kubectl create clusterrolebinding oidc-dex-group \
195272

196273
LDAP configuration would be different per organization regardless.
197274

198-
Some references:
199-
200-
- <https://dexidp.io/docs/connectors/oidc/>
201-
- <https://openid.net/specs/openid-connect-core-1_0.html#Claims>
202-
- <https://dexidp.io/docs/custom-scopes-claims-clients/>
275+
<!-- cSpell:ignore apiserver,kubelogin,endpoint,nodeport,pkce -->

0 commit comments

Comments
 (0)