Skip to content

Commit 7ad4491

Browse files
committed
Initial Commit
0 parents  commit 7ad4491

File tree

5 files changed

+370
-0
lines changed

5 files changed

+370
-0
lines changed

.gitignore

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Compiled Object files, Static and Dynamic libs (Shared Objects)
2+
*.o
3+
*.a
4+
*.so
5+
6+
# Folders
7+
_obj
8+
_test
9+
10+
# Coverage
11+
coverage.out
12+
13+
# Build artifacts
14+
k8s-oidc-helper
15+
16+
# Architecture specific extensions/prefixes
17+
*.[568vq]
18+
[568vq].out
19+
20+
*.cgo1.go
21+
*.cgo2.c
22+
_cgo_defun.c
23+
_cgo_gotypes.go
24+
_cgo_export.*
25+
26+
_testmain.go
27+
28+
*.exe
29+
*.test
30+
*.prof
31+
32+
.idea/
33+
34+
# Ignore vim files
35+
*.swp

Dockerfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# To build:
2+
# $ docker run --rm -v $(pwd):/go/src/github.com/micahhausler/k8s-oidc-helper -w /go/src/github.com/micahhausler/k8s-oidc-helper golang:1.7 go build -v -a -tags netgo -installsuffix netgo -ldflags '-w'
3+
# $ docker build -t micahhausler/k8s-oidc-helper .
4+
#
5+
# To run:
6+
# $ docker run micahhausler/k8s-oidc-helper
7+
8+
FROM busybox
9+
10+
MAINTAINER Micah Hausler, <[email protected]>
11+
12+
COPY k8s-oidc-helper /bin/k8s-oidc-helper
13+
RUN chmod 755 /bin/k8s-oidc-helper
14+
15+
ENTRYPOINT ["/bin/k8s-oidc-helper"]

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2016 Micah Hausler
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# k8s-oidc-helper
2+
3+
This is a small helper tool to get a user get authenticated with
4+
[Kubernetes OIDC](http://kubernetes.io/docs/admin/authentication/) using Google
5+
as the Identity Provider.
6+
7+
Given a ClientID and ClientSecret, the tool will output the necessary
8+
configurtion for `kubectl` that you can add to `~/.kube/config`
9+
10+
## Setup
11+
12+
There is a bit of setup involved before you can use this tool.
13+
14+
First, you'll need to create a project and OAuth 2.0 Credential in the Google
15+
Cloud Console. You can follow [this guide](https://developers.google.com/identity/sign-in/web/devconsole-project)
16+
on creating an application, but do *NOT* create a web application. You'll need
17+
to select "Other" as the Application Type. Once that is created, you can
18+
download the ClientID and ClientSecret as a JSON file for ease of use.
19+
20+
21+
Second, your kube-apiserver will need the following flags on to use OpenID Connect.
22+
23+
```
24+
--oidc-issuer-url=https://accounts.google.com \
25+
--oidc-username-claim=email \
26+
--oidc-client-id=<Your client ID>\
27+
```
28+
29+
### Role-Based Access Control
30+
31+
If you are using [RBAC](http://kubernetes.io/docs/admin/authorization/) as your
32+
`--authorization-mode`, you can use the following `ClusterRole` and
33+
`ClusterRoleBinding` for administrators that need cluster-wide access.
34+
35+
```yaml
36+
kind: ClusterRole
37+
apiVersion: rbac.authorization.k8s.io/v1alpha1
38+
metadata:
39+
name: admin-role
40+
rules:
41+
- apiGroups: ["*"]
42+
resources: ["*"]
43+
verbs: ["*"]
44+
nonResourceURLs: ["*"]
45+
---
46+
kind: ClusterRoleBinding
47+
apiVersion: rbac.authorization.k8s.io/v1alpha1
48+
metadata:
49+
name: admin-binding
50+
subjects:
51+
- kind: User
52+
53+
roleRef:
54+
kind: ClusterRole
55+
name: admin-role
56+
```
57+
58+
## Installation
59+
60+
```
61+
go install github.com/micahhausler/k8s-oidc-helper
62+
```
63+
64+
## Usage
65+
66+
```
67+
Usage of ./k8s-oidc-helper:
68+
69+
-client-id string
70+
The ClientID for the application
71+
-client-secret string
72+
The ClientSecret for the application
73+
-config string
74+
Path to a json file containing your application's ClientID and ClientSecret.
75+
-open
76+
Open the oauth approval URL in the browser
77+
-version
78+
print version and exit
79+
```
80+
81+
## Wishlist
82+
83+
- [ ] Add tests/CI
84+
- [ ] Add docker builds to CI
85+
86+
## License
87+
88+
MIT License. See [License](/LICENSE) for full text

main.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"encoding/json"
6+
"flag"
7+
"fmt"
8+
yaml "gopkg.in/yaml.v2"
9+
"net/http"
10+
"net/url"
11+
"os"
12+
"os/exec"
13+
"strings"
14+
)
15+
16+
const Version = "0.0.1"
17+
18+
var version = flag.Bool("version", false, "print version and exit")
19+
20+
var openBrowser = flag.Bool("open", false, "Open the oauth approval URL in the browser")
21+
22+
var clientIDFlag = flag.String("client-id", "", "The ClientID for the application")
23+
var clientSecretFlag = flag.String("client-secret", "", "The ClientSecret for the application")
24+
var appFile = flag.String("config", "", "Path to a json file containing your application's ClientID and ClientSecret.")
25+
26+
const oauthUrl = "https://accounts.google.com/o/oauth2/auth?redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&client_id=%s&scope=openid+email+profile&approval_prompt=force&access_type=offline"
27+
28+
type ConfigFile struct {
29+
Installed *GoogleConfig `json:"installed"`
30+
}
31+
32+
type GoogleConfig struct {
33+
ClientID string `json:"client_id"`
34+
ClientSecret string `json:"client_secret"`
35+
}
36+
37+
type TokenResponse struct {
38+
AccessToken string `json:"access_token"`
39+
RefreshToken string `json:"refresh_token"`
40+
IdToken string `json:"id_token"`
41+
}
42+
43+
func readConfig(path string) (*GoogleConfig, error) {
44+
f, err := os.Open(path)
45+
defer f.Close()
46+
if err != nil {
47+
return nil, err
48+
}
49+
cf := &ConfigFile{}
50+
err = json.NewDecoder(f).Decode(cf)
51+
if err != nil {
52+
return nil, err
53+
}
54+
return cf.Installed, nil
55+
}
56+
57+
// Get the id_token and refresh_token from google
58+
func getTokens(clientID, clientSecret, code string) (*TokenResponse, error) {
59+
val := url.Values{}
60+
val.Add("grant_type", "authorization_code")
61+
val.Add("redirect_uri", "urn:ietf:wg:oauth:2.0:oob")
62+
val.Add("client_id", clientID)
63+
val.Add("client_secret", clientSecret)
64+
val.Add("code", code)
65+
66+
resp, err := http.PostForm("https://www.googleapis.com/oauth2/v3/token", val)
67+
defer resp.Body.Close()
68+
if err != nil {
69+
return nil, err
70+
}
71+
tr := &TokenResponse{}
72+
err = json.NewDecoder(resp.Body).Decode(tr)
73+
if err != nil {
74+
return nil, err
75+
}
76+
return tr, nil
77+
}
78+
79+
type KubectlUser struct {
80+
Name string `yaml:"name"`
81+
KubeUserInfo *KubeUserInfo `yaml:"user"`
82+
}
83+
84+
type KubeUserInfo struct {
85+
AuthProvider *AuthProvider `yaml:"auth-provider"`
86+
}
87+
88+
type AuthProvider struct {
89+
APConfig *APConfig `yaml:"config"`
90+
Name string `yaml:"name"`
91+
}
92+
93+
type APConfig struct {
94+
ClientID string `yaml:"client-id"`
95+
ClientSecret string `yaml:"client-secret"`
96+
IdToken string `yaml:"id-token"`
97+
IdpIssuerUrl string `yaml:"idp-issuer-url"`
98+
RefreshToken string `yaml:"refresh-token"`
99+
}
100+
101+
type UserInfo struct {
102+
Email string `json:"email"`
103+
}
104+
105+
func getUserEmail(accessToken string) (string, error) {
106+
uri, _ := url.Parse("https://www.googleapis.com/oauth2/v1/userinfo")
107+
q := uri.Query()
108+
q.Set("alt", "json")
109+
q.Set("access_token", accessToken)
110+
uri.RawQuery = q.Encode()
111+
resp, err := http.Get(uri.String())
112+
defer resp.Body.Close()
113+
if err != nil {
114+
return "", err
115+
}
116+
ui := &UserInfo{}
117+
err = json.NewDecoder(resp.Body).Decode(ui)
118+
if err != nil {
119+
return "", err
120+
}
121+
return ui.Email, nil
122+
}
123+
124+
func generateUser(email, clientId, clientSecret, idToken, refreshToken string) *KubectlUser {
125+
return &KubectlUser{
126+
Name: email,
127+
KubeUserInfo: &KubeUserInfo{
128+
AuthProvider: &AuthProvider{
129+
APConfig: &APConfig{
130+
ClientID: clientId,
131+
ClientSecret: clientSecret,
132+
IdToken: idToken,
133+
IdpIssuerUrl: "https://accounts.google.com",
134+
RefreshToken: refreshToken,
135+
},
136+
Name: "oidc",
137+
},
138+
},
139+
}
140+
}
141+
142+
func main() {
143+
144+
flag.Usage = func() {
145+
fmt.Fprintf(os.Stderr, "Usage of %s:\n\n", os.Args[0])
146+
flag.PrintDefaults()
147+
}
148+
149+
flag.Parse()
150+
151+
if *version {
152+
fmt.Printf("k8s-oidc-helper %s\n", Version)
153+
os.Exit(0)
154+
}
155+
156+
var gcf *GoogleConfig
157+
var err error
158+
if len(*appFile) > 0 {
159+
gcf, err = readConfig(*appFile)
160+
if err != nil {
161+
fmt.Printf("Error reading config file %s: %s\n", *appFile, err)
162+
os.Exit(1)
163+
}
164+
}
165+
var clientID string
166+
var clientSecret string
167+
if gcf != nil {
168+
clientID = gcf.ClientID
169+
clientSecret = gcf.ClientSecret
170+
} else {
171+
clientID = *clientIDFlag
172+
clientSecret = *clientSecretFlag
173+
}
174+
175+
if *openBrowser {
176+
cmd := exec.Command("open", fmt.Sprintf(oauthUrl, clientID))
177+
err = cmd.Start()
178+
}
179+
if !*openBrowser || err != nil {
180+
fmt.Printf("Open this url in your browser: %s\n", fmt.Sprintf(oauthUrl, clientID))
181+
}
182+
183+
reader := bufio.NewReader(os.Stdin)
184+
fmt.Print("Enter the code Google gave you: ")
185+
code, _ := reader.ReadString('\n')
186+
code = strings.TrimSpace(code)
187+
188+
tokResponse, err := getTokens(clientID, clientSecret, code)
189+
if err != nil {
190+
fmt.Printf("Error getting tokens: %s\n", err)
191+
os.Exit(1)
192+
}
193+
194+
email, err := getUserEmail(tokResponse.AccessToken)
195+
if err != nil {
196+
fmt.Printf("Error getting user email: %s\n", err)
197+
os.Exit(1)
198+
}
199+
200+
userConfig := generateUser(email, clientID, clientSecret, tokResponse.IdToken, tokResponse.RefreshToken)
201+
output := map[string][]*KubectlUser{}
202+
output["users"] = []*KubectlUser{userConfig}
203+
response, err := yaml.Marshal(output)
204+
if err != nil {
205+
fmt.Printf("Error marshaling yaml: %s\n", err)
206+
os.Exit(1)
207+
}
208+
fmt.Println("\n# Add the following to your ~/.kube/config")
209+
fmt.Println(string(response))
210+
211+
}

0 commit comments

Comments
 (0)