Skip to content

Commit

Permalink
Add endpoint for GSSAPI Authentication
Browse files Browse the repository at this point in the history
Add the `login_kerberos` endpoint for handling GSSAPI authentications.
mod_auth_gssapi and gssproxy are included as dependencies. Additional
steps are included to the IPA domain addition, such as the addition of
the HTTP service and keytab retrieval. Additionally, `login_password`
endpoint is provided as well, which requests a ticket using the user and
password passed with the client request.

Signed-off-by: Antonio Torres <[email protected]>
  • Loading branch information
antoniotorresm committed Jan 24, 2024
1 parent 5fbc34a commit 99ef1f5
Show file tree
Hide file tree
Showing 12 changed files with 285 additions and 4 deletions.
11 changes: 11 additions & 0 deletions Containerfile.test
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ RUN dnf -y update && dnf -y install \
realmd \
freeipa-client \
oddjob-mkhomedir \
mod_auth_gssapi \
mod_session \
gssproxy \
openssh-clients \
sshpass \
&& dnf clean all

# Copy the source code
Expand Down Expand Up @@ -89,6 +94,12 @@ RUN chmod 740 /www/ipa-tuura/src/ipa-tuura/
RUN chown apache:apache /www/ipa-tuura/src/ipa-tuura/
RUN chown apache:apache /www/ipa-tuura/src/ipa-tuura/db.sqlite3

# Setup gssproxy
COPY prod/conf/gssproxy.conf /etc/gssproxy/80-httpd.conf
COPY prod/conf/httpd_env.conf /etc/systemd/system/httpd.service.d/env.conf
RUN mkdir /var/lib/ipatuura
RUN systemctl enable gssproxy

# Enable httpd service
RUN systemctl enable httpd

Expand Down
11 changes: 11 additions & 0 deletions prod/Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ RUN dnf -y update && dnf -y install \
realmd \
freeipa-client \
oddjob-mkhomedir \
mod_auth_gssapi \
mod_session \
gssproxy \
openssh-clients \
sshpass \
&& dnf clean all

# Copy the source code
Expand Down Expand Up @@ -90,6 +95,12 @@ RUN chmod 740 /www/ipa-tuura/src/ipa-tuura/
RUN chown apache:apache /www/ipa-tuura/src/ipa-tuura/
RUN chown apache:apache /www/ipa-tuura/src/ipa-tuura/db.sqlite3

# Setup gssproxy
COPY prod/conf/gssproxy.conf /etc/gssproxy/80-httpd.conf
COPY prod/conf/httpd_env.conf /etc/systemd/system/httpd.service.d/env.conf
RUN mkdir /var/lib/ipatuura
RUN systemctl enable gssproxy

# Enable httpd service
RUN systemctl enable httpd

Expand Down
5 changes: 5 additions & 0 deletions prod/conf/gssproxy.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[service/HTTP]
mechs = krb5
cred_store = keytab:/var/lib/ipatuura/httpd.keytab
cred_store = ccache:/var/lib/gssproxy/clients/krb5cc_%U
euid = apache
2 changes: 2 additions & 0 deletions prod/conf/httpd_env.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[Service]
Environment=GSS_USE_PROXY=1
14 changes: 14 additions & 0 deletions prod/conf/ipatuura.conf
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
LogLevel info
RewriteCond %{SERVER_PORT} !^443$$

# Skip mod_wsgi handling for GSSAPI auth endpoint
Alias /bridge/login_kerberos/ /dev/null
<Location /bridge/login_kerberos/>
AuthType GSSAPI
AuthName "Kerberos Login"
GssapiUseSessions On
Session On
SessionCookieName session path=/bridge;httponly;secure;
SessionHeader SESSION
GssapiSessionKey file:/etc/httpd/alias/session.key
Require valid-user
</Location>

<Directory /www/ipa-tuura/src/ipa-tuura/root/>
<Files wsgi.py>
Require all granted
Expand All @@ -11,6 +24,7 @@
WSGIDaemonProcess ipa-tuura python-path=/www/ipa-tuura/src/ipa-tuura/root
WSGIProcessGroup ipa-tuura
WSGIScriptAlias / /www/ipa-tuura/src/ipa-tuura/root/wsgi.py
WSGIPassAuthorization On

