Skip to content

Commit

Permalink
much improved implementation
Browse files Browse the repository at this point in the history
- client side uses a real proxy
- client and server side proxy will only forward to gke clusters endpoints
- cobra is used for CLI
  • Loading branch information
mvanholsteijn committed Dec 5, 2021
1 parent 3f71306 commit 986ba23
Show file tree
Hide file tree
Showing 18 changed files with 1,185 additions and 283 deletions.
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,6 @@ To configure your deployment, create a file `.auto.tfvars` with the following co
project = "my-project"
region = "europe-west4"
# target cluster to forward the requests to
target_cluster = {
name = "cluster-1"
location = "europe-west4-c"
}
## DNS managed zone accessible from the public internet
dns_managed_zone = "my-managed-zone"
Expand Down Expand Up @@ -55,13 +49,10 @@ $ terraform apply
After the apply, the required IAP proxy command is printed:
```
iap_proxy_command = <<EOT
simple-iap-proxy \
--rename-auth-header \
--target-url https://iap-proxy.my.cloud.dev \
--iap-audience 1234567890-j9onig1ofcgle7iogv8fceu04v8hriuv.apps.googleusercontent.com \
--service-account [email protected] \
--certificate-file server.crt \
--key-file server.key
simple-iap-proxy client \
--target-url https://iap-proxy.google.binx.dev \
--iap-audience 712731707077-j9onig1ofcgle7iogv8fceu04v8hriuv.apps.googleusercontent.com \
--service-account iap-proxy-accessor@speeltuin-mvanholsteijn.iam.gserviceaccount.com
EOT
```
Expand All @@ -78,6 +69,14 @@ $ openssl req -new -x509 -sha256 \
-days 3650 \
-out server.crt
```

To trust the proxy, type:

```
sudo security add-trusted-cert -d -p ssl -p basic -k /Library/Keychains/System.keychain ./server.crt
```


Now you can start the proxy, by copying the outputted command:

```shell-terminal
Expand All @@ -100,9 +99,10 @@ To configure the kubectl access via the IAP proxy, type:
```$shell-terminal
gcloud container clusters \
get-credentials cluster-1
context_name=$(kubectl config current-context)
kubectl config set clusters.$context_name.certificate-authority-data $(base64 < server.crt)
kubectl config set clusters.$context_name.server https://localhost:8443
kubectl config set clusters.$context_name.proxy-url https://localhost:8080
```

