├── .dockerignore ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── OWNERS ├── README.md ├── cmd ├── app │ ├── options │ │ ├── app.go │ │ ├── audit.go │ │ ├── client.go │ │ ├── misc.go │ │ ├── oidc.go │ │ ├── options.go │ │ ├── plugin.go │ │ └── serving.go │ └── run.go └── main.go ├── demo ├── .gitignore ├── Makefile ├── README.md ├── config.dist.jsonnet ├── infrastructure │ ├── .gitignore │ ├── amazon │ │ ├── .gitignore │ │ ├── dns.tf │ │ ├── outputs.tf │ │ ├── providers.tf │ │ ├── secrets.tf │ │ └── suffix.tf │ ├── digitalocean │ │ ├── .gitignore │ │ ├── dns.tf │ │ ├── outputs.tf │ │ ├── providers.tf │ │ ├── secrets.tf │ │ └── suffix.tf │ ├── google │ │ ├── cluster.tf │ │ ├── dns.tf │ │ ├── output.tf │ │ ├── providers.tf │ │ ├── secrets.tf │ │ ├── suffix.tf │ │ └── variables.tf │ └── modules │ │ ├── amazon-cluster │ │ ├── cluster.tf │ │ └── outputs.tf │ │ ├── ca │ │ └── ca.tf │ │ ├── digitalocean-cluster │ │ ├── cluster.tf │ │ └── outputs.tf │ │ ├── gangway │ │ └── secrets.tf │ │ ├── google-cluster │ │ └── cluster.tf │ │ ├── google-dns │ │ └── dns.tf │ │ └── oauth2-secrets │ │ └── secrets.tf └── manifests │ ├── .gitignore │ ├── components │ ├── base32.libsonnet │ ├── cert-manager.jsonnet │ ├── cert-manager │ │ └── cert-manager.json │ ├── contour-clusterrole.json │ ├── contour-crds.json │ ├── contour.jsonnet │ ├── dex.jsonnet │ ├── gangway.jsonnet │ ├── kube-oidc-proxy.jsonnet │ ├── landingpage.jsonnet │ └── landingpage │ │ ├── amazon.svg │ │ ├── digitalocean.svg │ │ ├── google.svg │ │ └── index.html │ ├── jsonnetfile.json │ ├── jsonnetfile.lock.json │ ├── main.jsonnet │ └── vendor │ └── kube-prod-runtime │ ├── Makefile │ ├── VERSION │ ├── components │ ├── alertmanager-config.jsonnet │ ├── cert-manager.jsonnet │ ├── elasticsearch-config │ │ └── java.security │ ├── elasticsearch.jsonnet │ ├── externaldns.jsonnet │ ├── fluentd-es-config │ │ ├── containers.input.conf │ │ ├── fluentd.conf │ │ ├── import-from-upstream.py │ │ ├── monitoring.conf │ │ ├── output.conf │ │ ├── system.conf │ │ └── system.input.conf │ ├── fluentd-es.jsonnet │ ├── grafana.jsonnet │ ├── images.json │ ├── kibana.jsonnet │ ├── nginx-ingress.jsonnet │ ├── oauth2-proxy.jsonnet │ ├── prometheus-config.jsonnet │ ├── prometheus.jsonnet │ └── version.jsonnet │ ├── lib │ ├── kube.libsonnet │ └── utils.libsonnet │ ├── platforms │ ├── aks.jsonnet │ └── gke.jsonnet │ └── tests │ ├── aks.jsonnet │ └── gke.jsonnet ├── deploy ├── charts │ └── kube-oidc-proxy │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── README.md │ │ ├── templates │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── clusterrole.yaml │ │ ├── clusterrolebinding.yaml │ │ ├── deployment.yaml │ │ ├── ingress.yaml │ │ ├── poddisruptionbudget.yaml │ │ ├── secret_config.yaml │ │ ├── secret_tls.yaml │ │ ├── service.yaml │ │ ├── serviceaccount.yaml │ │ └── tests │ │ │ └── test-connection.yaml │ │ └── values.yaml └── yaml │ ├── kube-oidc-proxy.yaml │ └── secrets.yaml ├── docs └── tasks │ ├── auditing.md │ ├── development-testing.md │ ├── extra-impersonation-headers.md │ ├── no-impersonation.md │ └── token-passthrough.md ├── go.mod ├── go.sum ├── hack ├── boilerplate │ ├── boilerplate.Dockerfile.txt │ ├── boilerplate.Makefile.txt │ ├── boilerplate.go.txt │ ├── boilerplate.py │ ├── boilerplate.py.txt │ ├── boilerplate.sh.txt │ ├── boilerplate_test.py │ └── test │ │ ├── fail.go │ │ ├── fail.py │ │ ├── pass.go │ │ └── pass.py ├── cherry-pick-pull.sh ├── docker-start-wrapper.sh ├── lib │ └── version.sh ├── tools │ └── tools.go ├── update-vendor.sh ├── verify-boilerplate.sh └── version-ldflags.sh ├── img ├── kube-oidc-proxy.png └── kube-oidc-proxy.xml ├── pkg ├── mocks │ ├── .gitignore │ └── mocks.go ├── probe │ ├── probe.go │ └── probe_test.go ├── proxy │ ├── audit │ │ ├── audit.go │ │ └── handler.go │ ├── context │ │ └── context.go │ ├── handlers.go │ ├── hooks │ │ └── hooks.go │ ├── proxy.go │ ├── proxy_test.go │ └── tokenreview │ │ ├── fake │ │ └── tokenreview.go │ │ ├── tokenreview.go │ │ └── tokenreview_test.go └── util │ ├── flags │ ├── string_to_string_slice.go │ └── string_to_string_slice_test.go │ ├── port.go │ ├── signals.go │ ├── token.go │ └── token_test.go └── test ├── e2e ├── framework │ ├── config │ │ └── config.go │ ├── framework.go │ ├── helper │ │ ├── deploy.go │ │ ├── helper.go │ │ ├── kubectl.go │ │ ├── poll.go │ │ ├── requester.go │ │ ├── secrets.go │ │ └── token.go │ └── util.go └── suite │ ├── cases │ ├── audit │ │ └── audit.go │ ├── doc.go │ ├── headers │ │ └── headers.go │ ├── impersonation │ │ └── impersonation.go │ ├── passthrough │ │ └── passthrough.go │ ├── probe │ │ └── probe.go │ ├── rbac │ │ └── rbac.go │ ├── token │ │ └── token.go │ └── upgrade │ │ └── upgrade.go │ ├── suite.go │ └── suite_test.go ├── environment ├── dev │ └── dev.go └── environment.go ├── kind ├── image.go └── kind.go ├── tools ├── audit-webhook │ ├── .gitignore │ ├── Dockerfile │ ├── cmd │ │ ├── main.go │ │ └── options │ │ │ └── options.go │ └── pkg │ │ └── sink │ │ └── sink.go ├── fake-apiserver │ ├── .gitignore │ ├── Dockerfile │ ├── cmd │ │ ├── main.go │ │ └── options │ │ │ └── options.go │ └── pkg │ │ └── server │ │ └── server.go └── issuer │ ├── .gitignore │ ├── Dockerfile │ ├── cmd │ ├── main.go │ └── options │ │ └── options.go │ └── pkg │ └── issuer │ └── issuer.go └── util ├── strings.go └── tls.go /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !/bin/kube-oidc-proxy-linux 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /kube-oidc-proxy 2 | /bin 3 | /demo/config.jsonnet 4 | /artifacts/ 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright Jetstack Ltd. See LICENSE for details. 2 | FROM alpine:3.10 3 | LABEL description="OIDC reverse proxy authenticator based on Kubernetes" 4 | 5 | RUN apk --no-cache add ca-certificates \ 6 | && apk --no-cache add --upgrade openssl 7 | 8 | COPY ./bin/kube-oidc-proxy-linux /usr/bin/kube-oidc-proxy 9 | 10 | CMD ["/usr/bin/kube-oidc-proxy"] 11 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | approvers: 2 | - simonswine 3 | - joshvanl 4 | reviewers: 5 | - simonswine 6 | - joshvanl 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 15 | func NewClientOptions(nfs *cliflag.NamedFlagSets) *ClientOptions { 16 | c := &ClientOptions{ 17 | ConfigFlags: genericclioptions.NewConfigFlags(true), 18 | } 19 | 20 | // Disable unwanted options 21 | c.CacheDir = nil 22 | c.Impersonate = nil 23 | c.ImpersonateGroup = nil 24 | 25 | return c.AddFlags(nfs.FlagSet("Client")) 26 | } 27 | 28 | func (c *ClientOptions) AddFlags(fs *pflag.FlagSet) *ClientOptions { 29 | c.ConfigFlags.AddFlags(fs) 30 | return c 31 | } 32 | 33 | func (c *ClientOptions) ClientFlagsChanged(cmd *cobra.Command) bool { 34 | for _, f := range clientOptionFlags() { 35 | if ff := cmd.Flag(f); ff != nil && ff.Changed { 36 | return true 37 | } 38 | } 39 | 40 | return false 41 | } 42 | 43 | func clientOptionFlags() []string { 44 | return []string{"certificate-authority", "client-certificate", "client-key", "cluster", 45 | "context", "insecure-skip-tls-verify", "kubeconfig", "namespace", 46 | "request-timeout", "server", "token", "user", 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | k8sErrors "k8s.io/apimachinery/pkg/util/errors" 10 | "k8s.io/apiserver/pkg/util/term" 11 | cliflag "k8s.io/component-base/cli/flag" 12 | ) 13 | 14 | const ( 15 | AppName = "kube-oidc-proxy" 16 | ) 17 | 18 | type Options struct { 19 | App *KubeOIDCProxyOptions 20 | OIDCAuthentication *OIDCAuthenticationOptions 21 | SecureServing *SecureServingOptions 22 | Audit *AuditOptions 23 | Client *ClientOptions 24 | Misc *MiscOptions 25 | 26 | nfs *cliflag.NamedFlagSets 27 | } 28 | 29 | func New() *Options { 30 | nfs := new(cliflag.NamedFlagSets) 31 | 32 | // Add flags to command sets 33 | return &Options{ 34 | App: NewKubeOIDCProxyOptions(nfs), 35 | OIDCAuthentication: NewOIDCAuthenticationOptions(nfs), 36 | SecureServing: NewSecureServingOptions(nfs), 37 | Audit: NewAuditOptions(nfs), 38 | Client: NewClientOptions(nfs), 39 | Misc: NewMiscOptions(nfs), 40 | 41 | nfs: nfs, 42 | } 43 | } 44 | 45 | func (o *Options) AddFlags(cmd *cobra.Command) { 46 | // pretty output from kube-apiserver 47 | usageFmt := "Usage:\n %s\n" 48 | cols, _, _ := term.TerminalSize(cmd.OutOrStdout()) 49 | cmd.SetUsageFunc(func(cmd *cobra.Command) error { 50 | fmt.Fprintf(cmd.OutOrStderr(), usageFmt, cmd.UseLine()) 51 | cliflag.PrintSections(cmd.OutOrStderr(), *o.nfs, cols) 52 | return nil 53 | }) 54 | 55 | cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { 56 | fmt.Fprintf(cmd.OutOrStdout(), "%s\n\n"+usageFmt, cmd.Long, cmd.UseLine()) 57 | cliflag.PrintSections(cmd.OutOrStdout(), *o.nfs, cols) 58 | }) 59 | 60 | fs := cmd.Flags() 61 | for _, f := range o.nfs.FlagSets { 62 | fs.AddFlagSet(f) 63 | } 64 | } 65 | 66 | func (o *Options) Validate(cmd *cobra.Command) error { 67 | if cmd.Flag("version").Value.String() == "true" { 68 | o.Misc.PrintVersionAndExit() 69 | } 70 | 71 | var errs []error 72 | 73 | if err := o.OIDCAuthentication.Validate(); err != nil { 74 | errs = append(errs, err) 75 | } 76 | 77 | if err := o.SecureServing.Validate(); len(err) > 0 { 78 | errs = append(errs, err...) 79 | } 80 | 81 | if o.SecureServing.BindPort == o.App.ReadinessProbePort { 82 | errs = append(errs, errors.New("unable to securely serve on port 8080 (used by readiness probe)")) 83 | } 84 | 85 | if err := o.Audit.Validate(); len(err) > 0 { 86 | errs = append(errs, err...) 87 | } 88 | 89 | if o.App.DisableImpersonation && 90 | (o.App.ExtraHeaderOptions.EnableClientIPExtraUserHeader || len(o.App.ExtraHeaderOptions.ExtraUserHeaders) > 0) { 91 | errs = append(errs, errors.New("cannot add extra user headers when impersonation disabled")) 92 | } 93 | 94 | if o.Audit.DynamicOptions.Enabled { 95 | errs = append(errs, errors.New("The flag --audit-dynamic-configuration may not be set")) 96 | } 97 | 98 | if len(errs) > 0 { 99 | return k8sErrors.NewAggregate(errs) 100 | } 101 | 102 | return nil 103 | } 104 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cmd/app/run.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package app 3 | 4 | import ( 5 | "strconv" 6 | 7 | "github.com/spf13/cobra" 8 | "k8s.io/apiserver/pkg/server" 9 | "k8s.io/client-go/rest" 10 | 11 | "github.com/jetstack/kube-oidc-proxy/cmd/app/options" 12 | "github.com/jetstack/kube-oidc-proxy/pkg/probe" 13 | "github.com/jetstack/kube-oidc-proxy/pkg/proxy" 14 | "github.com/jetstack/kube-oidc-proxy/pkg/proxy/tokenreview" 15 | "github.com/jetstack/kube-oidc-proxy/pkg/util" 16 | ) 17 | 18 | func NewRunCommand(stopCh <-chan struct{}) *cobra.Command { 19 | // Build options 20 | opts := options.New() 21 | 22 | // Build command 23 | cmd := buildRunCommand(stopCh, opts) 24 | 25 | // Add option flags to command 26 | opts.AddFlags(cmd) 27 | 28 | return cmd 29 | } 30 | 31 | // Proxy command 32 | func buildRunCommand(stopCh <-chan struct{}, opts *options.Options) *cobra.Command { 33 | return &cobra.Command{ 34 | Use: options.AppName, 35 | Long: "kube-oidc-proxy is a reverse proxy to authenticate users to Kubernetes API servers with Open ID Connect Authentication.", 36 | RunE: func(cmd *cobra.Command, args []string) error { 37 | if err := opts.Validate(cmd); err != nil { 38 | return err 39 | } 40 | 41 | // Here we determine to either use custom or 'in-cluster' client configuration 42 | var err error 43 | var restConfig *rest.Config 44 | if opts.Client.ClientFlagsChanged(cmd) { 45 | // One or more client flags have been set to use client flag built 46 | // config 47 | restConfig, err = opts.Client.ToRESTConfig() 48 | if err != nil { 49 | return err 50 | } 51 | 52 | } else { 53 | // No client flags have been set so default to in-cluster config 54 | restConfig, err = rest.InClusterConfig() 55 | if err != nil { 56 | return err 57 | } 58 | } 59 | 60 | // Initialise token reviewer if enabled 61 | var tokenReviewer *tokenreview.TokenReview 62 | if opts.App.TokenPassthrough.Enabled { 63 | tokenReviewer, err = tokenreview.New(restConfig, opts.App.TokenPassthrough.Audiences) 64 | if err != nil { 65 | return err 66 | } 67 | } 68 | 69 | // Initialise Secure Serving Config 70 | secureServingInfo := new(server.SecureServingInfo) 71 | if err := opts.SecureServing.ApplyTo(&secureServingInfo); err != nil { 72 | return err 73 | } 74 | 75 | proxyConfig := &proxy.Config{ 76 | TokenReview: opts.App.TokenPassthrough.Enabled, 77 | DisableImpersonation: opts.App.DisableImpersonation, 78 | 79 | FlushInterval: opts.App.FlushInterval, 80 | ExternalAddress: opts.SecureServing.BindAddress.String(), 81 | 82 | ExtraUserHeaders: opts.App.ExtraHeaderOptions.ExtraUserHeaders, 83 | ExtraUserHeadersClientIPEnabled: opts.App.ExtraHeaderOptions.EnableClientIPExtraUserHeader, 84 | } 85 | 86 | // Initialise proxy with OIDC token authenticator 87 | p, err := proxy.New(restConfig, opts.OIDCAuthentication, opts.Audit, 88 | tokenReviewer, secureServingInfo, proxyConfig) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | // Create a fake JWT to set up readiness probe 94 | fakeJWT, err := util.FakeJWT(opts.OIDCAuthentication.IssuerURL) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | // Start readiness probe 100 | if err := probe.Run(strconv.Itoa(opts.App.ReadinessProbePort), 101 | fakeJWT, p.OIDCTokenAuthenticator()); err != nil { 102 | return err 103 | } 104 | 105 | // Run proxy 106 | waitCh, err := p.Run(stopCh) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | <-waitCh 112 | 113 | if err := p.RunPreShutdownHooks(); err != nil { 114 | return err 115 | } 116 | 117 | return nil 118 | }, 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /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 | ) 11 | 12 | func main() { 13 | stopCh := util.SignalHandler() 14 | cmd := app.NewRunCommand(stopCh) 15 | 16 | if err := cmd.Execute(); err != nil { 17 | fmt.Fprintf(os.Stderr, "error: %v\n", err) 18 | os.Exit(1) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | /secrets 2 | /.kubeconfig-* 3 | /bin/ 4 | /.backup-certificates.yaml 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/infrastructure/.gitignore: -------------------------------------------------------------------------------- 1 | .terraform/ 2 | *.tfstate 3 | *.tfstate.backup 4 | *.tfvars 5 | -------------------------------------------------------------------------------- /demo/infrastructure/amazon/.gitignore: -------------------------------------------------------------------------------- 1 | /config-map-aws-auth_* 2 | /kubeconfig_* 3 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /demo/infrastructure/amazon/secrets.tf: -------------------------------------------------------------------------------- 1 | module "gangway" { 2 | source = "../modules/gangway" 3 | length = 24 4 | } 5 | -------------------------------------------------------------------------------- /demo/infrastructure/amazon/suffix.tf: -------------------------------------------------------------------------------- 1 | resource "random_id" "suffix" { 2 | byte_length = 4 3 | } 4 | -------------------------------------------------------------------------------- /demo/infrastructure/digitalocean/.gitignore: -------------------------------------------------------------------------------- 1 | /config-map-aws-auth_* 2 | /kubeconfig_* 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/digitalocean/secrets.tf: -------------------------------------------------------------------------------- 1 | module "gangway" { 2 | source = "../modules/gangway" 3 | length = 24 4 | } 5 | -------------------------------------------------------------------------------- /demo/infrastructure/digitalocean/suffix.tf: -------------------------------------------------------------------------------- 1 | resource "random_id" "suffix" { 2 | byte_length = 4 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/infrastructure/google/secrets.tf: -------------------------------------------------------------------------------- 1 | module "gangway" { 2 | source = "../modules/gangway" 3 | length = 24 4 | } 5 | -------------------------------------------------------------------------------- /demo/infrastructure/google/suffix.tf: -------------------------------------------------------------------------------- 1 | resource "random_id" "suffix" { 2 | byte_length = 4 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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 = <> 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/manifests/components/landingpage/amazon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/manifests/components/landingpage/google.svg: -------------------------------------------------------------------------------- 1 | 2 | Container Engine 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/manifests/vendor/kube-prod-runtime/VERSION: -------------------------------------------------------------------------------- 1 | dev-untagged 2 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/system.conf: -------------------------------------------------------------------------------- 1 | 2 | root_dir /tmp/fluentd-buffers/ 3 | 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /deploy/charts/kube-oidc-proxy/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "v0.3.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.1 7 | maintainers: 8 | - name: mhrabovcin 9 | - name: joshvanl 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/_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 | -------------------------------------------------------------------------------- /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 | verbs: 23 | - "create" 24 | - "impersonate" 25 | -------------------------------------------------------------------------------- /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/charts/kube-oidc-proxy/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "kube-oidc-proxy.fullname" . -}} 3 | apiVersion: extensions/v1beta1 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 | rules: 25 | {{- range .Values.ingress.hosts }} 26 | - host: {{ .host | quote }} 27 | http: 28 | paths: 29 | {{- range .paths }} 30 | - path: {{ . }} 31 | backend: 32 | serviceName: {{ $fullName }} 33 | servicePort: https 34 | {{- end }} 35 | {{- end }} 36 | {{- end }} 37 | -------------------------------------------------------------------------------- /deploy/charts/kube-oidc-proxy/templates/poddisruptionbudget.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.podDisruptionBudget.enabled -}} 2 | apiVersion: policy/v1beta1 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /deploy/charts/kube-oidc-proxy/templates/secret_tls.yaml: -------------------------------------------------------------------------------- 1 | {{- if (not .Values.tls.secretName) }} 2 | {{ $fullname := include "kube-oidc-proxy.fullname" . }} 3 | {{ $ca := genCA (printf "%s-ca" $fullname) 3650 }} 4 | {{ $cn := printf "%s.%s.svc.cluster.local" $fullname .Release.Namespace }} 5 | {{ $server := genSignedCert $cn nil nil 365 $ca }} 6 | 7 | apiVersion: v1 8 | kind: Secret 9 | type: kubernetes.io/tls 10 | metadata: 11 | name: {{ template "kube-oidc-proxy.fullname" . }}-tls 12 | labels: 13 | {{ include "kube-oidc-proxy.labels" . | indent 4 }} 14 | data: 15 | tls.crt: {{ b64enc $server.Cert }} 16 | tls.key: {{ b64enc $server.Key }} 17 | {{ end }} 18 | -------------------------------------------------------------------------------- /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: 443 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 32 | # These values needs to be set in overrides in order to get kube-oidc-proxy 33 | # working. 34 | oidc: 35 | # A minimal configuration requires setting clientId, issuerUrl and usernameClaim 36 | # values. 37 | clientId: "" 38 | issuerUrl: "" 39 | usernameClaim: "" 40 | 41 | # PEM encoded value of CA cert that will verify TLS connection to 42 | # OIDC issuer URL. If not provided, default hosts root CA's will be used. 43 | caPEM: 44 | 45 | usernamePrefix: 46 | groupsClaim: 47 | groupsPrefix: 48 | 49 | signingAlgs: 50 | - RS256 51 | requiredClaims: {} 52 | 53 | # To enable token passthrough feature 54 | # https://github.com/jetstack/kube-oidc-proxy/blob/master/docs/tasks/token-passthrough.md 55 | tokenPassthrough: 56 | enabled: false 57 | audiences: [] 58 | 59 | # To add extra impersonation headers 60 | # https://github.com/jetstack/kube-oidc-proxy/blob/master/docs/tasks/extra-impersonation-headers.md 61 | extraImpersonationHeaders: 62 | clientIP: false 63 | #headers: key1=foo,key2=bar,key1=bar 64 | 65 | extraArgs: {} 66 | #audit-log-path: /audit-log 67 | #audit-policy-file: /audit/audit.yaml 68 | 69 | extraVolumeMounts: {} 70 | #- name: audit 71 | # mountPath: /audit 72 | # readOnly: true 73 | 74 | extraVolumes: {} 75 | #- configMap: 76 | #defaultMode: 420 77 | #name: kube-oidc-proxy-policy 78 | #name: audit 79 | 80 | ingress: 81 | enabled: false 82 | annotations: {} 83 | # kubernetes.io/ingress.class: nginx 84 | # kubernetes.io/tls-acme: "true" 85 | hosts: 86 | - host: chart-example.local 87 | paths: [] 88 | 89 | tls: [] 90 | # - secretName: chart-example-tls 91 | # hosts: 92 | # - chart-example.local 93 | 94 | # Allows setting the Deployment update strategy 95 | #rollingUpdateStrategy: 96 | # type: RollingUpdate 97 | # rollingUpdate: 98 | # maxSurge: 34% 99 | # maxUnavailable: 33% 100 | 101 | # Enable Pod Disruption Budget 102 | podDisruptionBudget: 103 | enabled: false 104 | minAvailable: 1 105 | 106 | resources: {} 107 | # We usually recommend not to specify default resources and to leave this as a conscious 108 | # choice for the user. This also increases chances charts run on environments with little 109 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 110 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 111 | # limits: 112 | # cpu: 100m 113 | # memory: 128Mi 114 | # requests: 115 | # cpu: 100m 116 | # memory: 128Mi 117 | # 118 | 119 | initContainers: [] 120 | 121 | nodeSelector: {} 122 | 123 | tolerations: [] 124 | 125 | affinity: {} 126 | -------------------------------------------------------------------------------- /deploy/yaml/kube-oidc-proxy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: kube-oidc-proxy 5 | --- 6 | apiVersion: v1 7 | kind: ServiceAccount 8 | metadata: 9 | creationTimestamp: null 10 | name: kube-oidc-proxy 11 | namespace: kube-oidc-proxy 12 | --- 13 | apiVersion: apps/v1 14 | kind: Deployment 15 | metadata: 16 | labels: 17 | app: kube-oidc-proxy 18 | name: kube-oidc-proxy 19 | namespace: kube-oidc-proxy 20 | spec: 21 | replicas: 1 22 | selector: 23 | matchLabels: 24 | app: kube-oidc-proxy 25 | template: 26 | metadata: 27 | labels: 28 | app: kube-oidc-proxy 29 | spec: 30 | serviceAccountName: kube-oidc-proxy 31 | containers: 32 | - image: quay.io/jetstack/kube-oidc-proxy:v0.3.0 33 | ports: 34 | - containerPort: 443 35 | - containerPort: 8080 36 | readinessProbe: 37 | httpGet: 38 | path: /ready 39 | port: 8080 40 | initialDelaySeconds: 15 41 | periodSeconds: 10 42 | name: kube-oidc-proxy 43 | command: ["kube-oidc-proxy"] 44 | args: 45 | - "--secure-port=443" 46 | - "--tls-cert-file=/etc/oidc/tls/crt.pem" 47 | - "--tls-private-key-file=/etc/oidc/tls/key.pem" 48 | - "--oidc-client-id=$(OIDC_CLIENT_ID)" 49 | - "--oidc-issuer-url=$(OIDC_ISSUER_URL)" 50 | - "--oidc-username-claim=$(OIDC_USERNAME_CLAIM)" 51 | - "--oidc-ca-file=/etc/oidc/oidc-ca.pem" 52 | env: 53 | - name: OIDC_CLIENT_ID 54 | valueFrom: 55 | secretKeyRef: 56 | name: kube-oidc-proxy-config 57 | key: oidc.client-id 58 | - name: OIDC_ISSUER_URL 59 | valueFrom: 60 | secretKeyRef: 61 | name: kube-oidc-proxy-config 62 | key: oidc.issuer-url 63 | - name: OIDC_USERNAME_CLAIM 64 | valueFrom: 65 | secretKeyRef: 66 | name: kube-oidc-proxy-config 67 | key: oidc.username-claim 68 | volumeMounts: 69 | - name: kube-oidc-proxy-config 70 | mountPath: /etc/oidc 71 | readOnly: true 72 | - name: kube-oidc-proxy-tls 73 | mountPath: /etc/oidc/tls 74 | readOnly: true 75 | volumes: 76 | - name: kube-oidc-proxy-config 77 | secret: 78 | secretName: kube-oidc-proxy-config 79 | items: 80 | - key: oidc.ca-pem 81 | path: oidc-ca.pem 82 | - name: kube-oidc-proxy-tls 83 | secret: 84 | secretName: kube-oidc-proxy-tls 85 | items: 86 | - key: tls.crt 87 | path: crt.pem 88 | - key: tls.key 89 | path: key.pem 90 | --- 91 | apiVersion: v1 92 | kind: Service 93 | metadata: 94 | creationTimestamp: null 95 | labels: 96 | app: kube-oidc-proxy 97 | name: kube-oidc-proxy 98 | namespace: kube-oidc-proxy 99 | spec: 100 | ports: 101 | - port: 443 102 | protocol: TCP 103 | targetPort: 443 104 | type: LoadBalancer 105 | selector: 106 | app: kube-oidc-proxy 107 | --- 108 | kind: ClusterRoleBinding 109 | apiVersion: rbac.authorization.k8s.io/v1 110 | metadata: 111 | name: kube-oidc-proxy 112 | roleRef: 113 | apiGroup: rbac.authorization.k8s.io 114 | kind: ClusterRole 115 | name: kube-oidc-proxy 116 | subjects: 117 | - kind: ServiceAccount 118 | name: kube-oidc-proxy 119 | namespace: kube-oidc-proxy 120 | --- 121 | kind: ClusterRole 122 | apiVersion: rbac.authorization.k8s.io/v1 123 | metadata: 124 | name: kube-oidc-proxy 125 | rules: 126 | - apiGroups: 127 | - "" 128 | resources: 129 | - "users" 130 | - "groups" 131 | - "serviceaccounts" 132 | verbs: 133 | - "impersonate" 134 | - apiGroups: 135 | - "authentication.k8s.io" 136 | resources: 137 | - "userextras/scopes" 138 | - "userextras/remote-client-ip" 139 | - "tokenreviews" 140 | verbs: 141 | - "create" 142 | - "impersonate" 143 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jetstack/kube-oidc-proxy 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/golang/mock v1.2.0 7 | github.com/heptiolabs/healthcheck v0.0.0-20180807145615-6ff867650f40 8 | github.com/onsi/ginkgo v1.11.0 9 | github.com/onsi/gomega v1.7.0 10 | github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35 11 | github.com/sirupsen/logrus v1.4.2 12 | github.com/spf13/cobra v0.0.5 13 | github.com/spf13/pflag v1.0.5 14 | gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 // indirect 15 | gopkg.in/square/go-jose.v2 v2.3.1 16 | k8s.io/api v0.18.14 17 | k8s.io/apimachinery v0.18.14 18 | k8s.io/apiserver v0.18.0 19 | k8s.io/cli-runtime v0.18.0 20 | k8s.io/client-go v0.18.14 21 | k8s.io/component-base v0.18.0 22 | k8s.io/klog v1.0.0 23 | sigs.k8s.io/kind v0.7.0 24 | ) 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /hack/tools/tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | // Copyright Jetstack Ltd. See LICENSE for details. 4 | package tools 5 | 6 | // This file is used to vendor packages we use to build binaries. 7 | // This is the current canonical way with go modules. 8 | 9 | import ( 10 | _ "github.com/golang/mock/mockgen" 11 | ) 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /img/kube-oidc-proxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetstack/kube-oidc-proxy/6ed44b437a43ec17b4ce9eb56aebd264029bab44/img/kube-oidc-proxy.png -------------------------------------------------------------------------------- /pkg/mocks/.gitignore: -------------------------------------------------------------------------------- 1 | /authenticator.go 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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" 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).Infof("OIDC provider initialized, readiness check returned expected error: %s", err) 70 | klog.Info("OIDC provider initialized, proxy ready") 71 | 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 13 | "github.com/jetstack/kube-oidc-proxy/cmd/app/options" 14 | ) 15 | 16 | type Audit struct { 17 | opts *options.AuditOptions 18 | serverConfig *server.CompletedConfig 19 | } 20 | 21 | // New creates a new Audit struct to handle auditing for proxy requests. This 22 | // is mostly a wrapper for the apiserver auditing handlers to combine them with 23 | // the proxy. 24 | func New(opts *options.AuditOptions, externalAddress string, secureServingInfo *server.SecureServingInfo) (*Audit, error) { 25 | serverConfig := &server.Config{ 26 | ExternalAddress: externalAddress, 27 | SecureServing: secureServingInfo, 28 | 29 | // Default to treating watch as a long-running operation. 30 | // Generic API servers have no inherent long-running subresources. 31 | // This is so watch requests are handled correctly in the audit log. 32 | LongRunningFunc: genericfilters.BasicLongRunningRequestCheck( 33 | sets.NewString("watch"), sets.NewString()), 34 | } 35 | 36 | // We do not support dynamic auditing, so leave nil 37 | if err := opts.ApplyTo(serverConfig, nil, nil, nil, nil); err != nil { 38 | return nil, err 39 | } 40 | 41 | completed := serverConfig.Complete(nil) 42 | 43 | return &Audit{ 44 | opts: opts, 45 | serverConfig: &completed, 46 | }, nil 47 | } 48 | 49 | // Run will run the audit backend if configured. 50 | func (a *Audit) Run(stopCh <-chan struct{}) error { 51 | if a.serverConfig.AuditBackend != nil { 52 | if err := a.serverConfig.AuditBackend.Run(stopCh); err != nil { 53 | return fmt.Errorf("failed to run the audit backend: %s", err) 54 | } 55 | } 56 | 57 | return nil 58 | } 59 | 60 | // Shutdown will shutdown the audit backend if configured. 61 | func (a *Audit) Shutdown() error { 62 | if a.serverConfig.AuditBackend != nil { 63 | a.serverConfig.AuditBackend.Shutdown() 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // WithRequest will wrap the given handler to inject the request information 70 | // into the context which is then used by the wrapped audit handler. 71 | func (a *Audit) WithRequest(handler http.Handler) http.Handler { 72 | handler = genericapifilters.WithAudit(handler, a.serverConfig.AuditBackend, a.serverConfig.AuditPolicyChecker, a.serverConfig.LongRunningFunc) 73 | return genericapifilters.WithRequestInfo(handler, a.serverConfig.RequestInfoResolver) 74 | } 75 | 76 | // WithUnauthorized will wrap the given handler to inject the request 77 | // information into the context which is then used by the wrapped audit 78 | // handler. 79 | func (a *Audit) WithUnauthorized(handler http.Handler) http.Handler { 80 | handler = genericapifilters.WithFailedAuthenticationAudit(handler, a.serverConfig.AuditBackend, a.serverConfig.AuditPolicyChecker) 81 | return genericapifilters.WithRequestInfo(handler, a.serverConfig.RequestInfoResolver) 82 | } 83 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/endpoints/request" 9 | "k8s.io/client-go/transport" 10 | ) 11 | 12 | type key int 13 | 14 | const ( 15 | // noImpersonationKey is the context key for whether to use impersonation. 16 | noImpersonationKey key = iota 17 | 18 | // impersonationConfigKey is the context key for the impersonation config. 19 | impersonationConfigKey 20 | 21 | // bearerTokenKey is the context key for the bearer token. 22 | bearerTokenKey 23 | 24 | // bearerTokenKey is the context key for the client address. 25 | clientAddressKey 26 | ) 27 | 28 | // WithNoImpersonation returns a copy of the request in which the noImpersonation context value is set. 29 | func WithNoImpersonation(req *http.Request) *http.Request { 30 | return req.WithContext(request.WithValue(req.Context(), noImpersonationKey, true)) 31 | } 32 | 33 | // NoImpersonation returns whether the noImpersonation context key has been set 34 | func NoImpersonation(req *http.Request) bool { 35 | noImp, _ := req.Context().Value(noImpersonationKey).(bool) 36 | return noImp 37 | } 38 | 39 | // WithImpersonationConfig returns a copy of parent in which contains the impersonation configuration. 40 | func WithImpersonationConfig(req *http.Request, conf *transport.ImpersonationConfig) *http.Request { 41 | return req.WithContext(request.WithValue(req.Context(), impersonationConfigKey, conf)) 42 | } 43 | 44 | // ImpersonationConfig returns the impersonation configuration held in the context if existing. 45 | func ImpersonationConfig(req *http.Request) *transport.ImpersonationConfig { 46 | conf, _ := req.Context().Value(impersonationConfigKey).(*transport.ImpersonationConfig) 47 | return conf 48 | } 49 | 50 | // WithBearerToken will add the bearer token to the request context from an http.Header to the request context. 51 | func WithBearerToken(req *http.Request, header http.Header) *http.Request { 52 | return req.WithContext(request.WithValue(req.Context(), bearerTokenKey, header.Get("Authorization"))) 53 | } 54 | 55 | // BearerToken will return the bearer token stored in the request context. 56 | func BearerToken(req *http.Request) string { 57 | token, _ := req.Context().Value(bearerTokenKey).(string) 58 | return token 59 | } 60 | 61 | // RemoteAddress will attempt to return the source client address if available 62 | // in the request context. If it is not, it will be gathered from the request 63 | // and entered into the context. 64 | func RemoteAddr(req *http.Request) (*http.Request, string) { 65 | ctx := req.Context() 66 | 67 | clientAddress, ok := ctx.Value(clientAddressKey).(string) 68 | if !ok { 69 | clientAddress = xff.GetRemoteAddr(req) 70 | req = req.WithContext(request.WithValue(ctx, clientAddressKey, clientAddress)) 71 | } 72 | 73 | return req, clientAddress 74 | } 75 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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" 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pkg/util/token_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package util 3 | 4 | import ( 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestParseTokenFromHeader(t *testing.T) { 10 | tests := map[string]struct { 11 | req *http.Request 12 | token string 13 | ok bool 14 | }{ 15 | "should return !ok if request is nil": { 16 | req: nil, 17 | token: "", 18 | ok: false, 19 | }, 20 | "should return !ok if request.Header is nil": { 21 | req: &http.Request{ 22 | Header: nil, 23 | }, 24 | token: "", 25 | ok: false, 26 | }, 27 | "should return !ok if no Authorization header given": { 28 | req: &http.Request{ 29 | Header: map[string][]string{ 30 | "Random-Header1": []string{"foo bar"}, 31 | "Random-Header2": []string{"boo koo"}, 32 | }, 33 | }, 34 | token: "", 35 | ok: false, 36 | }, 37 | "should return !ok if Authorization header is empty": { 38 | req: &http.Request{ 39 | Header: map[string][]string{ 40 | "Random-Header1": []string{"foo bar"}, 41 | "Authorization": []string{}, 42 | }, 43 | }, 44 | token: "", 45 | ok: false, 46 | }, 47 | "should return !ok if Authorization header is only 'bearer'": { 48 | req: &http.Request{ 49 | Header: map[string][]string{ 50 | "Random-Header1": []string{"foo bar"}, 51 | "Authorization": []string{"bearer"}, 52 | }, 53 | }, 54 | token: "", 55 | ok: false, 56 | }, 57 | "should return !ok if Authorization header is only 'bearertoken'": { 58 | req: &http.Request{ 59 | Header: map[string][]string{ 60 | "Random-Header1": []string{"foo bar"}, 61 | "Authorization": []string{"bearertoken"}, 62 | }, 63 | }, 64 | token: "", 65 | ok: false, 66 | }, 67 | "should return 'token' if Authorization header is 'bearer token'": { 68 | req: &http.Request{ 69 | Header: map[string][]string{ 70 | "Random-Header1": []string{"foo bar"}, 71 | "Authorization": []string{"bearer token"}, 72 | }, 73 | }, 74 | token: "token", 75 | ok: true, 76 | }, 77 | "should return !ok if Authorization header is 'bearer token' but not first element": { 78 | req: &http.Request{ 79 | Header: map[string][]string{ 80 | "Random-Header1": []string{"foo bar"}, 81 | "Authorization": []string{"foo bar", "bearer token"}, 82 | }, 83 | }, 84 | token: "", 85 | ok: false, 86 | }, 87 | "should return 'token' if Authorization header is 'bearer token some-other-string'": { 88 | req: &http.Request{ 89 | Header: map[string][]string{ 90 | "Random-Header1": []string{"foo bar"}, 91 | "Authorization": []string{"bearer token some-other-string"}, 92 | }, 93 | }, 94 | token: "token", 95 | ok: true, 96 | }, 97 | "should return 'token' if Authorization header is 'bearer token' but mixed capitals on bearer": { 98 | req: &http.Request{ 99 | Header: map[string][]string{ 100 | "Random-Header1": []string{"foo bar"}, 101 | "Authorization": []string{"BeAREr token"}, 102 | }, 103 | }, 104 | token: "token", 105 | ok: true, 106 | }, 107 | "should return !ok if Authorization header is 'bearer token' but the header name is title capitalised": { 108 | req: &http.Request{ 109 | Header: map[string][]string{ 110 | "Random-Header1": []string{"foo bar"}, 111 | "authorization": []string{"bearer token"}, 112 | }, 113 | }, 114 | token: "", 115 | ok: false, 116 | }, 117 | "should return !ok if Authorization header has multiple spaces between 'bearer' and 'token'": { 118 | req: &http.Request{ 119 | Header: map[string][]string{ 120 | "Random-Header1": []string{"foo bar"}, 121 | "Authorization": []string{"bearer token"}, 122 | }, 123 | }, 124 | token: "", 125 | ok: false, 126 | }, 127 | } 128 | 129 | for name, test := range tests { 130 | t.Run(name, func(t *testing.T) { 131 | gToken, gok := ParseTokenFromRequest(test.req) 132 | 133 | if test.ok != gok { 134 | t.Errorf("unexpected ok, exp=%t got=%t", 135 | test.ok, gok) 136 | } 137 | 138 | if test.token != gToken { 139 | t.Errorf("unexpected token, exp=%s got=%s", 140 | test.token, gToken) 141 | } 142 | }) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/e2e/framework/helper/poll.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package helper 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "time" 8 | 9 | log "github.com/sirupsen/logrus" 10 | corev1 "k8s.io/api/core/v1" 11 | k8sErrors "k8s.io/apimachinery/pkg/api/errors" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/util/wait" 14 | ) 15 | 16 | func (h *Helper) WaitForDeploymentReady(namespace, name string, timeout time.Duration) error { 17 | log.Infof("Waiting for Deployment to become ready %s/%s", namespace, name) 18 | 19 | err := wait.PollImmediate(time.Second*2, timeout, func() (bool, error) { 20 | deploy, err := h.KubeClient.AppsV1().Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{}) 21 | if err != nil { 22 | return false, err 23 | } 24 | 25 | if deploy.Spec.Replicas == nil { 26 | return false, nil 27 | } 28 | 29 | if *deploy.Spec.Replicas == deploy.Status.ReadyReplicas { 30 | return true, nil 31 | } 32 | 33 | return false, nil 34 | }) 35 | 36 | if err != nil { 37 | kErr := h.Kubectl(namespace).DescribeResource("deployment", name) 38 | if kErr != nil { 39 | err = fmt.Errorf("%s\n%s", err, kErr) 40 | } 41 | 42 | return err 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func (h *Helper) WaitForPodReady(namespace, name string, timeout time.Duration) error { 49 | log.Infof("Waiting for Pod to become ready %s/%s", namespace, name) 50 | 51 | err := wait.PollImmediate(time.Second*2, timeout, func() (bool, error) { 52 | pod, err := h.KubeClient.CoreV1().Pods(namespace).Get(context.TODO(), name, metav1.GetOptions{}) 53 | if err != nil { 54 | return false, err 55 | } 56 | 57 | if len(pod.Status.Conditions) == 0 { 58 | return false, nil 59 | } 60 | 61 | var ready bool 62 | for _, cond := range pod.Status.Conditions { 63 | if cond.Type == corev1.PodReady && 64 | cond.Status == corev1.ConditionTrue { 65 | ready = true 66 | break 67 | } 68 | } 69 | 70 | if !ready { 71 | log.Infof("helper: pod not ready %s/%s: %v", 72 | pod.Namespace, pod.Name, pod.Status.Conditions) 73 | return false, nil 74 | } 75 | 76 | return true, nil 77 | }) 78 | if err != nil { 79 | kErr := h.Kubectl(namespace).DescribeResource("pod", name) 80 | if kErr != nil { 81 | err = fmt.Errorf("%s\n%s", err, kErr) 82 | } 83 | 84 | return err 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func (h *Helper) WaitForDeploymentToDelete(namespace, name string, timeout time.Duration) error { 91 | log.Infof("Waiting for Deployment to be deleted: %s/%s", namespace, name) 92 | 93 | err := wait.PollImmediate(time.Second*2, timeout, func() (bool, error) { 94 | _, err := h.KubeClient.AppsV1().Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{}) 95 | if k8sErrors.IsNotFound(err) { 96 | return true, nil 97 | } 98 | 99 | if err != nil { 100 | return false, nil 101 | } 102 | 103 | return false, nil 104 | }) 105 | 106 | if err != nil { 107 | kErr := h.Kubectl(namespace).DescribeResource("deployment", name) 108 | if kErr != nil { 109 | err = fmt.Errorf("%s\n%s", err, kErr) 110 | } 111 | 112 | return err 113 | } 114 | 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/e2e/framework/helper/secrets.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package helper 3 | 4 | import ( 5 | "context" 6 | 7 | corev1 "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | func (h *Helper) GetServiceAccountSecret(ns, name string) (*corev1.Secret, error) { 12 | sa, err := h.KubeClient.CoreV1().ServiceAccounts(ns).Get(context.TODO(), name, metav1.GetOptions{}) 13 | if err != nil { 14 | return nil, err 15 | } 16 | sec, err := h.KubeClient.CoreV1().Secrets(ns).Get(context.TODO(), sa.Secrets[0].Name, metav1.GetOptions{}) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | return sec, nil 22 | } 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/environment/dev/dev.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package main 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "net/url" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "time" 12 | 13 | corev1 "k8s.io/api/core/v1" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/cli-runtime/pkg/genericclioptions" 16 | "k8s.io/client-go/kubernetes" 17 | 18 | "github.com/jetstack/kube-oidc-proxy/test/e2e/framework/config" 19 | "github.com/jetstack/kube-oidc-proxy/test/e2e/framework/helper" 20 | "github.com/jetstack/kube-oidc-proxy/test/environment" 21 | "github.com/jetstack/kube-oidc-proxy/test/kind" 22 | ) 23 | 24 | const ( 25 | clientID = "kube-oidc-proxy-e2e-client-id" 26 | 27 | EnvFakeAPIServer = "KUBE_OIDC_PROXY_FAKE_APISERVER" 28 | ) 29 | 30 | func main() { 31 | if len(os.Args) != 2 { 32 | errExit(fmt.Errorf("expecting 2 arguments, got=%d", 33 | len(os.Args))) 34 | } 35 | 36 | switch os.Args[1] { 37 | case "create": 38 | create() 39 | case "deploy": 40 | deploy() 41 | case "destroy": 42 | destroy() 43 | default: 44 | errExit(fmt.Errorf("unexpected argument %q, expecting [%q %q %q]", 45 | os.Args[1], "create", "deploy", "destroy")) 46 | } 47 | 48 | os.Exit(0) 49 | } 50 | 51 | func create() { 52 | env, err := environment.New(1, 1) 53 | errExit(err) 54 | 55 | errExit(env.Create()) 56 | 57 | fmt.Printf("> dev environment created.\n") 58 | fmt.Printf("export KUBECONFIG=%s\n", env.KubeConfigPath()) 59 | } 60 | 61 | func deploy() { 62 | env, err := environment.New(1, 1) 63 | errExit(err) 64 | 65 | fmt.Printf("> reloading all images\n") 66 | errExit(env.Kind().LoadAllImages()) 67 | 68 | kubeconfig := env.KubeConfigPath() 69 | 70 | cfg := &config.Config{ 71 | KubeConfigPath: kubeconfig, 72 | RepoRoot: env.RootPath(), 73 | Kubectl: filepath.Join(env.RootPath(), "bin", "kubectl"), 74 | } 75 | 76 | err = cfg.Validate() 77 | errExit(err) 78 | 79 | helper := helper.NewHelper(cfg) 80 | 81 | clientConfigFlags := genericclioptions.NewConfigFlags(true) 82 | clientConfigFlags.KubeConfig = &kubeconfig 83 | config, err := clientConfigFlags.ToRESTConfig() 84 | errExit(err) 85 | 86 | kubeClient, err := kubernetes.NewForConfig(config) 87 | errExit(err) 88 | 89 | helper.KubeClient = kubeClient 90 | 91 | ns := &corev1.Namespace{ 92 | ObjectMeta: metav1.ObjectMeta{ 93 | GenerateName: "kube-oidc-proxy-e2e-", 94 | }, 95 | } 96 | ns, err = kubeClient.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{}) 97 | errExit(err) 98 | 99 | fmt.Printf("> created new namespace %s\n", ns.Name) 100 | 101 | issuerKeyBundle, issuerURL, err := helper.DeployIssuer(ns.Name) 102 | errExit(err) 103 | 104 | fmt.Printf("> deployed issuer at url %s\n", issuerURL) 105 | 106 | var proxyURL *url.URL 107 | if e := os.Getenv(EnvFakeAPIServer); strings.ToLower(e) == "true" { 108 | extraOIDCVolume, fURL, err := helper.DeployFakeAPIServer(ns.Name) 109 | errExit(err) 110 | 111 | fmt.Printf("> deployed fake API server at url %s\n", fURL) 112 | 113 | _, proxyURL, err = helper.DeployProxy(ns, issuerURL, 114 | "kube-oidc-proxy-e2e-client-id", issuerKeyBundle, extraOIDCVolume, 115 | fmt.Sprintf("--server=%s", fURL), "--certificate-authority=/fake-apiserver/ca.pem") 116 | errExit(err) 117 | 118 | } else { 119 | _, proxyURL, err = helper.DeployProxy(ns, issuerURL, 120 | "kube-oidc-proxy-e2e-client-id", issuerKeyBundle, nil) 121 | errExit(err) 122 | } 123 | 124 | fmt.Printf("> deployed proxy at url %s\n", proxyURL) 125 | 126 | tokenPayload := helper.NewTokenPayload(issuerURL, clientID, time.Now().Add(time.Hour*48)) 127 | 128 | signedToken, err := helper.SignToken(issuerKeyBundle, tokenPayload) 129 | errExit(err) 130 | fmt.Printf("> signed token valid for 48 hours:\n%s\n", signedToken) 131 | 132 | fmt.Printf("export KUBECONFIG=%s\n", kubeconfig) 133 | } 134 | 135 | func destroy() { 136 | errExit(kind.DeleteCluster("kube-oidc-proxy-e2e")) 137 | fmt.Printf("> dev environment destroyed.\n") 138 | } 139 | 140 | func errExit(err error) { 141 | if err != nil { 142 | fmt.Fprintf(os.Stderr, "%s\n", err) 143 | os.Exit(1) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /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.18.2" 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 | -------------------------------------------------------------------------------- /test/kind/image.go: -------------------------------------------------------------------------------- 1 | // Copyright Jetstack Ltd. See LICENSE for details. 2 | package kind 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "strings" 13 | 14 | log "github.com/sirupsen/logrus" 15 | "sigs.k8s.io/kind/pkg/cluster/nodeutils" 16 | ) 17 | 18 | const ( 19 | ProxyImageName = "kube-oidc-proxy-e2e" 20 | IssuerImageName = "oidc-issuer-e2e" 21 | FakeAPIServerImageName = "fake-apiserver-e2e" 22 | AuditWebhookImageName = "audit-webhook-e2e" 23 | ) 24 | 25 | func (k *Kind) LoadAllImages() error { 26 | if err := k.LoadKubeOIDCProxy(); err != nil { 27 | return err 28 | } 29 | 30 | if err := k.LoadIssuer(); err != nil { 31 | return err 32 | } 33 | 34 | if err := k.LoadFakeAPIServer(); err != nil { 35 | return err 36 | } 37 | 38 | if err := k.LoadAuditWebhook(); err != nil { 39 | return err 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func (k *Kind) LoadKubeOIDCProxy() error { 46 | binPath := filepath.Join(k.rootPath, "./bin/kube-oidc-proxy-linux") 47 | mainPath := filepath.Join(k.rootPath, "./cmd/.") 48 | 49 | return k.loadImage(binPath, mainPath, ProxyImageName, k.rootPath) 50 | } 51 | 52 | func (k *Kind) LoadIssuer() error { 53 | binPath := filepath.Join(k.rootPath, "./test/tools/issuer/bin/oidc-issuer-linux") 54 | dockerfilePath := filepath.Join(k.rootPath, "./test/tools/issuer") 55 | mainPath := filepath.Join(dockerfilePath, "cmd") 56 | 57 | return k.loadImage(binPath, mainPath, IssuerImageName, dockerfilePath) 58 | } 59 | 60 | func (k *Kind) LoadFakeAPIServer() error { 61 | binPath := filepath.Join(k.rootPath, "./test/tools/fake-apiserver/bin/fake-apiserver-linux") 62 | dockerfilePath := filepath.Join(k.rootPath, "./test/tools/fake-apiserver") 63 | mainPath := filepath.Join(dockerfilePath, "cmd") 64 | 65 | return k.loadImage(binPath, mainPath, FakeAPIServerImageName, dockerfilePath) 66 | } 67 | 68 | func (k *Kind) LoadAuditWebhook() error { 69 | binPath := filepath.Join(k.rootPath, "./test/tools/audit-webhook/bin/audit-webhook") 70 | dockerfilePath := filepath.Join(k.rootPath, "./test/tools/audit-webhook") 71 | mainPath := filepath.Join(dockerfilePath, "cmd") 72 | 73 | return k.loadImage(binPath, mainPath, AuditWebhookImageName, dockerfilePath) 74 | } 75 | 76 | func (k *Kind) loadImage(binPath, mainPath, image, dockerfilePath string) error { 77 | log.Infof("kind: building %q", mainPath) 78 | 79 | if err := os.MkdirAll(filepath.Dir(binPath), 0755); err != nil { 80 | return err 81 | } 82 | 83 | err := k.runCmd("go", "build", "-v", "-o", binPath, mainPath) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | err = k.runCmd("docker", "build", "-t", image, dockerfilePath) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | tmpDir, err := ioutil.TempDir(os.TempDir(), "kube-oidc-proxy-e2e") 94 | if err != nil { 95 | return err 96 | } 97 | defer os.RemoveAll(tmpDir) 98 | 99 | imageArchive := filepath.Join(tmpDir, fmt.Sprintf("%s-e2e.tar", image)) 100 | log.Infof("kind: saving image to archive %q", imageArchive) 101 | 102 | err = k.runCmd("docker", "save", "--output="+imageArchive, image) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | nodes, err := k.Nodes() 108 | if err != nil { 109 | return err 110 | } 111 | 112 | b, err := ioutil.ReadFile(imageArchive) 113 | if err != nil { 114 | return err 115 | } 116 | 117 | for _, node := range nodes { 118 | log.Infof("kind: loading image %q to node %q", image, node.String()) 119 | r := bytes.NewBuffer(b) 120 | if err := nodeutils.LoadImageArchive(node, r); err != nil { 121 | return err 122 | } 123 | 124 | err := node.Command("mkdir", "-p", "/tmp/kube-oidc-proxy").Run() 125 | if err != nil { 126 | return fmt.Errorf("failed to create directory %q: %s", 127 | "/tmp/kube-oidc-proxy", err) 128 | } 129 | } 130 | 131 | return nil 132 | } 133 | 134 | func (k *Kind) runCmd(command string, args ...string) error { 135 | return k.runCmdWithOut(os.Stdout, command, args...) 136 | } 137 | 138 | func (k *Kind) runCmdWithOut(w io.Writer, command string, args ...string) error { 139 | log.Infof("kind: running command '%s %s'", command, strings.Join(args, " ")) 140 | cmd := exec.Command(command, args...) 141 | 142 | cmd.Stderr = os.Stderr 143 | cmd.Stdout = w 144 | cmd.Env = append(cmd.Env, 145 | "GO111MODULE=on", "CGO_ENABLED=0", "HOME="+os.Getenv("HOME"), 146 | "PATH="+os.Getenv("PATH"), 147 | "GOARCH=amd64", "GOOS=linux") 148 | 149 | if err := cmd.Start(); err != nil { 150 | return err 151 | } 152 | 153 | if err := cmd.Wait(); err != nil { 154 | return err 155 | } 156 | 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /test/tools/audit-webhook/.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /test/tools/fake-apiserver/.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /test/tools/issuer/.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------