SSLEngine on
SSLCertificateFile /etc/pki/tls/certs/apache-selfsigned.crt
Expand Down
1 change: 1 addition & 0 deletions src/ipa-tuura/domains/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ class Meta:
"user_object_classes",
"users_dn",
"ldap_tls_cacert",
"keycloak_hostname",
)
3 changes: 3 additions & 0 deletions src/ipa-tuura/domains/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ class DomainProviderType(models.TextChoices):
# Temporary admin service password
client_secret = models.CharField(max_length=20)

# External hostname for Keycloak host
keycloak_hostname = models.CharField(max_length=255, blank=True)

# Identity provider type
id_provider = models.CharField(
max_length=5,
Expand Down
62 changes: 62 additions & 0 deletions src/ipa-tuura/domains/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,22 @@ def activate_ifp(domain):
sssdconfig.write()


def run_ssh_command(user, host, password, command):
target = "{}@{}".format(user, host)
cmd = [
"sudo",
"sshpass",
"-p",
password,
"ssh",
target,
"-o",
"StrictHostKeyChecking=no",
command,
]
return subprocess.run(cmd, capture_output=True)


def install_client(domain):
"""
:param domain
Expand Down Expand Up @@ -212,8 +228,10 @@ def deploy_ipa_service(domain):
hostname = socket.gethostname()
realm = domain["name"].upper()
ipatuura_principal = "ipatuura/%s@%s" % (hostname, realm)
http_principal = "HTTP/%s@%s" % (domain["keycloak_hostname"], realm)
keytab_file = os.environ.get("KRB5_CLIENT_KTNAME", None)
keytab_path = os.path.dirname(keytab_file)
http_keytab_file = "/var/lib/ipatuura/httpd.keytab"

ipa_api_connect(domain)

Expand Down Expand Up @@ -254,6 +272,15 @@ def deploy_ipa_service(domain):
else:
logger.info(f"ipa: service_add result {result}")

# add HTTP service
try:
result = api.Command["service_add"](krbcanonicalname=http_principal)
except ipalib.errors.DuplicateEntry:
logger.info("service %s already exists", http_principal)
pass
else:
logger.info(f"ipa: service_add result {result}")

# add role
try:
result = api.Command["role_add"](cn="ipatuura writable interface")
Expand Down Expand Up @@ -291,6 +318,12 @@ def deploy_ipa_service(domain):
if proc.returncode != 0:
raise Exception("Error getkeytab:\n{}".format(proc.stderr))

# get keytab for HTTP service
args = ["ipa-getkeytab", "-p", http_principal, "-k", http_keytab_file]
proc = subprocess.run(args, capture_output=True, text=True)
if proc.returncode != 0:
raise Exception("Error getkeytab:\n{}".format(proc.stderr))


def remove_sssd_domain(domain):
try:
Expand Down Expand Up @@ -374,6 +407,35 @@ def join_ad_realm(domain):
# workaround until we have rootless SSSD
subprocess.run(["sudo", "chmod", "660", "/etc/sssd/sssd.conf"])

# Register user and SPN for HTTP, request keytab
ad_server = domainconfig.get_option("ad_server")
ad_realm = domainconfig.get_option("krb5_realm")
ad_passwd = domain["client_secret"]
kc_hostname = domain["keycloak_hostname"]
spn_commands = (
"powershell -c '"
"New-ADUser ipatuura;"
"Enable-ADAccount -Identity ipatuura;"
"Set-ADUser ipatuura -KerberosEncryptionType AES256;"
f"setspn -S HTTP/{kc_hostname} ipatuura;"
"$kvno = Get-ADuser ipatuura -property msDS-KeyVersionNumber | select -expand msDS-KeyVersionNumber;"
f"ktpass -out /httpd.keytab -mapUser ipatuura@{ad_realm} -pass {ad_passwd} -mapOp set +DumpSalt -crypto AES256-SHA1 -ptype KRB5_NT_PRINCIPAL -princ HTTP/{kc_hostname}@{ad_realm} -kvno $kvno'"
)
run_ssh_command("Administrator", ad_server, ad_passwd, spn_commands)

# Fetch generated keytab
subprocess.run(
[
"sudo",
"sshpass",
"-p",
ad_passwd,
"scp",
f"Administrator@{ad_server}:C:/httpd.keytab",
"/var/lib/ipatuura/httpd.keytab",
]
)


def config_default_sssd(domain):
"""
Expand Down
1 change: 1 addition & 0 deletions src/ipa-tuura/root/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@
path("scim/v2/", include("django_scim.urls")),
path("creds/", include("creds.urls")),
path("domains/v1/", include("domains.urls")),
path("bridge/", include("scim.urls")),
]
35 changes: 35 additions & 0 deletions src/ipa-tuura/scim/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#
# Copyright (C) 2024 FreeIPA Contributors see COPYING for license
#

