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:
parent
673da2a96e
commit
a2c587be43
|
@ -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