Launch the openldap and dex in kubernetes

Generate the PKI for openldap

  • Caution: the certificate’s subject's domain component (DC) can with openldap DN same to name
 1# install the certtool
 2apt install gnutls-bin
 3# generate the ca.key 
 4certtool --generate-privkey --outfile ca.key
 5# generate the ca certificate
 6certtool --generate-self-signed --load-privkey ca.key    --outfile ca.crt
 7# generate the server endpoint private key
 8certtool --generate-privkey --outfile tls.key --rsa
 9# server endpoint certificate request
10certtool --generate-request --load-privkey tls.key    --outfile request.crt
11# use ca certificate to sign the server endpoint certificate
12certtool --generate-certificate --load-request request.crt    --outfile tls.crt --load-ca-certificate ca.crt    --load-ca-privkey ca.key
  • Create the kubernetes secrets to serve the above certificates the data key is tls.crt, tls.key, ca.crt

Launch the Openldap helm chart

the image use only: jpgouin/openldap:2.6.7-fix about this issue: ldap_start_tls failed

 1        global:
 2          ldapDomain: "dc=mycomp,dc=test"        
 3        image:  
 4            # repository: bitnami/openldap  
 5            # tag: 2.6.8
 6            pullPolicy: IfNotPresent
 7        replicaCount: 2
 8        users: devloper,devops
 9        userPasswords: test,test
10        group: dev-team
11        initSchema:
12          image: 
13            pullPolicy: IfNotPresent
14        initTLSSecret:
15          tls_enabled: true 
16          secret: "ldap-tls-secret"
17        logLevel: info     
18        env:
19          BITNAMI_DEBUG: "true"
20          LDAP_LOGLEVEL: "256"
21          LDAP_TLS_ENFORCE: "false"
22          LDAPTLS_REQCERT: "allow"
23          LDAP_ENABLE_TLS: "yes"
24          LDAP_REQUIRE_TLS: "no"
25          LDAP_SKIP_DEFAULT_TREE: "no"
26        ltb-passwd:
27          image:
28            tag: 5.3.3
29            pullPolicy: IfNotPresent
30          ingress:
31            enabled: true
32            annotations:
33               kubernetes.io/ingress.class: nginx
34            hosts:
35            - "self-service.local.dev"
36        phpldapadmin:
37          image:
38            tag: 0.9.0
39            pullPolicy: IfNotPresent
40          ingress:
41            enabled: true
42            annotations:
43               kubernetes.io/ingress.class: nginx
44            hosts:
45            - "phpldap.local.dev"

Deployment the dex

use the official charts: Dex chart this the helm chart example values

 1        config:
 2            issuer: https://dexidp.local.dev
 3            storage:
 4                type: kubernetes
 5                config:
 6                    inCluster: true
 7            connectors:
 8            - type: ldap
 9              name: OpenLDAP
10              id: ldap
11              config:
12                host: openldap:389
13                insecureNoSSL: true
14                bindDN: cn=admin,dc=talos,dc=test
15                bindPW: "{{.Env.LDAP_ADMIN_PASSWORD}}"
16                usernamePrompt: Email Address
17                userSearch:
18                    baseDN: ou=users,dc=talos,dc=test
19                    filter: "(objectClass=inetOrgPerson)"
20                    username: mail                   
21                    idAttr: DN
22                    emailAttr: mail
23                    nameAttr: cn
24                groupSearch:
25                    baseDN: ou=users,dc=talos,dc=test
26                    filter: "(objectClass=groupOfNames)"
27                    userMatchers:                        
28                    - userAttr: DN
29                      groupAttr: member                    
30                    nameAttr: cn
31            staticClients:
32            - id: headlamp
33              redirectURIs:
34              - 'https://k8s.local.dev/oidc-callback'
35              name: 'headlamp'
36              secret: 'xxxxxxxxxxx'    
37        envVars:
38            - name: LDAP_ADMIN_PASSWORD
39              valueFrom:
40                 secretKeyRef:
41                   name: "openldap"
42                   key: "LDAP_ADMIN_PASSWORD"
43        ingress:
44            enabled: true
45            className: "nginx"
46            annotations:
47                kubernetes.io/ingress.class: nginx
48            hosts:
49            - host: dexidp.local.dev
50              paths:
51                - path: /
52                  pathType: ImplementationSpecific

Setup the kubernetes to use oidc

  • create the user rolebinding
1kubectl create clusterrolebinding oidc-cluster-admin \
2  --clusterrole=cluster-admin \
3  --user="admin1@example.org"
  • enable the api server oidc feature
1    - '--oidc-client-id=headlamp'
2    - '--oidc-issuer-url=https://dexidp.local.dev'
3    - '--oidc-username-claim=email'
  • user kubectl krew plugin to testing install the kubectl plugin management tool: krew
 1# install the oidc-login
 2kubectl krew install oidc-login
 3# set up the oidc-login
 4kubectl oidc-login setup --oidc-issuer-url=https://<YOUR-DEX-URL> \ --oidc-client-id=<CLIENT-ID> \
 5--oidc-client-secret=<CLIENT-SECRET>
 6# set the credentials 
 7kubectl config set-credentials oidc-user \
 8  --exec-api-version=client.authentication.k8s.io/v1beta1 \
 9  --exec-command=kubectl \
