mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-10 15:47:36 +02:00
authenticate: remove extra login page (#34)
- Fixed a bug where Lifetime TTL was set to a minute. - Remove nested mux in authenticate handlers. - Remove extra ping endpoint in authenticate and proxy. - Simplified sign in flow with multi-catch case statement. - Removed debugging logging. - Broke out cmd/pomerium options into own file. - Renamed msicreant cipher to just cipher. Closes #23
This commit is contained in:
parent
bcecee5ee3
commit
236e5cd7de
18 changed files with 228 additions and 328 deletions
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
59
cmd/pomerium/options.go
Normal file
59
cmd/pomerium/options.go
Normal file
|
@ -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
|
||||
}
|
|
@ -24,7 +24,7 @@ function guideSidebar(title) {
|
|||
{
|
||||
title,
|
||||
collapsable: false,
|
||||
children: ["", "docker", "kubernetes", "from-source"]
|
||||
children: ["", "kubernetes", "from-source"]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
And in your terminal.
|
||||
|
||||
[](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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
And in your terminal.
|
||||
|
||||
[](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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
27
proxy/handlers_test.go
Normal file
27
proxy/handlers_test.go
Normal file
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue