1

I am trying to solve the problem of accessing a service (HTTP) using the kerberos constrained delegation mechanism. It seems that I am forming the kerberos ticket correctly, but at the same time looking at its ASN.1 format, I do not find support for the SPNEGO mechanism. and the server cannot recognize this ticket, when it is transmitted in the Authorization:Negotiate header. I tried using the pyspnego library, but apparently I'm not an expert in it.

import gssapi
from gssapi import RequirementFlag, MechType
import os
import base64
import requests
import argparse
import gssapi.raw as gssapi_raw
import spnego

# parse argument
parser = argparse.ArgumentParser()
parser.add_argument('client_user', help='Client user principal')
args = parser.parse_args()

#Settings

realm = "REGION.INTRA.NET"
keytab = "/etc/httpd/conf.d/t_hrkrbtest.keytab"
server_principal = 'HOST/t_hrkrbtest.' + realm.lower() + '@' + realm
client_user = args.client_user # Use receive arg
target_service = 'HTTP/sap3.' + realm.lower() + '@' + realm

#Set keytab

os.environ["KRB5_KTNAME"] = keytab

# === Exist keytab ===

if not os.path.exists(keytab):
    raise FileNotFoundError(f"Keytab not found: {keytab}")

#print(">>> Check principals in keytab:")
kt = gssapi_raw.acquire_cred_from({
'keytab': keytab.encode('utf-8')
}, usage='accept')
#print("Principal:", kt[0])


try:
        server_name = gssapi.Name(server_principal, name_type=gssapi.NameType.kerberos_principal)
        creds = gssapi.Credentials(name=server_name, usage="initiate")

except Exception as e:
    print(f"Error getting TGT from keytab: {e}")
    exit(1)

#print(">>> S4U2Self")

try:
    user_name = gssapi.Name(client_user, name_type=gssapi.NameType.kerberos_principal)
    creds = gssapi.Credentials(usage='initiate')
    impersonated_creds = creds.impersonate(user_name)
#    print(f"Success S4U2Self {client_user}")

except Exception as e:
    print(f"Error S4U2Self: {e}")

try:
    target_name = gssapi.Name(target_service, name_type=gssapi.NameType.kerberos_principal)

#    ctx = gssapi.SecurityContext(name=target_name, creds=impersonated_creds, mech=MechType.kerberos, flags=RequirementFlag.delegate_to_peer |
#          RequirementFlag.identify |
#          RequirementFlag.mutual_authentication, usage="initiate")
    ctx = gssapi.SecurityContext(name=target_name, creds=impersonated_creds, mech=MechType.kerberos, usage="initiate")
    token = ctx.step()
    ctx1 = spnego.client(hostname="sap3.intra.net", service="HTTP", protocol="negotiate")
    out_token = ctx1.step(token)

    b64_ticket = base64.b64encode(out_token).decode('utf-8')
    print(b64_ticket)

except Exception as e:
    print(f"Error S4U2Proxy: {e}")
    exit(1)

How can I add the SPNEGO mechanism to the token in the current ticket?

1 Answer 1

1

First of all, you are confusing tickets and tokens.

  • Tickets are the reusable credential shown in klist and stored in gssapi.Credentials. A "ticket" is a Kerberos-specific concept.

    Tokens are the one-time credential returned from ctx.step() and sent in HTTP Authorization headers. A "token" can be produced by any mechanism (Kerberos, SPNEGO, NTLM, and whatnot).

  • Tickets do not contain tokens – it is the opposite; a mechanism token will contain a Kerberos ticket (plus a one-time authenticator).

  • Therefore, calling b64encode(token) does not create a ticket; the result is just a b64_token, not a b64_ticket.

Second, your mixing of gssapi.SecurityContext and spnego.client seems backwards.

  • You're configuring the SecurityContext with usage "initiate", therefore its tokens are meant to be sent to a server (acceptor) – it makes no sense to give them to a spnego.client which is a second initiator and expects to be given a token received from a server.

    Basically, initiator=client and acceptor=server. Two initiators cannot talk to each other; spnego.client will not convert a client token to a different type of client token.

  • Overall, it doesn't make sense to use both gssapi.SecurityContext and spnego.client (except in loopback tests) – either use one or the other.

    The advantage of spnego.client is that it is cross-platform (automatically using SSPI on Windows or GSSAPI on Linux). But in your case, you are already hard-depending on the gssapi module, therefore it's more appropriate to directly use gssapi.SecurityContext for the final token too.