10  --exec-arg=oidc-login \
11  --exec-arg=get-token \
12  --exec-arg=--oidc-issuer-url=<YOUR-DEX-URL> \
13  --exec-arg=--oidc-client-id=<CLIENT-ID> \
14  --exec-arg=--oidc-client-secret=<CLIENT-SECRET> \
15  --exec-arg=--oidc-extra-scope=email

To debug and testing the openldap

1# test the tls connect to each other
2ldapsearch -x -H ldaps://openldap-1.openldap-headless.tools.svc.cluster.local:1636 -D "cn=admin,dc=mycomp,dc=test" -w "xxxxx" -b "cn=test,ou=users,dc=mycomp,dc=test"
3#

Application integration the

  • View detail oauth2.0 in dex configuration: https://dexidp.local.dev/.well-known/openid-configuration
  • Add the staticClient to dex config
1staticClients:
2- id: local-app
3  name: local-app
4  redirectURIs:
5  - http://localhost:3000/oauth/callback
6  secret: xxxx
  • Write client app to testing Oauth2
1<html>
2  <head>
3    <title>oauth walkthrough</title>
4  </head>
5  <body>
6    <a href="/oauth/dex">login with dex</a>
7  </body>
8</html>
  • The TypeScript code to implementation logic
 1/*
 2The workflow with dex use oauth2.0
 31. When user click the index.html button: login with dex 
 42. Your website asks dex for permission
 5
 6*/
 7
 8import express from "express";
 9import crypto from "crypto";
10
11const PORT = 3000;
12const CLIENT_ID = process.env.CLIENT_ID as string;
13const CLIENT_SECRET = process.env.CLIENT_SECRET as string;
14
15const app = express();
16
17app.use(express.static("static"));
18
19app.get("/oauth/dex", (req, res) => {
20  const params = new URLSearchParams();
21
22  params.set("response_type", "code");
23  params.set("client_id", CLIENT_ID);
24  params.set("redirect_uri", "http://localhost:3000/oauth/callback");
25  params.set("scope", "openid email");
26
27  const state = crypto.randomBytes(16).toString("hex");
28  params.set("state", state);
29
30  const url = `https://dexidp.local.dev/auth?${params.toString()}`;
31
32  res.set("Set-Cookie", `oauth_state=${state}; HttpOnly; Secure; SameSite=Lax`);
33  res.redirect(url);
34});
35
36app.get("/oauth/callback", async (req, res) => {
37    const { code, state } = req.query;
38    const oauthState = getCookie("oauth_state", req.headers.cookie as string);
39  
40    if (state !== oauthState) {
41      return res.status(400).send("Invalid state");
42    }
43  
44    const jsonToken = await exchangeCodeForToken(code as string);
45    const userInfo = await getUserInfo(jsonToken);
46  
47    res.header("Content-Type", "application/json").send(JSON.stringify(userInfo));
48  });
49  
50  app.listen(PORT, () => {
51    console.log(`Example app listening at http://localhost:${PORT}`);
52  });
53  
54  function getCookie(name: string, cookies: string) {
55    const cookie = cookies.split(";").find((cookie) => cookie.trim().startsWith(`${name}=`));
56    if (!cookie) {
57      return null;
58    }
59    return cookie.split("=")[1];
60  }
61  
62  async function exchangeCodeForToken(code: string) {
63    const resp = await fetch(`https://dexidp.local.dev/token`, {
64      method: "POST",
65      headers: {
66        "Content-Type": "application/x-www-form-urlencoded",
67      },
68      body: new URLSearchParams({
69        grant_type: "authorization_code",
70        code: code,
71        redirect_uri: "http://localhost:3000/oauth/callback",
72        client_id: CLIENT_ID,
73        client_secret: CLIENT_SECRET,
74      }).toString(),
75    });
76   
77    if (!resp.ok) {
78      throw new Error("Something went wrong");
79    }
80  
81    return resp.json() as Promise<{ access_token: string }>;
82  }
83  
84  async function getUserInfo(jsonToken: string) {
85    console.log(jsonToken)
86    const resp = await fetch(`https://dexidp.local.dev/userinfo`, {
87      headers: {
88        Authorization: `Bearer ${jsonToken['access_token']}`,
89      },
90    });
91  
92    return resp.json() as Promise<{ email: string }>;
93  }
  • The package.json
 1{
 2  "name": "dex-test",
 3  "version": "1.0.0",
 4  "description": "",
 5  "main": "index.js",
 6  "scripts": {
 7    "test": "echo \"Error: no test specified\" && exit 1",
 8    "start": "tsx index.ts"
 9  },
10  "dependencies": {
11    "express": "^4.18.2"
12  },
13  "devDependencies": {
14    "@types/express": "^4.17.21",
15    "@types/node": "^20.11.19",
16    "tsx": "^4.7.1",
17    "typescript": "^5.3.3"
18  },
19  "keywords": [],
20  "author": "",
21  "license": "ISC"
22}
  • to run CLIENT_SECRET=xxxxx CLIENT_ID=local-app pnpm run start

Reference