Open 3engel opened 7 months ago
You proposal makes sense however the implementation is not that straight forward as it also requires modifications on the certsrv module. I am planning to look into it as part of next release.
As I am not an MS expert. Do you know how to enable Kerberos for on the MS Web Enrollment Service?
Sure. Since the MS Web Enrollment Service is using IIS (Internet Information Services / MS Web Server) the authentication configured there. In IIS under the CertSrv node there is an "Authentication" Option. "Windows Authentication" should be enabled. In the providers list "Negotiate:Kerberos" should be configured as the only option.
Many thanks for considering it.
Feature made it into v0.35. Thus, closing this issue. In case you have comments feel free to reopen...
@grindsa First of all, thanks a lot for the effort. I tested implementation and could not get to work. Then I tried to dig deeper in it by extracting the relevant code from certsrv.py parts to do some basic tests.
import gssapi.creds
import gssapi.raw
import gssapi.raw
import gssapi.sec_contexts
import requests
from requests_gssapi import HTTPSPNEGOAuth
import gssapi
username = "USERNAME"
password = "PASSWORD"
realm = "EXAMPLE.COM"
url = "https://winpki.example.com/certsrv"
oid = "1.3.6.1.5.5.2" # SPNEGO
spnego = gssapi.OID.from_int_seq("1.3.6.1.5.5.2")
gssapiName = gssapi.Name(f"{username.upper()}@{realm.upper()}", gssapi.NameType.user)
accuire_cred = gssapi.raw.acquire_cred_with_password(
gssapiName, password.encode(), mechs=[spnego], usage="initiate"
)
kerberos_auth = HTTPSPNEGOAuth(creds=accuire_cred.creds, mech=spnego)
response = requests.get(url, auth=kerberos_auth, verify=False)
if response.status_code == 200:
print("Request was successful")
print("Response content:", response.content)
else:
print("Request failed with status code:", response.status_code)
print("Response content:", response.content)
I never got this working with every kind of combination, I tried. When debugging the response object has a www-authenticate header with value 'Negotiate' only without the kerberos ticket. Then I tried this:
import subprocess
import requests
from requests_kerberos import HTTPKerberosAuth, REQUIRED
username = "username"
password = "password"
realm = "EXAMPLE.COM"
url = 'https://winpki.example.com/certsrv'
fullUsername = f"{username}@{realm.upper()}"
kinit_command = ['kinit', fullUsername]
process = subprocess.Popen(kinit_command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process.communicate(input=password.encode())
# Check if kinit was successful
if process.returncode != 0:
raise Exception("kinit failed")
# Create a Kerberos authentication object
kerberos_auth = HTTPKerberosAuth(mutual_authentication=REQUIRED, principal=fullUsername)
response = requests.get(url, auth=kerberos_auth, verify=False)
# Check the response
if response.status_code == 200:
print('Request was successful')
print('Response content:', response.content)
else:
print('Request failed with status code:', response.status_code)
print('Response content:', response.content)
And that worked instantly. So I guess there is an issue with kerberos and the gssapi lib combined with the requests lib.
I modified certsrv.py and added like so:
elif self.auth_method == "kerberos":
from requests_kerberos import HTTPKerberosAuth, REQUIRED
import subprocess
kinit_command = ['kinit', username.upper()]
process = subprocess.Popen(kinit_command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process.communicate(input=password.encode())
if process.returncode != 0:
raise Exception("kinit failed with return code {0} for user {1}".format(process.returncode, username))
self.session.auth = HTTPKerberosAuth(mutual_authentication=REQUIRED, principal=username.upper())
In mscertsrv_ca_handler.py I added 'kerberos' as auth_method: if 'auth_method' in config_dic['CAhandler'] and config_dic['CAhandler']['auth_method'] in ['basic', 'ntlm', 'gssapi','kerberos']:
Then I modified the requirements.txt and the dockerfile (nginx/wsgi) by adding the required libaries and build a new docker image in order to test the certificate issueing and it worked.
It is not really clear to me why the gssapi method is not working. I even could not make this work:
kinit <username>
curl --negotiate -u: <user>:<password> -k https://<host>/certsrv/
Step kinit works. I'm getting a valid ticket when doing a klist. When doing the curl request I'm getting a 401 status code.
Can you describe your deployment (containerized, rpm, deb or manual installation).
Can you share your acme_srv.cfg
as well as the related krb5.cfg
? Can you please enable debugging in acme_srv.cfg
replicate the issue in a2c and share the log for troubleshooting? Feel free to share the information via email to grindelsack(at)gmail.com
@grindsa sorry for the late reply. I'm using docker with docker compose. docker-compose.yml looks like this:
version: '3.2'
services:
acme2certifier:
container_name: acme2certifier
image: grindsa/acme2certifier:nginx-wsgi
volumes:
- ./data:/var/www/acme2certifier/volume
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "22280:80"
# - "22443:443"
restart: always
dns:
- 10.193.132.1
- 10.193.132.12
acme_srv.cfg looks like this.
[DEFAULT]
debug: True
[Nonce]
nonce_check_disable: False
[CAhandler]
handler_file: examples/ca_handler/mscertsrv_ca_handler.py
host: subca1.mydomain.com
user: <user with Enroll permissions>
password:
ca_bundle: /var/www/acme2certifier/volume/RootCA.pem
auth_method: gssapi #in my working scenario I use "kerberos" instead
template: WebServerTemplate1Year
krb5_config: /var/www/acme2certifier/volume/krb5.conf
[Certificate]
revocation_reason_check_disable: False
[Challenge]
challenge_validation_disable: False
[Order]
tnauthlist_support: False
krb5.conf looks like this:
[libdefaults]
default_realm = MYDOMAIN.COM
[realms]
MYDOMAIN.COM = {
kdc = dc1.mydomain.com
admin_server = dc1.mydomain.com
default_domain = mydomain.com
}
[domain_realm]
.mydomain.com = MYDOMAIN.COM
mydomain.com = MYDOMAIN.COM
As far as I read in documentations a krb5.conf is not mandatory, when there is a well configured DNS server in the network. Nevertheless I used the config above. I tested the Kerberos authentication directly on the container shell via:
kinit -V causer@MYDOMAIN.COM
This worked and the Kerberos ticket is created. klist result is:
Default principal: causer@MYDOMAIN.COM
Valid starting Expires Service principal
09/10/24 09:14:15 09/10/24 19:14:15 krbtgt/MYDOMAIN.COM@MYDOMAIN.COM
renew until 09/11/24 09:14:06
The acme2certifier log Looks like this:
https://mysubca.mydomain.com:443 "GET /certsrv/ HTTP/11" 401 1293
authenticate_user(): returning <Response [401]>
handle_401(): returning <Response [401]>
handle_response(): returning <Response [401]>
handle_response() has seen 0 401 responses
handle_401(): Handling: 401
authenticate_user(): Authorization header: Negotiate REMOVED_DUE_TO_SECURITY_REASONS
https://mysubca.mydomain.com:443 "GET /certsrv/ HTTP/11" 401 1293
authenticate_user(): returning <Response [401]>
handle_401(): returning <Response [401]>
handle_response(): returning <Response [401]>
handle_response() has seen 1 401 responses
handle_response(): returning 401 <Response [401]>
Sent GET request to https://mysubca.mydomain.com/certsrv/, with headers:
User-agent: Mozilla/5.0 certsrv (https://github.com/magnuswatn/certsrv)
Authorization: Negotiate REMOVED_DUE_TO_SECURITY_REASONS
:
None
Recieved response:
HTTP 401
Content-Type: text/html
Server: Microsoft-IIS/10.0
WWW-Authenticate: Negotiate
Date: Tue, 10 Sep 2024 06:54:46 GMT
Content-Length: 1293
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"/>
<title>401 - Unauthorized: Access is denied due to invalid credentials.</title>
<style type="text/css">
<!--
body{margin:0;font-size:.7em;font-family:Verdana, Arial, Helvetica, sans-serif;background:#EEEEEE;}
fieldset{padding:0 15px 10px 15px;}
h1{font-size:2.4em;margin:0;color:#FFF;}
h2{font-size:1.7em;margin:0;color:#CC0000;}
h3{font-size:1.2em;margin:10px 0 0 0;color:#000000;}
#header{width:96%;margin:0 0 0 0;padding:6px 2% 6px 2%;font-family:"trebuchet MS", Verdana, sans-serif;color:#FFF;
background-color:#555555;}
#content{margin:0 0 0 2%;position:relative;}
.content-container{background:#FFF;width:96%;margin-top:8px;padding:10px;position:relative;}
-->
</head>
<body>
<div id="header"><h1>Server Error</h1></div>
<div id="content">
<div class="content-container"><fieldset>
<h2>401 - Unauthorized: Access is denied due to invalid credentials.</h2>
<h3>You do not have permission to view this directory or page using the credentials that you supplied.</h3>
</fieldset></div>
</div>
</body>
</html>
CAhandler.__check_credentials() ended with False
Connection or Credentialcheck failed
Certificate.enroll() ended
Certificate._enroll() ended
acme2certifier enrollment error: Connection or Credentialcheck failed.
Certificate._enrollerror_handler(Connection or Credentialcheck failed.)
Certificate._enrollerror_handler(): invalidating order as there is no certificate and no poll_identifier: Connection or Credentialcheck failed./mTxdFjIBwS8T
Certificate._order_update({'name': 'mTxdFjIBwS8T', 'status': 'invalid'})
order_update({'name': 'mTxdFjIBwS8T', 'status': 'invalid'})
DBStore._status_search(column:name, pattern:invalid)
DBStore._status_search() ended
DBStore.order_update() ended
Certificate._store_cert_error(V6WKWR2N7unT)
DBStore.certificate_add(V6WKWR2N7unT)
DBStore._certificate_search(column:name, pattern:V6WKWR2N7unT)
modified column to certificate.name
DBStore._certificate_search() ended with: True
_certificate_update() for V6WKWR2N7unT id:28
_certificate_update() ended with: 28
DBStore.certificate_add() ended with: 28
Certificate._store_cert_error(28) ended
Certificate._enrollerror_handler() ended with: None
Certificate._pre_hooks_process(V6WKWR2N7unT, mTxdFjIBwS8T
Certificate._post_hooks_process([])
Certificate._enroll_and_store() ended with: None:urn:ietf:params:acme:error:serverInternal
Certificate.enroll_and_store() ThreadWithReturnValue ended
Certificate.enroll_and_store() ended with: None:urn:ietf:params:acme:error:serverInternal
Order._csr_process() ended with order:mTxdFjIBwS8T 500:{urn:ietf:params:acme:error:serverInternal:None
Order._finalize() ended
Order._process() ended with order:mTxdFjIBwS8T 500:urn:ietf:params:acme:error:serverInternal:enrollment failed
Order._parse() ended with code: 500
Message.prepare_response()
Error.enrich_error()
Error.acme_errormessage(urn:ietf:params:acme:error:serverInternal)
Nonce.nonce_generate_and_add()
Nonce.nonce__new()
got nonce: de389a44fb9c466582aed2a78b56bc4e
DBStore.nonce_add(de389a44fb9c466582aed2a78b56bc4e)
DBStore.nonce_add() ended
Nonce.generate_and_add() ended with:de389a44fb9c466582aed2a78b56bc4e
Order.parse() returns: {"code": 500, "header": {"Replay-Nonce": "de389a44fb9c466582aed2a78b56bc4e"}, "data": {"status": 500, "type": "urn:ietf:params:acme:error:serverInternal", "detail": "enrollment failed"}}
172.20.0.1 /acme/order/mTxdFjIBwS8T/finalize {'code': 500, 'header': {'Replay-Nonce': '- modified -'}, 'data': {'status': 500, 'type': 'urn:ietf:params:acme:error:serverInternal', 'detail': 'enrollment failed'}}
[pid: 28|app: 0|req: 3/11] 172.20.0.1 () {48 vars in 779 bytes} [Tue Sep 10 08:54:16 2024] POST /acme/order/mTxdFjIBwS8T/finalize => generated 99 bytes in 1723 msecs (HTTP/1.1 500) 2 headers in 120 bytes (1 switches on core 0)
As client I used win-acme. With the exact same settings and the changes I made as described here I got it working. So I led back to the gssapi implementation and the way how the HTTP authentication header is created.
The mscertsrv_ca_handler.py handler doesn't support Kerberos authentications like mswcce_ca_handler.py. However, using NTLM is a security issue and therefore Kerberos is recommended. Therefore it would be great to support kerberos also for mscertsrv_ca_handler.py.