outposts: don't authenticate as service user for flows to set remote-ip
set outpost token as additional header and check that token (user) if they can override remote-ip Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		| @ -38,6 +38,7 @@ USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug" | |||||||
| USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account" | USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account" | ||||||
| USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources" | USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources" | ||||||
| USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires"  # nosec | USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires"  # nosec | ||||||
|  | USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips" | ||||||
|  |  | ||||||
| GRAVATAR_URL = "https://secure.gravatar.com" | GRAVATAR_URL = "https://secure.gravatar.com" | ||||||
| DEFAULT_AVATAR = static("dist/assets/images/user_default.png") | DEFAULT_AVATAR = static("dist/assets/images/user_default.png") | ||||||
|  | |||||||
| @ -2,10 +2,12 @@ | |||||||
| from typing import Any, Optional | from typing import Any, Optional | ||||||
|  |  | ||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
|  | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| OUTPOST_REMOTE_IP_HEADER = "HTTP_X_AUTHENTIK_REMOTE_IP" | OUTPOST_REMOTE_IP_HEADER = "HTTP_X_AUTHENTIK_REMOTE_IP" | ||||||
| USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips" | OUTPOST_TOKEN_HEADER = "HTTP_X_AUTHENTIK_OUTPOST_TOKEN"  # nosec | ||||||
| DEFAULT_IP = "255.255.255.255" | DEFAULT_IP = "255.255.255.255" | ||||||
|  | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
| def _get_client_ip_from_meta(meta: dict[str, Any]) -> str: | def _get_client_ip_from_meta(meta: dict[str, Any]) -> str: | ||||||
| @ -27,13 +29,25 @@ def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]: | |||||||
|     """Get the actual remote IP when set by an outpost. Only |     """Get the actual remote IP when set by an outpost. Only | ||||||
|     allowed when the request is authenticated, by a user with USER_ATTRIBUTE_CAN_OVERRIDE_IP set |     allowed when the request is authenticated, by a user with USER_ATTRIBUTE_CAN_OVERRIDE_IP set | ||||||
|     to outpost""" |     to outpost""" | ||||||
|     if not hasattr(request, "user"): |     from authentik.core.models import ( | ||||||
|  |         USER_ATTRIBUTE_CAN_OVERRIDE_IP, | ||||||
|  |         Token, | ||||||
|  |         TokenIntents, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     if ( | ||||||
|  |         OUTPOST_REMOTE_IP_HEADER not in request.META | ||||||
|  |         or OUTPOST_TOKEN_HEADER not in request.META | ||||||
|  |     ): | ||||||
|         return None |         return None | ||||||
|     if not request.user.is_authenticated: |     tokens = Token.filter_not_expired( | ||||||
|  |         key=request.META.get(OUTPOST_TOKEN_HEADER), intent=TokenIntents.INTENT_API | ||||||
|  |     ) | ||||||
|  |     if not tokens.exists(): | ||||||
|  |         LOGGER.warning("Attempted remote-ip override without token") | ||||||
|         return None |         return None | ||||||
|     if OUTPOST_REMOTE_IP_HEADER not in request.META: |     user = tokens.first().user | ||||||
|         return None |     if user.group_attributes().get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False): | ||||||
|     if request.user.group_attributes().get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False): |  | ||||||
|         return None |         return None | ||||||
|     return request.META[OUTPOST_REMOTE_IP_HEADER] |     return request.META[OUTPOST_REMOTE_IP_HEADER] | ||||||
|  |  | ||||||
|  | |||||||
| @ -28,12 +28,18 @@ from structlog.stdlib import get_logger | |||||||
| from urllib3.exceptions import HTTPError | from urllib3.exceptions import HTTPError | ||||||
|  |  | ||||||
| from authentik import ENV_GIT_HASH_KEY, __version__ | from authentik import ENV_GIT_HASH_KEY, __version__ | ||||||
| from authentik.core.models import USER_ATTRIBUTE_SA, Provider, Token, TokenIntents, User | from authentik.core.models import ( | ||||||
|  |     USER_ATTRIBUTE_CAN_OVERRIDE_IP, | ||||||
|  |     USER_ATTRIBUTE_SA, | ||||||
|  |     Provider, | ||||||
|  |     Token, | ||||||
|  |     TokenIntents, | ||||||
|  |     User, | ||||||
|  | ) | ||||||
| from authentik.crypto.models import CertificateKeyPair | from authentik.crypto.models import CertificateKeyPair | ||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG | ||||||
| from authentik.lib.models import InheritanceForeignKey | from authentik.lib.models import InheritanceForeignKey | ||||||
| from authentik.lib.sentry import SentryIgnoredException | from authentik.lib.sentry import SentryIgnoredException | ||||||
| from authentik.lib.utils.http import USER_ATTRIBUTE_CAN_OVERRIDE_IP |  | ||||||
| from authentik.managed.models import ManagedModel | from authentik.managed.models import ManagedModel | ||||||
| from authentik.outposts.controllers.k8s.utils import get_namespace | from authentik.outposts.controllers.k8s.utils import get_namespace | ||||||
| from authentik.outposts.docker_tls import DockerInlineTLS | from authentik.outposts.docker_tls import DockerInlineTLS | ||||||
|  | |||||||
| @ -4,10 +4,12 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"net" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/http/cookiejar" | 	"net/http/cookiejar" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	log "github.com/sirupsen/logrus" | 	log "github.com/sirupsen/logrus" | ||||||
| 	"goauthentik.io/api" | 	"goauthentik.io/api" | ||||||
| @ -24,6 +26,11 @@ const ( | |||||||
| 	StageAccessDenied          = StageComponent("ak-stage-access-denied") | 	StageAccessDenied          = StageComponent("ak-stage-access-denied") | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	HeaderAuthentikRemoteIP     = "X-authentik-remote-ip" | ||||||
|  | 	HeaderAuthentikOutpostToken = "X-authentik-outpost-token" | ||||||
|  | ) | ||||||
|  |  | ||||||
| type FlowExecutor struct { | type FlowExecutor struct { | ||||||
| 	Params  url.Values | 	Params  url.Values | ||||||
| 	Answers map[StageComponent]string | 	Answers map[StageComponent]string | ||||||
| @ -31,6 +38,7 @@ type FlowExecutor struct { | |||||||
| 	api      *api.APIClient | 	api      *api.APIClient | ||||||
| 	flowSlug string | 	flowSlug string | ||||||
| 	log      *log.Entry | 	log      *log.Entry | ||||||
|  | 	token    string | ||||||
| } | } | ||||||
|  |  | ||||||
| func NewFlowExecutor(flowSlug string, refConfig *api.Configuration) *FlowExecutor { | func NewFlowExecutor(flowSlug string, refConfig *api.Configuration) *FlowExecutor { | ||||||
| @ -56,6 +64,7 @@ func NewFlowExecutor(flowSlug string, refConfig *api.Configuration) *FlowExecuto | |||||||
| 		api:      apiClient, | 		api:      apiClient, | ||||||
| 		flowSlug: flowSlug, | 		flowSlug: flowSlug, | ||||||
| 		log:      l, | 		log:      l, | ||||||
|  | 		token:    strings.Split(refConfig.DefaultHeader["Authorization"], " ")[1], | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -69,6 +78,16 @@ type ChallengeInt interface { | |||||||
| 	GetResponseErrors() map[string][]api.ErrorDetail | 	GetResponseErrors() map[string][]api.ErrorDetail | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (fe *FlowExecutor) DelegateClientIP(a net.Addr) { | ||||||
|  | 	host, _, err := net.SplitHostPort(a.String()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fe.log.WithError(err).Warning("Failed to get remote IP") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	fe.api.GetConfig().AddDefaultHeader(HeaderAuthentikRemoteIP, host) | ||||||
|  | 	fe.api.GetConfig().AddDefaultHeader(HeaderAuthentikOutpostToken, fe.token) | ||||||
|  | } | ||||||
|  |  | ||||||
| func (fe *FlowExecutor) CheckApplicationAccess(appSlug string) (bool, error) { | func (fe *FlowExecutor) CheckApplicationAccess(appSlug string) (bool, error) { | ||||||
| 	p, _, err := fe.api.CoreApi.CoreApplicationsCheckAccessRetrieve(context.Background(), appSlug).Execute() | 	p, _, err := fe.api.CoreApi.CoreApplicationsCheckAccessRetrieve(context.Background(), appSlug).Execute() | ||||||
| 	if !p.Passing { | 	if !p.Passing { | ||||||
|  | |||||||
| @ -34,14 +34,8 @@ func (pi *ProviderInstance) getUsername(dn string) (string, error) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (pi *ProviderInstance) Bind(username string, bindDN, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) { | func (pi *ProviderInstance) Bind(username string, bindDN, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) { | ||||||
| 	host, _, err := net.SplitHostPort(conn.RemoteAddr().String()) |  | ||||||
| 	if err != nil { |  | ||||||
| 		pi.log.WithError(err).Warning("Failed to get remote IP") |  | ||||||
| 		return ldap.LDAPResultOperationsError, nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	fe := outpost.NewFlowExecutor(pi.flowSlug, pi.s.ac.Client.GetConfig()) | 	fe := outpost.NewFlowExecutor(pi.flowSlug, pi.s.ac.Client.GetConfig()) | ||||||
| 	fe.ApiClient().GetConfig().AddDefaultHeader("X-authentik-remote-ip", host) | 	fe.DelegateClientIP(conn.RemoteAddr()) | ||||||
| 	fe.Params.Add("goauthentik.io/outpost/ldap", "true") | 	fe.Params.Add("goauthentik.io/outpost/ldap", "true") | ||||||
|  |  | ||||||
| 	fe.Answers[outpost.StageIdentification] = username | 	fe.Answers[outpost.StageIdentification] = username | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer