feat: envoy support (#3793)
Adds support for Envoy and Istio using the X-Authelia-URL header. The documentation will be published just before the release. Co-authored-by: Amir Zarrinkafsh <nightah@me.com>pull/4112/head
parent
18a2bde62e
commit
ed7092c59a
|
@ -204,7 +204,7 @@ func cmdGitHubIssueTemplatesBugReportRunE(cmd *cobra.Command, args []string) (er
|
|||
data := &tmplIssueTemplateData{
|
||||
Labels: []string{labelTypeBugUnconfirmed.String(), labelStatusNeedsTriage.String(), labelPriorityNormal.String()},
|
||||
Versions: tagsRecent,
|
||||
Proxies: []string{"Caddy", "Traefik", "Envoy", "NGINX", "SWAG", "NGINX Proxy Manager", "HAProxy"},
|
||||
Proxies: []string{"Caddy", "Traefik", "Envoy", "Istio", "NGINX", "SWAG", "NGINX Proxy Manager", "HAProxy"},
|
||||
}
|
||||
|
||||
if err = tmplGitHubIssueTemplateBug.Execute(f, data); err != nil {
|
||||
|
|
|
@ -443,7 +443,7 @@ password_policy:
|
|||
## to anyone. Otherwise restrictions follow the rules defined.
|
||||
##
|
||||
## Note: One can use the wildcard * to match any subdomain.
|
||||
## It must stand at the beginning of the pattern. (example: *.mydomain.com)
|
||||
## It must stand at the beginning of the pattern. (example: *.example.com)
|
||||
##
|
||||
## Note: You must put patterns containing wildcards between simple quotes for the YAML to be syntactically correct.
|
||||
##
|
||||
|
|
|
@ -443,7 +443,7 @@ password_policy:
|
|||
## to anyone. Otherwise restrictions follow the rules defined.
|
||||
##
|
||||
## Note: One can use the wildcard * to match any subdomain.
|
||||
## It must stand at the beginning of the pattern. (example: *.mydomain.com)
|
||||
## It must stand at the beginning of the pattern. (example: *.example.com)
|
||||
##
|
||||
## Note: You must put patterns containing wildcards between simple quotes for the YAML to be syntactically correct.
|
||||
##
|
||||
|
|
|
@ -614,14 +614,14 @@ func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToSafeTargetURL() {
|
|||
duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(&response, nil)
|
||||
|
||||
bodyBytes, err := json.Marshal(signDuoRequestBody{
|
||||
TargetURL: "https://example.com",
|
||||
TargetURL: "https://mydomain.example.com",
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
s.mock.Ctx.Request.SetBody(bodyBytes)
|
||||
|
||||
DuoPOST(duoMock)(s.mock.Ctx)
|
||||
s.mock.Assert200OK(s.T(), redirectResponse{
|
||||
Redirect: "https://example.com",
|
||||
Redirect: "https://mydomain.example.com",
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -663,7 +663,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldNotRedirectToUnsafeURL() {
|
|||
duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(&response, nil)
|
||||
|
||||
bodyBytes, err := json.Marshal(signDuoRequestBody{
|
||||
TargetURL: "http://example.com",
|
||||
TargetURL: "http://mydomain.example.com",
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
s.mock.Ctx.Request.SetBody(bodyBytes)
|
||||
|
@ -710,7 +710,7 @@ func (s *SecondFactorDuoPostSuite) TestShouldRegenerateSessionForPreventingSessi
|
|||
duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(&response, nil)
|
||||
|
||||
bodyBytes, err := json.Marshal(signDuoRequestBody{
|
||||
TargetURL: "http://example.com",
|
||||
TargetURL: "http://mydomain.example.com",
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
s.mock.Ctx.Request.SetBody(bodyBytes)
|
||||
|
|
|
@ -167,7 +167,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
|
|||
|
||||
bodyBytes, err := json.Marshal(signTOTPRequestBody{
|
||||
Token: "abc",
|
||||
TargetURL: "https://example.com",
|
||||
TargetURL: "https://mydomain.example.com",
|
||||
})
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
@ -175,7 +175,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
|
|||
|
||||
TimeBasedOneTimePasswordPOST(s.mock.Ctx)
|
||||
s.mock.Assert200OK(s.T(), redirectResponse{
|
||||
Redirect: "https://example.com",
|
||||
Redirect: "https://mydomain.example.com",
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -205,7 +205,7 @@ func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
|
|||
|
||||
bodyBytes, err := json.Marshal(signTOTPRequestBody{
|
||||
Token: "abc",
|
||||
TargetURL: "http://example.com",
|
||||
TargetURL: "http://mydomain.example.com",
|
||||
})
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
|
|
@ -180,7 +180,6 @@ func verifySessionCookie(ctx *middlewares.AutheliaCtx, targetURL *url.URL, userS
|
|||
func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, isBasicAuth bool, username string, method []byte) {
|
||||
var (
|
||||
statusCode int
|
||||
redirectionURL string
|
||||
friendlyUsername string
|
||||
friendlyRequestMethod string
|
||||
)
|
||||
|
@ -200,10 +199,6 @@ func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, is
|
|||
return
|
||||
}
|
||||
|
||||
// Kubernetes ingress controller and Traefik use the rd parameter of the verify
|
||||
// endpoint to provide the URL of the login portal. The target URL of the user
|
||||
// is computed from X-Forwarded-* headers or X-Original-URL.
|
||||
rd := string(ctx.QueryArgs().Peek("rd"))
|
||||
rm := string(method)
|
||||
|
||||
switch rm {
|
||||
|
@ -213,17 +208,30 @@ func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, is
|
|||
friendlyRequestMethod = rm
|
||||
}
|
||||
|
||||
if rd != "" {
|
||||
switch rm {
|
||||
case "":
|
||||
redirectionURL = fmt.Sprintf("%s?rd=%s", rd, url.QueryEscape(targetURL.String()))
|
||||
default:
|
||||
redirectionURL = fmt.Sprintf("%s?rd=%s&rm=%s", rd, url.QueryEscape(targetURL.String()), rm)
|
||||
redirectionURL := ctxGetPortalURL(ctx)
|
||||
|
||||
if redirectionURL != nil {
|
||||
if !utils.IsURISafeRedirection(redirectionURL, ctx.Configuration.Session.Domain) {
|
||||
ctx.Logger.Errorf("Configured Portal URL '%s' does not appear to be able to write cookies for the '%s' domain", redirectionURL, ctx.Configuration.Session.Domain)
|
||||
|
||||
ctx.ReplyUnauthorized()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
qry := redirectionURL.Query()
|
||||
|
||||
qry.Set("rd", targetURL.String())
|
||||
|
||||
if rm != "" {
|
||||
qry.Set("rm", rm)
|
||||
}
|
||||
|
||||
redirectionURL.RawQuery = qry.Encode()
|
||||
}
|
||||
|
||||
switch {
|
||||
case ctx.IsXHR() || !ctx.AcceptsMIME("text/html") || rd == "":
|
||||
case ctx.IsXHR() || !ctx.AcceptsMIME("text/html") || redirectionURL == nil:
|
||||
statusCode = fasthttp.StatusUnauthorized
|
||||
default:
|
||||
switch rm {
|
||||
|
@ -234,9 +242,9 @@ func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, is
|
|||
}
|
||||
}
|
||||
|
||||
if redirectionURL != "" {
|
||||
if redirectionURL != nil {
|
||||
ctx.Logger.Infof("Access to %s (method %s) is not authorized to user %s, responding with status code %d with location redirect to %s", targetURL.String(), friendlyRequestMethod, friendlyUsername, statusCode, redirectionURL)
|
||||
ctx.SpecialRedirect(redirectionURL, statusCode)
|
||||
ctx.SpecialRedirect(redirectionURL.String(), statusCode)
|
||||
} else {
|
||||
ctx.Logger.Infof("Access to %s (method %s) is not authorized to user %s, responding with status code %d", targetURL.String(), friendlyRequestMethod, friendlyUsername, statusCode)
|
||||
ctx.ReplyUnauthorized()
|
||||
|
|
|
@ -453,7 +453,7 @@ func TestShouldRedirectWithGroups(t *testing.T) {
|
|||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, "app.example.com")
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", "/code-test/login")
|
||||
|
||||
mock.Ctx.Request.SetRequestURI("/?rd=https://auth.mydomain.com")
|
||||
mock.Ctx.Request.SetRequestURI("/api/verify/?rd=https://auth.example.com")
|
||||
|
||||
VerifyGET(verifyGetCfg)(mock.Ctx)
|
||||
|
||||
|
@ -537,6 +537,110 @@ func (p Pair) String() string {
|
|||
p.URL, p.Username, p.AuthenticationLevel, p.ExpectedStatusCode)
|
||||
}
|
||||
|
||||
//nolint:gocyclo // This is a test.
|
||||
func TestShouldRedirectAuthorizations(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
method, originalURL, autheliaURL string
|
||||
|
||||
expected int
|
||||
}{
|
||||
{"ShouldReturnFoundMethodNone", "", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusFound},
|
||||
{"ShouldReturnFoundMethodGET", "GET", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusFound},
|
||||
{"ShouldReturnFoundMethodOPTIONS", "OPTIONS", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusFound},
|
||||
{"ShouldReturnSeeOtherMethodPOST", "POST", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusSeeOther},
|
||||
{"ShouldReturnSeeOtherMethodPATCH", "PATCH", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusSeeOther},
|
||||
{"ShouldReturnSeeOtherMethodPUT", "PUT", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusSeeOther},
|
||||
{"ShouldReturnSeeOtherMethodDELETE", "DELETE", "https://one-factor.example.com/", "https://auth.example.com/", fasthttp.StatusSeeOther},
|
||||
{"ShouldReturnUnauthorizedBadDomain", "GET", "https://one-factor.example.com/", "https://auth.notexample.com/", fasthttp.StatusUnauthorized},
|
||||
}
|
||||
|
||||
handler := VerifyGET(verifyGetCfg)
|
||||
|
||||
for _, tc := range testCases {
|
||||
var (
|
||||
suffix string
|
||||
xhr bool
|
||||
)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
switch i {
|
||||
case 0:
|
||||
suffix += "QueryParameter"
|
||||
default:
|
||||
suffix += "RequestHeader"
|
||||
}
|
||||
|
||||
for j := 0; j < 2; j++ {
|
||||
switch j {
|
||||
case 0:
|
||||
xhr = false
|
||||
case 1:
|
||||
xhr = true
|
||||
suffix += "XHR"
|
||||
}
|
||||
|
||||
t.Run(tc.name+suffix, func(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
|
||||
mock.Clock.Set(time.Now())
|
||||
|
||||
autheliaURL, err := url.ParseRequestURI(tc.autheliaURL)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
originalURL, err := url.ParseRequestURI(tc.originalURL)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
if xhr {
|
||||
mock.Ctx.Request.Header.Set(fasthttp.HeaderXRequestedWith, "XMLHttpRequest")
|
||||
}
|
||||
|
||||
var rm string
|
||||
|
||||
if tc.method != "" {
|
||||
rm = fmt.Sprintf("&rm=%s", tc.method)
|
||||
mock.Ctx.Request.Header.Set("X-Forwarded-Method", tc.method)
|
||||
}
|
||||
|
||||
mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
|
||||
mock.Ctx.Request.Header.Set("X-Original-URL", originalURL.String())
|
||||
|
||||
if i == 0 {
|
||||
mock.Ctx.Request.SetRequestURI(fmt.Sprintf("/?rd=%s", url.QueryEscape(autheliaURL.String())))
|
||||
} else {
|
||||
mock.Ctx.Request.Header.Set("X-Authelia-URL", autheliaURL.String())
|
||||
}
|
||||
|
||||
handler(mock.Ctx)
|
||||
|
||||
if xhr && tc.expected != fasthttp.StatusUnauthorized {
|
||||
assert.Equal(t, fasthttp.StatusUnauthorized, mock.Ctx.Response.StatusCode())
|
||||
} else {
|
||||
assert.Equal(t, tc.expected, mock.Ctx.Response.StatusCode())
|
||||
}
|
||||
|
||||
switch {
|
||||
case xhr && tc.expected != fasthttp.StatusUnauthorized:
|
||||
href := utils.StringHTMLEscape(fmt.Sprintf("%s?rd=%s%s", autheliaURL.String(), url.QueryEscape(originalURL.String()), rm))
|
||||
assert.Equal(t, fmt.Sprintf("<a href=\"%s\">%d %s</a>", href, fasthttp.StatusUnauthorized, fasthttp.StatusMessage(fasthttp.StatusUnauthorized)), string(mock.Ctx.Response.Body()))
|
||||
case tc.expected >= fasthttp.StatusMultipleChoices && tc.expected < fasthttp.StatusBadRequest:
|
||||
href := utils.StringHTMLEscape(fmt.Sprintf("%s?rd=%s%s", autheliaURL.String(), url.QueryEscape(originalURL.String()), rm))
|
||||
assert.Equal(t, fmt.Sprintf("<a href=\"%s\">%d %s</a>", href, tc.expected, fasthttp.StatusMessage(tc.expected)), string(mock.Ctx.Response.Body()))
|
||||
case tc.expected < fasthttp.StatusMultipleChoices:
|
||||
assert.Equal(t, utils.StringHTMLEscape(fmt.Sprintf("%d %s", tc.expected, fasthttp.StatusMessage(tc.expected))), string(mock.Ctx.Response.Body()))
|
||||
default:
|
||||
assert.Equal(t, utils.StringHTMLEscape(fmt.Sprintf("%d %s", tc.expected, fasthttp.StatusMessage(tc.expected))), string(mock.Ctx.Response.Body()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldVerifyAuthorizationsUsingSessionCookie(t *testing.T) {
|
||||
testCases := []Pair{
|
||||
// should apply default policy.
|
||||
|
@ -837,11 +941,36 @@ func TestShouldURLEncodeRedirectionURLParameter(t *testing.T) {
|
|||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
||||
mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
|
||||
mock.Ctx.Request.SetHost("mydomain.com")
|
||||
mock.Ctx.Request.SetRequestURI("/?rd=https://auth.mydomain.com")
|
||||
mock.Ctx.Request.SetRequestURI("/?rd=https://auth.example.com")
|
||||
|
||||
VerifyGET(verifyGetCfg)(mock.Ctx)
|
||||
|
||||
assert.Equal(t, "<a href=\"https://auth.mydomain.com/?rd=https%3A%2F%2Ftwo-factor.example.com\">302 Found</a>",
|
||||
assert.Equal(t, "<a href=\"https://auth.example.com/?rd=https%3A%2F%2Ftwo-factor.example.com\">302 Found</a>",
|
||||
string(mock.Ctx.Response.Body()))
|
||||
}
|
||||
|
||||
func TestShouldURLEncodeRedirectionHeader(t *testing.T) {
|
||||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
|
||||
mock.Clock.Set(time.Now())
|
||||
|
||||
userSession := mock.Ctx.GetSession()
|
||||
userSession.Username = testUsername
|
||||
userSession.AuthenticationLevel = authentication.NotAuthenticated
|
||||
userSession.RefreshTTL = mock.Clock.Now().Add(5 * time.Minute)
|
||||
|
||||
err := mock.Ctx.SaveSession(userSession)
|
||||
require.NoError(t, err)
|
||||
|
||||
mock.Ctx.Request.Header.Set("X-Original-URL", "https://two-factor.example.com")
|
||||
mock.Ctx.Request.Header.Set("X-Authelia-URL", "https://auth.example.com")
|
||||
mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
|
||||
mock.Ctx.Request.SetHost("mydomain.com")
|
||||
|
||||
VerifyGET(verifyGetCfg)(mock.Ctx)
|
||||
|
||||
assert.Equal(t, "<a href=\"https://auth.example.com/?rd=https%3A%2F%2Ftwo-factor.example.com\">302 Found</a>",
|
||||
string(mock.Ctx.Response.Body()))
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/url"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
)
|
||||
|
||||
var bytesEmpty = []byte("")
|
||||
|
||||
func ctxGetPortalURL(ctx *middlewares.AutheliaCtx) (portalURL *url.URL) {
|
||||
var rawURL []byte
|
||||
|
||||
if rawURL = ctx.QueryArgRedirect(); rawURL != nil && !bytes.Equal(rawURL, bytesEmpty) {
|
||||
portalURL, _ = url.ParseRequestURI(string(rawURL))
|
||||
|
||||
return portalURL
|
||||
} else if rawURL = ctx.XAutheliaURL(); rawURL != nil && !bytes.Equal(rawURL, bytesEmpty) {
|
||||
portalURL, _ = url.ParseRequestURI(string(rawURL))
|
||||
|
||||
return portalURL
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -171,6 +171,16 @@ func (ctx *AutheliaCtx) XForwardedURI() (uri []byte) {
|
|||
return uri
|
||||
}
|
||||
|
||||
// XAutheliaURL return the content of the X-Authelia-URL header.
|
||||
func (ctx *AutheliaCtx) XAutheliaURL() (autheliaURL []byte) {
|
||||
return ctx.RequestCtx.Request.Header.PeekBytes(headerXAutheliaURL)
|
||||
}
|
||||
|
||||
// QueryArgRedirect return the content of the rd query argument.
|
||||
func (ctx *AutheliaCtx) QueryArgRedirect() (val []byte) {
|
||||
return ctx.RequestCtx.QueryArgs().PeekBytes(queryArgRedirect)
|
||||
}
|
||||
|
||||
// BasePath returns the base_url as per the path visited by the client.
|
||||
func (ctx *AutheliaCtx) BasePath() (base string) {
|
||||
if baseURL := ctx.UserValueBytes(UserValueKeyBaseURL); baseURL != nil {
|
||||
|
|
|
@ -7,6 +7,8 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
headerXAutheliaURL = []byte("X-Authelia-URL")
|
||||
|
||||
headerAccept = []byte(fasthttp.HeaderAccept)
|
||||
headerContentLength = []byte(fasthttp.HeaderContentLength)
|
||||
headerLocation = []byte(fasthttp.HeaderLocation)
|
||||
|
@ -71,6 +73,8 @@ var (
|
|||
protoHTTPS = []byte(strProtoHTTPS)
|
||||
protoHTTP = []byte(strProtoHTTP)
|
||||
|
||||
queryArgRedirect = []byte("rd")
|
||||
|
||||
// UserValueKeyBaseURL is the User Value key where we store the Base URL.
|
||||
UserValueKeyBaseURL = []byte("base_url")
|
||||
|
||||
|
|
|
@ -167,6 +167,9 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
|
|||
r.GET("/api/verify", middlewares.Wrap(metricsVRMW, middleware(handlers.VerifyGET(config.AuthenticationBackend))))
|
||||
r.HEAD("/api/verify", middlewares.Wrap(metricsVRMW, middleware(handlers.VerifyGET(config.AuthenticationBackend))))
|
||||
|
||||
r.GET("/api/verify/{path:*}", middlewares.Wrap(metricsVRMW, middleware(handlers.VerifyGET(config.AuthenticationBackend))))
|
||||
r.HEAD("/api/verify/{path:*}", middlewares.Wrap(metricsVRMW, middleware(handlers.VerifyGET(config.AuthenticationBackend))))
|
||||
|
||||
r.POST("/api/checks/safe-redirection", middlewareAPI(handlers.CheckSafeRedirectionPOST))
|
||||
|
||||
delayFunc := middlewares.TimingAttackDelay(10, 250, 85, time.Second, true)
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
---
|
||||
###############################################################
|
||||
# Authelia minimal configuration #
|
||||
###############################################################
|
||||
|
||||
jwt_secret: unsecure_secret
|
||||
|
||||
server:
|
||||
port: 9091
|
||||
asset_path: /config/assets/
|
||||
tls:
|
||||
certificate: /config/ssl/cert.pem
|
||||
key: /config/ssl/key.pem
|
||||
|
||||
log:
|
||||
level: debug
|
||||
|
||||
authentication_backend:
|
||||
file:
|
||||
path: /config/users.yml
|
||||
|
||||
session:
|
||||
secret: unsecure_session_secret
|
||||
domain: example.com
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
local:
|
||||
path: /config/db.sqlite
|
||||
|
||||
access_control:
|
||||
default_policy: bypass
|
||||
rules:
|
||||
- domain: "login.example.com"
|
||||
policy: bypass
|
||||
- domain: "public.example.com"
|
||||
policy: bypass
|
||||
- domain: "admin.example.com"
|
||||
policy: two_factor
|
||||
- domain: "secure.example.com"
|
||||
policy: two_factor
|
||||
- domain: "singlefactor.example.com"
|
||||
policy: one_factor
|
||||
|
||||
notifier:
|
||||
smtp:
|
||||
host: smtp
|
||||
port: 1025
|
||||
sender: admin@example.com
|
||||
disable_require_tls: true
|
||||
...
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
version: '3'
|
||||
services:
|
||||
authelia-backend:
|
||||
volumes:
|
||||
- './Envoy/configuration.yml:/config/configuration.yml:ro'
|
||||
- './Envoy/users.yml:/config/users.yml'
|
||||
- './common/ssl:/config/ssl:ro'
|
||||
...
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
###############################################################
|
||||
# Users Database #
|
||||
###############################################################
|
||||
|
||||
# This file can be used if you do not have an LDAP set up.
|
||||
|
||||
# List of users
|
||||
users:
|
||||
john:
|
||||
displayname: "John Doe"
|
||||
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
|
||||
email: john.doe@authelia.com
|
||||
groups:
|
||||
- admins
|
||||
- dev
|
||||
|
||||
harry:
|
||||
displayname: "Harry Potter"
|
||||
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
|
||||
email: harry.potter@authelia.com
|
||||
groups: []
|
||||
|
||||
bob:
|
||||
displayname: "Bob Dylan"
|
||||
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
|
||||
email: bob.dylan@authelia.com
|
||||
groups:
|
||||
- dev
|
||||
|
||||
james:
|
||||
displayname: "James Dean"
|
||||
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" # yamllint disable-line rule:line-length
|
||||
email: james.dean@authelia.com
|
||||
...
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
version: '3'
|
||||
services:
|
||||
envoy:
|
||||
image: envoyproxy/envoy:v1.23.0
|
||||
volumes:
|
||||
- ./example/compose/envoy/envoy.yaml:/etc/envoy/envoy.yaml
|
||||
- ./example/compose/nginx/portal/ssl:/etc/ssl
|
||||
networks:
|
||||
authelianet:
|
||||
ipv4_address: 192.168.240.100
|
||||
...
|
|
@ -0,0 +1,236 @@
|
|||
---
|
||||
static_resources:
|
||||
listeners:
|
||||
- name: listener_0
|
||||
address:
|
||||
socket_address:
|
||||
address: 0.0.0.0
|
||||
port_value: 8080
|
||||
filter_chains:
|
||||
- filters:
|
||||
- name: envoy.filters.network.http_connection_manager
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager # yamllint disable-line rule:line-length
|
||||
stat_prefix: ingress_http
|
||||
use_remote_address: true
|
||||
skip_xff_append: false
|
||||
access_log:
|
||||
- name: envoy.access_loggers.stdout
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
|
||||
route_config:
|
||||
name: local_route
|
||||
virtual_hosts:
|
||||
- name: login_service
|
||||
domains: ["login.example.com:8080"]
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.ext_authz:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
|
||||
disabled: true
|
||||
routes:
|
||||
- match:
|
||||
prefix: "/.well-known/"
|
||||
route:
|
||||
cluster: authelia-backend
|
||||
- match:
|
||||
prefix: "/api/"
|
||||
route:
|
||||
cluster: authelia-backend
|
||||
- match:
|
||||
prefix: "/locales/"
|
||||
route:
|
||||
cluster: authelia-backend
|
||||
- match:
|
||||
path: "/jwks.json"
|
||||
route:
|
||||
cluster: authelia-backend
|
||||
- match:
|
||||
prefix: "/"
|
||||
route:
|
||||
cluster: authelia-frontend
|
||||
- name: mail_service
|
||||
domains: ["mail.example.com:8080"]
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.ext_authz:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
|
||||
disabled: true
|
||||
routes:
|
||||
- match:
|
||||
prefix: "/"
|
||||
route:
|
||||
cluster: smtp
|
||||
- name: http_service
|
||||
domains: ["*.example.com:8080"]
|
||||
routes:
|
||||
- match:
|
||||
prefix: "/headers"
|
||||
route:
|
||||
cluster: httpbin
|
||||
- match:
|
||||
prefix: "/"
|
||||
route:
|
||||
cluster: nginx-backend
|
||||
http_filters:
|
||||
- name: envoy.filters.http.ext_authz
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
|
||||
http_service:
|
||||
path_prefix: /api/verify/
|
||||
server_uri:
|
||||
uri: authelia-backend:9091
|
||||
cluster: authelia-backend
|
||||
timeout: 0.25s
|
||||
authorization_request:
|
||||
allowed_headers:
|
||||
patterns:
|
||||
- exact: accept
|
||||
- exact: cookie
|
||||
- exact: proxy-authorization
|
||||
headers_to_add:
|
||||
- key: X-Authelia-URL
|
||||
value: 'https://login.example.com:8080/'
|
||||
- key: X-Forwarded-Method
|
||||
value: '%REQ(:METHOD)%'
|
||||
- key: X-Forwarded-Proto
|
||||
value: '%REQ(:SCHEME)%'
|
||||
- key: X-Forwarded-Host
|
||||
value: '%REQ(:AUTHORITY)%'
|
||||
- key: X-Forwarded-URI
|
||||
value: '%REQ(:PATH)%'
|
||||
- key: X-Forwarded-For
|
||||
value: '%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%'
|
||||
authorization_response:
|
||||
allowed_upstream_headers:
|
||||
patterns:
|
||||
- prefix: remote-
|
||||
allowed_client_headers:
|
||||
patterns:
|
||||
- exact: set-cookie
|
||||
allowed_client_headers_on_success:
|
||||
patterns:
|
||||
- exact: set-cookie
|
||||
failure_mode_allow: false
|
||||
- name: envoy.filters.http.router
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
||||
transport_socket:
|
||||
name: envoy.transport_sockets.tls
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
|
||||
common_tls_context:
|
||||
tls_certificates:
|
||||
- certificate_chain:
|
||||
filename: /etc/ssl/server.cert
|
||||
private_key:
|
||||
filename: /etc/ssl/server.key
|
||||
clusters:
|
||||
- name: authelia-frontend
|
||||
transport_socket_matches:
|
||||
- name: "enableTLS"
|
||||
match:
|
||||
enableTLS: true
|
||||
transport_socket:
|
||||
name: envoy.transport_sockets.tls
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
|
||||
common_tls_context: {}
|
||||
- name: "defaultTLSDisabled"
|
||||
match: {}
|
||||
transport_socket:
|
||||
name: envoy.transport_sockets.raw_buffer
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.transport_sockets.raw_buffer.v3.RawBuffer
|
||||
connect_timeout: 0.25s
|
||||
type: STRICT_DNS
|
||||
dns_lookup_family: V4_ONLY
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: authelia-frontend
|
||||
endpoints:
|
||||
- locality:
|
||||
region: dev
|
||||
priority: 0
|
||||
lb_endpoints:
|
||||
- endpoint:
|
||||
health_check_config:
|
||||
hostname: authelia-frontend
|
||||
port_value: 3000
|
||||
address:
|
||||
socket_address:
|
||||
address: authelia-frontend
|
||||
port_value: 3000
|
||||
- locality:
|
||||
region: ci
|
||||
priority: 1
|
||||
lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: authelia-backend
|
||||
port_value: 9091
|
||||
metadata:
|
||||
filter_metadata:
|
||||
envoy.transport_socket_match:
|
||||
enableTLS: true
|
||||
- name: authelia-backend
|
||||
connect_timeout: 0.25s
|
||||
type: LOGICAL_DNS
|
||||
dns_lookup_family: V4_ONLY
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: authelia-backend
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: authelia-backend
|
||||
port_value: 9091
|
||||
transport_socket:
|
||||
name: envoy.transport_sockets.tls
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
|
||||
common_tls_context: {}
|
||||
- name: smtp
|
||||
connect_timeout: 0.25s
|
||||
type: LOGICAL_DNS
|
||||
dns_lookup_family: V4_ONLY
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: smtp
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: smtp
|
||||
port_value: 1080
|
||||
- name: httpbin
|
||||
connect_timeout: 0.25s
|
||||
type: LOGICAL_DNS
|
||||
dns_lookup_family: V4_ONLY
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: httpbin
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: httpbin
|
||||
port_value: 8000
|
||||
- name: nginx-backend
|
||||
connect_timeout: 0.25s
|
||||
type: LOGICAL_DNS
|
||||
dns_lookup_family: V4_ONLY
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: nginx-backend
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: nginx-backend
|
||||
port_value: 80
|
||||
...
|
|
@ -0,0 +1,84 @@
|
|||
package suites
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var envoySuiteName = "Envoy"
|
||||
|
||||
func init() {
|
||||
dockerEnvironment := NewDockerEnvironment([]string{
|
||||
"internal/suites/docker-compose.yml",
|
||||
"internal/suites/Envoy/docker-compose.yml",
|
||||
"internal/suites/example/compose/authelia/docker-compose.backend.{}.yml",
|
||||
"internal/suites/example/compose/authelia/docker-compose.frontend.{}.yml",
|
||||
"internal/suites/example/compose/nginx/backend/docker-compose.yml",
|
||||
"internal/suites/example/compose/envoy/docker-compose.yml",
|
||||
"internal/suites/example/compose/smtp/docker-compose.yml",
|
||||
"internal/suites/example/compose/httpbin/docker-compose.yml",
|
||||
})
|
||||
|
||||
if os.Getenv("CI") == t {
|
||||
dockerEnvironment = NewDockerEnvironment([]string{
|
||||
"internal/suites/docker-compose.yml",
|
||||
"internal/suites/Envoy/docker-compose.yml",
|
||||
"internal/suites/example/compose/authelia/docker-compose.backend.{}.yml",
|
||||
"internal/suites/example/compose/nginx/backend/docker-compose.yml",
|
||||
"internal/suites/example/compose/envoy/docker-compose.yml",
|
||||
"internal/suites/example/compose/smtp/docker-compose.yml",
|
||||
"internal/suites/example/compose/httpbin/docker-compose.yml",
|
||||
})
|
||||
}
|
||||
|
||||
setup := func(suitePath string) error {
|
||||
if err := dockerEnvironment.Up(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return waitUntilAutheliaIsReady(dockerEnvironment, envoySuiteName)
|
||||
}
|
||||
|
||||
displayAutheliaLogs := func() error {
|
||||
backendLogs, err := dockerEnvironment.Logs("authelia-backend", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(backendLogs)
|
||||
|
||||
if os.Getenv("CI") != t {
|
||||
frontendLogs, err := dockerEnvironment.Logs("authelia-frontend", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(frontendLogs)
|
||||
}
|
||||
|
||||
envoyLogs, err := dockerEnvironment.Logs("envoy", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(envoyLogs)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
teardown := func(suitePath string) error {
|
||||
err := dockerEnvironment.Down()
|
||||
return err
|
||||
}
|
||||
|
||||
GlobalRegistry.Register(envoySuiteName, Suite{
|
||||
SetUp: setup,
|
||||
SetUpTimeout: 5 * time.Minute,
|
||||
OnSetupTimeout: displayAutheliaLogs,
|
||||
OnError: displayAutheliaLogs,
|
||||
TestTimeout: 2 * time.Minute,
|
||||
TearDown: teardown,
|
||||
TearDownTimeout: 2 * time.Minute,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package suites
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type EnvoySuite struct {
|
||||
*RodSuite
|
||||
}
|
||||
|
||||
func NewEnvoySuite() *EnvoySuite {
|
||||
return &EnvoySuite{RodSuite: new(RodSuite)}
|
||||
}
|
||||
|
||||
func (s *EnvoySuite) Test1FAScenario() {
|
||||
suite.Run(s.T(), New1FAScenario())
|
||||
}
|
||||
|
||||
func (s *EnvoySuite) Test2FAScenario() {
|
||||
suite.Run(s.T(), New2FAScenario())
|
||||
}
|
||||
|
||||
func (s *EnvoySuite) TestCustomHeaders() {
|
||||
suite.Run(s.T(), NewCustomHeadersScenario())
|
||||
}
|
||||
|
||||
func (s *EnvoySuite) TestResetPasswordScenario() {
|
||||
suite.Run(s.T(), NewResetPasswordScenario())
|
||||
}
|
||||
|
||||
func TestEnvoySuite(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping suite test in short mode")
|
||||
}
|
||||
|
||||
suite.Run(t, NewEnvoySuite())
|
||||
}
|
Loading…
Reference in New Issue