├── test ├── tools │ ├── issuer │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── cmd │ │ │ ├── main.go │ │ │ └── options │ │ │ │ └── options.go │ │ └── pkg │ │ │ └── issuer │ │ │ └── issuer.go │ ├── audit-webhook │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── cmd │ │ │ ├── main.go │ │ │ └── options │ │ │ │ └── options.go │ │ └── pkg │ │ │ └── sink │ │ │ └── sink.go │ └── fake-apiserver │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── cmd │ │ ├── options │ │ │ └── options.go │ │ └── main.go │ │ └── pkg │ │ └── server │ │ └── server.go ├── util │ ├── strings.go │ └── tls.go ├── e2e │ ├── framework │ │ ├── helper │ │ │ ├── helper.go │ │ │ ├── requester.go │ │ │ ├── kubectl.go │ │ │ ├── secrets.go │ │ │ └── token.go │ │ ├── config │ │ │ └── config.go │ │ └── util.go │ └── suite │ │ ├── cases │ │ ├── doc.go │ │ ├── probe │ │ │ └── probe.go │ │ ├── token │ │ │ └── token.go │ │ └── headers │ │ │ └── headers.go │ │ ├── suite_test.go │ │ └── suite.go └── environment │ └── environment.go ├── .dockerignore ├── pkg ├── mocks │ ├── .gitignore │ └── mocks.go ├── util │ ├── port.go │ ├── signals.go │ ├── token.go │ └── flags │ │ ├── string_to_string_slice_test.go │ │ └── string_to_string_slice.go ├── proxy │ ├── audit │ │ ├── handler.go │ │ └── audit.go │ ├── hooks │ │ └── hooks.go │ ├── tokenreview │ │ ├── fake │ │ │ └── tokenreview.go │ │ ├── tokenreview.go │ │ └── tokenreview_test.go │ ├── logging │ │ ├── accesslog_test.go │ │ └── accesslog.go │ ├── subjectaccessreview │ │ └── fake │ │ │ └── subjectaccessreview.go │ └── context │ │ └── context.go └── probe │ ├── probe.go │ └── probe_test.go ├── demo ├── manifests │ ├── .gitignore │ ├── vendor │ │ └── kube-prod-runtime │ │ │ ├── VERSION │ │ │ ├── components │ │ │ ├── fluentd-es-config │ │ │ │ ├── system.conf │ │ │ │ ├── fluentd.conf │ │ │ │ ├── output.conf │ │ │ │ ├── monitoring.conf │ │ │ │ └── import-from-upstream.py │ │ │ ├── images.json │ │ │ ├── elasticsearch-config │ │ │ │ └── java.security │ │ │ ├── alertmanager-config.jsonnet │ │ │ ├── version.jsonnet │ │ │ ├── oauth2-proxy.jsonnet │ │ │ ├── externaldns.jsonnet │ │ │ └── kibana.jsonnet │ │ │ ├── Makefile │ │ │ ├── tests │ │ │ ├── aks.jsonnet │ │ │ └── gke.jsonnet │ │ │ └── lib │ │ │ └── utils.libsonnet │ ├── jsonnetfile.json │ ├── jsonnetfile.lock.json │ └── components │ │ ├── landingpage │ │ ├── google.svg │ │ ├── index.html │ │ └── amazon.svg │ │ ├── contour-clusterrole.json │ │ ├── cert-manager.jsonnet │ │ ├── base32.libsonnet │ │ └── gangway.jsonnet ├── infrastructure │ ├── amazon │ │ ├── .gitignore │ │ ├── suffix.tf │ │ ├── secrets.tf │ │ ├── dns.tf │ │ ├── providers.tf │ │ └── outputs.tf │ ├── digitalocean │ │ ├── .gitignore │ │ ├── suffix.tf │ │ ├── secrets.tf │ │ ├── dns.tf │ │ ├── providers.tf │ │ └── outputs.tf │ ├── .gitignore │ ├── google │ │ ├── suffix.tf │ │ ├── secrets.tf │ │ ├── variables.tf │ │ ├── cluster.tf │ │ ├── dns.tf │ │ ├── providers.tf │ │ └── output.tf │ └── modules │ │ ├── digitalocean-cluster │ │ ├── outputs.tf │ │ └── cluster.tf │ │ ├── amazon-cluster │ │ ├── outputs.tf │ │ └── cluster.tf │ │ ├── ca │ │ └── ca.tf │ │ ├── oauth2-secrets │ │ └── secrets.tf │ │ ├── gangway │ │ └── secrets.tf │ │ ├── google-dns │ │ └── dns.tf │ │ └── google-cluster │ │ └── cluster.tf ├── .gitignore └── config.dist.jsonnet ├── .gitignore ├── hack ├── boilerplate │ ├── boilerplate.go.txt │ ├── boilerplate.py.txt │ ├── boilerplate.sh.txt │ ├── boilerplate.Dockerfile.txt │ ├── boilerplate.Makefile.txt │ ├── test │ │ ├── pass.go │ │ ├── fail.go │ │ ├── pass.py │ │ └── fail.py │ └── boilerplate_test.py ├── tools │ └── tools.go ├── docker-start-wrapper.sh ├── version-ldflags.sh ├── verify-boilerplate.sh └── update-vendor.sh ├── OWNERS ├── deploy ├── charts │ └── kube-oidc-proxy │ │ ├── templates │ │ ├── serviceaccount.yaml │ │ ├── tests │ │ │ └── test-connection.yaml │ │ ├── clusterrolebinding.yaml │ │ ├── poddisruptionbudget.yaml │ │ ├── clusterrole.yaml │ │ ├── service.yaml │ │ ├── ingress.yaml │ │ ├── secret_config.yaml │ │ ├── NOTES.txt │ │ ├── secret_tls.yaml │ │ └── _helpers.tpl │ │ ├── Chart.yaml │ │ ├── .helmignore │ │ ├── README.md │ │ └── values.yaml └── yaml │ └── secrets.yaml ├── cmd ├── app │ └── options │ │ ├── plugin.go │ │ ├── audit.go │ │ ├── serving.go │ │ ├── client.go │ │ ├── misc.go │ │ ├── options.go │ │ ├── oidc.go │ │ └── app.go └── main.go ├── Dockerfile ├── docs └── tasks │ ├── auditing.md │ ├── no-impersonation.md │ ├── token-passthrough.md │ ├── extra-impersonation-headers.md │ └── development-testing.md ├── CONTRIBUTING.md ├── GenGitChangeLog.py ├── CHANGELOG.md ├── patchlog.txt ├── CODE_OF_CONDUCT.md └── .github └── workflows └── build.yaml /test/tools/issuer/.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | !/bin/kube-oidc-proxy 2 | -------------------------------------------------------------------------------- /pkg/mocks/.gitignore: -------------------------------------------------------------------------------- 1 | /authenticator.go 2 | -------------------------------------------------------------------------------- /test/tools/audit-webhook/.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | -------------------------------------------------------------------------------- /test/tools/fake-apiserver/.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | -------------------------------------------------------------------------------- /demo/manifests/.gitignore: -------------------------------------------------------------------------------- 1 | /config.json 2 | /*-config.json 3 | -------------------------------------------------------------------------------- /demo/manifests/vendor/kube-prod-runtime/VERSION: -------------------------------------------------------------------------------- 1 | dev-untagged 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /kube-oidc-proxy 2 | /bin 3 | /demo/config.jsonnet 4 | /artifacts/ 5 | -------------------------------------------------------------------------------- /demo/infrastructure/amazon/.gitignore: -------------------------------------------------------------------------------- 1 | /config-map-aws-auth_* 2 | /kubeconfig_* 3 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | /secrets 2 | /.kubeconfig-* 3 | /bin/ 4 | /.backup-certificates.yaml 5 | -------------------------------------------------------------------------------- /demo/infrastructure/digitalocean/.gitignore: -------------------------------------------------------------------------------- 1 | /config-map-aws-auth_* 2 | /kubeconfig_* 3 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.py.txt: -------------------------------------------------------------------------------- 1 | # Copyright Jetstack Ltd. See LICENSE for details. 2 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.sh.txt: -------------------------------------------------------------------------------- 1 | # Copyright Jetstack Ltd. See LICENSE for details. 2 | -------------------------------------------------------------------------------- /demo/infrastructure/.gitignore: -------------------------------------------------------------------------------- 1 | .terraform/ 2 | *.tfstate 3 | *.tfstate.backup 4 | *.tfvars 5 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.Dockerfile.txt: -------------------------------------------------------------------------------- 1 | # Copyright Jetstack Ltd. See LICENSE for details. 2 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.Makefile.txt: -------------------------------------------------------------------------------- 1 | # Copyright Jetstack Ltd. See LICENSE for details. 2 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | approvers: 2 | - simonswine 3 | - joshvanl 4 | reviewers: 5 | - simonswine 6 | - joshvanl 7 | -------------------------------------------------------------------------------- /demo/infrastructure/amazon/suffix.tf: -------------------------------------------------------------------------------- 1 | resource "random_id" "suffix" { 2 | byte_length = 4 3 | } 4 | -------------------------------------------------------------------------------- /demo/infrastructure/google/suffix.tf: -------------------------------------------------------------------------------- 1 | resource "random_id" "suffix" { 2 | byte_length = 4 3 | } 4 | -------------------------------------------------------------------------------- /demo/infrastructure/digitalocean/suffix.tf: -------------------------------------------------------------------------------- 1 | resource "random_id" "suffix" { 2 | byte_length = 4 3 | } 4 | -------------------------------------------------------------------------------- /demo/infrastructure/amazon/secrets.tf: -------------------------------------------------------------------------------- 1 | module "gangway" { 2 | source = "../modules/gangway" 3 | length = 24 4 | } 5 | -------------------------------------------------------------------------------- /demo/infrastructure/google/secrets.tf: -------------------------------------------------------------------------------- 1 | module "gangway" { 2 | source = "../modules/gangway" 3 | length = 24 4 | } 5 | -------------------------------------------------------------------------------- /demo/infrastructure/digitalocean/secrets.tf: -------------------------------------------------------------------------------- 1 | module "gangway" { 2 | source = "../modules/gangway" 3 | length = 24 4 | } 5 | -------------------------------------------------------------------------------- /demo/manifests/vendor/kube-prod-runtime/components/fluentd-es-config/system.conf: -------------------------------------------------------------------------------- 1 | 2 | root_dir /tmp/fluentd-buffers/ 3 | 4 | -------------------------------------------------------------------------------- /demo/manifests/vendor/kube-prod-runtime/components/fluentd-es-config/fluentd.conf: -------------------------------------------------------------------------------- 1 | # Include config files in the ./config.d directory 2 | @include config.d/*.conf 3 | -------------------------------------------------------------------------------- /demo/infrastructure/modules/digitalocean-cluster/outputs.tf: -------------------------------------------------------------------------------- 1 | output "kubeconfig" { 2 | value = "${digitalocean_kubernetes_cluster.cluster.kube_config.0.raw_config}" 3 | } 4 | -------------------------------------------------------------------------------- /demo/infrastructure/modules/amazon-cluster/outputs.tf: -------------------------------------------------------------------------------- 1 | output "cluster_node_arn" { 2 | value = "${module.eks.worker_iam_role_arn}" 3 | } 4 | 5 | output "kubeconfig" { 6 | value = "${module.eks.kubeconfig}" 7 | } 8 | -------------------------------------------------------------------------------- /demo/infrastructure/google/variables.tf: -------------------------------------------------------------------------------- 1 | variable "cluster_provider" { 2 | default = "google" 3 | } 4 | 5 | variable "dns_provider" { 6 | default = "google" 7 | } 8 | 9 | variable "oidc_provider" { 10 | default = "dex" 11 | } 12 | -------------------------------------------------------------------------------- /deploy/charts/kube-oidc-proxy/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | {{ include "kube-oidc-proxy.labels" . | indent 4 }} 6 | name: {{ include "kube-oidc-proxy.fullname" . }} 7 | 8 | -------------------------------------------------------------------------------- /cmd/app/options/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package options 3 | 4 | import ( 5 | // This package is required to be imported to register all client 6 | // plugins. 7 | _ "k8s.io/client-go/plugin/pkg/client/auth" 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/mocks/mocks.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package mocks 3 | 4 | // This package contains generated mocks 5 | 6 | //go:generate mockgen -package=mocks -destination authenticator.go k8s.io/apiserver/pkg/authentication/authenticator Token 7 | -------------------------------------------------------------------------------- /demo/infrastructure/google/cluster.tf: -------------------------------------------------------------------------------- 1 | module "cluster" { 2 | source = "../modules/google-cluster" 3 | suffix = "${random_id.suffix.hex}" 4 | zone = "${var.google_zone}" 5 | } 6 | 7 | output "cluster_kubeconfig" { 8 | value = "${module.cluster.kubeconfig}" 9 | } 10 | -------------------------------------------------------------------------------- /demo/infrastructure/google/dns.tf: -------------------------------------------------------------------------------- 1 | module "dns" { 2 | source = "../modules/google-dns" 3 | suffix = "${random_id.suffix.hex}" 4 | } 5 | 6 | module "ca" { 7 | source = "../modules/ca" 8 | 9 | ca_crt_file = "${var.ca_crt_file}" 10 | ca_key_file = "${var.ca_key_file}" 11 | } 12 | -------------------------------------------------------------------------------- /deploy/charts/kube-oidc-proxy/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "v1.0.0" 3 | description: A Helm chart for kube-oidc-proxy 4 | home: https://github.com/jetstack/kube-oidc-proxy 5 | name: kube-oidc-proxy 6 | version: 0.3.2 7 | maintainers: 8 | - name: mhrabovcin 9 | - name: joshvanl 10 | -------------------------------------------------------------------------------- /hack/tools/tools.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | //go:build tools 3 | // +build tools 4 | 5 | package tools 6 | 7 | // This file is used to vendor packages we use to build binaries. 8 | // This is the current canonical way with go modules. 9 | 10 | import ( 11 | _ "github.com/golang/mock/mockgen" 12 | ) 13 | -------------------------------------------------------------------------------- /test/tools/audit-webhook/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright Jetstack Ltd. See LICENSE for details. 2 | FROM alpine:3.10 3 | 4 | LABEL description="A audit webhook sink to read audit events and write to file." 5 | 6 | RUN apk --no-cache add ca-certificates 7 | 8 | COPY ./bin/audit-webhook /usr/bin/audit-webhook 9 | 10 | CMD ["/usr/bin/audit-webhook"] 11 | -------------------------------------------------------------------------------- /test/tools/issuer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright Jetstack Ltd. See LICENSE for details. 2 | FROM alpine:3.10 3 | 4 | LABEL description="A basic OIDC issuer that prsents a well-known and certs endpoint." 5 | 6 | RUN apk --no-cache add ca-certificates 7 | 8 | COPY ./bin/oidc-issuer-linux /usr/bin/oidc-issuer 9 | 10 | CMD ["/usr/bin/oidc-issuer"] 11 | -------------------------------------------------------------------------------- /test/tools/fake-apiserver/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright Jetstack Ltd. See LICENSE for details. 2 | FROM alpine:3.10 3 | 4 | LABEL description="A fake API server that will respond to requests with the same body and headers." 5 | 6 | RUN apk --no-cache add ca-certificates 7 | 8 | COPY ./bin/fake-apiserver-linux /usr/bin/fake-apiserver 9 | 10 | CMD ["/usr/bin/fake-apiserver"] 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright Jetstack Ltd. See LICENSE for details. 2 | FROM ubuntu:22.04 3 | LABEL description="OIDC reverse proxy authenticator based on Kubernetes" 4 | 5 | ARG TARGETARCH 6 | 7 | RUN apt-get update;apt-get -y install ca-certificates;apt-get -y upgrade;apt-get clean;rm -rf /var/lib/apt/lists/* 8 | 9 | COPY bin/${TARGETARCH}/kube-oidc-proxy /usr/bin/ 10 | 11 | CMD ["/usr/bin/kube-oidc-proxy"] 12 | -------------------------------------------------------------------------------- /demo/manifests/jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "kube-prod-runtime", 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/bitnami/kube-prod-runtime.git", 8 | "subdir": "manifests" 9 | } 10 | }, 11 | "version": "v1.1.2" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /hack/docker-start-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | export KUBE_ROOT=$(dirname "${BASH_SOURCE}")/.. 8 | 9 | # start docker 10 | /etc/init.d/docker start 11 | 12 | # wait for it to be started 13 | n=0 14 | until [ $n -ge 15 ] 15 | do 16 | docker info > /dev/null && break 17 | n=$[$n+1] 18 | sleep 1 19 | done 20 | 21 | exec $@ 22 | -------------------------------------------------------------------------------- /demo/infrastructure/modules/ca/ca.tf: -------------------------------------------------------------------------------- 1 | variable "ca_crt_file" {} 2 | variable "ca_key_file" {} 3 | 4 | data "local_file" "crt_file" { 5 | filename = "${var.ca_crt_file}" 6 | } 7 | 8 | data "local_file" "key_file" { 9 | filename = "${var.ca_key_file}" 10 | } 11 | 12 | 13 | output "crt" { 14 | value = "${data.local_file.crt_file.content}" 15 | } 16 | 17 | output "key" { 18 | value = "${data.local_file.key_file.content}" 19 | } 20 | -------------------------------------------------------------------------------- /demo/manifests/jsonnetfile.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "kube-prod-runtime", 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/bitnami/kube-prod-runtime.git", 8 | "subdir": "manifests" 9 | } 10 | }, 11 | "version": "edcd38494e95f7af2571b24d52301859719c56e5" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /pkg/util/port.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package util 3 | 4 | import ( 5 | "net" 6 | "strconv" 7 | ) 8 | 9 | func FreePort() (string, error) { 10 | l, err := net.ListenTCP("tcp", &net.TCPAddr{ 11 | IP: net.ParseIP("127.0.0.1"), 12 | Port: 0, 13 | }) 14 | if err != nil { 15 | return "", err 16 | } 17 | defer l.Close() 18 | 19 | port := l.Addr().(*net.TCPAddr).Port 20 | return strconv.Itoa(port), nil 21 | } 22 | -------------------------------------------------------------------------------- /demo/infrastructure/amazon/dns.tf: -------------------------------------------------------------------------------- 1 | data "external" "cert_manager" { 2 | program = ["jq", ".cert_manager", "../../manifests/google-config.json"] 3 | query = { } 4 | } 5 | 6 | data "external" "externaldns" { 7 | program = ["jq", ".externaldns", "../../manifests/google-config.json"] 8 | query = { } 9 | } 10 | 11 | module "ca" { 12 | source = "../modules/ca" 13 | 14 | ca_crt_file = "${var.ca_crt_file}" 15 | ca_key_file = "${var.ca_key_file}" 16 | } 17 | -------------------------------------------------------------------------------- /demo/infrastructure/digitalocean/dns.tf: -------------------------------------------------------------------------------- 1 | data "external" "cert_manager" { 2 | program = ["jq", ".cert_manager", "../../manifests/google-config.json"] 3 | query = {} 4 | } 5 | 6 | data "external" "externaldns" { 7 | program = ["jq", ".externaldns", "../../manifests/google-config.json"] 8 | query = {} 9 | } 10 | 11 | module "ca" { 12 | source = "../modules/ca" 13 | 14 | ca_crt_file = "${var.ca_crt_file}" 15 | ca_key_file = "${var.ca_key_file}" 16 | } 17 | -------------------------------------------------------------------------------- /docs/tasks/auditing.md: -------------------------------------------------------------------------------- 1 | # Auditing 2 | 3 | kube-oidc-proxy allows for the ability to audit requests to the proxy. The proxy 4 | exposes all the same options for auditing that the Kubernetes API server 5 | provides, however does _not_ support dynamic configuration 6 | (`--audit-dynamic-configuration`). 7 | 8 | You can read more on how to configure and manage auditing in the [Kubernetes 9 | documentation](https://kubernetes.io/docs/tasks/debug-application-cluster/audit). 10 | -------------------------------------------------------------------------------- /demo/manifests/vendor/kube-prod-runtime/Makefile: -------------------------------------------------------------------------------- 1 | KUBECFG = kubecfg 2 | 3 | COMPONENTS := $(wildcard components/*.jsonnet) 4 | PLATFORMS := $(wildcard tests/*.jsonnet) 5 | 6 | JFILES := \ 7 | $(wildcard */*.libsonnet) \ 8 | $(COMPONENTS) $(PLATFORMS) 9 | 10 | validate: $(PLATFORMS:%.jsonnet=%.jsonnet-validate) 11 | 12 | %.jsonnet-validate: %.jsonnet $(JFILES) 13 | $(KUBECFG) validate --ignore-unknown $< 14 | 15 | .PHONY: all validate 16 | .PHONY: %.jsonnet-validate 17 | -------------------------------------------------------------------------------- /demo/infrastructure/modules/oauth2-secrets/secrets.tf: -------------------------------------------------------------------------------- 1 | variable "length" {} 2 | 3 | resource "random_string" "client_id" { 4 | length = "${var.length}" 5 | special = false 6 | } 7 | 8 | resource "random_string" "client_secret" { 9 | length = "${var.length}" 10 | special = false 11 | } 12 | 13 | output "config" { 14 | value = { 15 | client_id = "${random_string.client_id.result}" 16 | client_secret = "${random_string.client_secret.result}" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /deploy/charts/kube-oidc-proxy/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /demo/infrastructure/google/providers.tf: -------------------------------------------------------------------------------- 1 | variable "google_region" { 2 | default = "europe-west1" 3 | } 4 | 5 | variable "google_zone" { 6 | default = "europe-west1-d" 7 | } 8 | 9 | variable "google_project" {} 10 | 11 | variable "ca_crt_file" {} 12 | variable "ca_key_file" {} 13 | 14 | provider "google" { 15 | region = "${var.google_region}" 16 | credentials = "${file("~/.config/gcloud/terraform-admin.json")}" 17 | project = "${var.google_project}" 18 | version = "~> 2.0" 19 | } 20 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | 8 | "github.com/jetstack/kube-oidc-proxy/cmd/app" 9 | "github.com/jetstack/kube-oidc-proxy/pkg/util" 10 | "k8s.io/klog/v2" 11 | ) 12 | 13 | func main() { 14 | klog.InitFlags(nil) 15 | stopCh := util.SignalHandler() 16 | cmd := app.NewRunCommand(stopCh) 17 | 18 | if err := cmd.Execute(); err != nil { 19 | fmt.Fprintf(os.Stderr, "error: %v\n", err) 20 | os.Exit(1) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /demo/infrastructure/digitalocean/providers.tf: -------------------------------------------------------------------------------- 1 | provider "digitalocean" {} 2 | 3 | variable "digitalocean_region" { 4 | default = "fra1" 5 | } 6 | 7 | variable "cluster_version" { 8 | default = "1.16.2-do.1" 9 | } 10 | 11 | module "cluster" { 12 | source = "../modules/digitalocean-cluster" 13 | suffix = "${random_id.suffix.hex}" 14 | 15 | cluster_version = "${var.cluster_version}" 16 | region = "${var.digitalocean_region}" 17 | } 18 | 19 | variable "ca_crt_file" {} 20 | variable "ca_key_file" {} 21 | -------------------------------------------------------------------------------- /demo/infrastructure/modules/gangway/secrets.tf: -------------------------------------------------------------------------------- 1 | variable "length" {} 2 | 3 | resource "random_id" "session_security_key" { 4 | byte_length = "32" 5 | } 6 | 7 | module "oauth2" { 8 | source = "../oauth2-secrets" 9 | length = "${var.length}" 10 | } 11 | 12 | resource "random_string" "client_secret" { 13 | length = "${var.length}" 14 | special = false 15 | } 16 | 17 | output "config" { 18 | value = "${merge(module.oauth2.config,map("session_security_key",random_id.session_security_key.b64_std))}" 19 | } 20 | -------------------------------------------------------------------------------- /demo/infrastructure/modules/digitalocean-cluster/cluster.tf: -------------------------------------------------------------------------------- 1 | variable "region" {} 2 | variable "suffix" {} 3 | variable "cluster_version" {} 4 | 5 | locals { 6 | cluster_name = "cluster-${var.suffix}" 7 | } 8 | 9 | resource "digitalocean_kubernetes_cluster" "cluster" { 10 | name = "${local.cluster_name}" 11 | version = "${var.cluster_version}" 12 | region = "${var.region}" 13 | 14 | node_pool { 15 | name = "default-pool" 16 | size = "s-2vcpu-2gb" 17 | node_count = 3 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/util/strings.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package util 3 | 4 | import "sort" 5 | 6 | func StringSlicesEqual(s1, s2 []string) bool { 7 | if len(s1) != len(s2) { 8 | return false 9 | } 10 | 11 | s12, s22 := make([]string, len(s1)), make([]string, len(s2)) 12 | 13 | copy(s12, s1) 14 | copy(s22, s2) 15 | 16 | sort.Strings(s12) 17 | sort.Strings(s22) 18 | 19 | for i, s := range s12 { 20 | if s != s22[i] { 21 | return false 22 | } 23 | } 24 | 25 | return true 26 | } 27 | -------------------------------------------------------------------------------- /deploy/charts/kube-oidc-proxy/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "kube-oidc-proxy.fullname" . }}-test-connection" 5 | labels: 6 | {{ include "kube-oidc-proxy.labels" . | indent 4 }} 7 | annotations: 8 | "helm.sh/hook": test-success 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "kube-oidc-proxy.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /test/e2e/framework/helper/helper.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package helper 3 | 4 | import ( 5 | "k8s.io/client-go/kubernetes" 6 | 7 | "github.com/jetstack/kube-oidc-proxy/test/e2e/framework/config" 8 | ) 9 | 10 | // Helper provides methods for common operations needed during tests. 11 | type Helper struct { 12 | cfg *config.Config 13 | 14 | KubeClient kubernetes.Interface 15 | } 16 | 17 | func NewHelper(cfg *config.Config) *Helper { 18 | return &Helper{ 19 | cfg: cfg, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /demo/infrastructure/amazon/providers.tf: -------------------------------------------------------------------------------- 1 | variable "aws_region" { 2 | default = "eu-west-1" 3 | } 4 | 5 | variable "cloud" { 6 | default = "amazon" 7 | } 8 | 9 | variable "cluster_version" { 10 | default = "1.14" 11 | } 12 | 13 | provider "aws" { 14 | region = "${var.aws_region}" 15 | } 16 | 17 | module "cluster" { 18 | source = "../modules/amazon-cluster" 19 | suffix = "${random_id.suffix.hex}" 20 | 21 | cluster_version = "${var.cluster_version}" 22 | } 23 | 24 | variable "ca_crt_file" {} 25 | variable "ca_key_file" {} 26 | -------------------------------------------------------------------------------- /deploy/charts/kube-oidc-proxy/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | kind: ClusterRoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | labels: 5 | {{ include "kube-oidc-proxy.labels" . | indent 4 }} 6 | name: {{ include "kube-oidc-proxy.fullname" . }} 7 | roleRef: 8 | apiGroup: rbac.authorization.k8s.io 9 | kind: ClusterRole 10 | name: {{ include "kube-oidc-proxy.fullname" . }} 11 | subjects: 12 | - kind: ServiceAccount 13 | name: {{ include "kube-oidc-proxy.fullname" . }} 14 | namespace: {{ .Release.Namespace }} 15 | -------------------------------------------------------------------------------- /deploy/yaml/secrets.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | tls.crt: {{ SERVING_TLS_CERT }} 4 | tls.key: {{ SERVING_TLS_KEY }} 5 | kind: Secret 6 | metadata: 7 | name: kube-oidc-proxy-tls 8 | namespace: kube-oidc-proxy 9 | type: kubernetes.io/tls 10 | --- 11 | apiVersion: v1 12 | data: 13 | oidc.ca-pem: {{ OIDC_CA }} 14 | oidc.issuer-url: {{ OIDC_ISSUER_URL }} 15 | oidc.username-claim: {{ OIDC_USERNAME_CLAIM }} 16 | oidc.client-id: {{ OIDC_CLIENT_ID }} 17 | kind: Secret 18 | metadata: 19 | name: kube-oidc-proxy-config 20 | namespace: kube-oidc-proxy 21 | type: Opaque 22 | -------------------------------------------------------------------------------- /docs/tasks/no-impersonation.md: -------------------------------------------------------------------------------- 1 | # No Impersonation 2 | 3 | kube-oidc-proxy can be configured to disable impersonation. When a request has 4 | been successfully authenticated, the request is forwarded as-is, without changes 5 | to the HTTP header and no authentication injected by the proxy. The OIDC 6 | bearer token is also kept in the request. This can be useful for securing 7 | endpoints that do not provide OIDC or any authentication methods and do not 8 | implement any authorization. 9 | 10 | To disable impersonation, provide the following flag: 11 | 12 | ``` 13 | --disable-impersonation 14 | ``` 15 | -------------------------------------------------------------------------------- /deploy/charts/kube-oidc-proxy/templates/poddisruptionbudget.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.podDisruptionBudget.enabled -}} 2 | apiVersion: policy/v1 3 | kind: PodDisruptionBudget 4 | metadata: 5 | name: {{ include "kube-oidc-proxy.fullname" . }} 6 | namespace: {{ $.Release.Namespace }} 7 | labels: 8 | {{ include "kube-oidc-proxy.labels" . | indent 4 }} 9 | spec: 10 | minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} 11 | selector: 12 | matchLabels: 13 | app.kubernetes.io/name: {{ include "kube-oidc-proxy.name" . }} 14 | app.kubernetes.io/instance: {{ .Release.Name }} 15 | {{- end }} 16 | -------------------------------------------------------------------------------- /hack/boilerplate/test/pass.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | -------------------------------------------------------------------------------- /cmd/app/options/audit.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package options 3 | 4 | import ( 5 | "github.com/spf13/pflag" 6 | apiserveroptions "k8s.io/apiserver/pkg/server/options" 7 | cliflag "k8s.io/component-base/cli/flag" 8 | ) 9 | 10 | type AuditOptions struct { 11 | *apiserveroptions.AuditOptions 12 | } 13 | 14 | func NewAuditOptions(nfs *cliflag.NamedFlagSets) *AuditOptions { 15 | a := &AuditOptions{ 16 | AuditOptions: apiserveroptions.NewAuditOptions(), 17 | } 18 | 19 | return a.AddFlags(nfs.FlagSet("Audit")) 20 | } 21 | 22 | func (a *AuditOptions) AddFlags(fs *pflag.FlagSet) *AuditOptions { 23 | a.AuditOptions.AddFlags(fs) 24 | return a 25 | } 26 | -------------------------------------------------------------------------------- /hack/boilerplate/test/fail.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The Kubernetes Authors. 3 | 4 | fail 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package main 20 | -------------------------------------------------------------------------------- /demo/manifests/vendor/kube-prod-runtime/components/images.json: -------------------------------------------------------------------------------- 1 | { 2 | "alertmanager": "bitnami/alertmanager:0.15.3-r43", 3 | "cert-manager": "bitnami/cert-manager:0.5.2-r37", 4 | "configmap-reload": "jimmidyson/configmap-reload:v0.2.2", 5 | "elasticsearch": "bitnami/elasticsearch:5.6.15-r0", 6 | "external-dns": "bitnami/external-dns:0.5.11-r1", 7 | "fluentd": "bitnami/fluentd:1.3.3-r23", 8 | "grafana": "bitnami/grafana:5.4.3-r18", 9 | "kibana": "bitnami/kibana:5.6.15-r0", 10 | "nginx-ingress-controller": "bitnami/nginx-ingress-controller:0.21.0-r12", 11 | "oauth2_proxy": "bitnami/oauth2-proxy:3.0.0-r0", 12 | "prometheus": "bitnami/prometheus:2.6.1-r12" 13 | } 14 | -------------------------------------------------------------------------------- /pkg/util/signals.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package util 3 | 4 | import ( 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "k8s.io/klog/v2" 10 | ) 11 | 12 | func SignalHandler() chan struct{} { 13 | stopCh := make(chan struct{}) 14 | ch := make(chan os.Signal, 2) 15 | signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) 16 | 17 | go func() { 18 | sig := <-ch 19 | 20 | close(stopCh) 21 | 22 | for i := 0; i < 3; i++ { 23 | klog.V(0).Infof("received signal %s, shutting down gracefully...", sig) 24 | sig = <-ch 25 | } 26 | 27 | klog.V(0).Infof("received signal %s, force closing", sig) 28 | 29 | os.Exit(1) 30 | }() 31 | 32 | return stopCh 33 | } 34 | -------------------------------------------------------------------------------- /hack/boilerplate/test/pass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2015 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | True 18 | -------------------------------------------------------------------------------- /test/e2e/suite/cases/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package cases 3 | 4 | import ( 5 | _ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases/audit" 6 | _ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases/headers" 7 | _ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases/impersonation" 8 | _ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases/passthrough" 9 | _ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases/probe" 10 | _ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases/rbac" 11 | _ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases/token" 12 | _ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases/upgrade" 13 | ) 14 | -------------------------------------------------------------------------------- /hack/boilerplate/test/fail.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2015 The Kubernetes Authors. 4 | # 5 | # failed 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | -------------------------------------------------------------------------------- /demo/infrastructure/amazon/outputs.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | config = { 3 | cert_manager = "${data.external.cert_manager.result}" 4 | externaldns = "${data.external.externaldns.result}" 5 | gangway = "${module.gangway.config}" 6 | 7 | ca = { 8 | key = "${module.ca.key}" 9 | crt = "${module.ca.crt}" 10 | } 11 | } 12 | } 13 | 14 | output "config" { 15 | value = "${jsonencode(local.config)}" 16 | } 17 | 18 | output "kubeconfig_command" { 19 | value = "cp infrastructure/${var.cloud}/kubeconfig_cluster-${random_id.suffix.hex} $KUBECONFIG" 20 | } 21 | 22 | output "kubeconfig" { 23 | description = "kubectl config as generated by the module." 24 | value = "${module.cluster.kubeconfig}" 25 | } 26 | -------------------------------------------------------------------------------- /demo/infrastructure/digitalocean/outputs.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | config = { 3 | cert_manager = "${data.external.cert_manager.result}" 4 | externaldns = "${data.external.externaldns.result}" 5 | gangway = "${module.gangway.config}" 6 | 7 | ca = { 8 | key = "${module.ca.key}" 9 | crt = "${module.ca.crt}" 10 | } 11 | } 12 | } 13 | 14 | output "config" { 15 | value = "${jsonencode(local.config)}" 16 | } 17 | 18 | output "kubeconfig_command" { 19 | value = "cd infrastructure/digitalocean/ && terraform output kubeconfig > $$KUBECONFIG" 20 | } 21 | 22 | output "kubeconfig" { 23 | description = "kubectl config as generated by the module." 24 | value = "${module.cluster.kubeconfig}" 25 | } 26 | -------------------------------------------------------------------------------- /test/e2e/framework/config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package config 3 | 4 | import ( 5 | "errors" 6 | 7 | utilerrors "k8s.io/apimachinery/pkg/util/errors" 8 | 9 | "github.com/jetstack/kube-oidc-proxy/test/environment" 10 | ) 11 | 12 | type Config struct { 13 | KubeConfigPath string 14 | Kubectl string 15 | 16 | RepoRoot string 17 | 18 | Environment *environment.Environment 19 | } 20 | 21 | func (c *Config) Validate() error { 22 | var errs []error 23 | 24 | if c.KubeConfigPath == "" { 25 | errs = append(errs, errors.New("kubeconfig path not defined")) 26 | } 27 | 28 | if c.RepoRoot == "" { 29 | errs = append(errs, errors.New("repo root not defined")) 30 | } 31 | 32 | return utilerrors.NewAggregate(errs) 33 | } 34 | -------------------------------------------------------------------------------- /demo/manifests/vendor/kube-prod-runtime/components/elasticsearch-config/java.security: -------------------------------------------------------------------------------- 1 | # 2 | # This is an alternate "security properties file". 3 | # 4 | # An alternate java.security properties file may be specified 5 | # from the command line via the system property 6 | # 7 | # -Djava.security.properties= 8 | # 9 | 10 | # The JVM defaults to caching positive hostname resolutions indefinitely. 11 | # Elasticsearch nodes rely on DNS (Kubernetes headless service), where DNS 12 | # resolutions vary with time (e.g., for node-to-node discovery). This 13 | # behaviour can be modified by adding networkaddress.cache.ttl= 14 | # to an alternate Java security properties file. 15 | # https://www.elastic.co/guide/en/elasticsearch/reference/6.3/networkaddress-cache-ttl.html 16 | networkaddress.cache.ttl=60 17 | -------------------------------------------------------------------------------- /demo/manifests/vendor/kube-prod-runtime/components/fluentd-es-config/output.conf: -------------------------------------------------------------------------------- 1 | # Enriches records with Kubernetes metadata 2 | 3 | @type kubernetes_metadata 4 | 5 | 6 | 7 | @id elasticsearch 8 | @type elasticsearch 9 | @log_level info 10 | type_name fluentd 11 | include_tag_key true 12 | host "#{ENV['ES_HOST']}" 13 | port 9200 14 | logstash_format true 15 | 16 | @type file 17 | path /var/log/fluentd-buffers/kubernetes.system.buffer 18 | flush_mode interval 19 | retry_type exponential_backoff 20 | flush_thread_count 2 21 | flush_interval 5s 22 | retry_forever 23 | retry_max_interval 30 24 | chunk_limit_size 2M 25 | queue_limit_length 8 26 | overflow_action block 27 | 28 | 29 | -------------------------------------------------------------------------------- /demo/manifests/vendor/kube-prod-runtime/components/fluentd-es-config/monitoring.conf: -------------------------------------------------------------------------------- 1 | # Prometheus Exporter Plugin 2 | # input plugin that exports metrics 3 | 4 | @type prometheus 5 | 6 | 7 | 8 | @type monitor_agent 9 | 10 | 11 | # input plugin that collects metrics from MonitorAgent 12 | 13 | @type prometheus_monitor 14 | 15 | host ${hostname} 16 | 17 | 18 | 19 | # input plugin that collects metrics for output plugin 20 | 21 | @type prometheus_output_monitor 22 | 23 | host ${hostname} 24 | 25 | 26 | 27 | # input plugin that collects metrics for in_tail plugin 28 | 29 | @type prometheus_tail_monitor 30 | 31 | host ${hostname} 32 | 33 | 34 | -------------------------------------------------------------------------------- /demo/infrastructure/google/output.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | config = { 3 | cert_manager = "${module.dns.config}" 4 | externaldns = "${module.dns.config}" 5 | gangway = "${module.gangway.config}" 6 | 7 | ca = { 8 | key = "${module.ca.key}" 9 | crt = "${module.ca.crt}" 10 | } 11 | } 12 | } 13 | 14 | output "config" { 15 | value = "${jsonencode(local.config)}" 16 | } 17 | 18 | # This fetches KUBECONFIG and grants full cluster admin access to the current user 19 | output "kubeconfig_command" { 20 | value = "gcloud container clusters get-credentials ${module.cluster.name} --zone ${var.google_zone} --project ${module.cluster.project} && kubectl create clusterrolebinding cluster-admin-binding --clusterrole=cluster-admin --user=$(gcloud config get-value account) --dry-run -o yaml | kubectl apply -f -" 21 | } 22 | -------------------------------------------------------------------------------- /deploy/charts/kube-oidc-proxy/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | kind: ClusterRole 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | labels: 5 | {{ include "kube-oidc-proxy.labels" . | indent 4 }} 6 | name: {{ include "kube-oidc-proxy.fullname" . }} 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - "users" 12 | - "groups" 13 | - "serviceaccounts" 14 | verbs: 15 | - "impersonate" 16 | - apiGroups: 17 | - "authentication.k8s.io" 18 | resources: 19 | - "userextras/scopes" 20 | - "userextras/remote-client-ip" 21 | - "tokenreviews" 22 | # to support end user impersonation 23 | - "userextras/authentication.kubernetes.io/credential-id" 24 | - "userextras/originaluser.jetstack.io-user" 25 | - "userextras/originaluser.jetstack.io-groups" 26 | - "userextras/originaluser.jetstack.io-extra" 27 | verbs: 28 | - "create" 29 | - "impersonate" 30 | -------------------------------------------------------------------------------- /deploy/charts/kube-oidc-proxy/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "kube-oidc-proxy.fullname" . }} 5 | labels: 6 | {{ include "kube-oidc-proxy.labels" . | indent 4 }} 7 | annotations: 8 | {{- range $key, $val := .Values.service.annotations }} 9 | {{ $key }}: {{ $val | quote }} 10 | {{- end }} 11 | spec: 12 | type: {{ .Values.service.type }} 13 | {{- if .Values.service.loadBalancerIP }} 14 | loadBalancerIP: "{{ .Values.service.loadBalancerIP }}" 15 | {{- end }} 16 | {{- if .Values.service.loadBalancerSourceRanges }} 17 | loadBalancerSourceRanges: 18 | {{ toYaml .Values.service.loadBalancerSourceRanges | indent 4 }} 19 | {{- end }} 20 | ports: 21 | - port: {{ .Values.service.port }} 22 | targetPort: 8443 23 | protocol: TCP 24 | name: https 25 | selector: 26 | app.kubernetes.io/name: {{ include "kube-oidc-proxy.name" . }} 27 | app.kubernetes.io/instance: {{ .Release.Name }} 28 | -------------------------------------------------------------------------------- /demo/infrastructure/modules/google-dns/dns.tf: -------------------------------------------------------------------------------- 1 | variable "suffix" {} 2 | 3 | resource "google_service_account" "external_dns" { 4 | account_id = "external-dns-${var.suffix}" 5 | display_name = "External DNS/Cert Manager service account for GKE cluster cluster-${var.suffix}" 6 | } 7 | 8 | # https://github.com/kubernetes-incubator/external-dns/blob/v0.4.0/docs/tutorials/gke.md#set-up-your-environment 9 | resource "google_project_iam_member" "dns_admin" { 10 | role = "roles/dns.admin" 11 | member = "serviceAccount:${google_service_account.external_dns.email}" 12 | } 13 | 14 | resource "google_service_account_key" "external_dns" { 15 | service_account_id = "${google_service_account.external_dns.account_id}" 16 | } 17 | 18 | output "config" { 19 | value = { 20 | service_account_credentials = "${base64decode(google_service_account_key.external_dns.private_key)}" 21 | 22 | project = "${google_service_account.external_dns.project}" 23 | provider = "google" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pkg/proxy/audit/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package audit 3 | 4 | import ( 5 | "net/http" 6 | ) 7 | 8 | // This struct is used to implement an http.Handler interface. This will not 9 | // actually serve but instead implements auditing during unauthenticated 10 | // requests. It is expected that consumers of this type will call `ServeHTTP` 11 | // when an unauthenticated request is received. 12 | type unauthenticatedHandler struct { 13 | serveFunc func(http.ResponseWriter, *http.Request) 14 | } 15 | 16 | func NewUnauthenticatedHandler(a *Audit, serveFunc func(http.ResponseWriter, *http.Request)) http.Handler { 17 | u := &unauthenticatedHandler{ 18 | serveFunc: serveFunc, 19 | } 20 | 21 | // if auditor is nil then return without wrapping 22 | if a == nil { 23 | return u 24 | } 25 | 26 | return a.WithUnauthorized(u) 27 | } 28 | 29 | func (u *unauthenticatedHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { 30 | u.serveFunc(rw, r) 31 | } 32 | -------------------------------------------------------------------------------- /cmd/app/options/serving.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package options 3 | 4 | import ( 5 | "net" 6 | 7 | "github.com/spf13/pflag" 8 | apiserveroptions "k8s.io/apiserver/pkg/server/options" 9 | cliflag "k8s.io/component-base/cli/flag" 10 | ) 11 | 12 | type SecureServingOptions struct { 13 | *apiserveroptions.SecureServingOptions 14 | } 15 | 16 | func NewSecureServingOptions(nfs *cliflag.NamedFlagSets) *SecureServingOptions { 17 | s := &SecureServingOptions{ 18 | SecureServingOptions: &apiserveroptions.SecureServingOptions{ 19 | BindAddress: net.ParseIP("0.0.0.0"), 20 | BindPort: 6443, 21 | Required: true, 22 | ServerCert: apiserveroptions.GeneratableKeyCert{ 23 | PairName: AppName, 24 | CertDirectory: "/var/run/kubernetes", 25 | }, 26 | }, 27 | } 28 | 29 | return s.AddFlags(nfs.FlagSet("Secure Serving")) 30 | } 31 | 32 | func (s *SecureServingOptions) AddFlags(fs *pflag.FlagSet) *SecureServingOptions { 33 | s.SecureServingOptions.AddFlags(fs) 34 | return s 35 | } 36 | -------------------------------------------------------------------------------- /hack/version-ldflags.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2017 The Kubernetes Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # This command is used by bazel as the workspace_status_command 17 | # to implement build stamping with git information. 18 | 19 | set -o errexit 20 | set -o nounset 21 | set -o pipefail 22 | 23 | export KUBE_ROOT=$(dirname "${BASH_SOURCE}")/.. 24 | 25 | source "${KUBE_ROOT}/hack/lib/version.sh" 26 | KUBE_GO_PACKAGE=github.com/jetstack/kube-oidc-proxy 27 | kube::version::ldflags 28 | -------------------------------------------------------------------------------- /test/e2e/framework/helper/requester.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package helper 3 | 4 | import ( 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | ) 9 | 10 | type Requester struct { 11 | transport http.RoundTripper 12 | token string 13 | client *http.Client 14 | } 15 | 16 | func (h *Helper) NewRequester(transport http.RoundTripper, token string) *Requester { 17 | r := &Requester{ 18 | token: token, 19 | transport: transport, 20 | } 21 | 22 | r.client = http.DefaultClient 23 | r.client.Transport = r 24 | 25 | return r 26 | } 27 | 28 | func (r *Requester) RoundTrip(req *http.Request) (*http.Response, error) { 29 | req.Header.Add("Authorization", fmt.Sprintf("bearer %s", r.token)) 30 | return r.transport.RoundTrip(req) 31 | } 32 | 33 | func (r *Requester) Get(target string) ([]byte, *http.Response, error) { 34 | resp, err := r.client.Get(target) 35 | if err != nil { 36 | return nil, nil, err 37 | } 38 | 39 | body, err := ioutil.ReadAll(resp.Body) 40 | if err != nil { 41 | return nil, nil, err 42 | } 43 | 44 | return body, resp, nil 45 | } 46 | -------------------------------------------------------------------------------- /docs/tasks/token-passthrough.md: -------------------------------------------------------------------------------- 1 | # Token Passthrough 2 | 3 | kube-oidc-proxy can be configured to enable 'token passthrough' for tokens that 4 | fail OIDC authentication. If enabled, kube-oidc-proxy will perform a [token 5 | review](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication) 6 | API call to the configured target backend using the Kubernetes API. If 7 | successful, the request will be passed through as-is, with the token intact in 8 | the request and no other authentication used by kube-oidc-proxy. 9 | 10 | To enable token passthrough, include the following flag: 11 | 12 | ``` 13 | --token-passthrough 14 | ``` 15 | 16 | In the case of the Kubernetes API server, the authenticator, if audience aware, 17 | will validate the audiences of tokens using the audience of the API server. A 18 | new set of audiences can also be given which will be used to validate the token 19 | against. At least one of these audiences need to be present in the audiences of 20 | the token to be successful: 21 | 22 | ``` 23 | ---token-passthrough-audiences=aud1.foo.bar,aud2.foo.bar 24 | ``` 25 | -------------------------------------------------------------------------------- /demo/manifests/components/landingpage/google.svg: -------------------------------------------------------------------------------- 1 | 2 | Container Engine 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /pkg/proxy/hooks/hooks.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package hooks 3 | 4 | import ( 5 | "fmt" 6 | "sync" 7 | 8 | k8sErrors "k8s.io/apimachinery/pkg/util/errors" 9 | ) 10 | 11 | type Hooks struct { 12 | preShutdownHooks map[string]ShutdownHook 13 | preShutdownHookLock sync.Mutex 14 | } 15 | 16 | type ShutdownHook func() error 17 | 18 | func New() *Hooks { 19 | return &Hooks{ 20 | preShutdownHooks: make(map[string]ShutdownHook), 21 | } 22 | } 23 | 24 | func (h *Hooks) AddPreShutdownHook(name string, hook ShutdownHook) { 25 | h.preShutdownHookLock.Lock() 26 | defer h.preShutdownHookLock.Unlock() 27 | 28 | h.preShutdownHooks[name] = hook 29 | } 30 | 31 | // RunPreShutdownHooks runs the PreShutdownHooks for the server 32 | func (h *Hooks) RunPreShutdownHooks() error { 33 | var errs []error 34 | 35 | h.preShutdownHookLock.Lock() 36 | defer h.preShutdownHookLock.Unlock() 37 | 38 | for name, entry := range h.preShutdownHooks { 39 | if err := entry(); err != nil { 40 | errs = append(errs, fmt.Errorf("PreShutdownHook %q failed: %v", name, err)) 41 | } 42 | } 43 | 44 | return k8sErrors.NewAggregate(errs) 45 | } 46 | -------------------------------------------------------------------------------- /hack/verify-boilerplate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2014 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | KUBE_ROOT=$(dirname "${BASH_SOURCE}")/.. 22 | 23 | boilerDir="${KUBE_ROOT}/hack/boilerplate" 24 | boiler="${boilerDir}/boilerplate.py" 25 | 26 | files_need_boilerplate=($(${boiler} "$@")) 27 | 28 | # Run boilerplate check 29 | if [[ ${#files_need_boilerplate[@]} -gt 0 ]]; then 30 | for file in "${files_need_boilerplate[@]}"; do 31 | echo "Boilerplate header is wrong for: ${file}" 32 | done 33 | 34 | exit 1 35 | fi 36 | -------------------------------------------------------------------------------- /test/tools/issuer/cmd/main.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/jetstack/kube-oidc-proxy/pkg/util" 11 | "github.com/jetstack/kube-oidc-proxy/test/tools/issuer/cmd/options" 12 | "github.com/jetstack/kube-oidc-proxy/test/tools/issuer/pkg/issuer" 13 | ) 14 | 15 | func main() { 16 | opts := new(options.Options) 17 | stopCh := util.SignalHandler() 18 | 19 | cmd := &cobra.Command{ 20 | Use: "oidc-issuer", 21 | Short: "A very basic OIDC issuer to present a well-known endpoint.", 22 | RunE: func(cmd *cobra.Command, args []string) error { 23 | 24 | iss, err := issuer.New(opts.IssuerURL, opts.KeyFile, opts.CertFile, stopCh) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | compCh, err := iss.Run(opts.BindAddress, opts.ListenPort) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | <-compCh 35 | 36 | return nil 37 | }, 38 | } 39 | 40 | opts.AddFlags(cmd) 41 | 42 | if err := cmd.Execute(); err != nil { 43 | fmt.Fprintf(os.Stderr, "%s\n", err.Error()) 44 | os.Exit(1) 45 | } 46 | 47 | os.Exit(0) 48 | } 49 | -------------------------------------------------------------------------------- /test/tools/fake-apiserver/cmd/options/options.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package options 3 | 4 | import ( 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | type Options struct { 9 | BindAddress string 10 | ListenPort string 11 | 12 | KeyFile string 13 | CertFile string 14 | } 15 | 16 | func (o *Options) AddFlags(cmd *cobra.Command) { 17 | cmd.PersistentFlags().StringVar(&o.BindAddress, "bind-address", 18 | "0.0.0.0", "Bound Address to listen and serve on.") 19 | 20 | cmd.PersistentFlags().StringVar(&o.ListenPort, "secure-port", 21 | "6443", "Port to serve HTTPS.") 22 | o.must(cmd.MarkPersistentFlagRequired("secure-port")) 23 | 24 | cmd.PersistentFlags().StringVar(&o.KeyFile, "tls-private-key-file", 25 | "/etc/apiserver/key.pem", "File location to key for serving.") 26 | o.must(cmd.MarkPersistentFlagRequired("tls-private-key-file")) 27 | 28 | cmd.PersistentFlags().StringVar(&o.CertFile, "tls-cert-file", 29 | "/etc/apiserver/key.pem", "File location to certificate for serving.") 30 | o.must(cmd.MarkPersistentFlagRequired("tls-cert-file")) 31 | } 32 | 33 | func (o *Options) must(err error) { 34 | if err != nil { 35 | panic(err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/tools/audit-webhook/cmd/main.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/jetstack/kube-oidc-proxy/pkg/util" 11 | "github.com/jetstack/kube-oidc-proxy/test/tools/audit-webhook/cmd/options" 12 | "github.com/jetstack/kube-oidc-proxy/test/tools/audit-webhook/pkg/sink" 13 | ) 14 | 15 | func main() { 16 | opts := new(options.Options) 17 | stopCh := util.SignalHandler() 18 | 19 | cmd := &cobra.Command{ 20 | Use: "audit-webhook", 21 | Short: "An server that implements a Kubernetes audit webhook sink", 22 | RunE: func(cmd *cobra.Command, args []string) error { 23 | 24 | wh, err := sink.New(opts.LogPath, opts.KeyFile, opts.CertFile, stopCh) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | compCh, err := wh.Run(opts.BindAddress, opts.ListenPort) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | <-compCh 35 | 36 | return nil 37 | }, 38 | } 39 | 40 | opts.AddFlags(cmd) 41 | 42 | if err := cmd.Execute(); err != nil { 43 | fmt.Fprintf(os.Stderr, "%s\n", err.Error()) 44 | os.Exit(1) 45 | } 46 | 47 | os.Exit(0) 48 | } 49 | -------------------------------------------------------------------------------- /test/tools/fake-apiserver/cmd/main.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/jetstack/kube-oidc-proxy/pkg/util" 11 | "github.com/jetstack/kube-oidc-proxy/test/tools/fake-apiserver/cmd/options" 12 | "github.com/jetstack/kube-oidc-proxy/test/tools/fake-apiserver/pkg/server" 13 | ) 14 | 15 | func main() { 16 | opts := new(options.Options) 17 | stopCh := util.SignalHandler() 18 | 19 | cmd := &cobra.Command{ 20 | Use: "fake-apiserver", 21 | Short: "A fake apiserver that will respond to requests with the same body and headers", 22 | RunE: func(cmd *cobra.Command, args []string) error { 23 | server, err := server.New(opts.KeyFile, opts.CertFile, stopCh) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | compCh, err := server.Run(opts.BindAddress, opts.ListenPort) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | <-compCh 34 | 35 | return nil 36 | }, 37 | } 38 | 39 | opts.AddFlags(cmd) 40 | 41 | if err := cmd.Execute(); err != nil { 42 | fmt.Fprintf(os.Stderr, "%s\n", err.Error()) 43 | os.Exit(1) 44 | } 45 | 46 | os.Exit(0) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/proxy/tokenreview/fake/tokenreview.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package fake 3 | 4 | import ( 5 | "context" 6 | 7 | authv1 "k8s.io/api/authentication/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | clientauthv1 "k8s.io/client-go/kubernetes/typed/authentication/v1" 10 | ) 11 | 12 | var _ clientauthv1.TokenReviewInterface = &FakeReviewer{} 13 | 14 | type FakeReviewer struct { 15 | CreateFn func(*authv1.TokenReview) (*authv1.TokenReview, error) 16 | } 17 | 18 | func New() *FakeReviewer { 19 | return &FakeReviewer{ 20 | CreateFn: func(*authv1.TokenReview) (*authv1.TokenReview, error) { 21 | return nil, nil 22 | }, 23 | } 24 | } 25 | 26 | func (f *FakeReviewer) Create(ctx context.Context, req *authv1.TokenReview, co metav1.CreateOptions) (*authv1.TokenReview, error) { 27 | return f.CreateFn(req) 28 | } 29 | 30 | func (f *FakeReviewer) CreateContext(ctx context.Context, req *authv1.TokenReview) (*authv1.TokenReview, error) { 31 | return f.CreateFn(req) 32 | } 33 | 34 | func (f *FakeReviewer) WithCreate(req *authv1.TokenReview, err error) *FakeReviewer { 35 | f.CreateFn = func(*authv1.TokenReview) (*authv1.TokenReview, error) { 36 | return req, err 37 | } 38 | 39 | return f 40 | } 41 | -------------------------------------------------------------------------------- /deploy/charts/kube-oidc-proxy/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "kube-oidc-proxy.fullname" . -}} 3 | apiVersion: networking.k8s.io/v1 4 | kind: Ingress 5 | metadata: 6 | name: {{ $fullName }} 7 | labels: 8 | {{ include "kube-oidc-proxy.labels" . | indent 4 }} 9 | {{- with .Values.ingress.annotations }} 10 | annotations: 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | spec: 14 | {{- if .Values.ingress.tls }} 15 | tls: 16 | {{- range .Values.ingress.tls }} 17 | - hosts: 18 | {{- range .hosts }} 19 | - {{ . | quote }} 20 | {{- end }} 21 | secretName: {{ .secretName }} 22 | {{- end }} 23 | {{- end }} 24 | {{- if .Values.ingress.ingressClassName }} 25 | ingressClassName: {{ .Values.ingress.ingressClassName | quote }} 26 | {{- end }} 27 | rules: 28 | {{- range .Values.ingress.hosts }} 29 | - host: {{ .host | quote }} 30 | http: 31 | paths: 32 | {{- range .paths }} 33 | - path: {{ . }} 34 | pathType: Prefix 35 | backend: 36 | service: 37 | name: {{ $fullName }} 38 | port: 39 | name: https 40 | {{- end }} 41 | {{- end }} 42 | {{- end }} 43 | -------------------------------------------------------------------------------- /test/tools/audit-webhook/cmd/options/options.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package options 3 | 4 | import ( 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | type Options struct { 9 | BindAddress string 10 | ListenPort string 11 | 12 | LogPath string 13 | 14 | KeyFile string 15 | CertFile string 16 | } 17 | 18 | func (o *Options) AddFlags(cmd *cobra.Command) { 19 | cmd.PersistentFlags().StringVar(&o.BindAddress, "bind-address", 20 | "0.0.0.0", "Bound Address to listen and serve on.") 21 | 22 | cmd.PersistentFlags().StringVar(&o.ListenPort, "secure-port", 23 | "6443", "Port to serve HTTPS.") 24 | 25 | cmd.PersistentFlags().StringVar(&o.LogPath, "audit-file-path", 26 | "/var/log/audit", "Filepath to write audit logs to.") 27 | 28 | cmd.PersistentFlags().StringVar(&o.KeyFile, "tls-private-key-file", 29 | "/etc/oidc/key.pem", "File location to key for serving.") 30 | o.must(cmd.MarkPersistentFlagRequired("tls-private-key-file")) 31 | 32 | cmd.PersistentFlags().StringVar(&o.CertFile, "tls-cert-file", 33 | "/etc/oidc/key.pem", "File location to certificate for serving.") 34 | o.must(cmd.MarkPersistentFlagRequired("tls-cert-file")) 35 | } 36 | 37 | func (o *Options) must(err error) { 38 | if err != nil { 39 | panic(err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/e2e/suite/suite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package suite 3 | 4 | import ( 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | "time" 9 | 10 | "github.com/onsi/ginkgo" 11 | ginkgoconfig "github.com/onsi/ginkgo/config" 12 | "github.com/onsi/ginkgo/reporters" 13 | "github.com/onsi/gomega" 14 | "k8s.io/apimachinery/pkg/util/wait" 15 | 16 | _ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases" 17 | ) 18 | 19 | func init() { 20 | // Turn on verbose by default to get spec names 21 | ginkgoconfig.DefaultReporterConfig.Verbose = true 22 | // Turn on EmitSpecProgress to get spec progress (especially on interrupt) 23 | ginkgoconfig.GinkgoConfig.EmitSpecProgress = true 24 | // Randomize specs as well as suites 25 | ginkgoconfig.GinkgoConfig.RandomizeAllSpecs = true 26 | 27 | wait.ForeverTestTimeout = time.Second * 60 28 | } 29 | 30 | func TestE2E(t *testing.T) { 31 | gomega.RegisterFailHandler(ginkgo.Fail) 32 | 33 | junitPath := "../../../artifacts" 34 | if path := os.Getenv("ARTIFACTS"); path != "" { 35 | junitPath = path 36 | } 37 | 38 | junitReporter := reporters.NewJUnitReporter(filepath.Join( 39 | junitPath, 40 | "junit-go-e2e.xml", 41 | )) 42 | ginkgo.RunSpecsWithDefaultAndCustomReporters(t, "kube-oidc-proxy e2e suite", []ginkgo.Reporter{junitReporter}) 43 | } 44 | -------------------------------------------------------------------------------- /test/e2e/suite/suite.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package suite 3 | 4 | import ( 5 | "path/filepath" 6 | 7 | . "github.com/onsi/ginkgo" 8 | log "github.com/sirupsen/logrus" 9 | 10 | "github.com/jetstack/kube-oidc-proxy/test/e2e/framework" 11 | "github.com/jetstack/kube-oidc-proxy/test/environment" 12 | ) 13 | 14 | var ( 15 | env *environment.Environment 16 | cfg = framework.DefaultConfig 17 | ) 18 | 19 | var _ = SynchronizedBeforeSuite(func() []byte { 20 | var err error 21 | env, err = environment.New(1, 0) 22 | if err != nil { 23 | log.Fatalf("Error provisioning environment: %s", err) 24 | } 25 | 26 | if err := env.Create(); err != nil { 27 | log.Fatalf("Error creating environment: %s", err) 28 | } 29 | 30 | cfg.KubeConfigPath = env.KubeConfigPath() 31 | cfg.Kubectl = filepath.Join(env.RootPath(), "bin", "kubectl") 32 | cfg.RepoRoot = env.RootPath() 33 | cfg.Environment = env 34 | 35 | if err := framework.DefaultConfig.Validate(); err != nil { 36 | log.Fatalf("Invalid test config: %s", err) 37 | } 38 | 39 | return nil 40 | }, func([]byte) { 41 | }) 42 | 43 | var _ = SynchronizedAfterSuite(func() {}, 44 | func() { 45 | if env != nil { 46 | if err := env.Destory(); err != nil { 47 | log.Fatalf("Failed to destory environment: %s", err) 48 | } 49 | } 50 | }, 51 | ) 52 | -------------------------------------------------------------------------------- /test/e2e/framework/helper/kubectl.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package helper 3 | 4 | import ( 5 | "io" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | ) 10 | 11 | type Kubectl struct { 12 | namespace string 13 | kubectl string 14 | kubeconfig string 15 | } 16 | 17 | func (k *Kubectl) Describe(resources ...string) error { 18 | resourceNames := strings.Join(resources, ",") 19 | return k.Run("describe", resourceNames) 20 | } 21 | 22 | func (k *Kubectl) DescribeResource(resource, name string) error { 23 | return k.Run("describe", resource, name) 24 | } 25 | 26 | func (h *Helper) Kubectl(ns string) *Kubectl { 27 | return &Kubectl{ 28 | namespace: ns, 29 | kubectl: h.cfg.Kubectl, 30 | kubeconfig: h.cfg.KubeConfigPath, 31 | } 32 | } 33 | 34 | func (k *Kubectl) Run(args ...string) error { 35 | return k.RunWithStdout(os.Stdout, args...) 36 | } 37 | 38 | func (k *Kubectl) RunWithStdout(stdout io.Writer, args ...string) error { 39 | baseArgs := []string{"--kubeconfig", k.kubeconfig} 40 | if k.namespace == "" { 41 | baseArgs = append(baseArgs, "--all-namespaces") 42 | } else { 43 | baseArgs = []string{"--namespace", k.namespace} 44 | } 45 | args = append(baseArgs, args...) 46 | cmd := exec.Command(k.kubectl, args...) 47 | cmd.Stdout = stdout 48 | cmd.Stderr = os.Stderr 49 | return cmd.Run() 50 | } 51 | -------------------------------------------------------------------------------- /test/tools/issuer/cmd/options/options.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package options 3 | 4 | import ( 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | type Options struct { 9 | BindAddress string 10 | ListenPort string 11 | 12 | IssuerURL string 13 | 14 | KeyFile string 15 | CertFile string 16 | } 17 | 18 | func (o *Options) AddFlags(cmd *cobra.Command) { 19 | cmd.PersistentFlags().StringVar(&o.BindAddress, "bind-address", 20 | "0.0.0.0", "Bound Address to listen and serve on.") 21 | 22 | cmd.PersistentFlags().StringVar(&o.ListenPort, "secure-port", 23 | "6443", "Port to serve HTTPS.") 24 | o.must(cmd.MarkPersistentFlagRequired("secure-port")) 25 | 26 | cmd.PersistentFlags().StringVar(&o.IssuerURL, "issuer-url", 27 | "", "URL of the issuer that appears in well-known responses.") 28 | o.must(cmd.MarkPersistentFlagRequired("issuer-url")) 29 | 30 | cmd.PersistentFlags().StringVar(&o.KeyFile, "tls-private-key-file", 31 | "/etc/oidc/key.pem", "File location to key for serving.") 32 | o.must(cmd.MarkPersistentFlagRequired("tls-private-key-file")) 33 | 34 | cmd.PersistentFlags().StringVar(&o.CertFile, "tls-cert-file", 35 | "/etc/oidc/key.pem", "File location to certificate for serving.") 36 | o.must(cmd.MarkPersistentFlagRequired("tls-cert-file")) 37 | } 38 | 39 | func (o *Options) must(err error) { 40 | if err != nil { 41 | panic(err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/tasks/extra-impersonation-headers.md: -------------------------------------------------------------------------------- 1 | # Extra Impersonation Headers 2 | 3 | kube-oidc-proxy has support for adding 'extra' headers to the impersonation user 4 | info. This can be useful for passing extra information onto the target server 5 | about the proxy or client. kube-oidc-proxy currently supports two configuration 6 | options. 7 | 8 | # Client IP 9 | 10 | The following flag can be passed which will append the remote client IP as an 11 | extra header: 12 | 13 | `--extra-user-header-client-ip` 14 | 15 | Proxied requests will then contain the header 16 | `Impersonate-Extra-Remote-Client-Ip: ` where `` is 17 | the address of the source connection of the request. Note that this IP address 18 | may not be the real client IP address when the request is being proxied. If the 19 | `X-Forwarded-For` header is present in the request then the value will be used 20 | as the client address. 21 | 22 | # Extra User Headers 23 | 24 | The following flag accepts a number of key value pairs that will be added as 25 | extra impersonation headers with proxied requests. This flag accepts a number of 26 | key value pairs, separated by commas, where a single key may have multiple 27 | values: 28 | 29 | `--extra-user-headers=key1=foo,key2=bar,key1=bar` 30 | 31 | Proxied requests will then contain the headers 32 | 33 | `Impersonate-Extra-Key1: foo,bar` 34 | `Impersonate-Extra-Key2: foo` 35 | -------------------------------------------------------------------------------- /demo/config.dist.jsonnet: -------------------------------------------------------------------------------- 1 | local main = import './manifests/main.jsonnet'; 2 | 3 | function(cloud='google') main { 4 | cloud: cloud, 5 | // this will only run the google cluster 6 | clouds: { 7 | google: main.clouds.google, 8 | amazon: main.clouds.amazon, 9 | digitalocean: main.clouds.digitalocean, 10 | }, 11 | base_domain: '.kubernetes.example.net', 12 | cert_manager+: { 13 | letsencrypt_contact_email:: 'certificates@example.net', 14 | solvers+: [ 15 | //{ 16 | // http01: { 17 | // ingress: {}, 18 | // }, 19 | //}, 20 | { 21 | dns01: { 22 | clouddns: { 23 | project: $.config.cert_manager.project, 24 | serviceAccountSecretRef: { 25 | name: $.cert_manager.google_secret.metadata.name, 26 | key: 'credentials.json', 27 | }, 28 | }, 29 | }, 30 | }, 31 | ], 32 | }, 33 | dex+: if $.master then { 34 | users: [ 35 | $.dex.Password('admin@example.net', '$2y$10$i2.tSLkchjnpvnI73iSW/OPAVriV9BWbdfM6qemBM1buNRu81.ZG.'), // plaintext: secure 36 | ], 37 | // This shows how to add dex connectors 38 | connectors: [ 39 | $.dex.Connector('github', 'GitHub', 'github', { 40 | clientID: '0123', 41 | clientSecret: '4567', 42 | orgs: [{ 43 | name: 'example-net', 44 | }], 45 | }), 46 | ], 47 | } else { 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /deploy/charts/kube-oidc-proxy/templates/secret_config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ include "kube-oidc-proxy.fullname" . }}-config 5 | labels: 6 | {{ include "kube-oidc-proxy.labels" . | indent 4 }} 7 | type: Opaque 8 | data: 9 | {{- if .Values.oidc.caPEM }} 10 | oidc.ca-pem: {{ .Values.oidc.caPEM | default "" | b64enc }} 11 | {{- end }} 12 | 13 | {{- if .Values.oidc.issuerUrl }} 14 | oidc.issuer-url: {{ .Values.oidc.issuerUrl | b64enc }} 15 | {{- end }} 16 | 17 | {{- if .Values.oidc.usernameClaim }} 18 | oidc.username-claim: {{ .Values.oidc.usernameClaim | default "" | b64enc }} 19 | {{- end }} 20 | 21 | {{- if .Values.oidc.clientId }} 22 | oidc.client-id: {{ .Values.oidc.clientId | b64enc }} 23 | {{- end }} 24 | 25 | {{- if .Values.oidc.usernamePrefix }} 26 | oidc.username-prefix: {{ .Values.oidc.usernamePrefix | default "" | b64enc }} 27 | {{- end }} 28 | 29 | {{- if .Values.oidc.groupsClaim }} 30 | oidc.groups-claim: {{ .Values.oidc.groupsClaim | default "" | b64enc }} 31 | {{- end }} 32 | 33 | {{- if .Values.oidc.groupsPrefix }} 34 | oidc.groups-prefix: {{ .Values.oidc.groupsPrefix | default "" | b64enc }} 35 | {{- end }} 36 | 37 | {{- if .Values.oidc.signingAlgs }} 38 | oidc.signing-algs: {{ join "," .Values.oidc.signingAlgs | default "" | b64enc }} 39 | {{- end }} 40 | 41 | {{- if .Values.oidc.requiredClaims }} 42 | oidc.required-claims: {{ include "requiredClaims" . | b64enc }} 43 | {{- end }} 44 | -------------------------------------------------------------------------------- /demo/manifests/vendor/kube-prod-runtime/tests/aks.jsonnet: -------------------------------------------------------------------------------- 1 | /* 2 | * Bitnami Kubernetes Production Runtime - A collection of services that makes it 3 | * easy to run production workloads in Kubernetes. 4 | * 5 | * Copyright 2018-2019 Bitnami 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | // Test AKS 21 | 22 | (import "../platforms/aks.jsonnet") { 23 | "letsencrypt_contact_email": "noone@nowhere.com", 24 | config: { 25 | dnsZone: "test.example.com", 26 | externalDns: { 27 | tenantId: "mytenant", 28 | subscriptionId: "mysubscription", 29 | aadClientId: "myclientid", 30 | aadClientSecret: "mysecret", 31 | resourceGroup: "test-resource-group", 32 | }, 33 | oauthProxy: { 34 | client_id: "myclientid", 35 | client_secret: "mysecret", 36 | cookie_secret: "cookiesecret", 37 | authz_domain: "test.invalid", 38 | azure_tenant: "mytenant", 39 | }, 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /demo/manifests/vendor/kube-prod-runtime/tests/gke.jsonnet: -------------------------------------------------------------------------------- 1 | /* 2 | * Bitnami Kubernetes Production Runtime - A collection of services that makes it 3 | * easy to run production workloads in Kubernetes. 4 | * 5 | * Copyright 2018-2019 Bitnami 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | // Test GKE 21 | 22 | (import "../platforms/gke.jsonnet") { 23 | "letsencrypt_contact_email": "noone@nowhere.com", 24 | config: { 25 | dnsZone: "test.example.com", 26 | externalDns: { 27 | credentials: "google credentials json contents", 28 | project: "dns_gcp_project", 29 | }, 30 | oauthProxy: { 31 | client_id: "myclientid", 32 | client_secret: "mysecret", 33 | cookie_secret: "cookiesecret", 34 | authz_domain: "test.invalid", 35 | google_groups: [], 36 | google_admin_email: "admin@example.com", 37 | google_service_account_json: "", 38 | }, 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /demo/manifests/vendor/kube-prod-runtime/components/fluentd-es-config/import-from-upstream.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Bitnami Kubernetes Production Runtime - A collection of services that makes it 4 | # easy to run production workloads in Kubernetes. 5 | # 6 | # Copyright 2018-2019 Bitnami 7 | # 8 | # Licensed under the Apache License, Version 2.0 (the "License"); 9 | # you may not use this file except in compliance with the License. 10 | # You may obtain a copy of the License at 11 | # 12 | # http://www.apache.org/licenses/LICENSE-2.0 13 | # 14 | # Unless required by applicable law or agreed to in writing, software 15 | # distributed under the License is distributed on an "AS IS" BASIS, 16 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | # See the License for the specific language governing permissions and 18 | # limitations under the License. 19 | 20 | # Save *.conf files needed for fluentd-es log pre-processing from upstream 21 | # yaml-s 22 | 23 | import os 24 | import yaml 25 | import requests 26 | 27 | r = requests.get("https://raw.githubusercontent.com/kubernetes/kubernetes/" 28 | "master/cluster/addons/fluentd-elasticsearch/" 29 | "fluentd-es-configmap.yaml") 30 | DIR = os.path.dirname(__file__) 31 | 32 | for fname, content in yaml.safe_load(r.text)['data'].items(): 33 | fname = os.path.join(DIR, fname) 34 | print("Saving to " + fname) 35 | with open(fname, "w") as f: 36 | f.write(content) 37 | f.write("\n") # Add trailing newline 38 | -------------------------------------------------------------------------------- /test/e2e/suite/cases/probe/probe.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package probe 3 | 4 | import ( 5 | "time" 6 | 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | 10 | "github.com/jetstack/kube-oidc-proxy/test/e2e/framework" 11 | "github.com/jetstack/kube-oidc-proxy/test/kind" 12 | ) 13 | 14 | var _ = framework.CasesDescribe("Readiness Probe", func() { 15 | f := framework.NewDefaultFramework("readiness-probe") 16 | 17 | It("Should not become ready if the issuer is unavailable", func() { 18 | By("Deleting the Issuer so no longer becomes reachable") 19 | Expect(f.Helper().DeleteIssuer(f.Namespace.Name)).NotTo(HaveOccurred()) 20 | 21 | By("Deleting the current proxy that is ready") 22 | Expect(f.Helper().DeleteProxy(f.Namespace.Name)).NotTo(HaveOccurred()) 23 | 24 | By("Deploying the Proxy should never become ready as the issuer is unavailable") 25 | _, _, err := f.Helper().DeployProxy(f.Namespace, f.IssuerURL(), 26 | f.ClientID(), f.IssuerKeyBundle(), nil) 27 | // Error should occur (not ready) 28 | Expect(err).To(HaveOccurred()) 29 | }) 30 | 31 | It("Should continue to be ready even if the issuer becomes unavailable", func() { 32 | By("Deleting the Issuer so no longer becomes reachable") 33 | Expect(f.Helper().DeleteIssuer(f.Namespace.Name)).NotTo(HaveOccurred()) 34 | 35 | time.Sleep(time.Second * 10) 36 | 37 | err := f.Helper().WaitForDeploymentReady(f.Namespace.Name, kind.ProxyImageName, time.Second*5) 38 | Expect(err).NotTo(HaveOccurred()) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /demo/manifests/components/contour-clusterrole.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "rbac.authorization.k8s.io/v1beta1", 3 | "kind": "ClusterRole", 4 | "metadata": { 5 | "name": "contour" 6 | }, 7 | "rules": [ 8 | { 9 | "apiGroups": [ 10 | "" 11 | ], 12 | "resources": [ 13 | "configmaps", 14 | "endpoints", 15 | "nodes", 16 | "pods", 17 | "secrets" 18 | ], 19 | "verbs": [ 20 | "list", 21 | "watch" 22 | ] 23 | }, 24 | { 25 | "apiGroups": [ 26 | "" 27 | ], 28 | "resources": [ 29 | "nodes" 30 | ], 31 | "verbs": [ 32 | "get" 33 | ] 34 | }, 35 | { 36 | "apiGroups": [ 37 | "" 38 | ], 39 | "resources": [ 40 | "services" 41 | ], 42 | "verbs": [ 43 | "get", 44 | "list", 45 | "watch" 46 | ] 47 | }, 48 | { 49 | "apiGroups": [ 50 | "extensions" 51 | ], 52 | "resources": [ 53 | "ingresses" 54 | ], 55 | "verbs": [ 56 | "get", 57 | "list", 58 | "watch" 59 | ] 60 | }, 61 | { 62 | "apiGroups": [ 63 | "contour.heptio.com" 64 | ], 65 | "resources": [ 66 | "ingressroutes", 67 | "tlscertificatedelegations" 68 | ], 69 | "verbs": [ 70 | "get", 71 | "list", 72 | "watch", 73 | "put", 74 | "post", 75 | "patch" 76 | ] 77 | } 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2016 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import boilerplate 18 | import unittest 19 | import StringIO 20 | import os 21 | import sys 22 | 23 | class TestBoilerplate(unittest.TestCase): 24 | """ 25 | Note: run this test from the hack/boilerplate directory. 26 | 27 | $ python -m unittest boilerplate_test 28 | """ 29 | 30 | def test_boilerplate(self): 31 | os.chdir("test/") 32 | 33 | class Args(object): 34 | def __init__(self): 35 | self.filenames = [] 36 | self.rootdir = "." 37 | self.boilerplate_dir = "../" 38 | self.verbose = True 39 | 40 | # capture stdout 41 | old_stdout = sys.stdout 42 | sys.stdout = StringIO.StringIO() 43 | 44 | boilerplate.args = Args() 45 | ret = boilerplate.main() 46 | 47 | output = sorted(sys.stdout.getvalue().split()) 48 | 49 | sys.stdout = old_stdout 50 | 51 | self.assertEquals( 52 | output, ['././fail.go', '././fail.py']) 53 | -------------------------------------------------------------------------------- /pkg/util/token.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package util 3 | 4 | import ( 5 | "net/http" 6 | "strings" 7 | "time" 8 | 9 | "gopkg.in/square/go-jose.v2" 10 | "gopkg.in/square/go-jose.v2/jwt" 11 | ) 12 | 13 | // Return just the token from the header of the request, without 'bearer'. 14 | func ParseTokenFromRequest(req *http.Request) (string, bool) { 15 | if req == nil || req.Header == nil { 16 | return "", false 17 | } 18 | 19 | auth := strings.TrimSpace(req.Header.Get("Authorization")) 20 | if auth == "" { 21 | return "", false 22 | } 23 | 24 | parts := strings.Split(auth, " ") 25 | if len(parts) < 2 || strings.ToLower(parts[0]) != "bearer" { 26 | return "", false 27 | } 28 | 29 | token := parts[1] 30 | 31 | // Empty bearer tokens aren't valid 32 | if len(token) == 0 { 33 | return "", false 34 | } 35 | 36 | return token, true 37 | } 38 | 39 | // fakeJWT generates a valid JWT using the passed input parameters which is 40 | // signed by a generated key. This is useful for checking the status of a 41 | // signer. 42 | func FakeJWT(issuerURL string) (string, error) { 43 | key := []byte("secret") 44 | 45 | sig, err := jose.NewSigner( 46 | jose.SigningKey{Algorithm: jose.HS256, Key: key}, 47 | (&jose.SignerOptions{}).WithType("JWT")) 48 | if err != nil { 49 | return "", err 50 | } 51 | 52 | cl := jwt.Claims{ 53 | Subject: "fake", 54 | Issuer: issuerURL, 55 | NotBefore: jwt.NewNumericDate(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC)), 56 | Audience: jwt.Audience(nil), 57 | } 58 | 59 | return jwt.Signed(sig).Claims(cl).CompactSerialize() 60 | } 61 | -------------------------------------------------------------------------------- /deploy/charts/kube-oidc-proxy/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "kube-oidc-proxy.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "kube-oidc-proxy.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "kube-oidc-proxy.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "kube-oidc-proxy.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | echo "Visit http://127.0.0.1:8080 to use your application" 20 | kubectl port-forward $POD_NAME 8080:80 21 | {{- end }} 22 | -------------------------------------------------------------------------------- /deploy/charts/kube-oidc-proxy/templates/secret_tls.yaml: -------------------------------------------------------------------------------- 1 | {{ $fullname := include "kube-oidc-proxy.fullname" . }} 2 | {{ $ca := genCA (printf "%s-ca" $fullname) 3650 }} 3 | {{ $cn := printf "%s.%s.svc.cluster.local" $fullname .Release.Namespace }} 4 | {{ $in := printf "%s-issuer" $fullname }} 5 | 6 | {{ if .Values.tls.certManager }} 7 | {{ if .Values.tls.selfSigned }} 8 | apiVersion: cert-manager.io/v1 9 | kind: Issuer 10 | metadata: 11 | name: {{ template "kube-oidc-proxy.fullname" . }}-issuer 12 | spec: 13 | selfSigned: {} 14 | --- 15 | {{ end }} 16 | apiVersion: cert-manager.io/v1 17 | kind: Certificate 18 | metadata: 19 | name: {{ template "kube-oidc-proxy.fullname" . }}-tls 20 | spec: 21 | commonName: {{ template "kube-oidc-proxy.fullname" . }}-tls 22 | dnsNames: 23 | - {{ $cn }} 24 | secretName: {{ template "kube-oidc-proxy.fullname" . }}-tls 25 | issuerRef: 26 | group: cert-manager.io 27 | kind: Issuer 28 | name: {{ .Values.tls.issuerName | default $in }} 29 | {{ if .Values.tls.selfSigned }} 30 | duration: 3650h0m0s 31 | privateKey: 32 | algorithm: RSA 33 | encoding: PKCS8 34 | size: 2048 35 | renewBefore: 24h0m0s 36 | usages: 37 | - server auth 38 | {{ end }} 39 | {{ else }} 40 | {{- if (not .Values.tls.secretName) }} 41 | {{ $server := genSignedCert $cn nil nil 365 $ca }} 42 | apiVersion: v1 43 | kind: Secret 44 | type: kubernetes.io/tls 45 | metadata: 46 | name: {{ template "kube-oidc-proxy.fullname" . }}-tls 47 | labels: 48 | {{ include "kube-oidc-proxy.labels" . | indent 4 }} 49 | data: 50 | tls.crt: {{ b64enc $server.Cert }} 51 | tls.key: {{ b64enc $server.Key }} 52 | {{ end }} 53 | {{ end }} -------------------------------------------------------------------------------- /pkg/probe/probe.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package probe 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "strings" 10 | "time" 11 | 12 | "github.com/heptiolabs/healthcheck" 13 | "k8s.io/apiserver/pkg/authentication/authenticator" 14 | "k8s.io/klog/v2" 15 | ) 16 | 17 | const ( 18 | timeout = time.Second * 10 19 | ) 20 | 21 | type HealthCheck struct { 22 | handler healthcheck.Handler 23 | 24 | oidcAuther authenticator.Token 25 | fakeJWT string 26 | 27 | ready bool 28 | } 29 | 30 | func Run(port, fakeJWT string, oidcAuther authenticator.Token) error { 31 | h := &HealthCheck{ 32 | handler: healthcheck.NewHandler(), 33 | oidcAuther: oidcAuther, 34 | fakeJWT: fakeJWT, 35 | } 36 | 37 | h.handler.AddReadinessCheck("secure serving", h.Check) 38 | 39 | go func() { 40 | for { 41 | err := http.ListenAndServe(net.JoinHostPort("0.0.0.0", port), h.handler) 42 | if err != nil { 43 | klog.Errorf("ready probe listener failed: %s", err) 44 | } 45 | time.Sleep(5 * time.Second) 46 | } 47 | }() 48 | 49 | return nil 50 | } 51 | 52 | func (h *HealthCheck) Check() error { 53 | if h.ready { 54 | return nil 55 | } 56 | 57 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 58 | defer cancel() 59 | 60 | _, _, err := h.oidcAuther.AuthenticateToken(ctx, h.fakeJWT) 61 | if err != nil && strings.HasSuffix(err.Error(), "authenticator not initialized") { 62 | err = fmt.Errorf("OIDC provider not yet initialized: %s", err) 63 | klog.V(4).Infof(err.Error()) 64 | return err 65 | } 66 | 67 | h.ready = true 68 | 69 | klog.V(4).Info("OIDC provider initialized.") 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /test/util/tls.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package util 3 | 4 | import ( 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/pem" 8 | "fmt" 9 | "io/ioutil" 10 | "net" 11 | "os" 12 | "path/filepath" 13 | 14 | "k8s.io/client-go/util/cert" 15 | ) 16 | 17 | type KeyBundle struct { 18 | CertPath string 19 | KeyPath string 20 | CertBytes []byte 21 | KeyBytes []byte 22 | Key *rsa.PrivateKey 23 | } 24 | 25 | const ( 26 | prefix = "kube-oidc-proxy" 27 | ) 28 | 29 | func NewTLSSelfSignedCertKey(host string, netIPs []net.IP, dnsNames []string) (*KeyBundle, error) { 30 | certBytes, keyBytes, err := cert.GenerateSelfSignedCertKey(host, netIPs, dnsNames) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | dir, err := ioutil.TempDir(os.TempDir(), prefix) 36 | if err != nil { 37 | return nil, err 38 | } 39 | defer os.RemoveAll(dir) 40 | 41 | certPath := filepath.Join(dir, fmt.Sprintf("%s-ca.pem", prefix)) 42 | keyPath := filepath.Join(dir, fmt.Sprintf("%s-key.pem", prefix)) 43 | 44 | err = ioutil.WriteFile(certPath, certBytes, 0600) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | err = ioutil.WriteFile(keyPath, keyBytes, 0600) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | p, rest := pem.Decode(keyBytes) 55 | if len(rest) != 0 { 56 | return nil, fmt.Errorf("got rest decoding pem file %s: %s", keyPath, rest) 57 | } 58 | 59 | sk, err := x509.ParsePKCS1PrivateKey(p.Bytes) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | return &KeyBundle{ 65 | CertPath: certPath, 66 | KeyPath: keyPath, 67 | CertBytes: certBytes, 68 | KeyBytes: keyBytes, 69 | Key: sk, 70 | }, nil 71 | } 72 | -------------------------------------------------------------------------------- /demo/manifests/vendor/kube-prod-runtime/components/alertmanager-config.jsonnet: -------------------------------------------------------------------------------- 1 | /* 2 | * Bitnami Kubernetes Production Runtime - A collection of services that makes it 3 | * easy to run production workloads in Kubernetes. 4 | * 5 | * Copyright 2018-2019 Bitnami 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | // https://prometheus.io/docs/alerting/configuration/ 21 | { 22 | global: { 23 | resolve_timeout: "5m", 24 | }, 25 | 26 | //templates: [] 27 | 28 | route: { 29 | group_by: ["alertname", "cluster", "service"], 30 | 31 | group_wait: "30s", 32 | 33 | group_interval: "5m", 34 | repeat_interval: "7d", 35 | 36 | receiver: "email", 37 | 38 | routes: [ 39 | ], 40 | }, 41 | 42 | inhibit_rules: [ 43 | { 44 | source_match: {severity: "critical"}, 45 | target_match: {severity: "warning"}, 46 | equal: ["alertname", "cluster", "service"], 47 | }, 48 | ], 49 | 50 | receivers_:: { 51 | email: { 52 | //email_configs: [{to: "foo@example.com"}], 53 | }, 54 | }, 55 | receivers: [{name: k} + self.receivers_[k] for k in std.objectFields(self.receivers_)], 56 | } 57 | -------------------------------------------------------------------------------- /demo/infrastructure/modules/google-cluster/cluster.tf: -------------------------------------------------------------------------------- 1 | variable "suffix" {} 2 | 3 | variable "zone" {} 4 | 5 | resource "google_container_cluster" "cluster" { 6 | name = "cluster-${var.suffix}" 7 | zone = "${var.zone}" 8 | 9 | initial_node_count = 3 10 | 11 | # Setting an empty username and password explicitly disables basic auth 12 | master_auth { 13 | username = "" 14 | password = "" 15 | } 16 | 17 | node_config { 18 | oauth_scopes = [ 19 | "https://www.googleapis.com/auth/compute", 20 | "https://www.googleapis.com/auth/devstorage.read_only", 21 | "https://www.googleapis.com/auth/logging.write", 22 | "https://www.googleapis.com/auth/monitoring", 23 | ] 24 | } 25 | } 26 | 27 | data "google_client_config" "default" {} 28 | 29 | output "name" { 30 | value = "${google_container_cluster.cluster.name}" 31 | } 32 | 33 | output "project" { 34 | value = "${google_container_cluster.cluster.project}" 35 | } 36 | 37 | output "kubeconfig" { 38 | value = < 0 { 22 | sec, err = h.KubeClient.CoreV1().Secrets(ns).Get(context.TODO(), sa.Secrets[0].Name, metav1.GetOptions{}) 23 | if err != nil { 24 | return nil, err 25 | } 26 | } else { 27 | var requestSeconds int64 28 | requestSeconds = 600 29 | 30 | // starting in 1.24 ServiceAccounts no longer get Secrets, need to request one bound to a ServiceAccount 31 | serviceAccountToken, err := h.KubeClient.CoreV1().ServiceAccounts(ns).CreateToken(context.TODO(), sa.Name, &v1.TokenRequest{ 32 | Spec: v1.TokenRequestSpec{ 33 | Audiences: []string{"https://kubernetes.default.svc.cluster.local"}, 34 | ExpirationSeconds: &requestSeconds, 35 | }, 36 | }, metav1.CreateOptions{}) 37 | 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | fmt.Printf("TOKEN!!!!! : %v\n", serviceAccountToken.Status.Token) 43 | 44 | secretToReturn := &corev1.Secret{ 45 | ObjectMeta: metav1.ObjectMeta{ 46 | Namespace: ns, 47 | Name: name, 48 | }, 49 | Data: map[string][]byte{corev1.ServiceAccountTokenKey: []byte(serviceAccountToken.Status.Token)}, 50 | } 51 | 52 | return secretToReturn, nil 53 | 54 | } 55 | 56 | return sec, nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/proxy/logging/accesslog_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package logging 3 | 4 | import ( 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestXForwardedFor(t *testing.T) { 10 | 11 | tests := map[string]struct { 12 | headers http.Header 13 | remoteAddr string 14 | exp string 15 | }{ 16 | "no x-forwarded-for": { 17 | headers: http.Header{}, 18 | remoteAddr: "1.2.3.4", 19 | exp: "", 20 | }, 21 | "empty x-forwarded-for": { 22 | headers: http.Header{ 23 | "X-Forwarded-For": []string{""}, 24 | }, 25 | remoteAddr: "1.2.3.4", 26 | exp: "", 27 | }, 28 | "x-forwarded-for is remoteaddr": { 29 | headers: http.Header{ 30 | "X-Forwarded-For": []string{"1.2.3.4"}, 31 | }, 32 | remoteAddr: "1.2.3.4", 33 | exp: "", 34 | }, 35 | "x-forwarded-for with no remoteaddr": { 36 | headers: http.Header{ 37 | "X-Forwarded-For": []string{"1.2.3.1"}, 38 | }, 39 | remoteAddr: "1.2.3.4", 40 | exp: "1.2.3.1", 41 | }, 42 | "x-forwarded-for with with remoteaddr at the end": { 43 | headers: http.Header{ 44 | "X-Forwarded-For": []string{"1.2.3.1, 1.2.3.4"}, 45 | }, 46 | remoteAddr: "1.2.3.4", 47 | exp: "1.2.3.1", 48 | }, 49 | "x-forwarded-for with with remoteaddr at the beginning": { 50 | headers: http.Header{ 51 | "X-Forwarded-For": []string{"1.2.3.4, 1.2.3.1"}, 52 | }, 53 | remoteAddr: "1.2.3.4", 54 | exp: "1.2.3.1", 55 | }, 56 | } 57 | 58 | for name, test := range tests { 59 | t.Run(name, func(t *testing.T) { 60 | forwarded := findXForwardedFor(test.headers, test.remoteAddr) 61 | 62 | if test.exp != forwarded { 63 | t.Errorf("failed for %s: unexpected result : %s", name, forwarded) 64 | t.FailNow() 65 | } 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/e2e/framework/util.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package framework 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "time" 8 | 9 | corev1 "k8s.io/api/core/v1" 10 | apierrors "k8s.io/apimachinery/pkg/api/errors" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/util/wait" 13 | "k8s.io/client-go/kubernetes" 14 | ) 15 | 16 | const ( 17 | clientID = "kube-oidc-proxy-e2e-client_id" 18 | ) 19 | 20 | // CreateKubeNamespace creates a new Kubernetes Namespace for a test. 21 | func (f *Framework) CreateKubeNamespace(baseName string) (*corev1.Namespace, error) { 22 | ns := &corev1.Namespace{ 23 | ObjectMeta: metav1.ObjectMeta{ 24 | GenerateName: fmt.Sprintf("kube-oidc-proxy-e2e-%v-", baseName), 25 | }, 26 | } 27 | 28 | return f.KubeClientSet.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{}) 29 | } 30 | 31 | // DeleteKubeNamespace will delete a namespace resource 32 | func (f *Framework) DeleteKubeNamespace(namespace string) error { 33 | return f.KubeClientSet.CoreV1().Namespaces().Delete(context.TODO(), namespace, metav1.DeleteOptions{}) 34 | } 35 | 36 | // WaitForKubeNamespaceNotExist will wait for the namespace with the given name 37 | // to not exist for up to 2 minutes. 38 | func (f *Framework) WaitForKubeNamespaceNotExist(namespace string) error { 39 | return wait.PollImmediate(time.Second*2, time.Minute*2, namespaceNotExist(f.KubeClientSet, namespace)) 40 | } 41 | 42 | func namespaceNotExist(c kubernetes.Interface, namespace string) wait.ConditionFunc { 43 | return func() (bool, error) { 44 | _, err := c.CoreV1().Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{}) 45 | if apierrors.IsNotFound(err) { 46 | return true, nil 47 | } 48 | if err != nil { 49 | return false, err 50 | } 51 | return false, nil 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cmd/app/options/client.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package options 3 | 4 | import ( 5 | "github.com/spf13/cobra" 6 | "github.com/spf13/pflag" 7 | "k8s.io/cli-runtime/pkg/genericclioptions" 8 | cliflag "k8s.io/component-base/cli/flag" 9 | ) 10 | 11 | type ClientOptions struct { 12 | *genericclioptions.ConfigFlags 13 | 14 | KubeClientQPS float32 15 | KubeClientBurst int 16 | } 17 | 18 | func NewClientOptions(nfs *cliflag.NamedFlagSets) *ClientOptions { 19 | c := &ClientOptions{ 20 | ConfigFlags: genericclioptions.NewConfigFlags(true), 21 | } 22 | 23 | // Disable unwanted options 24 | c.CacheDir = nil 25 | c.Impersonate = nil 26 | c.ImpersonateGroup = nil 27 | 28 | return c.AddFlags(nfs.FlagSet("Client")) 29 | } 30 | 31 | func (c *ClientOptions) AddFlags(fs *pflag.FlagSet) *ClientOptions { 32 | c.ConfigFlags.AddFlags(fs) 33 | 34 | // Extra flags 35 | fs.Float32Var(&c.KubeClientQPS, "kube-client-qps", c.KubeClientQPS, "Sets the QPS on the app "+ 36 | "kubernetes client, this will configure throttling on requests sent to the apiserver "+ 37 | "(If not set, it will use client default ones)") 38 | fs.IntVar(&c.KubeClientBurst, "kube-client-burst", c.KubeClientBurst, "Sets the burst on the app "+ 39 | "kubernetes client, this will configure throttling on requests sent to the apiserver"+ 40 | "(If not set, it will use client default ones)") 41 | 42 | return c 43 | } 44 | 45 | func (c *ClientOptions) ClientFlagsChanged(cmd *cobra.Command) bool { 46 | for _, f := range clientOptionFlags() { 47 | if ff := cmd.Flag(f); ff != nil && ff.Changed { 48 | return true 49 | } 50 | } 51 | 52 | return false 53 | } 54 | 55 | func clientOptionFlags() []string { 56 | return []string{"certificate-authority", "client-certificate", "client-key", "cluster", 57 | "context", "insecure-skip-tls-verify", "kubeconfig", "namespace", 58 | "request-timeout", "server", "token", "user", 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pkg/proxy/subjectaccessreview/fake/subjectaccessreview.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package fake 3 | 4 | import ( 5 | "context" 6 | 7 | azv1 "k8s.io/api/authorization/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | clientazv1 "k8s.io/client-go/kubernetes/typed/authorization/v1" 10 | ) 11 | 12 | var _ clientazv1.SubjectAccessReviewInterface = &FakeReviewer{} 13 | 14 | type FakeReviewer struct { 15 | err error 16 | } 17 | 18 | func New(err error) *FakeReviewer { 19 | return &FakeReviewer{ 20 | err: err, 21 | } 22 | } 23 | 24 | func (f *FakeReviewer) Create(ctx context.Context, req *azv1.SubjectAccessReview, co metav1.CreateOptions) (*azv1.SubjectAccessReview, error) { 25 | if f.err != nil { 26 | return nil, f.err 27 | } 28 | 29 | if req.Spec.ResourceAttributes.Resource == "users" && req.Spec.ResourceAttributes.Name == "jjackson" { 30 | req.Status = azv1.SubjectAccessReviewStatus{ 31 | Allowed: true, 32 | } 33 | 34 | return req, nil 35 | } 36 | 37 | if req.Spec.ResourceAttributes.Resource == "groups" && req.Spec.ResourceAttributes.Name == "group3" { 38 | req.Status = azv1.SubjectAccessReviewStatus{ 39 | Allowed: true, 40 | } 41 | 42 | return req, nil 43 | } 44 | 45 | if req.Spec.ResourceAttributes.Resource == "uids" && req.Spec.ResourceAttributes.Name == "1-2-3-4" { 46 | req.Status = azv1.SubjectAccessReviewStatus{ 47 | Allowed: true, 48 | } 49 | 50 | return req, nil 51 | } 52 | 53 | if req.Spec.ResourceAttributes.Resource == "userextras" && req.Spec.ResourceAttributes.Subresource == "remoteaddr" && req.Spec.ResourceAttributes.Name == "1.2.3.4" { 54 | req.Status = azv1.SubjectAccessReviewStatus{ 55 | Allowed: true, 56 | } 57 | 58 | return req, nil 59 | } 60 | 61 | // not an expcted test, or didn't conform to known allowed, fail 62 | req.Status = azv1.SubjectAccessReviewStatus{ 63 | Allowed: false, 64 | } 65 | 66 | return req, nil 67 | 68 | } 69 | -------------------------------------------------------------------------------- /pkg/proxy/tokenreview/tokenreview.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package tokenreview 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "time" 10 | 11 | authv1 "k8s.io/api/authentication/v1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/client-go/kubernetes" 14 | clientauthv1 "k8s.io/client-go/kubernetes/typed/authentication/v1" 15 | "k8s.io/client-go/rest" 16 | 17 | "github.com/jetstack/kube-oidc-proxy/pkg/util" 18 | ) 19 | 20 | var ( 21 | timeout = time.Second * 10 22 | ) 23 | 24 | type TokenReview struct { 25 | reviewRequester clientauthv1.TokenReviewInterface 26 | audiences []string 27 | } 28 | 29 | func New(restConfig *rest.Config, audiences []string) (*TokenReview, error) { 30 | kubeclient, err := kubernetes.NewForConfig(restConfig) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | return &TokenReview{ 36 | reviewRequester: kubeclient.AuthenticationV1().TokenReviews(), 37 | audiences: audiences, 38 | }, nil 39 | } 40 | 41 | func (t *TokenReview) Review(req *http.Request) (bool, error) { 42 | token, ok := util.ParseTokenFromRequest(req) 43 | if !ok { 44 | return false, errors.New("bearer token not found in request") 45 | } 46 | 47 | review := t.buildReview(token) 48 | 49 | ctx, cancel := context.WithTimeout(req.Context(), timeout) 50 | defer cancel() 51 | 52 | resp, err := t.reviewRequester.Create(ctx, review, metav1.CreateOptions{}) 53 | if err != nil { 54 | return false, err 55 | } 56 | 57 | if len(resp.Status.Error) > 0 { 58 | return false, fmt.Errorf("error authenticating using token review: %s", 59 | resp.Status.Error) 60 | } 61 | 62 | return resp.Status.Authenticated, nil 63 | } 64 | 65 | func (t *TokenReview) buildReview(token string) *authv1.TokenReview { 66 | return &authv1.TokenReview{ 67 | Spec: authv1.TokenReviewSpec{ 68 | Token: token, 69 | Audiences: t.audiences, 70 | }, 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /deploy/charts/kube-oidc-proxy/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "kube-oidc-proxy.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "kube-oidc-proxy.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "kube-oidc-proxy.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "kube-oidc-proxy.labels" -}} 38 | app.kubernetes.io/name: {{ include "kube-oidc-proxy.name" . }} 39 | helm.sh/chart: {{ include "kube-oidc-proxy.chart" . }} 40 | app.kubernetes.io/instance: {{ .Release.Name }} 41 | {{- if .Chart.AppVersion }} 42 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 43 | {{- end }} 44 | app.kubernetes.io/managed-by: {{ .Release.Service }} 45 | {{- end -}} 46 | 47 | {{/* 48 | Required claims serialized to CLI argument 49 | */}} 50 | {{- define "requiredClaims" -}} 51 | {{- if .Values.oidc.requiredClaims -}} 52 | {{- $local := (list) -}} 53 | {{- range $k, $v := .Values.oidc.requiredClaims -}} 54 | {{- $local = (printf "%s=%s" $k $v | append $local) -}} 55 | {{- end -}} 56 | {{ join "," $local }} 57 | {{- end -}} 58 | {{- end -}} 59 | -------------------------------------------------------------------------------- /demo/manifests/components/cert-manager.jsonnet: -------------------------------------------------------------------------------- 1 | local kube = import '../vendor/kube-prod-runtime/lib/kube.libsonnet'; 2 | local cert_manager_manifests = import './cert-manager/cert-manager.json'; 3 | local apiGroup = 'cert-manager.io/v1alpha2'; 4 | 5 | { 6 | ca_secret_name:: 'ca-key-pair', 7 | 8 | p:: '', 9 | metadata:: { 10 | metadata+: { 11 | namespace: 'kubeprod', 12 | }, 13 | }, 14 | letsencrypt_contact_email:: error 'Letsencrypt contact e-mail is undefined', 15 | 16 | // create simple to use certificate resource 17 | Certificate(namespace, name, issuer, solver, domains):: kube._Object(apiGroup, 'Certificate', name) + { 18 | metadata+: { 19 | namespace: namespace, 20 | name: name, 21 | }, 22 | spec+: { 23 | secretName: name + '-tls', 24 | dnsNames: domains, 25 | issuerRef: { 26 | name: issuer.metadata.name, 27 | kind: issuer.kind, 28 | }, 29 | }, 30 | }, 31 | 32 | // Letsencrypt environments 33 | letsencrypt_environments:: { 34 | prod: $.letsencryptProd.metadata.name, 35 | staging: $.letsencryptStaging.metadata.name, 36 | }, 37 | // Letsencrypt environment (defaults to the production one) 38 | letsencrypt_environment:: 'prod', 39 | 40 | Issuer(name):: kube._Object(apiGroup, 'Issuer', name) { 41 | }, 42 | 43 | ClusterIssuer(name):: kube._Object(apiGroup, 'ClusterIssuer', name) { 44 | }, 45 | 46 | deploy: cert_manager_manifests, 47 | 48 | letsencryptStaging: $.ClusterIssuer($.p + 'letsencrypt-staging') { 49 | local this = self, 50 | spec+: { 51 | acme+: { 52 | server: 'https://acme-staging-v02.api.letsencrypt.org/directory', 53 | email: $.letsencrypt_contact_email, 54 | privateKeySecretRef: { name: this.metadata.name }, 55 | http01: {}, 56 | }, 57 | }, 58 | }, 59 | 60 | letsencryptProd: $.letsencryptStaging { 61 | metadata+: { name: $.p + 'letsencrypt-prod' }, 62 | spec+: { 63 | acme+: { 64 | server: 'https://acme-v02.api.letsencrypt.org/directory', 65 | }, 66 | }, 67 | }, 68 | 69 | solvers+:: [], 70 | } 71 | -------------------------------------------------------------------------------- /docs/tasks/development-testing.md: -------------------------------------------------------------------------------- 1 | # Development Testing 2 | 3 | In order to help development for the proxy, there are a few tools in place for 4 | quick testing. 5 | 6 | # Creating a Cluster 7 | 8 | Use `make dev_cluster_create` to spin up a kind cluster locally. This will also 9 | build the proxy and other tooling from source, build their images, and load them 10 | onto each node. 11 | 12 | # Deploying the Proxy 13 | 14 | This will build the proxy and other tooling from source,build the images, and 15 | load them onto each node. This will then deploy the proxy alongside a fake OIDC 16 | issuer so that the proxy is fully functional. The proxy will then be reachable 17 | from a node port service in the cluster. 18 | 19 | 20 | ```bash 21 | make dev_cluster_deploy 22 | ``` 23 | 24 | This command will output a signed OIDC token that is valid for the proxy. You 25 | can then make calls to the proxy, like the following: 26 | 27 | ```bash 28 | curl -k https://172.17.0.2:30226 -H 'Authorization: bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.ewoJImlzcyI6Imh0dHBzOi8vb2lkYy1pc3N1ZXItZTJlLmt1YmUtb2lkYy1wcm94eS1lMmUtNmhiNGcuc3ZjLmNsdXN0ZXIubG9jYWw6NjQ0MyIsCgkiYXVkIjpbImt1YmUtb2lkYy1wcm94eS1lMmUtY2xpZW50LWlkIiwiYXVkLTIiXSwKCSJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLAoJImdyb3VwcyI6WyJncm91cC0xIiwiZ3JvdXAtMiJdLAoJImV4cCI6MTU4MjU1NTYzMQoJfQ.qWCM5zUHGslmwbgyZnMjhVeCLJd3R3c7xjtatjT_pv1VY-PpJ8IGBsbcCpur1fAm2CAbr0juM3yzwV1S3TUjhNhE8Wo6rxjA2Flnmwj7Nn2Got6T_cMFHQ_3A6YC72qkMwH-7SvXFB-C5Bk96vi9-clrxJ_b1XjfMPViZEVCJphh9HVzrZ5DPOAR0PDl-qnVys_CRkF0NEwEvAZL5SFumBqjtLBI9XUlWbB6VTljPOExL1zkv8NevZF8DxVsYFaW9HOYH8vNgC07kj_oUVkmAjP-2tVngcBKka0IBmuz2r-RfWNy9VJ-yb19AbtJNw6fjASy7O6VifuH4ZpjP5JSIg' 29 | ``` 30 | 31 | You are also able to deploy a server that the proxy connects to. This is useful 32 | for checking the headers and request body sent to the target server by the 33 | proxy which are present in the server logs. To enable this, set the following 34 | environment variable: 35 | 36 | ```bash 37 | KUBE_OIDC_PROXY_FAKE_APISERVER=true make dev_cluster_deploy 38 | ``` 39 | 40 | # Delete the cluster 41 | 42 | To delete the test kind cluster, use `make dev_cluster_destroy`. 43 | -------------------------------------------------------------------------------- /pkg/util/flags/string_to_string_slice_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package flags 3 | 4 | import ( 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestStringToStringSliceSet(t *testing.T) { 10 | tests := map[string]struct { 11 | val string 12 | expError bool 13 | expValues map[string][]string 14 | }{ 15 | "if empty string set, no values": { 16 | val: "", 17 | expError: false, 18 | expValues: make(map[string][]string), 19 | }, 20 | "if only key then error": { 21 | val: "key1", 22 | expError: true, 23 | expValues: make(map[string][]string), 24 | }, 25 | "if single key value return": { 26 | val: "key1=foo", 27 | expError: false, 28 | expValues: map[string][]string{ 29 | "key1": []string{"foo"}, 30 | }, 31 | }, 32 | "if two keys with two values return": { 33 | val: "key1=foo,key2=bar", 34 | expError: false, 35 | expValues: map[string][]string{ 36 | "key1": []string{"foo"}, 37 | "key2": []string{"bar"}, 38 | }, 39 | }, 40 | "if 3 keys with 5 values return": { 41 | val: "key1=foo,key2=bar,key1=a,key2=c,key3=c", 42 | expError: false, 43 | expValues: map[string][]string{ 44 | "key1": []string{"foo", "a"}, 45 | "key2": []string{"bar", "c"}, 46 | "key3": []string{"c"}, 47 | }, 48 | }, 49 | "if key with no value error": { 50 | val: "key1=foo,key2=bar,key1=a,key2=c,key3", 51 | expError: true, 52 | expValues: make(map[string][]string), 53 | }, 54 | } 55 | 56 | for name, test := range tests { 57 | t.Run(name, func(t *testing.T) { 58 | s := new(stringToStringSliceValue) 59 | 60 | err := s.Set(test.val) 61 | if test.expError != (err != nil) { 62 | t.Errorf("got unexpected error: %v", err) 63 | t.FailNow() 64 | } 65 | 66 | match := true 67 | if s.values == nil { 68 | if test.expValues != nil { 69 | match = false 70 | } 71 | } else if !reflect.DeepEqual(test.expValues, *s.values) { 72 | match = false 73 | } 74 | 75 | if !match { 76 | t.Errorf("unexpected values, exp=%v got=%v", test.expValues, *s.values) 77 | } 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /hack/update-vendor.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2019 The Kubernetes Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | set -o errexit 17 | set -o nounset 18 | set -o pipefail 19 | 20 | REPO_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. 21 | 22 | # Usage: 23 | # hack/pin-dependency.sh $MODULE $SHA-OR-TAG 24 | # 25 | # Example: 26 | # hack/pin-dependency.sh github.com/docker/docker 501cb131a7b7 27 | 28 | # Explicitly opt into go modules, even though we're inside a GOPATH directory 29 | export GO111MODULE=on 30 | # Explicitly clear GOPATH, to ensure nothing this script calls makes use of that path info 31 | export GOPATH= 32 | # Explicitly clear GOFLAGS, since GOFLAGS=-mod=vendor breaks dependency resolution while rebuilding vendor 33 | export GOFLAGS= 34 | # Detect problematic GOPROXY settings that prevent lookup of dependencies 35 | if [[ "${GOPROXY:-}" == "off" ]]; then 36 | echo "Cannot run hack/pin-dependency.sh with \$GOPROXY=off" 37 | exit 1 38 | fi 39 | 40 | dep="${1:-}" 41 | sha="${2:-}" 42 | if [[ -z "${dep}" || -z "${sha}" ]]; then 43 | echo "Usage:" 44 | echo " hack/pin-dependency.sh \$MODULE \$SHA-OR-TAG" 45 | echo "" 46 | echo "Example:" 47 | echo " hack/pin-dependency.sh github.com/docker/docker 501cb131a7b7" 48 | echo "" 49 | exit 1 50 | fi 51 | 52 | # Add the require directive 53 | echo "Running: go get ${dep}@${sha}" 54 | go get -d "${dep}@${sha}" 55 | 56 | # Find the resolved version 57 | rev=$(go mod edit -json | jq -r ".Require[] | select(.Path == \"${dep}\") | .Version") 58 | echo "Resolved to ${dep}@${rev}" 59 | 60 | # Add the replace directive 61 | echo "Running: go mod edit -replace ${dep}=${dep}@${rev}" 62 | go mod edit -replace "${dep}=${dep}@${rev}" 63 | -------------------------------------------------------------------------------- /pkg/util/flags/string_to_string_slice.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package flags 3 | 4 | import ( 5 | "bytes" 6 | "encoding/csv" 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/spf13/pflag" 11 | ) 12 | 13 | // This is a struct to implement the pflag.Value interface to introduce a 14 | // map[string][]string flag type. 15 | type stringToStringSliceValue struct { 16 | values *map[string][]string 17 | } 18 | 19 | var _ pflag.Value = &stringToStringSliceValue{} 20 | 21 | // NewStringToStringSliceValue returns a pflag.Value interface that implements 22 | // a flag that takes values into the map[string][]slice data structure. 23 | func NewStringToStringSliceValue(p *map[string][]string) pflag.Value { 24 | return &stringToStringSliceValue{ 25 | values: p, 26 | } 27 | } 28 | 29 | // This format is expecting a list of key value pairs, seperated by commas. A 30 | // single index may have multiple entries. 31 | // e.g.: a=-7,b=2,a=3 32 | func (s *stringToStringSliceValue) Set(val string) error { 33 | if s.values == nil { 34 | m := make(map[string][]string) 35 | s.values = &m 36 | } 37 | 38 | if *s.values == nil { 39 | *s.values = make(map[string][]string) 40 | } 41 | 42 | if len(val) == 0 { 43 | return nil 44 | } 45 | 46 | var ss []string 47 | 48 | r := csv.NewReader(strings.NewReader(val)) 49 | var err error 50 | ss, err = r.Read() 51 | if err != nil { 52 | *s.values = make(map[string][]string) 53 | return err 54 | } 55 | 56 | for _, pair := range ss { 57 | kv := strings.Split(pair, "=") 58 | if len(kv) != 2 { 59 | *s.values = make(map[string][]string) 60 | return fmt.Errorf("%s must be formatted as key=value", pair) 61 | } 62 | 63 | (*s.values)[kv[0]] = append((*s.values)[kv[0]], kv[1]) 64 | } 65 | 66 | return nil 67 | } 68 | 69 | func (s *stringToStringSliceValue) Type() string { 70 | return "stringToStringSlice" 71 | } 72 | 73 | func (s *stringToStringSliceValue) String() string { 74 | var records []string 75 | for k, vs := range *s.values { 76 | for _, v := range vs { 77 | records = append(records, k+"="+v) 78 | } 79 | } 80 | 81 | var buf bytes.Buffer 82 | w := csv.NewWriter(&buf) 83 | if err := w.Write(records); err != nil { 84 | panic(err) 85 | } 86 | 87 | w.Flush() 88 | return "[" + strings.TrimSpace(buf.String()) + "]" 89 | } 90 | -------------------------------------------------------------------------------- /test/e2e/framework/helper/token.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package helper 3 | 4 | import ( 5 | "crypto/tls" 6 | "crypto/x509" 7 | "fmt" 8 | "net/http" 9 | "net/url" 10 | "time" 11 | 12 | jose "gopkg.in/square/go-jose.v2" 13 | "k8s.io/client-go/rest" 14 | 15 | "github.com/jetstack/kube-oidc-proxy/test/util" 16 | ) 17 | 18 | func (h *Helper) NewValidRestConfig(issuerBundle, proxyBundle *util.KeyBundle, 19 | issuerURL, proxyURL *url.URL, clientID string) (*rest.Config, error) { 20 | 21 | // Valid token with exp in 10 minutes 22 | tokenPayload := h.NewTokenPayload(issuerURL, clientID, 23 | time.Now().Add(time.Minute*10)) 24 | signedToken, err := h.SignToken(issuerBundle, tokenPayload) 25 | if err != nil { 26 | return nil, fmt.Errorf("failed to sign token %q: %s", tokenPayload, err) 27 | } 28 | 29 | certPool := x509.NewCertPool() 30 | if ok := certPool.AppendCertsFromPEM(proxyBundle.CertBytes); !ok { 31 | return nil, fmt.Errorf("failed to append proxy cert data to cert pool %s", proxyBundle.CertBytes) 32 | } 33 | 34 | return &rest.Config{ 35 | Host: proxyURL.String(), 36 | Burst: rest.DefaultBurst, 37 | BearerToken: signedToken, 38 | Transport: &http.Transport{ 39 | TLSClientConfig: &tls.Config{ 40 | RootCAs: certPool, 41 | }, 42 | }, 43 | }, nil 44 | } 45 | 46 | func (h *Helper) SignToken(issuerBundle *util.KeyBundle, tokenPayload []byte) (string, error) { 47 | signer, err := jose.NewSigner(jose.SigningKey{ 48 | Algorithm: jose.SignatureAlgorithm("RS256"), 49 | Key: issuerBundle.Key, 50 | }, nil) 51 | if err != nil { 52 | return "", fmt.Errorf("failed to initialise new jwt signer: %s", err) 53 | } 54 | 55 | jwt, err := signer.Sign(tokenPayload) 56 | if err != nil { 57 | return "", fmt.Errorf("failed to sign jwt: %s", err) 58 | } 59 | 60 | signedToken, err := jwt.CompactSerialize() 61 | if err != nil { 62 | return "", err 63 | } 64 | 65 | return signedToken, nil 66 | } 67 | 68 | func (h *Helper) NewTokenPayload(issuerURL *url.URL, clientID string, exp time.Time) []byte { 69 | // Valid for 10 mins 70 | return []byte(fmt.Sprintf(`{ 71 | "iss":"%s", 72 | "aud":["%s","aud-2"], 73 | "email":"user@example.com", 74 | "groups":["group-1","group-2"], 75 | "exp":%d 76 | }`, issuerURL, clientID, exp.Unix())) 77 | } 78 | -------------------------------------------------------------------------------- /demo/manifests/components/landingpage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Kube-OIDC-Proxy 7 | 8 | 9 | 10 | 11 | 29 | 30 | 31 | 43 |
44 |
45 |

46 |

Kube-OIDC-Proxy Demo

47 |
48 |
This demo will help you to understand the use cases for kube-oidc-proxy.
49 |
50 |
51 | #CONTENT# 52 |
53 |

54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /cmd/app/options/misc.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package options 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "runtime" 8 | 9 | "github.com/spf13/pflag" 10 | apimachineryversion "k8s.io/apimachinery/pkg/version" 11 | cliflag "k8s.io/component-base/cli/flag" 12 | "k8s.io/component-base/cli/globalflag" 13 | ) 14 | 15 | type MiscOptions struct { 16 | gitMajor string // major version, always numeric 17 | gitMinor string // minor version, numeric possibly followed by "+" 18 | gitVersion string 19 | gitCommit string // sha1 from git, output of $(git rev-parse HEAD) 20 | gitTreeState string // state of git tree, either "clean" or "dirty" 21 | 22 | buildDate string // build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ') 23 | } 24 | 25 | var ( 26 | gitMajor string // major version, always numeric 27 | gitMinor string // minor version, numeric possibly followed by "+" 28 | gitVersion = "v0.0.0-master+$Format:%h$" 29 | gitCommit = "$Format:%H$" // sha1 from git, output of $(git rev-parse HEAD) 30 | gitTreeState = "" // state of git tree, either "clean" or "dirty" 31 | 32 | buildDate = "1970-01-01T00:00:00Z" // build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ') 33 | ) 34 | 35 | func NewMiscOptions(nfs *cliflag.NamedFlagSets) *MiscOptions { 36 | m := &MiscOptions{ 37 | gitMajor: gitMajor, 38 | gitMinor: gitMinor, 39 | gitVersion: gitVersion, 40 | gitCommit: gitCommit, 41 | gitTreeState: gitTreeState, 42 | buildDate: buildDate, 43 | } 44 | 45 | return m.AddFlags(nfs.FlagSet("Misc")) 46 | } 47 | 48 | func (m *MiscOptions) AddFlags(fs *pflag.FlagSet) *MiscOptions { 49 | globalflag.AddGlobalFlags(fs, AppName) 50 | fs.Bool("version", false, "Print version information and quit") 51 | return m 52 | } 53 | 54 | func (m *MiscOptions) PrintVersionAndExit() { 55 | fmt.Printf("%s version: %#v\n", AppName, 56 | apimachineryversion.Info{ 57 | Major: m.gitMajor, 58 | Minor: m.gitMinor, 59 | GitVersion: m.gitVersion, 60 | GitCommit: m.gitCommit, 61 | GitTreeState: m.gitTreeState, 62 | BuildDate: m.buildDate, 63 | GoVersion: runtime.Version(), 64 | Compiler: runtime.Compiler, 65 | Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), 66 | }, 67 | ) 68 | 69 | os.Exit(0) 70 | } 71 | -------------------------------------------------------------------------------- /demo/manifests/vendor/kube-prod-runtime/components/version.jsonnet: -------------------------------------------------------------------------------- 1 | /* 2 | * Bitnami Kubernetes Production Runtime - A collection of services that makes it 3 | * easy to run production workloads in Kubernetes. 4 | * 5 | * Copyright 2018-2019 Bitnami 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | local kube = import "../lib/kube.libsonnet"; 21 | 22 | local trim = function(str) ( 23 | if std.startsWith(str, " ") || std.startsWith(str, "\n") then 24 | trim(std.substr(str, 1, std.length(str) - 1)) 25 | else if std.endsWith(str, " ") || std.endsWith(str, "\n") then 26 | trim(std.substr(str, 0, std.length(str) - 1)) 27 | else 28 | str 29 | ); 30 | 31 | local VERSION = trim(importstr "../VERSION"); 32 | 33 | { 34 | p:: "", 35 | metadata:: { 36 | metadata+: { 37 | namespace: "kubeprod", 38 | }, 39 | }, 40 | 41 | // This is intended as a publicly available place to see the details 42 | // of the BKPR install. If you ever want to know which version of 43 | // BKPR is currently installed, this is the place you should look. 44 | config: kube.ConfigMap("release") + $.metadata { 45 | data+: { 46 | release: VERSION, 47 | // There may be additional fields here in future 48 | }, 49 | }, 50 | 51 | readerRole: kube.Role($.p + "release-reader") + $.metadata { 52 | rules: [ 53 | { 54 | apiGroups: [""], 55 | resources: ["configmap"], 56 | resourceNames: [$.config.metadata.name], 57 | verbs: ["get", "list", "watch"], 58 | }, 59 | ], 60 | }, 61 | 62 | readerRoleBinding: kube.RoleBinding($.p + "release-read-public") + $.metadata { 63 | roleRef_: $.readerRole, 64 | subjects: [{ 65 | kind: "Group", 66 | name: "system:authenticated", 67 | apiGroup: "rbac.authorization.k8s.io", 68 | }], 69 | }, 70 | } 71 | -------------------------------------------------------------------------------- /test/tools/fake-apiserver/pkg/server/server.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package server 3 | 4 | import ( 5 | "encoding/pem" 6 | "fmt" 7 | "io/ioutil" 8 | "net" 9 | "net/http" 10 | 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | type Server struct { 15 | keyFile, certFile string 16 | 17 | stopCh <-chan struct{} 18 | } 19 | 20 | func New(keyFile, certFile string, stopCh <-chan struct{}) (*Server, error) { 21 | b, err := ioutil.ReadFile(keyFile) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | block, _ := pem.Decode(b) 27 | if block == nil { 28 | return nil, 29 | fmt.Errorf("failed to parse PEM block containing the key: %q", keyFile) 30 | } 31 | 32 | return &Server{ 33 | keyFile: keyFile, 34 | certFile: certFile, 35 | stopCh: stopCh, 36 | }, nil 37 | } 38 | 39 | func (s *Server) Run(bindAddress, listenPort string) (<-chan struct{}, error) { 40 | serveAddr := fmt.Sprintf("%s:%s", bindAddress, listenPort) 41 | 42 | l, err := net.Listen("tcp", serveAddr) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | go func() { 48 | <-s.stopCh 49 | if l != nil { 50 | l.Close() 51 | } 52 | }() 53 | 54 | compCh := make(chan struct{}) 55 | go func() { 56 | defer close(compCh) 57 | 58 | err := http.ServeTLS(l, s, s.certFile, s.keyFile) 59 | if err != nil { 60 | log.Errorf("stopped serving TLS (%s): %s", serveAddr, err) 61 | } 62 | }() 63 | 64 | log.Infof("fake API server listening and serving on %s", serveAddr) 65 | 66 | return compCh, nil 67 | } 68 | 69 | func (s *Server) ServeHTTP(rw http.ResponseWriter, r *http.Request) { 70 | log.Infof("(%s) Fake API server received url %s", r.URL, r.RemoteAddr) 71 | 72 | log.Infof("(%s) Request headers:", r.RemoteAddr) 73 | for k, vs := range r.Header { 74 | for _, v := range vs { 75 | log.Infof("(%s) %s: %s", r.RemoteAddr, k, v) 76 | rw.Header().Add(k, v) 77 | } 78 | } 79 | 80 | body, err := ioutil.ReadAll(r.Body) 81 | if err != nil { 82 | log.Errorf("failed to read request body: %s", err) 83 | rw.WriteHeader(http.StatusInternalServerError) 84 | return 85 | } 86 | 87 | log.Infof("(%s) Request Body: %s", r.RemoteAddr, body) 88 | 89 | if _, err := rw.Write(body); err != nil { 90 | log.Errorf("failed to write request body to response: %s", err) 91 | rw.WriteHeader(http.StatusInternalServerError) 92 | return 93 | } 94 | 95 | rw.WriteHeader(http.StatusOK) 96 | } 97 | -------------------------------------------------------------------------------- /pkg/proxy/tokenreview/tokenreview_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package tokenreview 3 | 4 | import ( 5 | "errors" 6 | "net/http" 7 | "reflect" 8 | "testing" 9 | 10 | authv1 "k8s.io/api/authentication/v1" 11 | 12 | "github.com/jetstack/kube-oidc-proxy/pkg/proxy/tokenreview/fake" 13 | ) 14 | 15 | type testT struct { 16 | reviewResp *authv1.TokenReview 17 | errResp error 18 | 19 | expAuth bool 20 | expErr error 21 | } 22 | 23 | func TestReview(t *testing.T) { 24 | 25 | tests := map[string]testT{ 26 | "if a create fails then this error is returned": { 27 | reviewResp: nil, 28 | errResp: errors.New("create error response"), 29 | expAuth: false, 30 | expErr: errors.New("create error response"), 31 | }, 32 | 33 | "if an error exists in the status of the response pass error back": { 34 | reviewResp: &authv1.TokenReview{ 35 | Status: authv1.TokenReviewStatus{ 36 | Error: "status error", 37 | }, 38 | }, 39 | errResp: nil, 40 | expAuth: false, 41 | expErr: errors.New("error authenticating using token review: status error"), 42 | }, 43 | 44 | "if the response returns unauthenticated, return false": { 45 | reviewResp: &authv1.TokenReview{ 46 | Status: authv1.TokenReviewStatus{ 47 | Authenticated: false, 48 | }, 49 | }, 50 | errResp: nil, 51 | expAuth: false, 52 | expErr: nil, 53 | }, 54 | 55 | "if the response returns authenticated, return true": { 56 | reviewResp: &authv1.TokenReview{ 57 | Status: authv1.TokenReviewStatus{ 58 | Authenticated: true, 59 | }, 60 | }, 61 | errResp: nil, 62 | expAuth: true, 63 | expErr: nil, 64 | }, 65 | } 66 | 67 | for name, test := range tests { 68 | t.Run(name, func(t *testing.T) { 69 | runTest(t, test) 70 | }) 71 | } 72 | } 73 | 74 | func runTest(t *testing.T, test testT) { 75 | tReviewer := &TokenReview{ 76 | audiences: nil, 77 | reviewRequester: fake.New().WithCreate(test.reviewResp, test.errResp), 78 | } 79 | 80 | authed, err := tReviewer.Review( 81 | &http.Request{ 82 | Header: map[string][]string{ 83 | "Authorization": []string{"bearer test-token"}, 84 | }, 85 | }, 86 | ) 87 | 88 | if !reflect.DeepEqual(test.expErr, err) { 89 | t.Errorf("got unexpected error, exp=%v got=%v", 90 | test.expErr, err) 91 | } 92 | 93 | if test.expAuth != authed { 94 | t.Errorf("got unexpected authed, exp=%t got=%t", 95 | test.expAuth, authed) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | ## DCO Sign off 3 | 4 | All authors to the project retain copyright to their work. However, to ensure 5 | that they are only submitting work that they have rights to, we are requiring 6 | everyone to acknowledge this by signing their work. 7 | 8 | Any copyright notices in this repo should specify the authors as "the Jetstack 9 | kube-oidc-proxy contributors". 10 | 11 | To sign your work, just add a line like this at the end of your commit message: 12 | 13 | ``` 14 | Signed-off-by: Joe Bloggs 15 | ``` 16 | 17 | This can easily be done with the `--signoff` option to `git commit`. 18 | You can also mass sign-off a whole PR with `git rebase --signoff master`, replacing 19 | `master` with the branch you are creating a pull request again if not master. 20 | 21 | By doing this you state that you can certify the following (from https://developercertificate.org/): 22 | 23 | ``` 24 | Developer Certificate of Origin 25 | Version 1.1 26 | 27 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 28 | 1 Letterman Drive 29 | Suite D4700 30 | San Francisco, CA, 94129 31 | 32 | Everyone is permitted to copy and distribute verbatim copies of this 33 | license document, but changing it is not allowed. 34 | 35 | 36 | Developer's Certificate of Origin 1.1 37 | 38 | By making a contribution to this project, I certify that: 39 | 40 | (a) The contribution was created in whole or in part by me and I 41 | have the right to submit it under the open source license 42 | indicated in the file; or 43 | 44 | (b) The contribution is based upon previous work that, to the best 45 | of my knowledge, is covered under an appropriate open source 46 | license and I have the right under that license to submit that 47 | work with modifications, whether created in whole or in part 48 | by me, under the same open source license (unless I am 49 | permitted to submit under a different license), as indicated 50 | in the file; or 51 | 52 | (c) The contribution was provided directly to me by some other 53 | person who certified (a), (b) or (c) and I have not modified 54 | it. 55 | 56 | (d) I understand and agree that this project and the contribution 57 | are public and that a record of the contribution (including all 58 | personal information I submit with it, including my sign-off) is 59 | maintained indefinitely and may be redistributed consistent with 60 | this project or the open source license(s) involved. 61 | ``` 62 | -------------------------------------------------------------------------------- /pkg/probe/probe_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package probe 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | "testing" 10 | 11 | "k8s.io/apiserver/pkg/authentication/authenticator" 12 | 13 | "github.com/jetstack/kube-oidc-proxy/pkg/util" 14 | ) 15 | 16 | type fakeTokenAuthenticator struct { 17 | returnErr bool 18 | } 19 | 20 | var _ authenticator.Token = &fakeTokenAuthenticator{} 21 | 22 | func (f *fakeTokenAuthenticator) AuthenticateToken(context.Context, string) (*authenticator.Response, bool, error) { 23 | if f.returnErr { 24 | return nil, false, errors.New("foo bar authenticator not initialized") 25 | } 26 | 27 | return nil, false, errors.New("some other error") 28 | } 29 | 30 | func TestRun(t *testing.T) { 31 | f := &fakeTokenAuthenticator{ 32 | returnErr: true, 33 | } 34 | 35 | port, err := util.FreePort() 36 | if err != nil { 37 | t.Error(err.Error()) 38 | t.FailNow() 39 | } 40 | 41 | fakeJWT, err := util.FakeJWT("issuer") 42 | if err != nil { 43 | t.Error(err.Error()) 44 | t.FailNow() 45 | } 46 | 47 | if err := Run(port, fakeJWT, f); err != nil { 48 | t.Error(err.Error()) 49 | t.FailNow() 50 | } 51 | 52 | url := fmt.Sprintf("http://0.0.0.0:%s", port) 53 | 54 | var resp *http.Response 55 | var i int 56 | 57 | for { 58 | resp, err = http.Get(url + "/ready") 59 | if err == nil { 60 | break 61 | } 62 | 63 | if i >= 5 { 64 | t.Errorf("unexpected error: %s", err) 65 | t.FailNow() 66 | } 67 | i++ 68 | } 69 | 70 | if resp.StatusCode != 503 { 71 | t.Errorf("expected ready probe to be responding and not ready, exp=%d got=%d", 72 | 503, resp.StatusCode) 73 | } 74 | 75 | f.returnErr = false 76 | 77 | resp, err = http.Get(url + "/ready") 78 | if err != nil { 79 | t.Fatalf("unexpected error: %s", err) 80 | } 81 | 82 | if resp.StatusCode != 200 { 83 | t.Errorf("expected ready probe to be responding and ready, exp=%d got=%d", 84 | 200, resp.StatusCode) 85 | } 86 | 87 | // Once the authenticator has returned with an non-initialised error, then 88 | // should always return ready 89 | 90 | f.returnErr = true 91 | 92 | resp, err = http.Get(url + "/ready") 93 | if err != nil { 94 | t.Fatalf("unexpected error: %s", err) 95 | } 96 | 97 | if resp.StatusCode != 200 { 98 | t.Errorf("expected ready probe to be responding and ready, exp=%d got=%d", 99 | 200, resp.StatusCode) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /GenGitChangeLog.py: -------------------------------------------------------------------------------- 1 | # Copyright Jetstack Ltd. See LICENSE for details. 2 | 3 | # Generates kube-oidc-proxy Changelog 4 | # Call from the branch with 3 parameters: 5 | # 1. Date from which to start looking 6 | # 2. Github Token 7 | 8 | # requires python-dateutil and requests from pip 9 | 10 | from subprocess import * 11 | import re 12 | from datetime import datetime 13 | import dateutil.parser 14 | import sys 15 | import requests 16 | 17 | 18 | 19 | def parseIssues(message): 20 | issuesRet = [] 21 | issues = re.findall('[#][0-9]+',message) 22 | if issues != None: 23 | for issue in issues: 24 | issuesRet.append(issue[1:]) 25 | return issuesRet 26 | 27 | 28 | def f4(seq): 29 | # order preserving 30 | noDupes = [] 31 | [noDupes.append(i) for i in seq if not noDupes.count(i)] 32 | return noDupes 33 | 34 | 35 | 36 | 37 | 38 | 39 | headers = {'Authorization':'token ' + sys.argv[2]} 40 | 41 | 42 | GIT_COMMIT_FIELDS = ['id', 'author_name', 'author_email', 'date', 'message'] 43 | GIT_LOG_FORMAT = ['%H', '%an', '%ae', '%ai', '%s'] 44 | GIT_LOG_FORMAT = '%x1f'.join(GIT_LOG_FORMAT) + '%x1e' 45 | 46 | #print repo.git.log(p=False) 47 | 48 | allIssues = [] 49 | 50 | p = Popen('git log --format="%s" ' % GIT_LOG_FORMAT, shell=True, stdout=PIPE) 51 | (logb, _) = p.communicate() 52 | log = str(logb,"utf-8") 53 | log = log.strip('\n\x1e').split("\x1e") 54 | log = [row.strip().split("\x1f") for row in log] 55 | log = [dict(zip(GIT_COMMIT_FIELDS, row)) for row in log] 56 | 57 | notbefore = dateutil.parser.parse(sys.argv[1] + ' 00:00:00 -0400') 58 | 59 | for commit in log: 60 | created = dateutil.parser.parse(commit['date']) 61 | if created > notbefore: 62 | message = commit['message'] 63 | allIssues.extend(parseIssues(message)) 64 | 65 | 66 | allIssues = f4(allIssues) 67 | 68 | bylabels = {} 69 | 70 | for issue in allIssues: 71 | issueURL = 'https://api.github.com/repos/TremoloSecurity/kube-oidc-proxy/issues/' + issue 72 | r = requests.get(issueURL,headers=headers) 73 | json = r.json(); 74 | 75 | if "labels" in json: 76 | for label in json['labels']: 77 | if not (label['name'] in bylabels): 78 | labelGroup = [] 79 | bylabels[label["name"]] = labelGroup 80 | labelGroup = bylabels[label['name']] 81 | labelGroup.append(json) 82 | 83 | 84 | for label in bylabels: 85 | print('**' + label + 's:**') 86 | for issue in bylabels[label]: 87 | print(' - ' + issue['title'] + ' [\\#' + str(issue['number']) + '](' + issue['html_url'] + ')') 88 | print() 89 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.9 2 | 3 | **tasks:** 4 | - 1.0.9 [\#64](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/64) 5 | 6 | **enhancements:** 7 | - Add flags to be able to configure kubernetes client throttling [\#65](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/65) 8 | 9 | # 1.0.8 10 | 11 | **tasks:** 12 | - 1.0.8 [\#61](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/61) 13 | 14 | # 1.0.7 15 | 16 | **enhancements:** 17 | - change oidc config to line up with new kube authenticator [\#55](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/55) 18 | 19 | **tasks:** 20 | - 1.0.7 Release [\#54](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/54) 21 | 22 | # 1.0.6 23 | 24 | **bugs:** 25 | - e2e tests failing to complete [\#45](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/45) 26 | - Auditing is not working anymore [\#39](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/39) 27 | 28 | **tasks:** 29 | - 1.0.6 build [\#41](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/41) 30 | 31 | # 1.0.5 32 | 33 | **tasks:** 34 | - 1.0.5 build [\#34](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/34) 35 | 36 | # 1.0.4 37 | 38 | **tasks:** 39 | - 1.0.4 build [\#29](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/29) 40 | 41 | # 1.0.3 42 | 43 | **enhancements:** 44 | - 1.0.3 release [\#26](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/26) 45 | 46 | # 1.0.2 47 | 48 | **bugs:** 49 | - CVE-2022-1996 [\#20](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/20) 50 | 51 | # 1.0.1 52 | 53 | **enhancements:** 54 | - 1.0.1 [\#14](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/14) 55 | 56 | **bugs:** 57 | - fix timing issues in e2e tests [\#18](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/18) 58 | - runtime error: slice bounds out of range [:-2] [\#17](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/17) 59 | 60 | # 1.0.0 61 | 62 | **enhancements:** 63 | - 1.0.0 Release [\#10](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/10) 64 | - Access logging to standard out [\#2](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/2) 65 | - create github action to automate builds [\#8](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/8) 66 | - Switch from alpine --> ubuntu 20.04 [\#9](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/9) 67 | - Support `kubectl --as` [\#3](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/3) 68 | - Upgrade KinD [\#1](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/1) 69 | 70 | **bugs:** 71 | - update dependencies [\#5](https://github.com/TremoloSecurity/kube-oidc-proxy/issues/5) 72 | 73 | -------------------------------------------------------------------------------- /patchlog.txt: -------------------------------------------------------------------------------- 1 | # Automatic Patch Log 2 | 3 | Automatic Update - 20211216T025617.851Z 4 | Automatic Update - 20220311T201831.205Z 5 | Automatic Update - 20220318T020336.250Z 6 | Automatic Update - 20220403T020307.625Z 7 | Automatic Update - 20220415T020318.763Z 8 | Automatic Update - 20220422T020314.907Z 9 | Automatic Update - 20220429T020319.615Z 10 | Automatic Update - 20220506T020321.057Z 11 | Automatic Update - 20220519T020313.080Z 12 | Automatic Update - 20220529T020344.385Z 13 | Automatic Update - 20220609T020332.886Z 14 | Automatic Update - 20220624T020345.647Z 15 | Automatic Update - 20220707T020312.276Z 16 | Automatic Update - 20220807T020800.447Z 17 | Automatic Update - 20220925T020336.718Z 18 | Automatic Update - 20221014T020348.280Z 19 | Automatic Update - 20221020T020349.425Z 20 | Automatic Update - 20221130T020413.421Z 21 | Automatic Update - 20221211T020437.196Z 22 | Automatic Update - 20230131T020526.940Z 23 | Automatic Update - 20230214T020526.050Z 24 | Automatic Update - 20230306T020416.148Z 25 | Automatic Update - 20230310T020312.894Z 26 | Automatic Update - 20230427T020320.801Z 27 | Automatic Update - 20230525T020428.881Z 28 | Automatic Update - 20230601T020342.442Z 29 | Automatic Update - 20230607T020351.116Z 30 | Automatic Update - 20230616T020248.766Z 31 | Automatic Update - 20231005T020301.893Z 32 | Automatic Update - 20231026T020312.403Z 33 | Automatic Update - 20231108T020319.537Z 34 | Automatic Update - 20231116T020337.574Z 35 | Automatic Update - 20231123T020342.310Z 36 | Automatic Update - 20231209T020329.153Z 37 | Automatic Update - 20231213T020333.022Z 38 | Automatic Update - 20240119T020325.770Z 39 | Automatic Update - 20240124T020357.219Z 40 | Automatic Update - 20240207T020401.740Z 41 | Automatic Update - 20240217T020411.845Z 42 | Automatic Update - 20240320T020336.148Z 43 | Automatic Update - 20240329T020424.531Z 44 | Automatic Update - 20240417T020415.214Z 45 | Automatic Update - 20240420T020411.899Z 46 | Automatic Update - 20240602T020353.512Z 47 | Automatic Update - 20240629T020356.989Z 48 | Automatic Update - 20240802T020420.511Z 49 | Automatic Update - 20240810T020429.835Z 50 | 51 | force rebuild 2024-09-15 52 | Automatic Update - 20250208T020419.731Z 53 | Automatic Update - 20250220T020420.716Z 54 | Automatic Update - 20250222T020423.982Z 55 | Automatic Update - 20250226T020423.793Z 56 | Automatic Update - 20250305T020424.511Z 57 | Automatic Update - 20250405T020413.953Z 58 | Automatic Update - 20250416T020355.722Z 59 | Automatic Update - 20250530T020358.267Z 60 | Automatic Update - 20250611T020416.398Z 61 | Automatic Update - 20250620T020418.225Z 62 | Automatic Update - 20250921T020340.284Z 63 | Automatic Update - 20250924T020329.316Z 64 | Automatic Update - 20250930T020342.328Z 65 | Automatic Update - 20251002T020421.143Z -------------------------------------------------------------------------------- /test/e2e/suite/cases/token/token.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package token 3 | 4 | import ( 5 | "bytes" 6 | "context" 7 | "fmt" 8 | "net/http" 9 | "net/url" 10 | "time" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | 15 | k8sErrors "k8s.io/apimachinery/pkg/api/errors" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | 18 | "github.com/jetstack/kube-oidc-proxy/test/e2e/framework" 19 | ) 20 | 21 | var _ = framework.CasesDescribe("Token", func() { 22 | f := framework.NewDefaultFramework("token") 23 | 24 | It("should error when tokens are wrong for the issuer", func() { 25 | By("No token should error") 26 | expectProxyUnauthorized(f, nil) 27 | 28 | By("Bad token should error") 29 | expectProxyUnauthorized(f, []byte("bad token")) 30 | 31 | By("Wrong issuer should error") 32 | badURL, err := url.Parse("incorrect-issuer.io") 33 | Expect(err).NotTo(HaveOccurred()) 34 | 35 | expectProxyUnauthorized(f, f.Helper().NewTokenPayload( 36 | badURL, f.ClientID(), time.Now().Add(time.Minute))) 37 | 38 | By("Wrong audience should error") 39 | expectProxyUnauthorized(f, f.Helper().NewTokenPayload( 40 | f.IssuerURL(), "wrong-aud", time.Now().Add(time.Minute))) 41 | 42 | By("Token expires now") 43 | expectProxyUnauthorized(f, f.Helper().NewTokenPayload( 44 | f.IssuerURL(), f.ClientID(), time.Now())) 45 | 46 | By("Valid token should return Kubernetes forbidden") 47 | client := f.NewProxyClient() 48 | 49 | // If does not return with Kubernetes forbidden error then error 50 | _, err = client.CoreV1().Pods(f.Namespace.Name).List(context.TODO(), metav1.ListOptions{}) 51 | if !k8sErrors.IsForbidden(err) { 52 | Expect(err).NotTo(HaveOccurred()) 53 | } 54 | }) 55 | }) 56 | 57 | func expectProxyUnauthorized(f *framework.Framework, tokenPayload []byte) { 58 | // Build client using given token payload 59 | signedToken, err := f.Helper().SignToken(f.IssuerKeyBundle(), tokenPayload) 60 | Expect(err).NotTo(HaveOccurred()) 61 | 62 | proxyConfig := f.NewProxyRestConfig() 63 | requester := f.Helper().NewRequester(proxyConfig.Transport, signedToken) 64 | 65 | // Send request with signed token to proxy 66 | target := fmt.Sprintf("%s/api/v1/namespaces/%s/pods", 67 | proxyConfig.Host, f.Namespace.Name) 68 | 69 | body, resp, err := requester.Get(target) 70 | body = bytes.TrimSpace(body) 71 | Expect(err).NotTo(HaveOccurred()) 72 | 73 | // Check body and status code the token was rejected 74 | if resp.StatusCode != http.StatusUnauthorized || 75 | !bytes.Equal(body, []byte("Unauthorized")) { 76 | Expect(fmt.Errorf("expected status code %d with body Unauthorized, got= %d %q", 77 | http.StatusUnauthorized, resp.StatusCode, body)).NotTo(HaveOccurred()) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /demo/manifests/components/base32.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | local base32_table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', 3 | local base32_inv = { [base32_table[i]]: i for i in std.range(0, 31) }, 4 | 5 | 6 | base32(input):: 7 | local bytes = 8 | if std.type(input) == 'string' then 9 | std.map(function(c) std.codepoint(c), input) 10 | else 11 | input; 12 | 13 | local oneChar(arr, i) = 14 | // 5 MSB of i 15 | base32_table[(arr[i] & 248) >> 3]; 16 | 17 | local twoChars(arr, i) = 18 | oneChar(arr, i) + 19 | // 3 LSB of i, 2 MSB of i+1 20 | base32_table[(arr[i] & 7) << 2 | (arr[i + 1] & 192) >> 6] + 21 | // 5 NSB of i+1 22 | base32_table[(arr[i + 1] & 62) >> 1]; 23 | 24 | local threeChars(arr, i) = 25 | twoChars(arr, i) + 26 | // 1 LSB of i+1, 4 MSB of i+2 27 | base32_table[(arr[i + 1] & 1) << 4 | (arr[i + 2] & 240) >> 4]; 28 | 29 | local fourChars(arr, i) = 30 | threeChars(arr, i) + 31 | // 4 LSB of i+2 + 1 MSB of i+3 32 | base32_table[(arr[i + 2] & 15) << 1 | (arr[i + 3] & 128) >> 7] + 33 | // 5 NSB of i+3 34 | base32_table[(arr[i + 3] & 124) >> 2]; 35 | 36 | local aux(arr, i, r) = 37 | if i >= std.length(arr) then 38 | r 39 | else if i + 1 >= std.length(arr) then 40 | local str = 41 | oneChar(arr, i) + 42 | // 3 LSB of i 43 | base32_table[(arr[i] & 7) << 2] + 44 | '======'; 45 | aux(arr, i + 5, r + str) tailstrict 46 | else if i + 2 >= std.length(arr) then 47 | local str = 48 | twoChars(arr, i) + 49 | // 1 LSB of i+1 50 | base32_table[(arr[i + 1] & 1) << 4] + 51 | '===='; 52 | aux(arr, i + 5, r + str) tailstrict 53 | else if i + 3 >= std.length(arr) then 54 | local str = 55 | threeChars(arr, i) + 56 | // 4 LSB of i+2 57 | base32_table[(arr[i + 2] & 15) << 1] + 58 | '==='; 59 | aux(arr, i + 5, r + str) tailstrict 60 | else if i + 4 >= std.length(arr) then 61 | local str = 62 | fourChars(arr, i) + 63 | // 2 LSB of i+3 64 | base32_table[(arr[i + 3] & 3) << 3] + 65 | '='; 66 | aux(arr, i + 5, r + str) tailstrict 67 | else 68 | local str = 69 | fourChars(arr, i) + 70 | // 2 LSB of i+3, 3 MSB of i+4 71 | base32_table[(arr[i + 3] & 3) << 3 | (arr[i + 4] & 224) >> 5] + 72 | // 5 LSB 73 | base32_table[(arr[i + 4] & 31)]; 74 | aux(arr, i + 5, r + str) tailstrict; 75 | 76 | local sanity = std.foldl(function(r, a) r && (a < 256), bytes, true); 77 | if !sanity then 78 | error 'Can only base32 encode strings / arrays of single bytes.' 79 | else 80 | aux(bytes, 0, ''), 81 | 82 | 83 | } 84 | -------------------------------------------------------------------------------- /test/environment/environment.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package environment 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "k8s.io/client-go/kubernetes" 10 | "sigs.k8s.io/kind/pkg/cluster/nodes" 11 | 12 | "github.com/jetstack/kube-oidc-proxy/test/kind" 13 | ) 14 | 15 | const ( 16 | defaultNodeImage = "1.32.0" 17 | defaultRootPath = "../../." 18 | ) 19 | 20 | type Environment struct { 21 | kind *kind.Kind 22 | 23 | rootPath string 24 | nodeImage string 25 | } 26 | 27 | func New(masterNodesCount, workerNodesCount int) (*Environment, error) { 28 | nodeImage := os.Getenv("KUBE_OIDC_PROXY_K8S_VERSION") 29 | if nodeImage == "" { 30 | nodeImage = defaultNodeImage 31 | } 32 | nodeImage = fmt.Sprintf("kindest/node:v%s", nodeImage) 33 | 34 | rootPath, err := RootPath() 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | kind := kind.New(rootPath, nodeImage, masterNodesCount, workerNodesCount) 40 | 41 | return &Environment{ 42 | rootPath: rootPath, 43 | nodeImage: nodeImage, 44 | kind: kind, 45 | }, nil 46 | } 47 | 48 | func (e *Environment) Create() error { 49 | if err := e.kind.Create(); err != nil { 50 | return fmt.Errorf("failed to create kind cluster: %s", err) 51 | } 52 | 53 | if err := e.kind.LoadAllImages(); err != nil { 54 | return err 55 | } 56 | 57 | return nil 58 | } 59 | 60 | func (e *Environment) Destory() error { 61 | if e.kind != nil { 62 | if err := e.kind.Destroy(); err != nil { 63 | return err 64 | } 65 | } 66 | 67 | return nil 68 | } 69 | 70 | func (e *Environment) KubeClient() *kubernetes.Clientset { 71 | return e.kind.KubeClient() 72 | } 73 | 74 | func (e *Environment) KubeConfigPath() string { 75 | return e.kind.KubeConfigPath() 76 | } 77 | 78 | func (e *Environment) RootPath() string { 79 | return e.rootPath 80 | } 81 | 82 | func (e *Environment) Node(name string) (*nodes.Node, error) { 83 | ns, err := e.kind.Nodes() 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | var node *nodes.Node 89 | for _, n := range ns { 90 | if n.String() == name { 91 | node = &n 92 | break 93 | } 94 | } 95 | 96 | if node == nil { 97 | return nil, fmt.Errorf("failed to find node %q", name) 98 | } 99 | 100 | return node, nil 101 | } 102 | 103 | func (e *Environment) Kind() *kind.Kind { 104 | return e.kind 105 | } 106 | 107 | func RootPath() (string, error) { 108 | rootPath := os.Getenv("KUBE_OIDC_PROXY_ROOT_PATH") 109 | if rootPath == "" { 110 | rootPath = defaultRootPath 111 | } 112 | 113 | rootPath, err := filepath.Abs(rootPath) 114 | if err != nil { 115 | return "", fmt.Errorf("failed to get absolute path %q: %s", 116 | rootPath, err) 117 | } 118 | 119 | return rootPath, nil 120 | } 121 | -------------------------------------------------------------------------------- /pkg/proxy/logging/accesslog.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package logging 3 | 4 | import ( 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | "time" 9 | 10 | "k8s.io/apiserver/pkg/authentication/user" 11 | ) 12 | 13 | const ( 14 | UserHeaderClientIPKey = "Remote-Client-IP" 15 | timestampLayout = "2006-01-02T15:04:05-0700" 16 | ) 17 | 18 | // logs the request 19 | func LogSuccessfulRequest(req *http.Request, inboundUser user.Info, outboundUser user.Info) { 20 | remoteAddr := req.RemoteAddr 21 | indexOfColon := strings.Index(remoteAddr, ":") 22 | if indexOfColon > 0 { 23 | remoteAddr = remoteAddr[0:indexOfColon] 24 | } 25 | 26 | inboundExtras := "" 27 | 28 | if inboundUser.GetExtra() != nil { 29 | for key, value := range inboundUser.GetExtra() { 30 | inboundExtras += key + "=" + strings.Join(value, "|") + " " 31 | } 32 | } 33 | 34 | outboundUserLog := "" 35 | 36 | if outboundUser != nil { 37 | outboundExtras := "" 38 | 39 | if outboundUser.GetExtra() != nil { 40 | for key, value := range outboundUser.GetExtra() { 41 | outboundExtras += key + "=" + strings.Join(value, "|") + " " 42 | } 43 | } 44 | 45 | outboundUserLog = fmt.Sprintf(" outbound:[%s / %s / %s / %s]", outboundUser.GetName(), strings.Join(outboundUser.GetGroups(), "|"), outboundUser.GetUID(), outboundExtras) 46 | } 47 | 48 | xFwdFor := findXForwardedFor(req.Header, remoteAddr) 49 | 50 | fmt.Printf("[%s] AuSuccess src:[%s / % s] URI:%s inbound:[%s / %s / %s]%s\n", time.Now().Format(timestampLayout), remoteAddr, xFwdFor, req.RequestURI, inboundUser.GetName(), strings.Join(inboundUser.GetGroups(), "|"), inboundExtras, outboundUserLog) 51 | } 52 | 53 | // determines if the x-forwarded-for header is present, if so remove 54 | // the remoteaddr since it is repetitive 55 | func findXForwardedFor(headers http.Header, remoteAddr string) string { 56 | xFwdFor := headers.Get("x-forwarded-for") 57 | // clean off remoteaddr from x-forwarded-for 58 | if xFwdFor != "" { 59 | 60 | newXFwdFor := "" 61 | oneFound := false 62 | xFwdForIps := strings.Split(xFwdFor, ",") 63 | 64 | for _, ip := range xFwdForIps { 65 | ip = strings.TrimSpace(ip) 66 | 67 | if ip != remoteAddr { 68 | newXFwdFor = newXFwdFor + ip + ", " 69 | oneFound = true 70 | } 71 | 72 | } 73 | 74 | if oneFound { 75 | newXFwdFor = newXFwdFor[0 : len(newXFwdFor)-2] 76 | } 77 | 78 | xFwdFor = newXFwdFor 79 | 80 | } 81 | 82 | return xFwdFor 83 | } 84 | 85 | // logs the failed request 86 | func LogFailedRequest(req *http.Request) { 87 | remoteAddr := req.RemoteAddr 88 | indexOfColon := strings.Index(remoteAddr, ":") 89 | if indexOfColon > 0 { 90 | remoteAddr = remoteAddr[0:indexOfColon] 91 | } 92 | 93 | fmt.Printf("[%s] AuFail src:[%s / % s] URI:%s\n", time.Now().Format(timestampLayout), remoteAddr, req.Header.Get(("x-forwarded-for")), req.RequestURI) 94 | } 95 | -------------------------------------------------------------------------------- /cmd/app/options/options.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package options 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/spf13/cobra" 9 | "golang.org/x/term" 10 | k8sErrors "k8s.io/apimachinery/pkg/util/errors" 11 | 12 | cliflag "k8s.io/component-base/cli/flag" 13 | ) 14 | 15 | const ( 16 | AppName = "kube-oidc-proxy" 17 | ) 18 | 19 | type Options struct { 20 | App *KubeOIDCProxyOptions 21 | OIDCAuthentication *OIDCAuthenticationOptions 22 | SecureServing *SecureServingOptions 23 | Audit *AuditOptions 24 | Client *ClientOptions 25 | Misc *MiscOptions 26 | 27 | nfs *cliflag.NamedFlagSets 28 | } 29 | 30 | func New() *Options { 31 | nfs := new(cliflag.NamedFlagSets) 32 | 33 | // Add flags to command sets 34 | return &Options{ 35 | App: NewKubeOIDCProxyOptions(nfs), 36 | OIDCAuthentication: NewOIDCAuthenticationOptions(nfs), 37 | SecureServing: NewSecureServingOptions(nfs), 38 | Audit: NewAuditOptions(nfs), 39 | Client: NewClientOptions(nfs), 40 | Misc: NewMiscOptions(nfs), 41 | 42 | nfs: nfs, 43 | } 44 | } 45 | 46 | func (o *Options) AddFlags(cmd *cobra.Command) { 47 | // pretty output from kube-apiserver 48 | usageFmt := "Usage:\n %s\n" 49 | cols, _, _ := term.GetSize(0) 50 | cmd.SetUsageFunc(func(cmd *cobra.Command) error { 51 | fmt.Fprintf(cmd.OutOrStderr(), usageFmt, cmd.UseLine()) 52 | cliflag.PrintSections(cmd.OutOrStderr(), *o.nfs, cols) 53 | return nil 54 | }) 55 | 56 | cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { 57 | fmt.Fprintf(cmd.OutOrStdout(), "%s\n\n"+usageFmt, cmd.Long, cmd.UseLine()) 58 | cliflag.PrintSections(cmd.OutOrStdout(), *o.nfs, cols) 59 | }) 60 | 61 | fs := cmd.Flags() 62 | for _, f := range o.nfs.FlagSets { 63 | fs.AddFlagSet(f) 64 | } 65 | } 66 | 67 | func (o *Options) Validate(cmd *cobra.Command) error { 68 | if cmd.Flag("version").Value.String() == "true" { 69 | o.Misc.PrintVersionAndExit() 70 | } 71 | 72 | var errs []error 73 | 74 | if err := o.OIDCAuthentication.Validate(); err != nil { 75 | errs = append(errs, err) 76 | } 77 | 78 | if err := o.SecureServing.Validate(); len(err) > 0 { 79 | errs = append(errs, err...) 80 | } 81 | 82 | if o.SecureServing.BindPort == o.App.ReadinessProbePort { 83 | errs = append(errs, errors.New("unable to securely serve on port 8080 (used by readiness probe)")) 84 | } 85 | 86 | if err := o.Audit.Validate(); len(err) > 0 { 87 | errs = append(errs, err...) 88 | } 89 | 90 | if o.App.DisableImpersonation && 91 | (o.App.ExtraHeaderOptions.EnableClientIPExtraUserHeader || len(o.App.ExtraHeaderOptions.ExtraUserHeaders) > 0) { 92 | errs = append(errs, errors.New("cannot add extra user headers when impersonation disabled")) 93 | } 94 | 95 | if len(errs) > 0 { 96 | return k8sErrors.NewAggregate(errs) 97 | } 98 | 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /deploy/charts/kube-oidc-proxy/README.md: -------------------------------------------------------------------------------- 1 | # kube-oidc-proxy helm chart 2 | 3 | This is a `helm` chart that installs [`kube-oidc-proxy`](https://github.com/jetstack/kube-oidc-proxy/). 4 | This helm chart cannot be installed out of the box without providing own 5 | configuration. 6 | 7 | This helm chart is based on example configuration provided in `kube-oidc-proxy` 8 | [repository](https://github.com/jetstack/kube-oidc-proxy/blob/master/deploy/yaml/kube-oidc-proxy.yaml). 9 | 10 | Minimal required configuration is `oidc` section of `value.yaml` file. 11 | 12 | ```yaml 13 | oidc: 14 | clientId: my-client 15 | issuerUrl: https://accounts.google.com 16 | usernameClaim: email 17 | ``` 18 | 19 | When a custom root CA certificate is required it should be added as PEM encoded 20 | text value: 21 | 22 | ```yaml 23 | oidc: 24 | caPEM: | 25 | -----BEGIN CERTIFICATE----- 26 | MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG 27 | A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv 28 | b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw 29 | MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i 30 | YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT 31 | aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ 32 | jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp 33 | xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp 34 | 1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG 35 | snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ 36 | U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 37 | 9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E 38 | BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B 39 | AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz 40 | yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE 41 | 38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP 42 | AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad 43 | DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME 44 | HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== 45 | -----END CERTIFICATE----- 46 | ``` 47 | 48 | This minimal configuration gives a cluster internal IP address that can be used 49 | with `kubectl` to authenticate requests to Kubernetes API server. 50 | 51 | The service can be exposed via ingress controller and give access to external 52 | clients. Example of exposing via ingress controller. 53 | 54 | ```yaml 55 | ingress: 56 | enabled: true 57 | annotations: 58 | kubernetes.io/ingress.class: traefik 59 | traefik.ingress.kubernetes.io/rule-type: PathPrefixStrip 60 | hosts: 61 | - host: "" 62 | paths: 63 | - /oidc-proxy 64 | ``` 65 | 66 | By default the helm chart will create self-signed TLS certificate for `kube-oidc-proxy` 67 | service. It is possible to provide secret name that contains TLS artifacts for 68 | service. The secret must be of `kubernetes.io/tls` type. 69 | 70 | ```yaml 71 | tls: 72 | secretName: my-tls-secret-with-key-and-cert 73 | ``` 74 | -------------------------------------------------------------------------------- /test/tools/audit-webhook/pkg/sink/sink.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package sink 3 | 4 | import ( 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/json" 8 | "encoding/pem" 9 | "fmt" 10 | "io/ioutil" 11 | "net" 12 | "net/http" 13 | "os" 14 | "sync" 15 | 16 | log "github.com/sirupsen/logrus" 17 | auditv1 "k8s.io/apiserver/pkg/apis/audit/v1" 18 | ) 19 | 20 | type Sink struct { 21 | logPath string 22 | keyFile, certFile string 23 | 24 | sk *rsa.PrivateKey 25 | 26 | stopCh <-chan struct{} 27 | mu sync.Mutex 28 | } 29 | 30 | func New(logPath, keyFile, certFile string, stopCh <-chan struct{}) (*Sink, error) { 31 | b, err := ioutil.ReadFile(keyFile) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | block, _ := pem.Decode(b) 37 | if block == nil { 38 | return nil, 39 | fmt.Errorf("failed to parse PEM block containing the key: %q", keyFile) 40 | } 41 | 42 | sk, err := x509.ParsePKCS1PrivateKey(block.Bytes) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return &Sink{ 48 | logPath: logPath, 49 | keyFile: keyFile, 50 | certFile: certFile, 51 | sk: sk, 52 | stopCh: stopCh, 53 | }, nil 54 | } 55 | 56 | func (s *Sink) Run(bindAddress, listenPort string) (<-chan struct{}, error) { 57 | serveAddr := fmt.Sprintf("%s:%s", bindAddress, listenPort) 58 | 59 | l, err := net.Listen("tcp", serveAddr) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | go func() { 65 | <-s.stopCh 66 | if l != nil { 67 | l.Close() 68 | } 69 | }() 70 | 71 | compCh := make(chan struct{}) 72 | go func() { 73 | defer close(compCh) 74 | 75 | err := http.ServeTLS(l, s, s.certFile, s.keyFile) 76 | if err != nil { 77 | log.Errorf("stopped serving TLS (%s): %s", serveAddr, err) 78 | } 79 | }() 80 | 81 | log.Infof("audit webhook listening and serving on %s", serveAddr) 82 | 83 | return compCh, nil 84 | } 85 | 86 | func (s *Sink) ServeHTTP(rw http.ResponseWriter, r *http.Request) { 87 | log.Infof("%s: audit webhook received url %s", r.RemoteAddr, r.URL) 88 | 89 | var events auditv1.EventList 90 | err := json.NewDecoder(r.Body).Decode(&events) 91 | if err != nil { 92 | log.Errorf("%s: failed to decode request body: %s", r.RemoteAddr, err) 93 | http.Error(rw, err.Error(), http.StatusBadRequest) 94 | return 95 | } 96 | log.Infof("%s: got events: %v", r.RemoteAddr, events) 97 | 98 | s.mu.Lock() 99 | defer s.mu.Unlock() 100 | 101 | f, err := os.OpenFile(s.logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 102 | if err != nil { 103 | log.Errorf("%s: failed to open log file: %s", r.RemoteAddr, err) 104 | http.Error(rw, err.Error(), http.StatusBadRequest) 105 | return 106 | } 107 | defer f.Close() 108 | 109 | for _, event := range events.Items { 110 | if err := json.NewEncoder(f).Encode(event); err != nil { 111 | log.Errorf("%s: failed to write audit event: %s", r.RemoteAddr, err) 112 | http.Error(rw, err.Error(), http.StatusBadRequest) 113 | return 114 | } 115 | } 116 | 117 | rw.WriteHeader(http.StatusOK) 118 | } 119 | -------------------------------------------------------------------------------- /demo/manifests/vendor/kube-prod-runtime/components/oauth2-proxy.jsonnet: -------------------------------------------------------------------------------- 1 | /* 2 | * Bitnami Kubernetes Production Runtime - A collection of services that makes it 3 | * easy to run production workloads in Kubernetes. 4 | * 5 | * Copyright 2018-2019 Bitnami 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | local kube = import "../lib/kube.libsonnet"; 21 | 22 | local OAUTH2_PROXY_IMAGE = (import "images.json")["oauth2_proxy"]; 23 | 24 | { 25 | p:: "", 26 | metadata:: { 27 | metadata+: { 28 | namespace: "kubeprod", 29 | }, 30 | }, 31 | 32 | secret: kube.Secret($.p + "oauth2-proxy") + $.metadata { 33 | data_+: { 34 | client_id: error "client_id is required", 35 | client_secret: error "client_secret is required", 36 | cookie_secret: error "cookie_secret is required", 37 | }, 38 | }, 39 | 40 | svc: kube.Service($.p + "oauth2-proxy") + $.metadata { 41 | target_pod: $.deploy.spec.template, 42 | port: 4180, 43 | }, 44 | 45 | hpa: kube.HorizontalPodAutoscaler($.p + "oauth2-proxy") + $.metadata { 46 | target: $.deploy, 47 | spec+: {maxReplicas: 10}, 48 | }, 49 | 50 | deploy: kube.Deployment($.p + "oauth2-proxy") + $.metadata { 51 | spec+: { 52 | template+: { 53 | spec+: { 54 | containers_+: { 55 | proxy: kube.Container("oauth2-proxy") { 56 | image: OAUTH2_PROXY_IMAGE, 57 | args_+: { 58 | "email-domain": "*", 59 | "http-address": "0.0.0.0:4180", 60 | "cookie-secure": "true", 61 | "cookie-refresh": "3h", 62 | "set-xauthrequest": true, 63 | "tls-cert": "", 64 | "upstream": "file:///dev/null", 65 | }, 66 | env_+: { 67 | OAUTH2_PROXY_CLIENT_ID: kube.SecretKeyRef($.secret, "client_id"), 68 | OAUTH2_PROXY_CLIENT_SECRET: kube.SecretKeyRef($.secret, "client_secret"), 69 | OAUTH2_PROXY_COOKIE_SECRET: kube.SecretKeyRef($.secret, "cookie_secret"), 70 | }, 71 | ports_+: { 72 | http: {containerPort: 4180}, 73 | }, 74 | readinessProbe: { 75 | httpGet: {path: "/ping", port: "http"}, 76 | }, 77 | livenessProbe: self.readinessProbe { 78 | initialDelaySeconds: 30, 79 | }, 80 | resources+: { 81 | requests+: {cpu: "10m"}, 82 | }, 83 | }, 84 | }, 85 | }, 86 | }, 87 | }, 88 | }, 89 | } 90 | -------------------------------------------------------------------------------- /cmd/app/options/oidc.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package options 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/spf13/pflag" 8 | 9 | cliflag "k8s.io/component-base/cli/flag" 10 | ) 11 | 12 | type OIDCAuthenticationOptions struct { 13 | CAFile string 14 | ClientID string 15 | IssuerURL string 16 | UsernameClaim string 17 | UsernamePrefix string 18 | GroupsClaim string 19 | GroupsPrefix string 20 | SigningAlgs []string 21 | RequiredClaims map[string]string 22 | } 23 | 24 | func NewOIDCAuthenticationOptions(nfs *cliflag.NamedFlagSets) *OIDCAuthenticationOptions { 25 | return new(OIDCAuthenticationOptions).AddFlags(nfs.FlagSet("OIDC")) 26 | } 27 | 28 | func (o *OIDCAuthenticationOptions) Validate() error { 29 | if o != nil && (len(o.IssuerURL) > 0) != (len(o.ClientID) > 0) { 30 | return fmt.Errorf("oidc-issuer-url and oidc-client-id should be specified together") 31 | } 32 | 33 | return nil 34 | } 35 | 36 | func (o *OIDCAuthenticationOptions) AddFlags(fs *pflag.FlagSet) *OIDCAuthenticationOptions { 37 | fs.StringVar(&o.IssuerURL, "oidc-issuer-url", o.IssuerURL, ""+ 38 | "The URL of the OpenID issuer, only HTTPS scheme will be accepted.") 39 | 40 | fs.StringVar(&o.ClientID, "oidc-client-id", o.ClientID, 41 | "The client ID for the OpenID Connect client.") 42 | 43 | fs.StringVar(&o.CAFile, "oidc-ca-file", o.CAFile, ""+ 44 | "The OpenID server's certificate will be verified by one of the authorities "+ 45 | "in the oidc-ca-file, otherwise the host's root CA set will be used") 46 | 47 | fs.StringVar(&o.UsernameClaim, "oidc-username-claim", "sub", ""+ 48 | "The OpenID claim to use as the username. Note that claims other than the default ('sub') "+ 49 | "is not guaranteed to be unique and immutable") 50 | 51 | fs.StringVar(&o.UsernamePrefix, "oidc-username-prefix", "", ""+ 52 | "If provided, all usernames will be prefixed with this value. If not provided, "+ 53 | "username claims other than 'email' are prefixed by the issuer URL to avoid "+ 54 | "clashes. To skip any prefixing, provide the value '-'.") 55 | 56 | fs.StringVar(&o.GroupsClaim, "oidc-groups-claim", "", ""+ 57 | "If provided, the name of a custom OpenID Connect claim for specifying user groups. "+ 58 | "The claim value is expected to be a string or array of strings.") 59 | 60 | fs.StringVar(&o.GroupsPrefix, "oidc-groups-prefix", "", ""+ 61 | "If provided, all groups will be prefixed with this value to prevent conflicts with "+ 62 | "other authentication strategies.") 63 | 64 | fs.StringSliceVar(&o.SigningAlgs, "oidc-signing-algs", []string{"RS256"}, ""+ 65 | "Comma-separated list of allowed JOSE asymmetric signing algorithms. JWTs with a "+ 66 | "'alg' header value not in this list will be rejected. "+ 67 | "Values are defined by RFC 7518 https://tools.ietf.org/html/rfc7518#section-3.1.") 68 | 69 | fs.Var(cliflag.NewMapStringStringNoSplit(&o.RequiredClaims), "oidc-required-claim", ""+ 70 | "A key=value pair that describes a required claim in the ID Token. "+ 71 | "If set, the claim is verified to be present in the ID Token with a matching value. "+ 72 | "Repeat this flag to specify multiple claims.") 73 | 74 | return o 75 | } 76 | -------------------------------------------------------------------------------- /pkg/proxy/audit/audit.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package audit 3 | 4 | import ( 5 | "fmt" 6 | "net/http" 7 | 8 | "k8s.io/apimachinery/pkg/util/sets" 9 | genericapifilters "k8s.io/apiserver/pkg/endpoints/filters" 10 | "k8s.io/apiserver/pkg/server" 11 | genericfilters "k8s.io/apiserver/pkg/server/filters" 12 | "k8s.io/component-base/version" 13 | 14 | "github.com/jetstack/kube-oidc-proxy/cmd/app/options" 15 | ) 16 | 17 | type Audit struct { 18 | opts *options.AuditOptions 19 | serverConfig *server.CompletedConfig 20 | } 21 | 22 | // New creates a new Audit struct to handle auditing for proxy requests. This 23 | // is mostly a wrapper for the apiserver auditing handlers to combine them with 24 | // the proxy. 25 | func New(opts *options.AuditOptions, externalAddress string, secureServingInfo *server.SecureServingInfo) (*Audit, error) { 26 | serverConfig := &server.Config{ 27 | ExternalAddress: externalAddress, 28 | SecureServing: secureServingInfo, 29 | 30 | // Default to treating watch as a long-running operation. 31 | // Generic API servers have no inherent long-running subresources. 32 | // This is so watch requests are handled correctly in the audit log. 33 | LongRunningFunc: genericfilters.BasicLongRunningRequestCheck( 34 | sets.NewString("watch"), sets.NewString()), 35 | } 36 | 37 | // We do not support dynamic auditing, so leave nil 38 | if err := opts.ApplyTo(serverConfig); err != nil { 39 | return nil, err 40 | } 41 | 42 | serverConfig.EffectiveVersion = version.NewEffectiveVersion("1.0.31") 43 | completed := serverConfig.Complete(nil) 44 | 45 | return &Audit{ 46 | opts: opts, 47 | serverConfig: &completed, 48 | }, nil 49 | } 50 | 51 | // Run will run the audit backend if configured. 52 | func (a *Audit) Run(stopCh <-chan struct{}) error { 53 | if a.serverConfig.AuditBackend != nil { 54 | if err := a.serverConfig.AuditBackend.Run(stopCh); err != nil { 55 | return fmt.Errorf("failed to run the audit backend: %s", err) 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | 62 | // Shutdown will shutdown the audit backend if configured. 63 | func (a *Audit) Shutdown() error { 64 | if a.serverConfig.AuditBackend != nil { 65 | a.serverConfig.AuditBackend.Shutdown() 66 | } 67 | 68 | return nil 69 | } 70 | 71 | // WithRequest will wrap the given handler to inject the request information 72 | // into the context which is then used by the wrapped audit handler. 73 | func (a *Audit) WithRequest(handler http.Handler) http.Handler { 74 | handler = genericapifilters.WithAudit(handler, a.serverConfig.AuditBackend, a.serverConfig.AuditPolicyRuleEvaluator, a.serverConfig.LongRunningFunc) 75 | handler = genericapifilters.WithAuditInit(handler) 76 | return genericapifilters.WithRequestInfo(handler, a.serverConfig.RequestInfoResolver) 77 | } 78 | 79 | // WithUnauthorized will wrap the given handler to inject the request 80 | // information into the context which is then used by the wrapped audit 81 | // handler. 82 | func (a *Audit) WithUnauthorized(handler http.Handler) http.Handler { 83 | handler = genericapifilters.WithFailedAuthenticationAudit(handler, a.serverConfig.AuditBackend, a.serverConfig.AuditPolicyRuleEvaluator) 84 | return genericapifilters.WithRequestInfo(handler, a.serverConfig.RequestInfoResolver) 85 | } 86 | -------------------------------------------------------------------------------- /demo/manifests/vendor/kube-prod-runtime/lib/utils.libsonnet: -------------------------------------------------------------------------------- 1 | /* 2 | * Bitnami Kubernetes Production Runtime - A collection of services that makes it 3 | * easy to run production workloads in Kubernetes. 4 | * 5 | * Copyright 2018-2019 Bitnami 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | // Various opinionated helper functions, that might not be generally 21 | // useful in other deployments. 22 | local kube = import "kube.libsonnet"; 23 | 24 | { 25 | path_join(prefix, suffix):: ( 26 | if std.endsWith(prefix, "/") then prefix + suffix 27 | else prefix + "/" + suffix 28 | ), 29 | 30 | trimUrl(str):: ( 31 | if std.endsWith(str, "/") then 32 | std.substr(str, 1, std.length(str) - 1) 33 | else 34 | str 35 | ), 36 | 37 | toJson(x):: ( 38 | if std.type(x) == "string" then std.escapeStringJson(x) 39 | else std.toString(x) 40 | ), 41 | 42 | subdomain(fqdn):: ( 43 | local parts = std.split(fqdn, "."); 44 | local tail = [parts[i] for i in std.range(1, std.length(parts)-1)]; 45 | std.join(".", tail) 46 | ), 47 | 48 | TlsIngress(name):: kube.Ingress(name) { 49 | local this = self, 50 | metadata+: { 51 | annotations+: { 52 | "kubernetes.io/tls-acme": "true", 53 | "kubernetes.io/ingress.class": "nginx", 54 | }, 55 | }, 56 | spec+: { 57 | tls+: [{ 58 | hosts: std.set([r.host for r in this.spec.rules]), 59 | secretName: this.metadata.name + "-tls", 60 | }], 61 | }, 62 | }, 63 | 64 | AuthIngress(name):: $.TlsIngress(name) { 65 | local this = self, 66 | host:: error "host is required", 67 | metadata+: { 68 | annotations+: { 69 | // NB: Our nginx-ingress no-auth-locations includes "/oauth2" 70 | "nginx.ingress.kubernetes.io/auth-signin": "https://%s/oauth2/start" % this.host, 71 | "nginx.ingress.kubernetes.io/auth-url": "https://%s/oauth2/auth" % this.host, 72 | "nginx.ingress.kubernetes.io/auth-response-headers": "X-Auth-Request-User, X-Auth-Request-Email", 73 | }, 74 | }, 75 | 76 | spec+: { 77 | rules+: [{ 78 | // This is required until the oauth2-proxy domain whitelist 79 | // feature (or similar) is released. Until then, oauth2-proxy 80 | // *only supports* redirects to the same hostname (because we 81 | // don't want to allow "open redirects" to just anywhere). 82 | host: this.host, 83 | http: { 84 | paths: [{ 85 | path: "/oauth2", 86 | backend: { 87 | // TODO: parameterise this based on oauth2 deployment 88 | serviceName: "oauth2-proxy", 89 | servicePort: 4180, 90 | }, 91 | }], 92 | }, 93 | }], 94 | }, 95 | }, 96 | } 97 | -------------------------------------------------------------------------------- /pkg/proxy/context/context.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package context 3 | 4 | import ( 5 | "net/http" 6 | 7 | "github.com/sebest/xff" 8 | "k8s.io/apiserver/pkg/authentication/user" 9 | "k8s.io/apiserver/pkg/endpoints/request" 10 | "k8s.io/client-go/transport" 11 | 12 | genericapirequest "k8s.io/apiserver/pkg/endpoints/request" 13 | ) 14 | 15 | type key int 16 | 17 | const ( 18 | // noImpersonationKey is the context key for whether to use impersonation. 19 | noImpersonationKey key = iota 20 | 21 | // impersonationConfigKey is the context key for the impersonation config. 22 | impersonationConfigKey 23 | 24 | // bearerTokenKey is the context key for the bearer token. 25 | bearerTokenKey 26 | 27 | // bearerTokenKey is the context key for the client address. 28 | clientAddressKey 29 | ) 30 | 31 | type ImpersonationRequest struct { 32 | ImpersonationConfig *transport.ImpersonationConfig 33 | InboundUser *user.Info 34 | ImpersonatedUser *user.Info 35 | } 36 | 37 | // WithNoImpersonation returns a copy of the request in which the noImpersonation context value is set. 38 | func WithNoImpersonation(req *http.Request) *http.Request { 39 | return req.WithContext(request.WithValue(req.Context(), noImpersonationKey, true)) 40 | } 41 | 42 | // NoImpersonation returns whether the noImpersonation context key has been set 43 | func NoImpersonation(req *http.Request) bool { 44 | noImp, _ := req.Context().Value(noImpersonationKey).(bool) 45 | return noImp 46 | } 47 | 48 | // WithImpersonationConfig returns a copy of parent in which contains the impersonation configuration. 49 | func WithImpersonationConfig(req *http.Request, conf *ImpersonationRequest) *http.Request { 50 | ctxToReturn := request.WithValue(req.Context(), impersonationConfigKey, conf) 51 | if *conf.ImpersonatedUser != nil { 52 | ctxToReturn = genericapirequest.WithUser(ctxToReturn, *conf.ImpersonatedUser) 53 | } 54 | return req.WithContext(ctxToReturn) 55 | } 56 | 57 | // ImpersonationConfig returns the impersonation configuration held in the context if existing. 58 | func ImpersonationConfig(req *http.Request) *ImpersonationRequest { 59 | conf, _ := req.Context().Value(impersonationConfigKey).(*ImpersonationRequest) 60 | return conf 61 | } 62 | 63 | // WithBearerToken will add the bearer token to the request context from an http.Header to the request context. 64 | func WithBearerToken(req *http.Request, header http.Header) *http.Request { 65 | return req.WithContext(request.WithValue(req.Context(), bearerTokenKey, header.Get("Authorization"))) 66 | } 67 | 68 | // BearerToken will return the bearer token stored in the request context. 69 | func BearerToken(req *http.Request) string { 70 | token, _ := req.Context().Value(bearerTokenKey).(string) 71 | return token 72 | } 73 | 74 | // RemoteAddress will attempt to return the source client address if available 75 | // in the request context. If it is not, it will be gathered from the request 76 | // and entered into the context. 77 | func RemoteAddr(req *http.Request) (*http.Request, string) { 78 | ctx := req.Context() 79 | 80 | clientAddress, ok := ctx.Value(clientAddressKey).(string) 81 | if !ok { 82 | clientAddress = xff.GetRemoteAddr(req) 83 | req = req.WithContext(request.WithValue(ctx, clientAddressKey, clientAddress)) 84 | } 85 | 86 | return req, clientAddress 87 | } 88 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@jetstack.io or joshua.vanleeuwen@jetstack.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /demo/manifests/vendor/kube-prod-runtime/components/externaldns.jsonnet: -------------------------------------------------------------------------------- 1 | /* 2 | * Bitnami Kubernetes Production Runtime - A collection of services that makes it 3 | * easy to run production workloads in Kubernetes. 4 | * 5 | * Copyright 2018-2019 Bitnami 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | local kube = import '../lib/kube.libsonnet'; 21 | local EXTERNAL_DNS_IMAGE = (import 'images.json')['external-dns']; 22 | 23 | { 24 | p:: '', 25 | metadata:: { 26 | metadata+: { 27 | namespace: 'kubeprod', 28 | }, 29 | }, 30 | 31 | clusterRole: kube.ClusterRole($.p + 'external-dns') { 32 | rules: [ 33 | { 34 | apiGroups: [''], 35 | resources: ['services'], 36 | verbs: ['get', 'watch', 'list'], 37 | }, 38 | { 39 | apiGroups: [''], 40 | resources: ['pods'], 41 | verbs: ['get', 'watch', 'list'], 42 | }, 43 | { 44 | apiGroups: ['extensions'], 45 | resources: ['ingresses'], 46 | verbs: ['get', 'watch', 'list'], 47 | }, 48 | { 49 | apiGroups: [''], 50 | resources: ['nodes'], 51 | verbs: ['list'], 52 | }, 53 | ], 54 | }, 55 | 56 | clusterRoleBinding: kube.ClusterRoleBinding($.p + 'external-dns-viewer') { 57 | roleRef_: $.clusterRole, 58 | subjects_+: [$.sa], 59 | }, 60 | 61 | sa: kube.ServiceAccount($.p + 'external-dns') + $.metadata { 62 | }, 63 | 64 | deploy: kube.Deployment($.p + 'external-dns') + $.metadata { 65 | local this = self, 66 | ownerId:: error 'ownerId is required', 67 | domainFilter:: this.ownerId, 68 | spec+: { 69 | template+: { 70 | metadata+: { 71 | annotations+: { 72 | 'prometheus.io/scrape': 'true', 73 | 'prometheus.io/port': '7979', 74 | 'prometheus.io/path': '/metrics', 75 | }, 76 | }, 77 | spec+: { 78 | serviceAccountName: $.sa.metadata.name, 79 | containers_+: { 80 | edns: kube.Container('external-dns') { 81 | image: EXTERNAL_DNS_IMAGE, 82 | args_+: { 83 | sources_:: ['service', 'ingress'], 84 | 'txt-owner-id': this.ownerId, 85 | 'domain-filter': this.domainFilter, 86 | }, 87 | args+: ['--source=%s' % s for s in self.args_.sources_], 88 | ports_+: { 89 | metrics: { containerPort: 7979 }, 90 | }, 91 | readinessProbe: { 92 | httpGet: { path: '/healthz', port: 'metrics' }, 93 | }, 94 | livenessProbe: self.readinessProbe, 95 | }, 96 | }, 97 | }, 98 | }, 99 | }, 100 | }, 101 | } 102 | -------------------------------------------------------------------------------- /cmd/app/options/app.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package options 3 | 4 | import ( 5 | "time" 6 | 7 | "github.com/spf13/pflag" 8 | cliflag "k8s.io/component-base/cli/flag" 9 | 10 | "github.com/jetstack/kube-oidc-proxy/pkg/util/flags" 11 | ) 12 | 13 | type KubeOIDCProxyOptions struct { 14 | DisableImpersonation bool 15 | ReadinessProbePort int 16 | 17 | FlushInterval time.Duration 18 | 19 | ExtraHeaderOptions ExtraHeaderOptions 20 | TokenPassthrough TokenPassthroughOptions 21 | } 22 | 23 | type TokenPassthroughOptions struct { 24 | Audiences []string 25 | Enabled bool 26 | } 27 | 28 | type ExtraHeaderOptions struct { 29 | EnableClientIPExtraUserHeader bool 30 | 31 | ExtraUserHeaders map[string][]string 32 | } 33 | 34 | func NewKubeOIDCProxyOptions(nfs *cliflag.NamedFlagSets) *KubeOIDCProxyOptions { 35 | return new(KubeOIDCProxyOptions).AddFlags(nfs.FlagSet("Kube-OIDC-Proxy")) 36 | } 37 | 38 | func (k *KubeOIDCProxyOptions) AddFlags(fs *pflag.FlagSet) *KubeOIDCProxyOptions { 39 | fs.BoolVar(&k.DisableImpersonation, "disable-impersonation", k.DisableImpersonation, 40 | "(Alpha) Disable the impersonation of authenticated requests. All "+ 41 | "authenticated requests will be forwarded as is.") 42 | 43 | fs.IntVarP(&k.ReadinessProbePort, "readiness-probe-port", "P", 8080, 44 | "Port to expose readiness probe.") 45 | 46 | fs.DurationVar(&k.FlushInterval, "flush-interval", time.Millisecond*50, 47 | "Specifies the interval to flush request bodies. If 0ms, "+ 48 | "no periodic flushing is done. A negative value means to flush "+ 49 | "immediately after each write. Streaming requests such as 'kubectl exec' "+ 50 | "will ignore this option and flush immediately.") 51 | 52 | k.TokenPassthrough.AddFlags(fs) 53 | k.ExtraHeaderOptions.AddFlags(fs) 54 | 55 | return k 56 | } 57 | 58 | func (t *TokenPassthroughOptions) AddFlags(fs *pflag.FlagSet) { 59 | fs.StringSliceVar(&t.Audiences, "token-passthrough-audiences", t.Audiences, ""+ 60 | "(Alpha) List of the identifiers that the resource server presented with the token "+ 61 | "identifies as. The resource server will verify that non OIDC tokens are intended "+ 62 | "for at least one of the audiences in this list. If no audiences are "+ 63 | "provided, the audience will default to the audience of the Kubernetes "+ 64 | "apiserver. Only used when --token-passthrough is also enabled.") 65 | 66 | fs.BoolVar(&t.Enabled, "token-passthrough", t.Enabled, ""+ 67 | "(Alpha) Requests with Bearer tokens that fail OIDC validation are tried against "+ 68 | "the API server using the Token Review endpoint. If successful, the request "+ 69 | "is sent on as is, with no impersonation.") 70 | } 71 | 72 | func (e *ExtraHeaderOptions) AddFlags(fs *pflag.FlagSet) { 73 | fs.BoolVar(&e.EnableClientIPExtraUserHeader, "extra-user-header-client-ip", 74 | e.EnableClientIPExtraUserHeader, "(Alpha) If enabled, proxied requests will "+ 75 | "include the extra user header 'Impersonate-Extra-Remote-Client-IP: "+ 76 | "' where will contain the remote address of "+ 77 | "the source of the request.") 78 | 79 | fs.Var(flags.NewStringToStringSliceValue(&e.ExtraUserHeaders), "extra-user-headers", 80 | "(Alpha) A list of key value pairs of extra user headers to pass with "+ 81 | "proxied requests as part of the impersonated request. A single key can "+ 82 | "hold multiple values.") 83 | } 84 | -------------------------------------------------------------------------------- /demo/manifests/components/landingpage/amazon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/manifests/vendor/kube-prod-runtime/components/kibana.jsonnet: -------------------------------------------------------------------------------- 1 | /* 2 | * Bitnami Kubernetes Production Runtime - A collection of services that makes it 3 | * easy to run production workloads in Kubernetes. 4 | * 5 | * Copyright 2018-2019 Bitnami 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | local kube = import "../lib/kube.libsonnet"; 21 | local kubecfg = import "kubecfg.libsonnet"; 22 | local utils = import "../lib/utils.libsonnet"; 23 | 24 | local KIBANA_IMAGE = (import "images.json")["kibana"]; 25 | 26 | local strip_trailing_slash(s) = ( 27 | if std.endsWith(s, "/") then 28 | strip_trailing_slash(std.substr(s, 0, std.length(s) - 1)) 29 | else 30 | s 31 | ); 32 | 33 | { 34 | p:: "", 35 | metadata:: { 36 | metadata+: { 37 | namespace: "kubeprod", 38 | }, 39 | }, 40 | 41 | es: error "elasticsearch is required", 42 | 43 | serviceAccount: kube.ServiceAccount($.p + "kibana") + $.metadata { 44 | }, 45 | 46 | deploy: kube.Deployment($.p + "kibana") + $.metadata { 47 | spec+: { 48 | template+: { 49 | spec+: { 50 | securityContext: { 51 | fsGroup: 1001, 52 | }, 53 | containers_+: { 54 | kibana: kube.Container("kibana") { 55 | image: KIBANA_IMAGE, 56 | securityContext: { 57 | runAsUser: 1001, 58 | }, 59 | resources: { 60 | requests: { 61 | cpu: "10m", 62 | }, 63 | limits: { 64 | cpu: "1000m", // initial startup requires lots of cpu 65 | }, 66 | }, 67 | env_+: { 68 | KIBANA_ELASTICSEARCH_URL: $.es.svc.host, 69 | 70 | local route = $.ingress.spec.rules[1].http.paths[0], 71 | // Make sure we got the correct route 72 | assert route.backend == $.svc.name_port, 73 | SERVER_BASEPATH: strip_trailing_slash(route.path), 74 | KIBANA_HOST: "0.0.0.0", 75 | XPACK_MONITORING_ENABLED: "false", 76 | XPACK_SECURITY_ENABLED: "false", 77 | }, 78 | ports_+: { 79 | ui: { containerPort: 5601 }, 80 | }, 81 | }, 82 | }, 83 | }, 84 | }, 85 | }, 86 | }, 87 | 88 | svc: kube.Service($.p + "kibana-logging") + $.metadata { 89 | target_pod: $.deploy.spec.template, 90 | }, 91 | 92 | ingress: utils.AuthIngress($.p + "kibana-logging") + $.metadata { 93 | local this = self, 94 | host:: error "host is required", 95 | spec+: { 96 | rules+: [ 97 | { 98 | host: this.host, 99 | http: { 100 | paths: [ 101 | { path: "/", backend: $.svc.name_port }, 102 | ], 103 | }, 104 | }, 105 | ], 106 | }, 107 | }, 108 | } 109 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - 'master' 6 | 7 | permissions: 8 | id-token: write 9 | packages: write 10 | 11 | jobs: 12 | docker: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - 17 | name: Set up QEMU 18 | uses: docker/setup-qemu-action@v1 19 | - 20 | name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v1 22 | - uses: actions/checkout@v1 23 | 24 | - name: Setup Go 25 | uses: actions/setup-go@v3 26 | with: 27 | go-version: "1.23.0" 28 | 29 | - name: Install Cosign 30 | uses: sigstore/cosign-installer@main 31 | 32 | - name: Update go deps 33 | run: go mod tidy 34 | 35 | - name: install go mock 36 | run: go install github.com/golang/mock/mockgen@v1.6.0 37 | 38 | - name: install go-junit 39 | run: go get -u github.com/jstemmer/go-junit-report 40 | 41 | - name: run tests 42 | run: make test 43 | 44 | - name: build executable 45 | run: make build; ls; ls bin 46 | 47 | 48 | 49 | 50 | - 51 | name: Login to DockerHub 52 | uses: docker/login-action@v1 53 | with: 54 | username: ${{ secrets.OU_REG_USER }} 55 | password: ${{ secrets.OU_REG_PASSWORD }} 56 | 57 | - name: Login to container Registry 58 | uses: docker/login-action@v2 59 | with: 60 | username: ${{ github.repository_owner }} 61 | password: ${{ secrets.GITHUB_TOKEN }} 62 | registry: ghcr.io 63 | 64 | - name: downcase REPO 65 | run: | 66 | echo "REPO=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV} 67 | 68 | - name: generate tag 69 | run: |- 70 | export PROJ_VERSION="1.0.9" 71 | echo "Project Version: $PROJ_VERSION" 72 | echo "TAG=$PROJ_VERSION-$(echo $GITHUB_SHA | cut -c 1-6)" >> $GITHUB_ENV 73 | echo "SHORT_TAG=$PROJ_VERSION" >> $GITHUB_ENV 74 | 75 | 76 | - 77 | name: Build and push 78 | id: docker_build 79 | uses: docker/build-push-action@v2 80 | with: 81 | context: "." 82 | push: true 83 | platforms: linux/amd64,linux/arm64 84 | tags: | 85 | ${{ secrets.OU_CONTAINER_DEST }}:${{ env.TAG }} 86 | ${{ secrets.OU_CONTAINER_DEST }}:${{ env.SHORT_TAG }} 87 | ${{ secrets.OU_CONTAINER_DEST }} 88 | ghcr.io/${{ env.REPO }}:${{ env.TAG }} 89 | ghcr.io/${{ env.REPO }}:${{ env.SHORT_TAG }} 90 | ghcr.io/${{ env.REPO }}:latest 91 | 92 | - name: sign images 93 | run: |- 94 | cosign sign -y ghcr.io/${{ env.REPO }}:${{ env.TAG }} 95 | 96 | - uses: anchore/sbom-action@v0 97 | with: 98 | image: ghcr.io/${{ env.REPO }}:${{ env.TAG }} 99 | format: spdx 100 | output-file: /tmp/spdxg 101 | 102 | - name: attach sbom to images 103 | run: |- 104 | cosign attach sbom --sbom /tmp/spdxg ghcr.io/${{ env.REPO }}:${{ env.TAG }} 105 | 106 | 107 | GH_SBOM_SHA=$(cosign verify --certificate-oidc-issuer-regexp='.*' --certificate-identity-regexp='.*' ghcr.io/${{ env.REPO }}:${{ env.TAG }} 2>/dev/null | jq -r '.[0].critical.image["docker-manifest-digest"]' | cut -c 8-) 108 | 109 | 110 | echo "GH_SBOM_SHA: $GH_SBOM_SHA" 111 | 112 | 113 | cosign sign -y ghcr.io/${{ env.REPO }}:sha256-$GH_SBOM_SHA.sbom 114 | 115 | -------------------------------------------------------------------------------- /demo/infrastructure/modules/amazon-cluster/cluster.tf: -------------------------------------------------------------------------------- 1 | variable "suffix" {} 2 | variable "cluster_version" {} 3 | 4 | data "aws_availability_zones" "available" {} 5 | 6 | locals { 7 | cluster_name = "cluster-${var.suffix}" 8 | } 9 | 10 | resource "aws_security_group" "worker_group_mgmt_one" { 11 | name_prefix = "worker_group_mgmt_one" 12 | vpc_id = module.vpc.vpc_id 13 | 14 | ingress { 15 | from_port = 22 16 | to_port = 22 17 | protocol = "tcp" 18 | 19 | cidr_blocks = [ 20 | "10.0.0.0/8", 21 | ] 22 | } 23 | } 24 | 25 | resource "aws_security_group" "worker_group_mgmt_two" { 26 | name_prefix = "worker_group_mgmt_two" 27 | vpc_id = module.vpc.vpc_id 28 | 29 | ingress { 30 | from_port = 22 31 | to_port = 22 32 | protocol = "tcp" 33 | 34 | cidr_blocks = [ 35 | "192.168.0.0/16", 36 | ] 37 | } 38 | } 39 | 40 | resource "aws_security_group" "all_worker_mgmt" { 41 | name_prefix = "all_worker_management" 42 | vpc_id = module.vpc.vpc_id 43 | 44 | ingress { 45 | from_port = 22 46 | to_port = 22 47 | protocol = "tcp" 48 | 49 | cidr_blocks = [ 50 | "10.0.0.0/8", 51 | "172.16.0.0/12", 52 | "192.168.0.0/16", 53 | ] 54 | } 55 | } 56 | 57 | module "vpc" { 58 | source = "terraform-aws-modules/vpc/aws" 59 | version = "2.6.0" 60 | 61 | name = "test-vpc" 62 | cidr = "10.0.0.0/16" 63 | azs = "${data.aws_availability_zones.available.names}" 64 | private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] 65 | public_subnets = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"] 66 | enable_nat_gateway = true 67 | single_nat_gateway = true 68 | enable_dns_hostnames = true 69 | 70 | tags = { 71 | "kubernetes.io/cluster/${local.cluster_name}" = "shared" 72 | } 73 | 74 | public_subnet_tags = { 75 | "kubernetes.io/cluster/${local.cluster_name}" = "shared" 76 | "kubernetes.io/role/elb" = "1" 77 | } 78 | 79 | private_subnet_tags = { 80 | "kubernetes.io/cluster/${local.cluster_name}" = "shared" 81 | "kubernetes.io/role/internal-elb" = "1" 82 | } 83 | } 84 | 85 | module "eks" { 86 | source = "terraform-aws-modules/eks/aws" 87 | cluster_name = "${local.cluster_name}" 88 | subnets = "${module.vpc.private_subnets}" 89 | 90 | tags = { 91 | Environment = "test" 92 | GithubRepo = "terraform-aws-eks" 93 | GithubOrg = "terraform-aws-modules" 94 | } 95 | 96 | vpc_id = "${module.vpc.vpc_id}" 97 | 98 | worker_groups = [ 99 | { 100 | name = "worker-group-1" 101 | instance_type = "t2.small" 102 | additional_userdata = "echo foo bar" 103 | asg_desired_capacity = 2 104 | additional_security_group_ids = ["${aws_security_group.worker_group_mgmt_one.id}"] 105 | }, 106 | { 107 | name = "worker-group-2" 108 | instance_type = "t2.medium" 109 | additional_userdata = "echo foo bar" 110 | additional_security_group_ids = ["${aws_security_group.worker_group_mgmt_two.id}"] 111 | asg_desired_capacity = 1 112 | }, 113 | ] 114 | 115 | worker_additional_security_group_ids = ["${aws_security_group.all_worker_mgmt.id}"] 116 | } 117 | 118 | data "aws_eks_cluster" "cluster" { 119 | name = module.eks.cluster_id 120 | } 121 | 122 | data "aws_eks_cluster_auth" "cluster" { 123 | name = module.eks.cluster_id 124 | } 125 | 126 | provider "kubernetes" { 127 | host = data.aws_eks_cluster.cluster.endpoint 128 | cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority.0.data) 129 | token = data.aws_eks_cluster_auth.cluster.token 130 | load_config_file = false 131 | version = "~> 1.10" 132 | } 133 | -------------------------------------------------------------------------------- /deploy/charts/kube-oidc-proxy/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for kube-oidc-proxy. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: quay.io/jetstack/kube-oidc-proxy 9 | tag: v0.3.0 10 | pullPolicy: IfNotPresent 11 | 12 | imagePullSecrets: [] 13 | nameOverride: "" 14 | fullnameOverride: "" 15 | 16 | service: 17 | type: ClusterIP 18 | port: 443 19 | annotations: 20 | # You can use this field to add annotations to the Service. 21 | # Define it in a key-value pairs. E.g. 22 | # service.beta.kubernetes.io/aws-load-balancer-internal: true 23 | 24 | loadBalancerIP: "" 25 | loadBalancerSourceRanges: [] 26 | 27 | tls: 28 | # `secretName` must be a name of Secret of TLS type. If not provided a 29 | # self-signed certificate will get generated. 30 | secretName: 31 | # `certManager` if you have cert-manager in your cluster and dont want to manage manually 32 | certManager: false 33 | # `selfSigned` if you have cert-manager and perfer or not to use use default issuer or generate by using other issuer 34 | selfSigned: true 35 | # `issuerName` if `selfSigned` is false, you should add your own Issuer 36 | issuerName: 37 | 38 | # These values needs to be set in overrides in order to get kube-oidc-proxy 39 | # working. 40 | oidc: 41 | # A minimal configuration requires setting clientId, issuerUrl and usernameClaim 42 | # values. 43 | clientId: "" 44 | issuerUrl: "" 45 | usernameClaim: "" 46 | 47 | # PEM encoded value of CA cert that will verify TLS connection to 48 | # OIDC issuer URL. If not provided, default hosts root CA's will be used. 49 | caPEM: 50 | 51 | usernamePrefix: 52 | groupsClaim: 53 | groupsPrefix: 54 | 55 | signingAlgs: 56 | - RS256 57 | requiredClaims: {} 58 | 59 | # To enable token passthrough feature 60 | # https://github.com/jetstack/kube-oidc-proxy/blob/master/docs/tasks/token-passthrough.md 61 | tokenPassthrough: 62 | enabled: false 63 | audiences: [] 64 | 65 | # To add extra impersonation headers 66 | # https://github.com/jetstack/kube-oidc-proxy/blob/master/docs/tasks/extra-impersonation-headers.md 67 | extraImpersonationHeaders: 68 | clientIP: false 69 | #headers: key1=foo,key2=bar,key1=bar 70 | 71 | extraArgs: {} 72 | #audit-log-path: /audit-log 73 | #audit-policy-file: /audit/audit.yaml 74 | 75 | extraVolumeMounts: {} 76 | #- name: audit 77 | # mountPath: /audit 78 | # readOnly: true 79 | 80 | extraVolumes: {} 81 | #- configMap: 82 | #defaultMode: 420 83 | #name: kube-oidc-proxy-policy 84 | #name: audit 85 | 86 | ingress: 87 | enabled: false 88 | annotations: {} 89 | # kubernetes.io/ingress.class: nginx 90 | # kubernetes.io/tls-acme: "true" 91 | hosts: 92 | - host: chart-example.local 93 | paths: [] 94 | 95 | # ingressClassName: '' 96 | 97 | tls: [] 98 | # - secretName: chart-example-tls 99 | # hosts: 100 | # - chart-example.local 101 | 102 | # Allows setting the Deployment update strategy 103 | #rollingUpdateStrategy: 104 | # type: RollingUpdate 105 | # rollingUpdate: 106 | # maxSurge: 34% 107 | # maxUnavailable: 33% 108 | 109 | # Enable Pod Disruption Budget 110 | podDisruptionBudget: 111 | enabled: false 112 | minAvailable: 1 113 | 114 | resources: {} 115 | # We usually recommend not to specify default resources and to leave this as a conscious 116 | # choice for the user. This also increases chances charts run on environments with little 117 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 118 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 119 | # limits: 120 | # cpu: 100m 121 | # memory: 128Mi 122 | # requests: 123 | # cpu: 100m 124 | # memory: 128Mi 125 | # 126 | 127 | initContainers: [] 128 | 129 | nodeSelector: {} 130 | 131 | tolerations: [] 132 | 133 | affinity: {} 134 | -------------------------------------------------------------------------------- /test/tools/issuer/pkg/issuer/issuer.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package issuer 3 | 4 | import ( 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/base64" 8 | "encoding/pem" 9 | "fmt" 10 | "io/ioutil" 11 | "net" 12 | "net/http" 13 | 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | type Issuer struct { 18 | issuerURL string 19 | keyFile, certFile string 20 | 21 | sk *rsa.PrivateKey 22 | 23 | stopCh <-chan struct{} 24 | } 25 | 26 | func New(issuerURL, keyFile, certFile string, stopCh <-chan struct{}) (*Issuer, error) { 27 | b, err := ioutil.ReadFile(keyFile) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | block, _ := pem.Decode(b) 33 | if block == nil { 34 | return nil, 35 | fmt.Errorf("failed to parse PEM block containing the key: %q", keyFile) 36 | } 37 | 38 | sk, err := x509.ParsePKCS1PrivateKey(block.Bytes) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return &Issuer{ 44 | keyFile: keyFile, 45 | certFile: certFile, 46 | issuerURL: issuerURL, 47 | sk: sk, 48 | stopCh: stopCh, 49 | }, nil 50 | } 51 | 52 | func (i *Issuer) Run(bindAddress, listenPort string) (<-chan struct{}, error) { 53 | serveAddr := fmt.Sprintf("%s:%s", bindAddress, listenPort) 54 | 55 | l, err := net.Listen("tcp", serveAddr) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | go func() { 61 | <-i.stopCh 62 | if l != nil { 63 | l.Close() 64 | } 65 | }() 66 | 67 | compCh := make(chan struct{}) 68 | go func() { 69 | defer close(compCh) 70 | 71 | err := http.ServeTLS(l, i, i.certFile, i.keyFile) 72 | if err != nil { 73 | log.Errorf("stopped serving TLS (%s): %s", serveAddr, err) 74 | } 75 | }() 76 | 77 | log.Infof("mock issuer listening and serving on %s", serveAddr) 78 | 79 | return compCh, nil 80 | } 81 | 82 | func (i *Issuer) ServeHTTP(rw http.ResponseWriter, r *http.Request) { 83 | log.Infof("mock issuer received url %s", r.URL) 84 | 85 | rw.Header().Set("Content-Type", "application/json; charset=utf-8") 86 | 87 | switch r.URL.String() { 88 | case "/.well-known/openid-configuration": 89 | rw.WriteHeader(http.StatusOK) 90 | 91 | if _, err := rw.Write(i.wellKnownResponse()); err != nil { 92 | log.Errorf("failed to write openid-configuration response: %s", err) 93 | } 94 | 95 | case "/certs": 96 | rw.WriteHeader(http.StatusOK) 97 | 98 | certsDiscovery := i.certsDiscovery() 99 | if _, err := rw.Write(certsDiscovery); err != nil { 100 | log.Errorf("failed to write certificate discovery response: %s", err) 101 | } 102 | 103 | default: 104 | log.Errorf("unexpected URL request: %s", r.URL) 105 | rw.WriteHeader(http.StatusNotFound) 106 | if _, err := rw.Write([]byte("{}\n")); err != nil { 107 | log.Errorf("failed to write data to resposne: %s", err) 108 | } 109 | } 110 | } 111 | 112 | func (i *Issuer) wellKnownResponse() []byte { 113 | return []byte(fmt.Sprintf(`{ 114 | "issuer": "%s", 115 | "jwks_uri": "%s/certs", 116 | "subject_types_supported": [ 117 | "public" 118 | ], 119 | "id_token_signing_alg_values_supported": [ 120 | "RS256" 121 | ], 122 | "scopes_supported": [ 123 | "openid", 124 | "email" 125 | ], 126 | "token_endpoint_auth_methods_supported": [ 127 | "client_secret_post", 128 | "client_secret_basic" 129 | ], 130 | "claims_supported": [ 131 | "email", 132 | "e2e-username-claim", 133 | "e2e-groups-claim", 134 | "sub" 135 | ], 136 | "code_challenge_methods_supported": [ 137 | "plain", 138 | "S256" 139 | ] 140 | }`, i.issuerURL, i.issuerURL)) 141 | } 142 | 143 | func (i *Issuer) certsDiscovery() []byte { 144 | n := base64.RawURLEncoding.EncodeToString(i.sk.N.Bytes()) 145 | 146 | return []byte(fmt.Sprintf(`{ 147 | "keys": [ 148 | { 149 | "kid": "0905d6f9cd9b0f1f852e8b207e8f673abca4bf75", 150 | "e": "AQAB", 151 | "kty": "RSA", 152 | "alg": "RS256", 153 | "n": "%s", 154 | "use": "sig" 155 | } 156 | ] 157 | }`, n)) 158 | } 159 | -------------------------------------------------------------------------------- /test/e2e/suite/cases/headers/headers.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package headers 3 | 4 | import ( 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | "time" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | 13 | "github.com/jetstack/kube-oidc-proxy/test/e2e/framework" 14 | testutil "github.com/jetstack/kube-oidc-proxy/test/util" 15 | ) 16 | 17 | var _ = framework.CasesDescribe("Headers", func() { 18 | f := framework.NewDefaultFramework("headers") 19 | 20 | JustAfterEach(func() { 21 | By("Deleting fake API Server") 22 | err := f.Helper().DeleteFakeAPIServer(f.Namespace.Name) 23 | Expect(err).NotTo(HaveOccurred()) 24 | }) 25 | 26 | It("should not respond with any extra headers if none are set on the proxy", func() { 27 | extraOIDCVolumes, fakeAPIServerURL, err := f.Helper().DeployFakeAPIServer(f.Namespace.Name) 28 | Expect(err).NotTo(HaveOccurred()) 29 | 30 | By("Redeploying proxy to send traffic to fake API server") 31 | f.DeployProxyWith(extraOIDCVolumes, fmt.Sprintf("--server=%s", fakeAPIServerURL), "--certificate-authority=/fake-apiserver/ca.pem") 32 | 33 | resp := sendRequestToProxy(f) 34 | 35 | By("Ensuring no extra headers sent by proxy") 36 | for k := range resp.Header { 37 | if strings.HasPrefix(strings.ToLower(k), "impersonate-extra-") { 38 | Expect(fmt.Errorf("expected no extra user headers, got=%+v", resp.Header)).NotTo(HaveOccurred()) 39 | } 40 | } 41 | }) 42 | 43 | It("should respond with remote address and custom extra headers when they are set", func() { 44 | By("Deploying fake API Server") 45 | extraOIDCVolumes, fakeAPIServerURL, err := f.Helper().DeployFakeAPIServer(f.Namespace.Name) 46 | Expect(err).NotTo(HaveOccurred()) 47 | 48 | By("Redeploying proxy to send traffic to fake API server with extra headers set") 49 | f.DeployProxyWith(extraOIDCVolumes, fmt.Sprintf("--server=%s", fakeAPIServerURL), "--certificate-authority=/fake-apiserver/ca.pem", 50 | "--extra-user-header-client-ip", "--extra-user-headers=key1=foo,key2=foo,key1=bar") 51 | 52 | resp := sendRequestToProxy(f) 53 | 54 | By("Ensuring expected headers are present") 55 | cpyHeader := resp.Header.Clone() 56 | 57 | // Check expected headers 58 | for k, v := range map[string][]string{ 59 | "Impersonate-Extra-Key1": []string{"foo", "bar"}, 60 | "Impersonate-Extra-Key2": []string{"foo"}, 61 | } { 62 | if !testutil.StringSlicesEqual(v, cpyHeader[k]) { 63 | Expect(fmt.Errorf("expected key %q to have value %q, but got headers: %v", 64 | k, v, cpyHeader)).NotTo(HaveOccurred()) 65 | } 66 | 67 | cpyHeader.Del(k) 68 | } 69 | 70 | // Check expected client IP header 71 | // TODO: determine a reliable way to get ip to match 72 | headerIP, ok := cpyHeader["Impersonate-Extra-Remote-Client-Ip"] 73 | if !ok || len(headerIP) != 1 { 74 | Expect(fmt.Errorf("expected impersonate extra remote client ip user header, got=%v", resp.Header)).NotTo(HaveOccurred()) 75 | } 76 | 77 | cpyHeader.Del("Impersonate-Extra-Remote-Client-Ip") 78 | 79 | By("Ensuring no extra user headers where added") 80 | for k := range cpyHeader { 81 | if strings.HasPrefix(strings.ToLower(k), "impersonate-extra-") { 82 | Expect(fmt.Errorf("expected no impersonate extra user headers, got=%+v", resp.Header)).NotTo(HaveOccurred()) 83 | } 84 | } 85 | }) 86 | }) 87 | 88 | func sendRequestToProxy(f *framework.Framework) *http.Response { 89 | By("Building request to proxy") 90 | tokenPayload := f.Helper().NewTokenPayload( 91 | f.IssuerURL(), f.ClientID(), time.Now().Add(time.Minute)) 92 | 93 | signedToken, err := f.Helper().SignToken(f.IssuerKeyBundle(), tokenPayload) 94 | Expect(err).NotTo(HaveOccurred()) 95 | 96 | proxyConfig := f.NewProxyRestConfig() 97 | requester := f.Helper().NewRequester(proxyConfig.Transport, signedToken) 98 | 99 | By("Sending request to proxy") 100 | reqURL := fmt.Sprintf("%s/foo/bar", proxyConfig.Host) 101 | _, resp, err := requester.Get(reqURL) 102 | Expect(err).NotTo(HaveOccurred()) 103 | 104 | return resp 105 | } 106 | -------------------------------------------------------------------------------- /demo/manifests/components/gangway.jsonnet: -------------------------------------------------------------------------------- 1 | local kube = import '../vendor/kube-prod-runtime/lib/kube.libsonnet'; 2 | local utils = import '../vendor/kube-prod-runtime/lib/utils.libsonnet'; 3 | 4 | local GANGWAY_IMAGE = 'gcr.io/heptio-images/gangway:v3.0.0'; 5 | local GANGWAY_PORT = 8080; 6 | local GANGWAY_CONFIG_VOLUME_PATH = '/etc/gangway'; 7 | local GANGWAY_TLS_VOLUME_PATH = GANGWAY_CONFIG_VOLUME_PATH + '/tls'; 8 | 9 | { 10 | p:: '', 11 | 12 | sessionSecurityKey:: error 'sessionSecurityKey is undefined', 13 | 14 | base_domain:: '.cluster.local', 15 | 16 | app:: 'gangway', 17 | 18 | name:: $.p + $.app, 19 | 20 | domain:: $.name + $.base_domain, 21 | gangway_url:: 'https://' + $.domain, 22 | 23 | namespace:: 'gangway', 24 | 25 | config_path:: GANGWAY_CONFIG_VOLUME_PATH, 26 | 27 | labels:: { 28 | metadata+: { 29 | labels+: { 30 | app: $.app, 31 | }, 32 | }, 33 | }, 34 | 35 | metadata:: $.labels { 36 | metadata+: { 37 | namespace: $.namespace, 38 | }, 39 | }, 40 | 41 | config:: { 42 | usernameClaim: 'name', 43 | redirectURL: $.gangway_url + '/callback', 44 | clusterName: 'cluster-name', 45 | authorize_url: 'https://' + $.domain + '/auth', 46 | clientID: 'client-id', 47 | tokenURL: 'https://' + $.domain + '/token', 48 | scopes: [ 49 | 'openid', 50 | 'email', 51 | 'profile', 52 | 'groups', 53 | 'offline_access', 54 | ], 55 | serveTLS: true, 56 | certFile: GANGWAY_TLS_VOLUME_PATH + '/tls.crt', 57 | keyFile: GANGWAY_TLS_VOLUME_PATH + '/tls.key', 58 | }, 59 | 60 | 61 | configMap: kube.ConfigMap($.name) + $.metadata { 62 | data+: { 63 | 'gangway.yaml': std.manifestJsonEx($.config, ' '), 64 | }, 65 | }, 66 | 67 | secret: kube.Secret($.name) + $.metadata { 68 | data_+: { 69 | 'session-security-key': $.sessionSecurityKey, 70 | }, 71 | }, 72 | 73 | deployment: kube.Deployment($.name) + $.metadata { 74 | local this = self, 75 | spec+: { 76 | replicas: 1, 77 | template+: { 78 | metadata+: { 79 | annotations+: { 80 | 'config/hash': std.md5(std.escapeStringJson($.configMap)), 81 | 'secret/hash': std.md5(std.escapeStringJson($.secret)), 82 | }, 83 | }, 84 | spec+: { 85 | affinity: kube.PodZoneAntiAffinityAnnotation(this.spec.template), 86 | default_container: $.app, 87 | volumes_+: { 88 | config: kube.ConfigMapVolume($.configMap), 89 | tls: { 90 | secret: { 91 | secretName: $.name + '-tls', 92 | }, 93 | }, 94 | }, 95 | containers_+: { 96 | gangway: kube.Container($.app) { 97 | image: GANGWAY_IMAGE, 98 | command: [$.app], 99 | args: [ 100 | '-config', 101 | GANGWAY_CONFIG_VOLUME_PATH + '/gangway.yaml', 102 | ], 103 | ports_+: { 104 | http: { containerPort: GANGWAY_PORT }, 105 | }, 106 | env_+: { 107 | GANGWAY_SESSION_SECURITY_KEY: kube.SecretKeyRef($.secret, 'session-security-key'), 108 | GANGWAY_PORT: GANGWAY_PORT, 109 | }, 110 | readinessProbe: { 111 | httpGet: { path: '/', port: GANGWAY_PORT, scheme: 'HTTPS' }, 112 | periodSeconds: 10, 113 | }, 114 | livenessProbe: { 115 | httpGet: { path: '/', port: GANGWAY_PORT, scheme: 'HTTPS' }, 116 | initialDelaySeconds: 20, 117 | periodSeconds: 10, 118 | }, 119 | volumeMounts_+: { 120 | config: { mountPath: GANGWAY_CONFIG_VOLUME_PATH }, 121 | tls: { mountPath: GANGWAY_TLS_VOLUME_PATH }, 122 | }, 123 | }, 124 | }, 125 | }, 126 | }, 127 | }, 128 | }, 129 | 130 | svc: kube.Service($.name) + $.metadata { 131 | target_pod: $.deployment.spec.template, 132 | }, 133 | } 134 | --------------------------------------------------------------------------------