This points the context to the proxy and configure the self-signed certificate for the server.
Expand All @@ -115,5 +115,5 @@ $ kubectl cluster-info dump
```

## todo
- support proxying to multiple k8s clusters in the project
- upgrading to websockets is not supported (ie kubectl exec)
- deploy across multiple regions
166 changes: 166 additions & 0 deletions client/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package client

import (
"context"
"crypto/tls"
"fmt"
"github.com/binxio/gcloudconfig"
"github.com/binxio/simple-iap-proxy/clusterinfo"
"github.com/elazarl/goproxy"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/impersonate"
"google.golang.org/api/option"
"log"
"net/http"
"net/url"
"time"
)

type Proxy struct {
Debug bool
Port int
ProjectId string
KeyFile string
CertificateFile string
Certificate *tls.Certificate
Audience string
ServiceAccount string
ConfigurationName string
UseDefaultCredentials bool

TargetURL string
targetURL *url.URL
credentials *google.Credentials
tokenSource oauth2.TokenSource
clusterInfoCache *clusterinfo.ClusterInfoCache
}

func (p *Proxy) getCredentials(ctx context.Context) error {
var err error

if p.UseDefaultCredentials || !gcloudconfig.IsGCloudOnPath() {
p.credentials, err = google.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/cloud-platform.read-only")
} else {
p.credentials, err = gcloudconfig.GetCredentials(p.ConfigurationName)
}
if err != nil {
return fmt.Errorf("failed to obtain credentials, %s", err)
}
if p.ProjectId == "" {
p.ProjectId = p.credentials.ProjectID
}
if p.ProjectId == "" {
fmt.Errorf("specify a --project as there is no default one")
}
return nil
}

func (p *Proxy) IsClusterEndpoint() goproxy.ReqConditionFunc {
return func(req *http.Request, ctx *goproxy.ProxyCtx) bool {
return p.clusterInfoCache.GetClusterInfoForEndpoint(req.URL.Host) != nil
}
}
func (p *Proxy) OnRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
log.Printf("on request to %s", r.URL)

token, err := p.tokenSource.Token()
if err != nil {
return r, goproxy.NewResponse(r,
goproxy.ContentTypeText, http.StatusInternalServerError,
fmt.Sprintf("Failed to obtained IAP token, %s", err))
}

// If there is a Authorization header, make it X-Real-Authorization header
if authHeaders := r.Header.Values("Authorization"); len(authHeaders) > 0 {
for _, v := range r.Header.Values("Authorization") {
r.Header.Add("X-Real-Authorization", v)
}
r.Header.Del("Authorization")
}

authorization := fmt.Sprintf("%s %s", token.Type(), token.AccessToken)
r.Header.Set("Authorization", authorization)
RewriteRequestURL(r, p.targetURL)

return r, nil
}

func (p *Proxy) createProxy() *goproxy.ProxyHttpServer {
proxy := goproxy.NewProxyHttpServer()
proxy.Verbose = p.Debug
proxy.OnRequest(p.IsClusterEndpoint()).HandleConnect(goproxy.AlwaysMitm)
proxy.OnRequest(p.IsClusterEndpoint()).DoFunc(p.OnRequest)

if p.Certificate != nil {

goproxy.GoproxyCa = *p.Certificate
tlsConfig := goproxy.TLSConfigFromCA(p.Certificate)

goproxy.OkConnect = &goproxy.ConnectAction{
Action: goproxy.ConnectAccept,
TLSConfig: tlsConfig,
}
goproxy.MitmConnect = &goproxy.ConnectAction{
Action: goproxy.ConnectMitm,
TLSConfig: tlsConfig,
}
goproxy.HTTPMitmConnect = &goproxy.ConnectAction{
Action: goproxy.ConnectHTTPMitm,
TLSConfig: tlsConfig,
}
goproxy.RejectConnect = &goproxy.ConnectAction{
Action: goproxy.ConnectReject,
TLSConfig: tlsConfig,
}
}
return proxy
}

func (p *Proxy) Run() {
var err error

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

if p.targetURL, err = url.Parse(p.TargetURL); err != nil {
log.Fatalf("target url is not valid, %s", err)
}

err = p.getCredentials(ctx)
if err != nil {
log.Fatalf("%s", err)
}

p.clusterInfoCache, err = clusterinfo.NewClusterInfoCache(ctx, p.ProjectId, p.credentials, 5*time.Minute)
if err != nil {
log.Fatalf("%s", err)
}

p.tokenSource, err = impersonate.IDTokenSource(ctx, impersonate.IDTokenConfig{
TargetPrincipal: p.ServiceAccount,
Audience: p.Audience,
IncludeEmail: true,
},
option.WithTokenSource(p.credentials.TokenSource))
if err != nil {
log.Fatalf("failed to create a token source for audience %s as %s, %s",
p.Audience, p.ServiceAccount, err)
}

proxy := p.createProxy()

srv := &http.Server{
Handler: proxy,
Addr: fmt.Sprintf(":%d", p.Port),
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
}
if p.KeyFile == "" {
err = srv.ListenAndServe()
} else {
err = srv.ListenAndServeTLS(p.CertificateFile, p.KeyFile)
}
if err != nil {
log.Fatal(err)
}
}
58 changes: 58 additions & 0 deletions client/rewrite_request_url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package client

import (
"net/http"
"net/url"
"strings"
)

// function copied from httputil.reverseproxy.go
func SingleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
switch {
case aslash && bslash:
return a + b[1:]
case !aslash && !bslash:
return a + "/" + b
}
return a + b
}

// function copied from httputil.reverseproxy.go
func JoinURLPath(a, b *url.URL) (path, rawpath string) {
if a.RawPath == "" && b.RawPath == "" {
return SingleJoiningSlash(a.Path, b.Path), ""
}
// Same as singleJoiningSlash, but uses EscapedPath to determine
// whether a slash should be added
apath := a.EscapedPath()
bpath := b.EscapedPath()

aslash := strings.HasSuffix(apath, "/")
bslash := strings.HasPrefix(bpath, "/")

switch {
case aslash && bslash:
return a.Path + b.Path[1:], apath + bpath[1:]
case !aslash && !bslash:
return a.Path + "/" + b.Path, apath + "/" + bpath
}
return a.Path + b.Path, apath + bpath
}

func RewriteRequestURL(req *http.Request, target *url.URL) {
targetQuery := target.RawQuery
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path, req.URL.RawPath = JoinURLPath(target, req.URL)
if targetQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = targetQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
}
if _, ok := req.Header["User-Agent"]; !ok {
// explicitly disable User-Agent so it's not set to default value
req.Header.Set("User-Agent", "")
}
}
Loading

0 comments on commit 986ba23

Please sign in to comment.