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
James Elliott 2022-10-01 21:47:09 +10:00 committed by GitHub
parent 18a2bde62e
commit ed7092c59a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 676 additions and 27 deletions

View File

@ -204,7 +204,7 @@ func cmdGitHubIssueTemplatesBugReportRunE(cmd *cobra.Command, args []string) (er
data := &tmplIssueTemplateData{ data := &tmplIssueTemplateData{
Labels: []string{labelTypeBugUnconfirmed.String(), labelStatusNeedsTriage.String(), labelPriorityNormal.String()}, Labels: []string{labelTypeBugUnconfirmed.String(), labelStatusNeedsTriage.String(), labelPriorityNormal.String()},
Versions: tagsRecent, 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 { if err = tmplGitHubIssueTemplateBug.Execute(f, data); err != nil {

View File

@ -443,7 +443,7 @@ password_policy:
## to anyone. Otherwise restrictions follow the rules defined. ## to anyone. Otherwise restrictions follow the rules defined.
## ##
## Note: One can use the wildcard * to match any subdomain. ## 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. ## Note: You must put patterns containing wildcards between simple quotes for the YAML to be syntactically correct.
## ##

View File

@ -443,7 +443,7 @@ password_policy:
## to anyone. Otherwise restrictions follow the rules defined. ## to anyone. Otherwise restrictions follow the rules defined.
## ##
## Note: One can use the wildcard * to match any subdomain. ## 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. ## Note: You must put patterns containing wildcards between simple quotes for the YAML to be syntactically correct.
## ##

View File

@ -614,14 +614,14 @@ func (s *SecondFactorDuoPostSuite) TestShouldRedirectUserToSafeTargetURL() {
duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(&response, nil) duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(&response, nil)
bodyBytes, err := json.Marshal(signDuoRequestBody{ bodyBytes, err := json.Marshal(signDuoRequestBody{
TargetURL: "https://example.com", TargetURL: "https://mydomain.example.com",
}) })
s.Require().NoError(err) s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes) s.mock.Ctx.Request.SetBody(bodyBytes)
DuoPOST(duoMock)(s.mock.Ctx) DuoPOST(duoMock)(s.mock.Ctx)
s.mock.Assert200OK(s.T(), redirectResponse{ 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) duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(&response, nil)
bodyBytes, err := json.Marshal(signDuoRequestBody{ bodyBytes, err := json.Marshal(signDuoRequestBody{
TargetURL: "http://example.com", TargetURL: "http://mydomain.example.com",
}) })
s.Require().NoError(err) s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes) 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) duoMock.EXPECT().AuthCall(s.mock.Ctx, gomock.Any()).Return(&response, nil)
bodyBytes, err := json.Marshal(signDuoRequestBody{ bodyBytes, err := json.Marshal(signDuoRequestBody{
TargetURL: "http://example.com", TargetURL: "http://mydomain.example.com",
}) })
s.Require().NoError(err) s.Require().NoError(err)
s.mock.Ctx.Request.SetBody(bodyBytes) s.mock.Ctx.Request.SetBody(bodyBytes)

View File

@ -167,7 +167,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
bodyBytes, err := json.Marshal(signTOTPRequestBody{ bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc", Token: "abc",
TargetURL: "https://example.com", TargetURL: "https://mydomain.example.com",
}) })
s.Require().NoError(err) s.Require().NoError(err)
@ -175,7 +175,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
TimeBasedOneTimePasswordPOST(s.mock.Ctx) TimeBasedOneTimePasswordPOST(s.mock.Ctx)
s.mock.Assert200OK(s.T(), redirectResponse{ 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{ bodyBytes, err := json.Marshal(signTOTPRequestBody{
Token: "abc", Token: "abc",
TargetURL: "http://example.com", TargetURL: "http://mydomain.example.com",
}) })
s.Require().NoError(err) s.Require().NoError(err)

View File

@ -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) { func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, isBasicAuth bool, username string, method []byte) {
var ( var (
statusCode int statusCode int
redirectionURL string
friendlyUsername string friendlyUsername string
friendlyRequestMethod string friendlyRequestMethod string
) )
@ -200,10 +199,6 @@ func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, is
return 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) rm := string(method)
switch rm { switch rm {
@ -213,17 +208,30 @@ func handleUnauthorized(ctx *middlewares.AutheliaCtx, targetURL fmt.Stringer, is
friendlyRequestMethod = rm friendlyRequestMethod = rm
} }
if rd != "" { redirectionURL := ctxGetPortalURL(ctx)
switch rm {
case "": if redirectionURL != nil {
redirectionURL = fmt.Sprintf("%s?rd=%s", rd, url.QueryEscape(targetURL.String())) if !utils.IsURISafeRedirection(redirectionURL, ctx.Configuration.Session.Domain) {
default: 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)
redirectionURL = fmt.Sprintf("%s?rd=%s&rm=%s", rd, url.QueryEscape(targetURL.String()), rm)
ctx.ReplyUnauthorized()
return
} }
qry := redirectionURL.Query()
qry.Set("rd", targetURL.String())
if rm != "" {
qry.Set("rm", rm)
}
redirectionURL.RawQuery = qry.Encode()
} }
switch { switch {
case ctx.IsXHR() || !ctx.AcceptsMIME("text/html") || rd == "": case ctx.IsXHR() || !ctx.AcceptsMIME("text/html") || redirectionURL == nil:
statusCode = fasthttp.StatusUnauthorized statusCode = fasthttp.StatusUnauthorized
default: default:
switch rm { 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.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 { } 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.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() ctx.ReplyUnauthorized()

View File

@ -453,7 +453,7 @@ func TestShouldRedirectWithGroups(t *testing.T) {
mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, "app.example.com") mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, "app.example.com")
mock.Ctx.Request.Header.Set("X-Forwarded-Uri", "/code-test/login") 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) VerifyGET(verifyGetCfg)(mock.Ctx)
@ -537,6 +537,110 @@ func (p Pair) String() string {
p.URL, p.Username, p.AuthenticationLevel, p.ExpectedStatusCode) 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) { func TestShouldVerifyAuthorizationsUsingSessionCookie(t *testing.T) {
testCases := []Pair{ testCases := []Pair{
// should apply default policy. // 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("X-Original-URL", "https://two-factor.example.com")
mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8") mock.Ctx.Request.Header.Set("Accept", "text/html; charset=utf-8")
mock.Ctx.Request.SetHost("mydomain.com") 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) 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())) string(mock.Ctx.Response.Body()))
} }

