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:
Jens Langhammer 2021-07-19 13:17:13 +02:00
parent 673da2a96e
commit a2c587be43
5 changed files with 49 additions and 15 deletions

View File

@ -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")

View File

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

View File

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

View File

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

View File

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