Skip to content

Commit

Permalink
Merge pull request #277 from ibihim/secure-connection
Browse files Browse the repository at this point in the history
validate options: check for secure connection for h2c and identity headers
  • Loading branch information
ibihim committed Apr 8, 2024
2 parents f5d9a22 + 876382b commit 60234c7
Show file tree
Hide file tree
Showing 40 changed files with 1,118 additions and 18 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ on: [push, pull_request]

env:
QUAY_PATH: quay.io/brancz/kube-rbac-proxy
go-version: '1.19.4'
kind-version: 'v0.16.0'
go-version: '1.22.1'
kind-version: 'v0.22.0'

jobs:
check-license:
Expand Down
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,12 @@ test-unit:
test-e2e:
go test -timeout 55m -v ./test/e2e/ $(TEST_RUN_ARGS) --kubeconfig=$(KUBECONFIG)

test-local:
@echo 'run: VERSION=local make clean container kind-create-cluster test'
test-local-setup: VERSION = local
test-local-setup: VERSION_SEMVER = $(shell cat VERSION)
test-local-setup: container kind-create-cluster
test-local: test-local-setup test

test-e2e-local: test-local-setup test-e2e

kind-delete-cluster:
kind delete cluster
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ Secure serving flags:
--secure-port int The port on which to serve HTTPS with authentication and authorization. If 0, don't serve HTTPS at all. (default 443)
--tls-cert-file string File containing the default x509 Certificate for HTTPS. (CA cert, if any, concatenated after server cert). If HTTPS serving is enabled, and --tls-cert-file and --tls-private-key-file are not provided, a self-signed certificate and key are generated for the public address and saved to the directory specified by --cert-dir.
--tls-cipher-suites strings Comma-separated list of cipher suites for the server. If omitted, the default Go cipher suites will be used.
Preferred values: TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_RSA_WITH_AES_128_GCM_SHA256, TLS_RSA_WITH_AES_256_CBC_SHA, TLS_RSA_WITH_AES_256_GCM_SHA384.
Insecure values: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_RSA_WITH_RC4_128_SHA, TLS_RSA_WITH_3DES_EDE_CBC_SHA, TLS_RSA_WITH_AES_128_CBC_SHA256, TLS_RSA_WITH_RC4_128_SHA.
Preferred values: TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256.
Insecure values: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_RSA_WITH_RC4_128_SHA, TLS_RSA_WITH_3DES_EDE_CBC_SHA, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_RSA_WITH_AES_128_CBC_SHA256, TLS_RSA_WITH_AES_128_GCM_SHA256, TLS_RSA_WITH_AES_256_CBC_SHA, TLS_RSA_WITH_AES_256_GCM_SHA384, TLS_RSA_WITH_RC4_128_SHA.
--tls-min-version string Minimum TLS version supported. Possible values: VersionTLS10, VersionTLS11, VersionTLS12, VersionTLS13
--tls-private-key-file string File containing the default x509 private key matching --tls-cert-file.
--tls-sni-cert-key namedCertKey A pair of x509 certificate and private key file paths, optionally suffixed with a list of domain patterns which are fully qualified domain names, possibly with prefixed wildcard segments. The domain patterns also allow IP addresses, but IPs should only be used if the apiserver has visibility to the IP address requested by a client. If no domain patterns are provided, the names of the certificate are extracted. Non-wildcard matches trump over wildcard matches, explicit domain patterns trump over extracted names. For multiple key/certificate pairs, use the --tls-sni-cert-key multiple times. Examples: "example.crt,example.key" or "foo.crt,foo.key:*.foo.com,foo.com". (default [])
Expand Down
75 changes: 75 additions & 0 deletions cmd/kube-rbac-proxy/app/options/proxyoptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ limitations under the License.
package options

import (
"context"
"fmt"
"net"
"net/url"
"os"
"path"
"time"

"github.com/ghodss/yaml"
"github.com/spf13/pflag"
Expand All @@ -34,6 +37,8 @@ import (
"github.com/brancz/kube-rbac-proxy/pkg/server"
)

const loopbackLookupTimeout = 5

// ProxyOptions are options specific to the kube-rbac-proxy
type ProxyOptions struct {
Upstream string
Expand Down Expand Up @@ -103,6 +108,11 @@ func (o *ProxyOptions) Validate() []error {
}
}

// Verify secure connection settings, if necessary.
if err := validateSecureConnectionConfig(o); err != nil {
errs = append(errs, err)
}

return errs
}

Expand All @@ -125,13 +135,78 @@ func (o *ProxyOptions) ApplyTo(c *server.KubeRBACProxyInfo, a *serverconfig.Auth
}
}

c.UpstreamHeaders = o.UpstreamHeader
c.IgnorePaths = o.IgnorePaths
c.AllowPaths = o.AllowPaths
a.APIAudiences = o.TokenAudiences

return nil
}

