-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- 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
1 parent
3f71306
commit 986ba23
Showing
18 changed files
with
1,185 additions
and
283 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|
@@ -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 | ||
``` | ||
|
@@ -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 | ||
|
@@ -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. | ||
|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", "") | ||
} | ||
} |
Oops, something went wrong.