diff --git a/CHANGELOG.md b/CHANGELOG.md index 336d07f..444ea3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Changes since v3.2.0 +- [#111](https://github.com/pusher/oauth2_proxy/pull/111) Add option for telling where to find a login.gov JWT key file (@timothy-spencer) + # v3.2.0 ## Release highlights diff --git a/Dockerfile b/Dockerfile index 4391434..09db37a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,13 +11,19 @@ COPY . . # Fetch dependencies RUN dep ensure --vendor-only -# Build binary -RUN ./configure && make build +# Build binary and make sure there is at least an empty key file. +# This is useful for GCP App Engine custom runtime builds, because +# you cannot use multiline variables in their app.yaml, so you have to +# build the key into the container and then tell it where it is +# by setting OAUTH2_PROXY_JWT_KEY_FILE=/etc/ssl/private/jwt_signing_key.pem +# in app.yaml instead. +RUN ./configure && make build && touch jwt_signing_key.pem # Copy binary to alpine FROM alpine:3.8 COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/oauth2_proxy /bin/oauth2_proxy +COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem RUN addgroup -S -g 2000 oauth2proxy && adduser -S -u 2000 oauth2proxy -G oauth2proxy USER oauth2proxy diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 5abd9bb..d9eb0f6 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -11,13 +11,19 @@ COPY . . # Fetch dependencies RUN dep ensure --vendor-only -# Build binary -RUN ./configure && GOARCH=arm64 make build +# Build binary and make sure there is at least an empty key file. +# This is useful for GCP App Engine custom runtime builds, because +# you cannot use multiline variables in their app.yaml, so you have to +# build the key into the container and then tell it where it is +# by setting OAUTH2_PROXY_JWT_KEY_FILE=/etc/ssl/private/jwt_signing_key.pem +# in app.yaml instead. +RUN ./configure && GOARCH=arm64 make build && touch jwt_signing_key.pem # Copy binary to alpine FROM arm64v8/alpine:3.8 COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/oauth2_proxy /bin/oauth2_proxy +COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem RUN addgroup -S -g 2000 oauth2proxy && adduser -S -u 2000 oauth2proxy -G oauth2proxy USER oauth2proxy diff --git a/Dockerfile.armv6 b/Dockerfile.armv6 index d51f16d..acd57d0 100644 --- a/Dockerfile.armv6 +++ b/Dockerfile.armv6 @@ -11,13 +11,19 @@ COPY . . # Fetch dependencies RUN dep ensure --vendor-only -# Build binary -RUN ./configure && GOARCH=arm GOARM=6 make build +# Build binary and make sure there is at least an empty key file. +# This is useful for GCP App Engine custom runtime builds, because +# you cannot use multiline variables in their app.yaml, so you have to +# build the key into the container and then tell it where it is +# by setting OAUTH2_PROXY_JWT_KEY_FILE=/etc/ssl/private/jwt_signing_key.pem +# in app.yaml instead. +RUN ./configure && GOARCH=arm GOARM=6 make build && touch jwt_signing_key.pem # Copy binary to alpine FROM arm32v6/alpine:3.8 COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/oauth2_proxy /bin/oauth2_proxy +COPY --from=builder /go/src/github.com/pusher/oauth2_proxy/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem RUN addgroup -S -g 2000 oauth2proxy && adduser -S -u 2000 oauth2proxy -G oauth2proxy USER oauth2proxy diff --git a/README.md b/README.md index c313770..6b53a69 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,13 @@ Now start the proxy up with the following options: -jwt-key="${OAUTH2_PROXY_JWT_KEY}" ``` You can also set all these options with environment variables, for use in cloud/docker environments. +One tricky thing that you may encounter is that some cloud environments will pass in environment +variables in a docker env-file, which does not allow multiline variables like a PEM file. +If you encounter this, then you can create a `jwt_signing_key.pem` file in the top level +directory of the repo which contains the key in PEM format and then do your docker build. +The docker build process will copy that file into your image which you can then access by +setting the `OAUTH2_PROXY_JWT_KEY_FILE=/etc/ssl/private/jwt_signing_key.pem` +environment variable, or by setting `-jwt-key-file=/etc/ssl/private/jwt_signing_key.pem` on the commandline. Once it is running, you should be able to go to `http://localhost:4180/` in your browser, get authenticated by the login.gov integration server, and then get proxied on to your @@ -261,6 +268,7 @@ An example [oauth2_proxy.cfg](contrib/oauth2_proxy.cfg.example) config file is i ``` Usage of oauth2_proxy: + -acr-values string: optional, used by login.gov (default "http://idmanagement.gov/ns/assurance/loa/1") -approval-prompt string: OAuth approval_prompt (default "force") -authenticated-emails-file string: authenticate against emails via file (one per line) -azure-tenant string: go to a tenant-specific or common (tenant-independent) endpoint. (default "common") @@ -269,10 +277,10 @@ Usage of oauth2_proxy: -client-secret string: the OAuth Client Secret -config string: path to config file -cookie-domain string: an optional cookie domain to force cookies to (ie: .yourcompany.com) - -cookie-path string: an optional cookie path to force cookies to (ie: /foo) -cookie-expire duration: expire timeframe for cookie (default 168h0m0s) -cookie-httponly: set HttpOnly cookie flag (default true) -cookie-name string: the name of the cookie that the oauth_proxy creates (default "_oauth2_proxy") + -cookie-path string: an optional cookie path to force cookies to (ie: /poc/)* (default "/") -cookie-refresh duration: refresh the cookie after this duration; 0 to disable -cookie-secret string: the seed string for secure cookies (optionally base64 encoded) -cookie-secure: set secure (HTTPS) cookie flag (default true) @@ -290,6 +298,8 @@ Usage of oauth2_proxy: -htpasswd-file string: additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption -http-address string: [http://]: or unix:// to listen on for HTTP clients (default "127.0.0.1:4180") -https-address string: : to listen on for HTTPS clients (default ":443") + -jwt-key string: private key in PEM format used to sign JWT, so that you can say something like -jwt-key="${OAUTH2_PROXY_JWT_KEY}": required by login.gov + -jwt-key-file string: path to the private key file in PEM format used to sign the JWT so that you can say something like -jwt-key-file=/etc/ssl/private/jwt_signing_key.pem: required by login.gov -login-url string: Authentication endpoint -oidc-issuer-url: the OpenID Connect issuer URL. ie: "https://accounts.google.com" -oidc-jwks-url string: OIDC JWKS URI for token verification; required if OIDC discovery is disabled @@ -302,6 +312,7 @@ Usage of oauth2_proxy: -provider string: OAuth provider (default "google") -proxy-prefix string: the url root path that this proxy should be nested under (e.g. //sign_in) (default "/oauth2") -proxy-websockets: enables WebSocket proxying (default true) + -pubjwk-url string: JWK pubkey access endpoint: required by login.gov -redeem-url string: Token redemption endpoint -redirect-url string: the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback" -request-logging: Log requests to stdout (default true) diff --git a/main.go b/main.go index ac9f80f..d63c4ec 100644 --- a/main.go +++ b/main.go @@ -92,7 +92,8 @@ func main() { flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)") flagSet.String("acr-values", "http://idmanagement.gov/ns/assurance/loa/1", "acr values string: optional, used by login.gov") - flagSet.String("jwt-key", "", "private key used to sign JWT: required by login.gov") + flagSet.String("jwt-key", "", "private key in PEM format used to sign JWT, so that you can say something like -jwt-key=\"${OAUTH2_PROXY_JWT_KEY}\": required by login.gov") + flagSet.String("jwt-key-file", "", "path to the private key file in PEM format used to sign the JWT so that you can say something like -jwt-key-file=/etc/ssl/private/jwt_signing_key.pem: required by login.gov") flagSet.String("pubjwk-url", "", "JWK pubkey access endpoint: required by login.gov") flagSet.Bool("gcp-healthchecks", false, "Enable GCP/GKE healthcheck endpoints") diff --git a/options.go b/options.go index 620d626..b0587a6 100644 --- a/options.go +++ b/options.go @@ -6,6 +6,7 @@ import ( "crypto/tls" "encoding/base64" "fmt" + "io/ioutil" "net/http" "net/url" "os" @@ -90,6 +91,7 @@ type Options struct { SignatureKey string `flag:"signature-key" cfg:"signature_key" env:"OAUTH2_PROXY_SIGNATURE_KEY"` AcrValues string `flag:"acr-values" cfg:"acr_values" env:"OAUTH2_PROXY_ACR_VALUES"` JWTKey string `flag:"jwt-key" cfg:"jwt_key" env:"OAUTH2_PROXY_JWT_KEY"` + JWTKeyFile string `flag:"jwt-key-file" cfg:"jwt_key_file" env:"OAUTH2_PROXY_JWT_KEY_FILE"` PubJWKURL string `flag:"pubjwk-url" cfg:"pubjwk_url" env:"OAUTH2_PROXY_PUBJWK_URL"` GCPHealthChecks bool `flag:"gcp-healthchecks" cfg:"gcp_healthchecks" env:"OAUTH2_PROXY_GCP_HEALTHCHECKS"` @@ -328,15 +330,33 @@ func parseProviderInfo(o *Options, msgs []string) []string { case *providers.LoginGovProvider: p.AcrValues = o.AcrValues p.PubJWKURL, msgs = parseURL(o.PubJWKURL, "pubjwk", msgs) - if o.JWTKey == "" { + + // JWT key can be supplied via env variable or file in the filesystem, but not both. + switch { + case o.JWTKey != "" && o.JWTKeyFile != "": + msgs = append(msgs, "cannot set both jwt-key and jwt-key-file options") + case o.JWTKey == "" && o.JWTKeyFile == "": msgs = append(msgs, "login.gov provider requires a private key for signing JWTs") - } else { + case o.JWTKey != "": + // The JWT Key is in the commandline argument signKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(o.JWTKey)) if err != nil { msgs = append(msgs, "could not parse RSA Private Key PEM") } else { p.JWTKey = signKey } + case o.JWTKeyFile != "": + // The JWT key is in the filesystem + keyData, err := ioutil.ReadFile(o.JWTKeyFile) + if err != nil { + msgs = append(msgs, "could not read key file: "+o.JWTKeyFile) + } + signKey, err := jwt.ParseRSAPrivateKeyFromPEM(keyData) + if err != nil { + msgs = append(msgs, "could not parse private key from PEM file:"+o.JWTKeyFile) + } else { + p.JWTKey = signKey + } } } return msgs