Finally, the reason you're not getting SPNEGO is because your code did not ask for SPNEGO. In one case your code explicitly asks for mech=MechType.kerberos, and in another case your code doesn't specify any mechanism, therefore GSSAPI defaults to Kerberos again.

  • SPNEGO is not a native part of Kerberos; it is a Microsoft extension. So although SPNEGO may be the default for Microsoft SSPI, plain Kerberos is the default for standard GSSAPI.

  • The MIT Krb5 implementation of GSSAPI does support SPNEGO, but you have to explicitly request it like this:

    spnego_oid = gssapi.Mechanism.from_sasl_name("SPNEGO")
    # -or-
    # spnego_oid = gssapi.Mechanism.from_int_seq("1.3.6.1.5.5.2")
    # -or-
    # spnego_oid = gssapi.OID.from_int_seq("1.3.6.1.5.5.2")
    
    ctx = gssapi.SecurityContext(..., mech=spnego_oid, ...)
    
  • If you're using requests for the actual HTTP request, I'd recommend using the requests_gssapi module to handle the final bits (it will automatically create a context and obtain a token for each HTTP request):

    spnego_oid = gssapi.Mechanism.from_sasl_name("SPNEGO")
    
    auth_handler = requests_gssapi.HTTPSPNEGOAuth(mech=spnego_oid,
                                                  creds=impersonated_creds,
                                                  opportunistic_auth=True)
    
    http_client = requests.Session(auth=auth_handler)
    

    Recent versions of requests_gssapi already default to requesting SPNEGO, though in older versions it was necessary to specify it manually.

    The same recommendation goes for httpx_gssapi if you use httpx.

One additional note is that Kerberos principals should be treated as if they're case-sensitive (both the service name and the host name).

  • Active Directory domain controllers are case-insensitive and treat "HOST/foo" identically to "host/FOO" and so on, but that doesn't apply to other Kerberos implementations, so it would be best practice to use the standard spelling of service names.

  • HTTP is always upper-case, but host should be lower-case. (Of course, if your keytab contains "HOST/", then you might have no choice but to use "HOST/" yourself, but that would mean the keytab was generated incorrectly.)

  • Most other "generic" Kerberos service names such as imap, smtp, cifs are lower-case as well (HTTP being the exception, not the rule).

  • Realm names are always upper-case (REGION.INTRA.NET is correct).

    Domain names are usually lower-case ('HTTP/sap3.' + realm.lower() is correct).

Sign up to request clarification or add additional context in comments.

7 Comments

thank you so much for the detailed explanation. I use apache in order to change the authentication type during the request proxying. for incoming connections, I successfully receive login+realm from the jwt token. I call the python script in order to get the value from the script and insert it into the Authorization: Negotiate header xxxxxxxx. I added the SPNEGO mechanism, as you suggested, but it didn't help. Maybe I should use something else, or I need to finish updating my python script to a working state.
How are you actually using this – are you making HTTP requests directly from Python, or are you calling the script from some other program? Can you show the "surrounding" code, i.e. how the token gets placed in the header and how it gets sent? Is it only one HTTP request or multiple? (Note that the token you get from .step() is not reusable; the input ticket is reusable but every HTTP request needs a new token from a fresh SecurityContext.) And, I would suggest first testing without delegation/impersonation, that is, try to make the SPNEGO HTTP request directly with your own credent
...with your own credentials. If it works, then build up by adding impersonation. (Weird – it says "10 characters left" but it still cuts the comment off.)
I've put the directives and commands used in apache so that you can understand what I'm trying to implement. <VirtualHost *:80> SSLProxyEngine on Proxypass "/" "https://sap3.intra.net:44300/" ProxyPassReverse "/" "https://sap3.intra.net:44300/" RewriteEngine On RewriteMap kerb-ticket prg:/etc/httpd/conf.d/wrapper.sh <Location "/"> AuthJWTSignatureSharedSecret XXXXXXXXXXXXXX AuthJWTAttributeUsername userPrincipalNameCustom RequestHeader unset "Authorization"
AuthType jwt AuthName "JWT Auth" Require valid-user RewriteRule .* - [E=KERB_TICKET:${kerb-ticket:%{REMOTE_USER}}] RequestHeader set "Authorization" "Negotiate %{KERB_TICKET}e" env=KERB_TICKET </Location> </VirtualHost> wrapper.sh: #!/bin/bash while read -r line; do python3 /etc/httpd/conf.d/kcd.py "$line" done
StackOverflow comments don't really support code blocks or paragraphs (their idea was that all such extra information ought to be edited into the main post instead). But regarding this configuration – do you have a way of logging all of the requests forwarded by Apache? Can you test without Apache, using just curl --header "Authorization: Negotiate $(sudo -u www-data kcd.py someuser)"?
when i get this request - i got 401 WWW-Authenticate: Basic xxxxx curl -k --header "Authorization: Negotiate $(sudo -u root python3 kcd.py [email protected])" sap3.intra.net:44300/uri_some -vvv and when i do this request in win system i got 200 OK curl -k --negotiate -u [email protected] : sap3.intra.net:44300/uri_some -vvv

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.