diff --git a/authenticate/authenticate.go b/authenticate/authenticate.go index 032cb89f8..9d5717295 100644 --- a/authenticate/authenticate.go +++ b/authenticate/authenticate.go @@ -20,7 +20,6 @@ import ( var defaultOptions = &Options{ CookieName: "_pomerium_authenticate", CookieHTTPOnly: true, - SkipProviderButton: true, CookieExpire: time.Duration(168) * time.Hour, CookieRefresh: time.Duration(1) * time.Hour, SessionLifetimeTTL: time.Duration(720) * time.Hour, @@ -57,8 +56,7 @@ type Options struct { // Scopes is an optional setting corresponding to OAuth 2.0 specification's access scopes // issuing an Access Token. Named providers are already set with good defaults. // Most likely only overrides if using the generic OIDC provider. - Scopes []string `envconfig:"IDP_SCOPE"` - SkipProviderButton bool `envconfig:"SKIP_PROVIDER_BUTTON"` + Scopes []string `envconfig:"IDP_SCOPE"` } // OptionsFromEnvConfig builds the authentication service's configuration @@ -80,7 +78,7 @@ func (o *Options) Validate() error { } redirectPath := "/oauth2/callback" if o.RedirectURL.Path != redirectPath { - return fmt.Errorf("setting redirect-url was %s path should be %s", o.RedirectURL.Path, redirectPath) + return fmt.Errorf("`setting` redirect-url was %s path should be %s", o.RedirectURL.Path, redirectPath) } if o.ClientID == "" { return errors.New("missing setting: client id") @@ -127,8 +125,6 @@ type Authenticate struct { sessionStore sessions.SessionStore cipher cryptutil.Cipher - skipProviderButton bool - provider providers.Provider } @@ -153,7 +149,7 @@ func New(opts *Options, optionFuncs ...func(*Authenticate) error) (*Authenticate return nil, err } cookieStore, err := sessions.NewCookieStore(opts.CookieName, - sessions.CreateMiscreantCookieCipher(decodedCookieSecret), + sessions.CreateCookieCipher(decodedCookieSecret), func(c *sessions.CookieStore) error { c.CookieDomain = opts.CookieDomain c.CookieHTTPOnly = opts.CookieHTTPOnly @@ -167,16 +163,15 @@ func New(opts *Options, optionFuncs ...func(*Authenticate) error) (*Authenticate } p := &Authenticate{ - SharedKey: opts.SharedKey, - AllowedDomains: opts.AllowedDomains, - ProxyRootDomains: dotPrependDomains(opts.ProxyRootDomains), - CookieSecure: opts.CookieSecure, - RedirectURL: opts.RedirectURL, - templates: templates.New(), - csrfStore: cookieStore, - sessionStore: cookieStore, - cipher: cipher, - skipProviderButton: opts.SkipProviderButton, + SharedKey: opts.SharedKey, + AllowedDomains: opts.AllowedDomains, + ProxyRootDomains: dotPrependDomains(opts.ProxyRootDomains), + CookieSecure: opts.CookieSecure, + RedirectURL: opts.RedirectURL, + templates: templates.New(), + csrfStore: cookieStore, + sessionStore: cookieStore, + cipher: cipher, } p.provider, err = newProvider(opts) diff --git a/authenticate/handlers.go b/authenticate/handlers.go index bdd477718..b7dc48470 100644 --- a/authenticate/handlers.go +++ b/authenticate/handlers.go @@ -32,16 +32,16 @@ func (p *Authenticate) Handler() http.Handler { stdMiddleware := middleware.NewChain() stdMiddleware = stdMiddleware.Append(middleware.Healthcheck("/ping", version.UserAgent())) stdMiddleware = stdMiddleware.Append(middleware.NewHandler(log.Logger)) - stdMiddleware = stdMiddleware.Append(middleware.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) { - // executed after handler route handler - middleware.FromRequest(r).Info(). - Str("method", r.Method). - Str("url", r.URL.String()). - Int("status", status). - Int("size", size). - Dur("duration", duration). - Msg("request") - })) + stdMiddleware = stdMiddleware.Append(middleware.AccessHandler( + func(r *http.Request, status, size int, duration time.Duration) { + middleware.FromRequest(r).Debug(). + Str("method", r.Method). + Str("url", r.URL.String()). + Int("status", status). + Int("size", size). + Dur("duration", duration). + Msg("authenticate: request") + })) stdMiddleware = stdMiddleware.Append(middleware.SetHeaders(securityHeaders)) stdMiddleware = stdMiddleware.Append(middleware.ForwardedAddrHandler("fwd_ip")) stdMiddleware = stdMiddleware.Append(middleware.RemoteAddrHandler("ip")) @@ -55,29 +55,17 @@ func (p *Authenticate) Handler() http.Handler { validateClientSecret := stdMiddleware.Append(middleware.ValidateClientSecret(p.SharedKey)) mux := http.NewServeMux() - // we setup global endpoints that should respond to any hostname - mux.Handle("/ping", stdMiddleware.ThenFunc(p.PingPage)) - serviceMux := http.NewServeMux() - // standard rest and healthcheck endpoints - serviceMux.Handle("/ping", stdMiddleware.ThenFunc(p.PingPage)) - serviceMux.Handle("/robots.txt", stdMiddleware.ThenFunc(p.RobotsTxt)) - // Identity Provider (IdP) endpoints and callbacks - serviceMux.Handle("/start", stdMiddleware.ThenFunc(p.OAuthStart)) - serviceMux.Handle("/oauth2/callback", stdMiddleware.ThenFunc(p.OAuthCallback)) - // authenticator-server endpoints, todo(bdd): make gRPC - serviceMux.Handle("/sign_in", validateSignatureMiddleware.ThenFunc(p.SignIn)) - serviceMux.Handle("/sign_out", validateSignatureMiddleware.ThenFunc(p.SignOut)) // "GET", "POST" - serviceMux.Handle("/profile", validateClientSecret.ThenFunc(p.GetProfile)) // GET - serviceMux.Handle("/validate", validateClientSecret.ThenFunc(p.ValidateToken)) // GET - serviceMux.Handle("/redeem", validateClientSecret.ThenFunc(p.Redeem)) // POST - serviceMux.Handle("/refresh", validateClientSecret.ThenFunc(p.Refresh)) //POST - - // NOTE: we have to include trailing slash for the router to match the host header - host := p.RedirectURL.Host - if !strings.HasSuffix(host, "/") { - host = fmt.Sprintf("%s/", host) - } - mux.Handle(host, serviceMux) + mux.Handle("/robots.txt", stdMiddleware.ThenFunc(p.RobotsTxt)) + // Identity Provider (IdP) callback endpoints and callbacks + mux.Handle("/start", stdMiddleware.ThenFunc(p.OAuthStart)) + mux.Handle("/oauth2/callback", stdMiddleware.ThenFunc(p.OAuthCallback)) + // authenticate-server endpoints + mux.Handle("/sign_in", validateSignatureMiddleware.ThenFunc(p.SignIn)) + mux.Handle("/sign_out", validateSignatureMiddleware.ThenFunc(p.SignOut)) // "GET", "POST" + mux.Handle("/profile", validateClientSecret.ThenFunc(p.GetProfile)) // GET + mux.Handle("/validate", validateClientSecret.ThenFunc(p.ValidateToken)) // GET + mux.Handle("/redeem", validateClientSecret.ThenFunc(p.Redeem)) // POST + mux.Handle("/refresh", validateClientSecret.ThenFunc(p.Refresh)) //POST return mux } @@ -88,16 +76,11 @@ func (p *Authenticate) RobotsTxt(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "User-agent: *\nDisallow: /") } -// PingPage handles the /ping route -func (p *Authenticate) PingPage(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, "OK") -} - // SignInPage directs the user to the sign in page. Takes a `redirect_uri` param. func (p *Authenticate) SignInPage(w http.ResponseWriter, r *http.Request) { redirectURL := p.RedirectURL.ResolveReference(r.URL) - destinationURL, _ := url.Parse(redirectURL.Query().Get("redirect_uri")) // checked by middleware + + destinationURL, _ := url.Parse(redirectURL.Query().Get("redirect_uri")) t := struct { ProviderName string AllowedDomains []string @@ -196,26 +179,17 @@ func (p *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) { session, err := p.authenticate(w, r) switch err { case nil: - // User is authenticated, redirect back to the proxy application - // with the necessary state + // User is authenticated, redirect back to proxy p.ProxyOAuthRedirect(w, r, session) - case http.ErrNoCookie: - log.Error().Err(err).Msg("authenticate.SignIn : err no cookie") - if p.skipProviderButton { - p.skipButtonOAuthStart(w, r) - } else { - p.SignInPage(w, r) - } - case sessions.ErrLifetimeExpired, sessions.ErrInvalidSession: - log.Error().Err(err).Msg("authenticate.SignIn") - p.sessionStore.ClearSession(w, r) - if p.skipProviderButton { - p.skipButtonOAuthStart(w, r) - } else { - p.SignInPage(w, r) + case http.ErrNoCookie, sessions.ErrLifetimeExpired, sessions.ErrInvalidSession: + log.Debug().Err(err).Msg("authenticate.SignIn") + if err != http.ErrNoCookie { + p.sessionStore.ClearSession(w, r) } + p.OAuthStart(w, r) + default: - log.Error().Err(err).Msg("authenticate.SignIn : unknown error cookie") + log.Error().Err(err).Msg("authenticate.SignIn") httputil.ErrorResponse(w, r, err.Error(), httputil.CodeForError(err)) } } @@ -315,7 +289,6 @@ func (p *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) { // SignOutPage renders a sign out page with a message func (p *Authenticate) SignOutPage(w http.ResponseWriter, r *http.Request, message string) { - log.FromRequest(r).Debug().Msg("This is just a test to make sure signout works") // validateRedirectURI middleware already ensures that this is a valid URL redirectURI := r.Form.Get("redirect_uri") session, err := p.sessionStore.LoadSession(r) @@ -361,29 +334,24 @@ func (p *Authenticate) OAuthStart(w http.ResponseWriter, r *http.Request) { httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest) return } - p.helperOAuthStart(w, r, authRedirectURL) -} - -func (p *Authenticate) skipButtonOAuthStart(w http.ResponseWriter, r *http.Request) { - p.helperOAuthStart(w, r, p.RedirectURL.ResolveReference(r.URL)) -} - -func (p *Authenticate) helperOAuthStart(w http.ResponseWriter, r *http.Request, authRedirectURL *url.URL) { + authRedirectURL = p.RedirectURL.ResolveReference(r.URL) nonce := fmt.Sprintf("%x", cryptutil.GenerateKey()) p.csrfStore.SetCSRF(w, r, nonce) + // confirm the redirect uri is from the root domain if !middleware.ValidRedirectURI(authRedirectURL.String(), p.ProxyRootDomains) { httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest) return } - + // confirm proxy url is from the root domain proxyRedirectURL, err := url.Parse(authRedirectURL.Query().Get("redirect_uri")) if err != nil || !middleware.ValidRedirectURI(proxyRedirectURL.String(), p.ProxyRootDomains) { httputil.ErrorResponse(w, r, "Invalid redirect parameter", http.StatusBadRequest) return } + // get the signature and timestamp values then compare hmac proxyRedirectSig := authRedirectURL.Query().Get("sig") ts := authRedirectURL.Query().Get("ts") if !middleware.ValidSignature(proxyRedirectURL.String(), proxyRedirectSig, ts, p.SharedKey) { @@ -391,8 +359,8 @@ func (p *Authenticate) helperOAuthStart(w http.ResponseWriter, r *http.Request, return } + // embed authenticate service's state as the base64'd nonce and authenticate callback url state := base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%v:%v", nonce, authRedirectURL.String()))) - signInURL := p.provider.GetSignInURL(state) http.Redirect(w, r, signInURL, http.StatusFound) diff --git a/authenticate/handlers_test.go b/authenticate/handlers_test.go index bb5ef7d4a..9b91c72e5 100644 --- a/authenticate/handlers_test.go +++ b/authenticate/handlers_test.go @@ -23,24 +23,6 @@ func testAuthenticate() *Authenticate { return &auth } -func TestAuthenticate_PingPage(t *testing.T) { - auth := testAuthenticate() - req, err := http.NewRequest("GET", "/ping", nil) - if err != nil { - t.Fatal(err) - } - rr := httptest.NewRecorder() - handler := http.HandlerFunc(auth.PingPage) - handler.ServeHTTP(rr, req) - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - expected := "OK" - if rr.Body.String() != expected { - t.Errorf("handler returned wrong body: got %v want %v", rr.Body.String(), expected) - } -} - func TestAuthenticate_RobotsTxt(t *testing.T) { auth := testAuthenticate() req, err := http.NewRequest("GET", "/robots.txt", nil) diff --git a/cmd/pomerium/main.go b/cmd/pomerium/main.go index 7f8fc185e..a3c675df1 100644 --- a/cmd/pomerium/main.go +++ b/cmd/pomerium/main.go @@ -6,7 +6,6 @@ import ( "net/http" "os" - "github.com/pomerium/envconfig" "github.com/pomerium/pomerium/authenticate" "github.com/pomerium/pomerium/internal/https" "github.com/pomerium/pomerium/internal/log" @@ -23,7 +22,7 @@ var ( func main() { mainOpts, err := optionsFromEnvConfig() if err != nil { - log.Fatal().Err(err).Msg("cmd/pomerium: failed to parse authenticator settings") + log.Fatal().Err(err).Msg("cmd/pomerium: settings error") } flag.Parse() if *debugFlag || mainOpts.Debug { @@ -33,51 +32,46 @@ func main() { fmt.Printf("%s", version.FullVersion()) os.Exit(0) } - log.Debug().Str("version", version.FullVersion()).Str("user-agent", version.UserAgent()).Msg("cmd/pomerium") + log.Info().Str("version", version.FullVersion()).Msg("cmd/pomerium") - var auth *authenticate.Authenticate + var authenticateService *authenticate.Authenticate var authHost string if mainOpts.Services == "all" || mainOpts.Services == "authenticate" { authOpts, err := authenticate.OptionsFromEnvConfig() if err != nil { - log.Fatal().Err(err).Msg("cmd/pomerium: failed to parse authenticate settings") + log.Fatal().Err(err).Msg("cmd/pomerium: authenticate settings") } emailValidator := func(p *authenticate.Authenticate) error { p.Validator = options.NewEmailValidator(authOpts.AllowedDomains) return nil } - auth, err = authenticate.New(authOpts, emailValidator) + authenticateService, err = authenticate.New(authOpts, emailValidator) if err != nil { - log.Fatal().Err(err).Msg("cmd/pomerium: failed to create authenticate") + log.Fatal().Err(err).Msg("cmd/pomerium: new authenticate") } authHost = authOpts.RedirectURL.Host } - var p *proxy.Proxy + var proxyService *proxy.Proxy if mainOpts.Services == "all" || mainOpts.Services == "proxy" { proxyOpts, err := proxy.OptionsFromEnvConfig() if err != nil { - log.Fatal().Err(err).Msg("cmd/pomerium: failed to parse proxy settings") + log.Fatal().Err(err).Msg("cmd/pomerium: proxy settings") } - p, err = proxy.New(proxyOpts) + proxyService, err = proxy.New(proxyOpts) if err != nil { - log.Fatal().Err(err).Msg("cmd/pomerium: failed to create proxy") + log.Fatal().Err(err).Msg("cmd/pomerium: new proxy") } } topMux := http.NewServeMux() - if auth != nil { - // Need to handle ping without host lookup for LB - topMux.HandleFunc("/ping", func(rw http.ResponseWriter, _ *http.Request) { - rw.WriteHeader(http.StatusOK) - fmt.Fprintf(rw, "OK") - }) - topMux.Handle(authHost+"/", auth.Handler()) + if authenticateService != nil { + topMux.Handle(authHost+"/", authenticateService.Handler()) } - if p != nil { - topMux.Handle("/", p.Handler()) + if proxyService != nil { + topMux.Handle("/", proxyService.Handler()) } httpOpts := &https.Options{ Addr: mainOpts.Addr, @@ -86,57 +80,5 @@ func main() { CertFile: mainOpts.CertFile, KeyFile: mainOpts.KeyFile, } - log.Fatal().Err(https.ListenAndServeTLS(httpOpts, topMux)).Msg("cmd/pomerium: fatal") -} - -// Options are the global environmental flags used to set up pomerium's services. -// If a base64 encoded certificate and key are not provided as environmental variables, -// or if a file location is not provided, the server will attempt to find a matching keypair -// in the local directory as `./cert.pem` and `./privkey.pem` respectively. -type Options struct { - // Debug enables more verbose logging, and outputs human-readable logs to Stdout. - // Set with POMERIUM_DEBUG - Debug bool `envconfig:"POMERIUM_DEBUG"` - // Services is a list enabled service mode. If none are selected, "all" is used. - // Available options are : "all", "authenticate", "proxy". - Services string `envconfig:"SERVICES"` - // Addr specifies the host and port on which the server should serve - // HTTPS requests. If empty, ":https" is used. - Addr string `envconfig:"ADDRESS"` - // Cert and Key specifies the base64 encoded TLS certificates to use. - Cert string `envconfig:"CERTIFICATE"` - Key string `envconfig:"CERTIFICATE_KEY"` - // CertFile and KeyFile specifies the TLS certificates to use. - CertFile string `envconfig:"CERTIFICATE_FILE"` - KeyFile string `envconfig:"CERTIFICATE_KEY_FILE"` -} - -var defaultOptions = &Options{ - Debug: false, - Services: "all", -} - -// optionsFromEnvConfig builds the authentication service's configuration -// options from provided environmental variables -func optionsFromEnvConfig() (*Options, error) { - o := defaultOptions - if err := envconfig.Process("", o); err != nil { - return nil, err - } - if !isValidService(o.Services) { - return nil, fmt.Errorf("%s is an invalid service type", o.Services) - } - return o, nil -} - -// isValidService checks to see if a service is a valid service mode -func isValidService(service string) bool { - switch service { - case - "all", - "proxy", - "authenticate": - return true - } - return false + log.Fatal().Err(https.ListenAndServeTLS(httpOpts, topMux)).Msg("cmd/pomerium: https serve failure") } diff --git a/cmd/pomerium/options.go b/cmd/pomerium/options.go new file mode 100644 index 000000000..29dd8dc69 --- /dev/null +++ b/cmd/pomerium/options.go @@ -0,0 +1,59 @@ +package main // import "github.com/pomerium/pomerium/cmd/pomerium" + +import ( + "fmt" + + "github.com/pomerium/envconfig" +) + +// Options are the global environmental flags used to set up pomerium's services. +// If a base64 encoded certificate and key are not provided as environmental variables, +// or if a file location is not provided, the server will attempt to find a matching keypair +// in the local directory as `./cert.pem` and `./privkey.pem` respectively. +type Options struct { + // Debug enables more verbose logging, and outputs human-readable logs to Stdout. + // Set with POMERIUM_DEBUG + Debug bool `envconfig:"POMERIUM_DEBUG"` + // Services is a list enabled service mode. If none are selected, "all" is used. + // Available options are : "all", "authenticate", "proxy". + Services string `envconfig:"SERVICES"` + // Addr specifies the host and port on which the server should serve + // HTTPS requests. If empty, ":https" is used. + Addr string `envconfig:"ADDRESS"` + // Cert and Key specifies the base64 encoded TLS certificates to use. + Cert string `envconfig:"CERTIFICATE"` + Key string `envconfig:"CERTIFICATE_KEY"` + // CertFile and KeyFile specifies the TLS certificates to use. + CertFile string `envconfig:"CERTIFICATE_FILE"` + KeyFile string `envconfig:"CERTIFICATE_KEY_FILE"` +} + +var defaultOptions = &Options{ + Debug: false, + Services: "all", +} + +// optionsFromEnvConfig builds the authentication service's configuration +// options from provided environmental variables +func optionsFromEnvConfig() (*Options, error) { + o := defaultOptions + if err := envconfig.Process("", o); err != nil { + return nil, err + } + if !isValidService(o.Services) { + return nil, fmt.Errorf("%s is an invalid service type", o.Services) + } + return o, nil +} + +// isValidService checks to see if a service is a valid service mode +func isValidService(service string) bool { + switch service { + case + "all", + "proxy", + "authenticate": + return true + } + return false +} diff --git a/cmd/pomerium/main_test.go b/cmd/pomerium/options_test.go similarity index 100% rename from cmd/pomerium/main_test.go rename to cmd/pomerium/options_test.go diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 5e893cd3e..19ce2d13f 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -24,7 +24,7 @@ function guideSidebar(title) { { title, collapsable: false, - children: ["", "docker", "kubernetes", "from-source"] + children: ["", "kubernetes", "from-source"] } ]; } diff --git a/docs/docs/readme.md b/docs/docs/readme.md index 7d40a6644..3678a8bb2 100644 --- a/docs/docs/readme.md +++ b/docs/docs/readme.md @@ -13,7 +13,7 @@ Traditional [perimeter](https://www.redbooks.ibm.com/redpapers/pdfs/redp4397.pdf - Failure to encapsulate a heterogeneous mix of cloud, on-premise, cloud, and multi-cloud environments. - User's don't like VPNs. -Pomerium attempts to mitigate these shortcomings by by adopting the following principles. +Pomerium attempts to mitigate these shortcomings by adopting the following principles. - Trust flows from user, device, and context. - Network location _does not impart trust_. Treat both internal and external networks as completely untrusted. diff --git a/docs/guide/docker.md b/docs/guide/docker.md deleted file mode 100644 index 5725a2c1d..000000000 --- a/docs/guide/docker.md +++ /dev/null @@ -1,48 +0,0 @@ -# Docker - -## Prerequisites - -- A configured [identity provider] - -## Install - -Install [docker] and [docker-compose]. Docker-compose is a tool for defining and running multi-container Docker applications. We've created an example docker-compose file that creates a minimal, but complete test environnement for pomerium. - -## Download - -Copy and paste the contents of the provided example [basic.docker-compose.yml] and save it locally as `docker-compose.yml`. - -## Configure - -Edit the [docker-compose.yml] to match your [identity provider] settings. - -Place your domain's wild-card TLS certificate next to the compose file. If you don't have one handy, the included [script] generates one from [LetsEncrypt]. - -## Run - -You can then download the latest pomerium release of pomerium in docker form along some example containers and an nginx load balancer all in one step. - -```bash -docker-compose up -``` - -Pomerium is configured to delegate access to two test apps [helloworld] and [httpbin]. - -## Navigate - -Open a browser and navigate to `hello.your.domain.com` or `httpbin.your.domain.com`. You should see something like the following in your browser. - -![Getting started](./get-started.gif) - -And in your terminal. - -[![asciicast](https://asciinema.org/a/tfbSWkUZgMRxHAQDqmcjjNwUg.svg)](https://asciinema.org/a/tfbSWkUZgMRxHAQDqmcjjNwUg) - -[basic.docker-compose.yml]: ../docs/examples.html#basic-docker-compose-yml -[docker]: https://docs.docker.com/install/ -[docker-compose]: (https://docs.docker.com/compose/install/) -[helloworld]: https://hub.docker.com/r/tutum/hello-world -[httpbin]: https://httpbin.org/ -[identity provider]: ../docs/identity-providers.md -[letsencrypt]: https://letsencrypt.org/ -[script]: https://github.com/pomerium/pomerium/blob/master/scripts/generate_wildcard_cert.sh diff --git a/docs/guide/from-source.md b/docs/guide/from-source.md index baf94ad9f..5a266e710 100644 --- a/docs/guide/from-source.md +++ b/docs/guide/from-source.md @@ -4,7 +4,7 @@ - Install [git](https://git-scm.com/) version control system - Install the [go](https://golang.org/doc/install) programming language -- A configured [identity provider]. +- A configured [identity provider] ## Download diff --git a/docs/guide/readme.md b/docs/guide/readme.md index 6059b9179..ff066a84a 100644 --- a/docs/guide/readme.md +++ b/docs/guide/readme.md @@ -1 +1,48 @@ -# Prerequisites +# Docker + +Docker and docker-compose are tools for defining and running multi-container Docker applications. We've created an example docker-compose file that creates a minimal, but complete test environment for pomerium. + +## Prerequisites + +- A configured [identity provider] +- Install [docker] +- Install [docker-compose] + +## Download + +Copy and paste the contents of the provided example [basic.docker-compose.yml] and save it locally as `docker-compose.yml`. + +## Configure + +Edit the `docker-compose.yml` to match your [identity provider] settings. + +Place your domain's wild-card TLS certificate next to the compose file. If you don't have one handy, the included [script] generates one from [LetsEncrypt]. + +## Run + +Docker-compose will automatically download the latest pomerium release as well as two example containers and an nginx load balancer all in one step. + +```bash +docker-compose up +``` + +Pomerium is configured to delegate access to two test apps [helloworld] and [httpbin]. + +## Navigate + +Open a browser and navigate to `hello.your.domain.com` or `httpbin.your.domain.com`. You should see something like the following in your browser. + +![Getting started](./get-started.gif) + +And in your terminal. + +[![asciicast](https://asciinema.org/a/tfbSWkUZgMRxHAQDqmcjjNwUg.svg)](https://asciinema.org/a/tfbSWkUZgMRxHAQDqmcjjNwUg) + +[basic.docker-compose.yml]: ../docs/examples.html#basic-docker-compose-yml +[docker]: https://docs.docker.com/install/ +[docker-compose]: https://docs.docker.com/compose/install/ +[helloworld]: https://hub.docker.com/r/tutum/hello-world +[httpbin]: https://httpbin.org/ +[identity provider]: ../docs/identity-providers.md +[letsencrypt]: https://letsencrypt.org/ +[script]: https://github.com/pomerium/pomerium/blob/master/scripts/generate_wildcard_cert.sh diff --git a/internal/sessions/cookie_store.go b/internal/sessions/cookie_store.go index 90bb98627..d7af2be84 100644 --- a/internal/sessions/cookie_store.go +++ b/internal/sessions/cookie_store.go @@ -40,12 +40,12 @@ type CookieStore struct { SessionLifetimeTTL time.Duration } -// CreateMiscreantCookieCipher creates a new miscreant cipher with the cookie secret -func CreateMiscreantCookieCipher(cookieSecret []byte) func(s *CookieStore) error { +// CreateCookieCipher creates a new miscreant cipher with the cookie secret +func CreateCookieCipher(cookieSecret []byte) func(s *CookieStore) error { return func(s *CookieStore) error { cipher, err := cryptutil.NewCipher(cookieSecret) if err != nil { - return fmt.Errorf("miscreant cookie-secret error: %s", err.Error()) + return fmt.Errorf("cookie-secret error: %s", err.Error()) } s.CookieCipher = cipher return nil diff --git a/internal/sessions/cookie_store_test.go b/internal/sessions/cookie_store_test.go index 38aed03c8..91da9568d 100644 --- a/internal/sessions/cookie_store_test.go +++ b/internal/sessions/cookie_store_test.go @@ -13,7 +13,7 @@ import ( var testEncodedCookieSecret, _ = base64.StdEncoding.DecodeString("qICChm3wdjbjcWymm7PefwtPP6/PZv+udkFEubTeE38=") -func TestCreateMiscreantCookieCipher(t *testing.T) { +func TestCreateCookieCipher(t *testing.T) { testCases := []struct { name string cookieSecret []byte @@ -32,7 +32,7 @@ func TestCreateMiscreantCookieCipher(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - _, err := NewCookieStore("cookieName", CreateMiscreantCookieCipher(tc.cookieSecret)) + _, err := NewCookieStore("cookieName", CreateCookieCipher(tc.cookieSecret)) if !tc.expectedError { testutil.Ok(t, err) } else { @@ -309,7 +309,7 @@ func TestLoadCookiedSession(t *testing.T) { }, { name: "cookie set with cipher set", - optFuncs: []func(*CookieStore) error{CreateMiscreantCookieCipher(testEncodedCookieSecret)}, + optFuncs: []func(*CookieStore) error{CreateCookieCipher(testEncodedCookieSecret)}, setupCookies: func(t *testing.T, req *http.Request, s *CookieStore, sessionState *SessionState) { value, err := MarshalSession(sessionState, s.CookieCipher) testutil.Ok(t, err) @@ -323,7 +323,7 @@ func TestLoadCookiedSession(t *testing.T) { }, { name: "cookie set with invalid value cipher set", - optFuncs: []func(*CookieStore) error{CreateMiscreantCookieCipher(testEncodedCookieSecret)}, + optFuncs: []func(*CookieStore) error{CreateCookieCipher(testEncodedCookieSecret)}, setupCookies: func(t *testing.T, req *http.Request, s *CookieStore, sessionState *SessionState) { value := "574b776a7c934d6b9fc42ec63a389f79" req.AddCookie(s.makeSessionCookie(req, value, time.Hour, time.Now())) diff --git a/proxy/authenticator/authenticator.go b/proxy/authenticator/authenticator.go index d9ce97f29..ea9c7bd1d 100644 --- a/proxy/authenticator/authenticator.go +++ b/proxy/authenticator/authenticator.go @@ -239,7 +239,7 @@ func (p *AuthenticateClient) ValidateSessionState(s *sessions.SessionState) bool params.Add("shared_secret", p.SharedKey) req, err := p.newRequest("GET", fmt.Sprintf("%s?%s", p.ValidateURL.String(), params.Encode()), nil) if err != nil { - log.Error().Err(err).Str("user", s.Email).Msg("proxy/authenticator.ValidateSessionState : error validating session state") + log.Info().Err(err).Str("user", s.Email).Msg("proxy/authenticator: error validating session state") return false } req.Header.Set("X-Client-Secret", p.SharedKey) @@ -248,7 +248,7 @@ func (p *AuthenticateClient) ValidateSessionState(s *sessions.SessionState) bool resp, err := defaultHTTPClient.Do(req) if err != nil { - log.Error().Err(err).Str("user", s.Email).Msg("proxy/authenticator.ValidateSessionState : error making request to validate access token") + log.Info().Err(err).Str("user", s.Email).Msg("proxy/authenticator: error validating access token") return false } @@ -260,16 +260,13 @@ func (p *AuthenticateClient) ValidateSessionState(s *sessions.SessionState) bool s.ValidDeadline = extendDeadline(p.SessionValidTTL) return true } - log.Info().Str("user", s.Email).Int("status-code", resp.StatusCode).Msg("proxy/authenticator.ValidateSessionState : could not validate user access token") + log.Info().Str("user", s.Email).Int("status-code", resp.StatusCode).Msg("proxy/authenticator: bad status code") return false } s.ValidDeadline = extendDeadline(p.SessionValidTTL) s.GracePeriodStart = time.Time{} - - log.Info().Str("user", s.Email).Msg("proxy/authenticator.ValidateSessionState : validated session") - return true } diff --git a/proxy/handlers.go b/proxy/handlers.go index b6dadd860..9097ef961 100644 --- a/proxy/handlers.go +++ b/proxy/handlers.go @@ -29,6 +29,7 @@ var securityHeaders = map[string]string{ // Handler returns a http handler for an Proxy func (p *Proxy) Handler() http.Handler { + // routes mux := http.NewServeMux() mux.HandleFunc("/favicon.ico", p.Favicon) mux.HandleFunc("/robots.txt", p.RobotsTxt) @@ -37,19 +38,12 @@ func (p *Proxy) Handler() http.Handler { mux.HandleFunc("/.pomerium/auth", p.AuthenticateOnly) mux.HandleFunc("/", p.Proxy) - // Global middleware, which will be applied to each request in reverse - // order as applied here (i.e., we want to validate the host _first_ when - // processing a request) - var handler http.Handler = mux - // todo(bdd) : investigate if setting non-overridable headers makes sense - // handler = p.setResponseHeaderOverrides(handler) - - // Middleware chain + // middleware chain c := middleware.NewChain() c = c.Append(middleware.Healthcheck("/ping", version.UserAgent())) c = c.Append(middleware.NewHandler(log.Logger)) c = c.Append(middleware.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) { - middleware.FromRequest(r).Info(). + middleware.FromRequest(r).Debug(). Str("method", r.Method). Str("url", r.URL.String()). Int("status", status). @@ -57,7 +51,7 @@ func (p *Proxy) Handler() http.Handler { Dur("duration", duration). Str("pomerium-user", r.Header.Get(HeaderUserID)). Str("pomerium-email", r.Header.Get(HeaderEmail)). - Msg("request") + Msg("proxy: request") })) c = c.Append(middleware.SetHeaders(securityHeaders)) c = c.Append(middleware.RequireHTTPS) @@ -68,12 +62,7 @@ func (p *Proxy) Handler() http.Handler { c = c.Append(middleware.RequestIDHandler("req_id", "Request-Id")) c = c.Append(middleware.ValidateHost(p.mux)) h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Skip host validation for /ping requests because they hit the LB directly. - if r.URL.Path == "/ping" { - p.PingPage(w, r) - return - } - handler.ServeHTTP(w, r) + mux.ServeHTTP(w, r) })) return h } @@ -97,12 +86,6 @@ func (p *Proxy) Favicon(w http.ResponseWriter, r *http.Request) { p.Proxy(w, r) } -// PingPage send back a 200 OK response. -func (p *Proxy) PingPage(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, "OK") -} - // SignOut redirects the request to the sign out url. func (p *Proxy) SignOut(w http.ResponseWriter, r *http.Request) { p.sessionStore.ClearSession(w, r) @@ -266,28 +249,17 @@ func (p *Proxy) Proxy(w http.ResponseWriter, r *http.Request) { // OAuthStart. If successful, we proceed to proxy to the configured upstream. if err != nil { switch err { - case http.ErrNoCookie: - // No cookie is set, start the oauth flow - p.OAuthStart(w, r) - return case ErrUserNotAuthorized: - // We know the user is not authorized for the request, we show them a forbidden page - p.ErrorPage(w, r, http.StatusForbidden, "Forbidden", "You're not authorized to view this page") + //todo(bdd) : custom forbidden page with details and troubleshooting info + log.FromRequest(r).Debug().Err(err).Msg("proxy: user access forbidden") + p.ErrorPage(w, r, http.StatusForbidden, "Forbidden", "You don't have access") return - case sessions.ErrLifetimeExpired: - // User's lifetime expired, we trigger the start of the oauth flow - p.OAuthStart(w, r) - return - case sessions.ErrInvalidSession: - // The user session is invalid and we can't decode it. - // This can happen for a variety of reasons but the most common non-malicious - // case occurs when the session encoding schema changes. We manage this ux - // by triggering the start of the oauth flow. + case http.ErrNoCookie, sessions.ErrLifetimeExpired, sessions.ErrInvalidSession: + log.FromRequest(r).Debug().Err(err).Msg("proxy: starting auth flow") p.OAuthStart(w, r) return default: - log.FromRequest(r).Error().Err(err).Msg("unknown error") - // We don't know exactly what happened, but authenticating the user failed, show an error + log.FromRequest(r).Error().Err(err).Msg("proxy: unexpected error") p.ErrorPage(w, r, http.StatusInternalServerError, "Internal Error", "An unexpected error occurred") return } @@ -315,69 +287,28 @@ func (p *Proxy) Authenticate(w http.ResponseWriter, r *http.Request) (err error) session, err := p.sessionStore.LoadSession(r) if err != nil { - // We loaded a cookie but it wasn't valid, clear it, and reject the request - log.FromRequest(r).Error().Err(err).Msg("error authenticating user") return err } - - // Lifetime period is the entire duration in which the session is valid. - // This should be set to something like 14 to 30 days. if session.LifetimePeriodExpired() { - log.FromRequest(r).Warn().Str("user", session.Email).Msg("session lifetime has expired") return sessions.ErrLifetimeExpired } else if session.RefreshPeriodExpired() { - // Refresh period is the period in which the access token is valid. This is ultimately - // controlled by the upstream provider and tends to be around 1 hour. ok, err := p.authenticateClient.RefreshSession(session) - - // We failed to refresh the session successfully - // clear the cookie and reject the request if err != nil { - log.FromRequest(r).Error().Err(err).Str("user", session.Email).Msg("refreshing session failed") return err } - if !ok { - // User is not authorized after refresh - // clear the cookie and reject the request - log.FromRequest(r).Error().Str("user", session.Email).Msg("not authorized after refreshing session") return ErrUserNotAuthorized } - - err = p.sessionStore.SaveSession(w, r, session) - if err != nil { - // We refreshed the session successfully, but failed to save it. - // - // This could be from failing to encode the session properly. - // But, we clear the session cookie and reject the request! - log.FromRequest(r).Error().Err(err).Str("user", session.Email).Msg("could not save refresh session") - return err - } } else if session.ValidationPeriodExpired() { - // Validation period has expired, this is the shortest interval we use to - // check for valid requests. This should be set to something like a minute. - // This calls up the provider chain to validate this user is still active - // and hasn't been de-authorized. ok := p.authenticateClient.ValidateSessionState(session) if !ok { - // This user is now no longer authorized, or we failed to - // validate the user. - // Clear the cookie and reject the request - log.FromRequest(r).Error().Str("user", session.Email).Msg("no longer authorized after validation period") return ErrUserNotAuthorized } - - err = p.sessionStore.SaveSession(w, r, session) - if err != nil { - // We validated the session successfully, but failed to save it. - - // This could be from failing to encode the session properly. - // But, we clear the session cookie and reject the request! - log.FromRequest(r).Error().Err(err).Str("user", session.Email).Msg("could not save validated session") - return err - } } - + err = p.sessionStore.SaveSession(w, r, session) + if err != nil { + return err + } r.Header.Set(HeaderUserID, session.User) r.Header.Set(HeaderEmail, session.Email) diff --git a/proxy/handlers_test.go b/proxy/handlers_test.go new file mode 100644 index 000000000..46bcf6225 --- /dev/null +++ b/proxy/handlers_test.go @@ -0,0 +1,27 @@ +package proxy + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" +) + +func TestProxy_RobotsTxt(t *testing.T) { + auth := Proxy{} + req, err := http.NewRequest("GET", "/robots.txt", nil) + if err != nil { + t.Fatal(err) + } + rr := httptest.NewRecorder() + handler := http.HandlerFunc(auth.RobotsTxt) + handler.ServeHTTP(rr, req) + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) + } + expected := fmt.Sprintf("User-agent: *\nDisallow: /") + if rr.Body.String() != expected { + t.Errorf("handler returned wrong body: got %v want %v", rr.Body.String(), expected) + + } +} diff --git a/proxy/proxy.go b/proxy/proxy.go index 0149660df..59586da87 100755 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -161,7 +161,7 @@ func New(opts *Options) (*Proxy, error) { } cookieStore, err := sessions.NewCookieStore(opts.CookieName, - sessions.CreateMiscreantCookieCipher(decodedSecret), + sessions.CreateCookieCipher(decodedSecret), func(c *sessions.CookieStore) error { c.CookieDomain = opts.CookieDomain c.CookieHTTPOnly = opts.CookieHTTPOnly @@ -177,8 +177,8 @@ func New(opts *Options) (*Proxy, error) { opts.AuthenticateServiceURL, opts.SharedKey, // todo(bdd): fields below should be passed as function args - opts.SessionLifetimeTTL, opts.SessionValidTTL, + opts.SessionLifetimeTTL, opts.GracePeriodTTL, ) diff --git a/scripts/kubernetes_gke.sh b/scripts/kubernetes_gke.sh index 6e398a641..c7d4a431d 100755 --- a/scripts/kubernetes_gke.sh +++ b/scripts/kubernetes_gke.sh @@ -23,7 +23,7 @@ kubectl create secret tls -n pomerium pomerium-tls --key privkey.pem --cert cert # !!! IMPORTANT !!! # YOU MUST CHANGE THE Identity Provider Client Secret # !!! IMPORTANT !!! -# kubectl create secret generic -n pomerium idp-client-secret --from-literal=REPLACE_ME +# kubectl create secret generic -n pomerium idp-client-secret --from-literal=idp-client-secret=REPLACE_ME # Create the proxy & authenticate deployment kubectl create -f docs/docs/examples/kubernetes/authenticate.deploy.yml