"""Integration Domain URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
import logging

from django.urls import include, re_path
from rest_framework.routers import DefaultRouter
from scim.views import BridgeViewSet

logger = logging.getLogger(__name__)


router = DefaultRouter()
router.register("", BridgeViewSet, "bridge")

urlpatterns = [
# we only need /bridge/login_password
re_path("", include(router.urls[:1])),
]
77 changes: 76 additions & 1 deletion src/ipa-tuura/scim/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
#
# Copyright (C) 2022 FreeIPA Contributors see COPYING for license
# Copyright (C) 2024 FreeIPA Contributors see COPYING for license
#

from base64 import b64decode, b64encode

import gssapi
from django.db import NotSupportedError
from django_scim.filters import GroupFilterQuery, UserFilterQuery
from requests.auth import AuthBase
from scim.models import SSSDGroupToGroupModel, SSSDUserToUserModel
from scim.sssd import SSSD, SSSDNotFoundException

Expand Down Expand Up @@ -82,3 +86,74 @@ def search(cls, filter_query, request=None):

group = SSSDGroupToGroupModel(sssd_if, sssdgroup)
return [group]


class NegotiateAuth(AuthBase):
"""Negotiate Auth using python GSSAPI"""

def __init__(self, target_host, ccache_name=None):
self.context = None
self.target_host = target_host
self.ccache_name = ccache_name

def __call__(self, request):
self.initial_step(request)
request.register_hook("response", self.handle_response)
return request

def deregister(self, response):
response.request.deregister_hook("response", self.handle_response)

def _get_negotiate_token(self, response):
token = None
if response is not None:
h = response.headers.get("www-authenticate", "")
if h.startswith("Negotiate"):
val = h[h.find("Negotiate") + len("Negotiate") :].strip()
if len(val) > 0:
token = b64decode(val)
return token

def _set_authz_header(self, request, token):
request.headers["Authorization"] = "Negotiate {}".format(
b64encode(token).decode("utf-8")
)

def initial_step(self, request, response=None):
if self.context is None:
store = {"ccache": self.ccache_name}
creds = gssapi.Credentials(usage="initiate", store=store)
name = gssapi.Name(
"HTTP@{0}".format(self.target_host),
name_type=gssapi.NameType.hostbased_service,
)
self.context = gssapi.SecurityContext(
creds=creds, name=name, usage="initiate"
)

in_token = self._get_negotiate_token(response)
out_token = self.context.step(in_token)
self._set_authz_header(request, out_token)

def handle_response(self, response, **kwargs):
status = response.status_code
if status >= 400 and status != 401:
return response

in_token = self._get_negotiate_token(response)
if in_token is not None:
out_token = self.context.step(in_token)
if self.context.complete:
return response
elif not out_token:
return response

self._set_authz_header(response.request, out_token)
# use response so we can make another request
_ = response.content # pylint: disable=unused-variable
response.raw.release_conn()
newresp = response.connection.send(response.request, **kwargs)
newresp.history.append(response)
return self.handle_response(newresp, **kwargs)

return response
Loading

0 comments on commit 99ef1f5

Please sign in to comment.