├── README.md └── main.go /README.md: -------------------------------------------------------------------------------- 1 | # Webhook service for Kubernetes LDAP authentication 2 | 3 | This is a webhook service that implements LDAP authentication for Kubernetes with the [Webhook Token authentication plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication). 4 | 5 | ## Dependencies 6 | 7 | ```bash 8 | go get github.com/go-ldap/ldap 9 | go get k8s.io/api/authentication/v1 10 | ``` 11 | 12 | ## Compile 13 | 14 | Cross-compile for Linux: 15 | 16 | ```bash 17 | GOOS=linux GOARCH=amd64 go build main.go 18 | ``` 19 | 20 | ## Run 21 | 22 | ```bash 23 | main 24 | ``` 25 | 26 | Arguments: 27 | 28 | - ``: IP address of the LDAP directory 29 | - ``: HTTPS server private key 30 | - ``: HTTPS server certificate 31 | 32 | You can generate an HTTPS private key and a self-signed certificate with the following command: 33 | 34 | ```bash 35 | openssl req -x509 -newkey rsa:2048 -nodes -subj "/CN=localhost" -keyout key.pem -out cert.pem 36 | ``` 37 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/go-ldap/ldap" 7 | "io/ioutil" 8 | "k8s.io/api/authentication/v1" 9 | "log" 10 | "net/http" 11 | "os" 12 | "strings" 13 | ) 14 | 15 | var ldapURL string 16 | 17 | func main() { 18 | ldapURL = "ldap://" + os.Args[1] 19 | log.Printf("Using LDAP directory %s\n", ldapURL) 20 | log.Println("Listening on port 443 for requests...") 21 | http.HandleFunc("/", handler) 22 | log.Fatal(http.ListenAndServeTLS(":443", os.Args[3], os.Args[2], nil)) 23 | } 24 | 25 | func handler(w http.ResponseWriter, r *http.Request) { 26 | 27 | // Read body of POST request 28 | b, err := ioutil.ReadAll(r.Body) 29 | if err != nil { 30 | writeError(w, err) 31 | return 32 | } 33 | log.Printf("Receiving: %s\n", string(b)) 34 | 35 | // Unmarshal JSON from POST request to TokenReview object 36 | // TokenReview: https://github.com/kubernetes/api/blob/master/authentication/v1/types.go 37 | var tr v1.TokenReview 38 | err = json.Unmarshal(b, &tr) 39 | if err != nil { 40 | writeError(w, err) 41 | return 42 | } 43 | 44 | // Extract username and password from the token in the TokenReview object 45 | s := strings.SplitN(tr.Spec.Token, ":", 2) 46 | if len(s) != 2 { 47 | writeError(w, fmt.Errorf("badly formatted token: %s", tr.Spec.Token)) 48 | return 49 | } 50 | username, password := s[0], s[1] 51 | 52 | // Make LDAP Search request with extracted username and password 53 | userInfo, err := ldapSearch(username, password) 54 | if err != nil { 55 | writeError(w, fmt.Errorf("failed LDAP Search request: %v", err)) 56 | return 57 | } 58 | 59 | // Set status of TokenReview object 60 | if userInfo == nil { 61 | tr.Status.Authenticated = false 62 | } else { 63 | tr.Status.Authenticated = true 64 | tr.Status.User = *userInfo 65 | } 66 | 67 | // Marshal the TokenReview to JSON and send it back 68 | b, err = json.Marshal(tr) 69 | if err != nil { 70 | writeError(w, err) 71 | return 72 | } 73 | w.Write(b) 74 | log.Printf("Returning: %s\n", string(b)) 75 | } 76 | 77 | func writeError(w http.ResponseWriter, err error) { 78 | err = fmt.Errorf("Error: %v", err) 79 | w.WriteHeader(http.StatusInternalServerError) // 500 80 | fmt.Fprintln(w, err) 81 | log.Println(err) 82 | } 83 | 84 | func ldapSearch(username, password string) (*v1.UserInfo, error) { 85 | 86 | // Connect to LDAP directory 87 | l, err := ldap.DialURL(ldapURL) 88 | if err != nil { 89 | return nil, err 90 | } 91 | defer l.Close() 92 | 93 | // Authenticate as LDAP admin user 94 | err = l.Bind("cn=admin,dc=mycompany,dc=com", "adminpassword") 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | // Execute LDAP Search request 100 | searchRequest := ldap.NewSearchRequest( 101 | "dc=mycompany,dc=com", // Search base 102 | ldap.ScopeWholeSubtree, // Search scope 103 | ldap.NeverDerefAliases, // Dereference aliases 104 | 0, // Size limit (0 = no limit) 105 | 0, // Time limit (0 = no limit) 106 | false, // Types only 107 | fmt.Sprintf("(&(objectClass=inetOrgPerson)(cn=%s)(userPassword=%s))", username, password), // Filter 108 | nil, // Attributes (nil = all user attributes) 109 | nil, // Additional 'Controls' 110 | ) 111 | result, err := l.Search(searchRequest) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | // If LDAP Search produced a result, return UserInfo, otherwise, return nil 117 | if len(result.Entries) == 0 { 118 | return nil, nil 119 | } else { 120 | return &v1.UserInfo{ 121 | Username: username, 122 | UID: username, 123 | Groups: result.Entries[0].GetAttributeValues("ou"), 124 | }, nil 125 | } 126 | } 127 | --------------------------------------------------------------------------------