func validateSecureConnectionConfig(o *ProxyOptions) error {
if !identityheaders.HasIdentityHeadersEnabled(o.UpstreamHeader) && !o.UpstreamForceH2C {
return nil
}

errLoopback := validateLoopbackAddress(o.Upstream)
if errLoopback == nil {
return nil
}
if o.UpstreamForceH2C {
return fmt.Errorf("loopback address is required for h2c: %w", errLoopback)
}

klog.V(4).Info("Failed to validate loopback address: %v", errLoopback)

u, err := url.Parse(o.Upstream)
if err != nil {
return fmt.Errorf("failed to parse upstream URL: %w", err)
}

// If Identity Headers are configured and it is not a loopback address,
// verify that mTLS is configured.
if len(o.UpstreamClientCertFile) == 0 || len(o.UpstreamClientKeyFile) == 0 || u.Scheme != "https" {
return fmt.Errorf(
"loopback address (currently configured: %q) or client cert/key are required for identity headers",
o.Upstream,
)
}

return nil
}

func validateLoopbackAddress(address string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(loopbackLookupTimeout)*time.Second)
defer cancel()

u, err := url.Parse(address)
if err != nil {
return fmt.Errorf("failed to parse upstream URL: %w", err)
}

ip := net.ParseIP(u.Hostname())
if ip != nil {
if !ip.IsLoopback() {
return fmt.Errorf("not a loopback address: %s", ip.String())
}

return nil
}

ips, err := (&net.Resolver{}).LookupIPAddr(ctx, u.Hostname())
if err != nil {
return fmt.Errorf("failed to lookup ip: %w", err)
}

for _, ip := range ips {
if !ip.IP.IsLoopback() {
return fmt.Errorf("not a loopback address: %s", ip.IP.String())
}
}

return nil
}

type configfile struct {
AuthorizationConfig *authz.AuthzConfig `json:"authorization,omitempty"`
}
Expand Down
7 changes: 5 additions & 2 deletions pkg/authn/identityheaders/identityheaders.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ type AuthnHeaderConfig struct {
// WithAuthHeaders adds identity information to the headers.
// Must not be used, if connection is not encrypted with TLS.
func WithAuthHeaders(handler http.Handler, cfg *AuthnHeaderConfig) http.Handler {
upstreamHeadersEnabled := len(cfg.GroupsFieldName) > 0 || len(cfg.UserFieldName) > 0
if !upstreamHeadersEnabled {
if !HasIdentityHeadersEnabled(cfg) {
return handler
}

Expand All @@ -55,3 +54,7 @@ func WithAuthHeaders(handler http.Handler, cfg *AuthnHeaderConfig) http.Handler
handler.ServeHTTP(w, req)
})
}

func HasIdentityHeadersEnabled(cfg *AuthnHeaderConfig) bool {
return len(cfg.GroupsFieldName) > 0 || len(cfg.UserFieldName) > 0
}
35 changes: 35 additions & 0 deletions test/e2e/h2c-upstream/deployment-proxy-non-loopback.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: kube-rbac-proxy
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: kube-rbac-proxy
template:
metadata:
labels:
app: kube-rbac-proxy
spec:
serviceAccountName: kube-rbac-proxy
containers:
- name: kube-rbac-proxy
image: quay.io/brancz/kube-rbac-proxy:local
args:
- "--secure-port=8443"
- "--upstream=http://http-echo-service.default.svc.cluster.local:80/"
- "--authentication-skip-lookup"
- "--upstream-force-h2c=true"
- "--logtostderr=true"
- "--v=10"
ports:
- containerPort: 8443
name: https
- name: prometheus-example-app
image: quay.io/brancz/prometheus-example-app:v0.4.0
args:
- "--bind=127.0.0.1:8081"
- "--h2c=true"

36 changes: 36 additions & 0 deletions test/e2e/h2c-upstream/deployment-upstream-non-loopback.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: http-echo
labels:
app: http-echo
spec:
replicas: 1
selector:
matchLabels:
app: http-echo
template:
metadata:
labels:
app: http-echo
spec:
containers:
- name: http-echo
image: mendhak/http-https-echo
env:
- name: HTTP_PORT
value: 8080
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: http-echo-service
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: http-echo

24 changes: 23 additions & 1 deletion test/e2e/h2c_upstream.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,29 @@ func testH2CUpstream(client kubernetes.Interface) kubetest.TestSuite {
command := `curl --connect-timeout 5 -v -s -k --fail -H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" https://kube-rbac-proxy.default.svc.cluster.local:8443/metrics`

kubetest.Scenario{
Name: "With H2C Upstream",
Name: "With H2C non-local upstream",

Given: kubetest.Actions(
kubetest.CreatedManifests(
client,
"h2c-upstream/clusterRole.yaml",
"h2c-upstream/clusterRoleBinding.yaml",
"h2c-upstream/deployment-proxy-non-loopback.yaml",
"h2c-upstream/deployment-upstream-non-loopback.yaml",
"h2c-upstream/service.yaml",
"h2c-upstream/serviceAccount.yaml",
),
),
Then: kubetest.Actions(
kubetest.PodIsCrashLoopBackOff(
client,
"kube-rbac-proxy",
),
),
}.Run(t)

kubetest.Scenario{
Name: "With H2C local upstream",

Given: kubetest.Actions(
kubetest.CreatedManifests(
Expand Down
Loading

0 comments on commit 60234c7

Please sign in to comment.