View File

@ -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
}

View File

@ -171,6 +171,16 @@ func (ctx *AutheliaCtx) XForwardedURI() (uri []byte) {
return uri 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. // BasePath returns the base_url as per the path visited by the client.
func (ctx *AutheliaCtx) BasePath() (base string) { func (ctx *AutheliaCtx) BasePath() (base string) {
if baseURL := ctx.UserValueBytes(UserValueKeyBaseURL); baseURL != nil { if baseURL := ctx.UserValueBytes(UserValueKeyBaseURL); baseURL != nil {

View File

@ -7,6 +7,8 @@ import (
) )
var ( var (
headerXAutheliaURL = []byte("X-Authelia-URL")
headerAccept = []byte(fasthttp.HeaderAccept) headerAccept = []byte(fasthttp.HeaderAccept)
headerContentLength = []byte(fasthttp.HeaderContentLength) headerContentLength = []byte(fasthttp.HeaderContentLength)
headerLocation = []byte(fasthttp.HeaderLocation) headerLocation = []byte(fasthttp.HeaderLocation)
@ -71,6 +73,8 @@ var (
protoHTTPS = []byte(strProtoHTTPS) protoHTTPS = []byte(strProtoHTTPS)
protoHTTP = []byte(strProtoHTTP) protoHTTP = []byte(strProtoHTTP)
queryArgRedirect = []byte("rd")
// UserValueKeyBaseURL is the User Value key where we store the Base URL. // UserValueKeyBaseURL is the User Value key where we store the Base URL.
UserValueKeyBaseURL = []byte("base_url") UserValueKeyBaseURL = []byte("base_url")

View File

@ -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.GET("/api/verify", middlewares.Wrap(metricsVRMW, middleware(handlers.VerifyGET(config.AuthenticationBackend))))
r.HEAD("/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)) r.POST("/api/checks/safe-redirection", middlewareAPI(handlers.CheckSafeRedirectionPOST))
delayFunc := middlewares.TimingAttackDelay(10, 250, 85, time.Second, true) delayFunc := middlewares.TimingAttackDelay(10, 250, 85, time.Second, true)

View File

@ -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
...

View File

@ -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'
...

View File

@ -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
...

View File

@ -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
...

View File

@ -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
...

View File

@ -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,
})
}

View File

@ -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())
}