Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace service certificates with ipa-server-certinstall #6920

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 37 additions & 8 deletions ipaserver/install/ipa_server_certinstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from ipapython import ipaldap
from ipalib import api, errors
from ipaserver.install import certs, dsinstance, installutils, krbinstance
from ipaserver.install import cainstance
from ipaserver.install import cainstance, httpinstance


class ServerCertInstall(admintool.AdminTool):
Expand Down Expand Up @@ -138,6 +138,8 @@ def run(self):
api.Backend.ldap2.disconnect()

def install_dirsrv_cert(self):
from ipaserver.plugins.service import revoke_certs

serverid = ipaldap.realm_to_serverid(api.env.realm)
dirname = dsinstance.config_dirname(serverid)

Expand All @@ -147,21 +149,35 @@ def install_dirsrv_cert(self):
['nssslpersonalityssl'])
old_cert = entry.single_value['nssslpersonalityssl']

server_cert = self.import_cert(dirname, self.options.pin,
old_cert, 'ldap/%s' % api.env.host,
'restart_dirsrv %s' % serverid)
nickname, cert = self.import_cert(dirname, self.options.pin,
old_cert, 'ldap/%s' % api.env.host,
'restart_dirsrv %s' % serverid)

entry['nssslpersonalityssl'] = [server_cert]
entry['nssslpersonalityssl'] = [nickname]
try:
conn.update_entry(entry)
except errors.EmptyModlist:
pass

ds = dsinstance.DsInstance()
ds.suffix = api.env.basedn
ds.realm = api.env.realm
ds.fqdn = api.env.host
ds.service_prefix = 'ldap'
ds.cert = cert
name = f'{ds.service_prefix}/{ds.fqdn}'
oldcerts = api.Command.cert_find(service=name)['result']
ds.add_cert_to_service(append=False)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The option append=False introduces a slightly different behavior for IPA-issued certs and 3rd-party certs.
When the LDAP server cert is issued by IPA and renewed by IPA, the new certificate is added to the service entry and the old one is kept: https://github.com/freeipa/freeipa/blob/master/ipaserver/plugins/cert.py#L961-L968
With this code, if the LDAP server cert is a 3rd-party cert, it replaces the old cert in the service entry (the old cert is removed).

IMO the logic should be similar in both cases (either always replace or always add). No strong opinion on the best choice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been a while so I forget my reasoning. I think it may be related to what cert-find returned since it also looks in the service cert values. I'll double-check.


revoke_certs(oldcerts)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not comfortable with the revocation of the previous cert. For instance, if the same 3rd-party cert is used initially for both LDAP and HTTP but later on the administrator realizes he should reduce the attack surface and replaces the LDAP cert with another one (in order to have a distinct cert for each service), he doesn't want to revoke the HTTP cert.
I know that 3rd part certs can't be revoked inside IPA but that's just an example for a scenario where replacing a cert is not synonym of revoking a cert.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But they should be able to issue a new HTTP, LDAP or PKINIT cert from IPA using certmonger. I'll give that a try but I don't see why it wouldn't work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So yes, it's possible. I tested replacing the Apache and LDAP certs with 3rd party certs, then replaced those with IPA-issued certificates.

For Apache you first need to move the cert and key out of the way (or remove them). Then you can have certminger issue a new one:

ipa-getcert request -f /var/lib/ipa/certs/httpd.crt -k /var/lib/ipa/private/httpd.key -p /var/lib/ipa/passwds/ipa.example.test-443-RSA -D ipa.example.test -D ipa-ca.example.test -K HTTP/ipa.example.test@EXAMPLE.TEST -C /usr/libexec/ipa/certmonger/restart_httpd -T caIPAserviceCert -v -w

Restart httpd: systemctl restart httpd

The paths are the same for all HTTP certs, CA-issued or 3rd party

For 389-ds you can request a new cert:

ipa-getcert request -d /etc/dirsrv/slapd-EXAMPLE-TEST -n Server-Cert -p /etc/dirsrv/slapd-EXAMPLE-TEST/pwdfile.txt -D ipa.example.test -K ldap/ipa.example.test@EXAMPLE.TEST -C "/usr/libexec/ipa/certmonger/restart_dirsrv EXAMPLE-TEST" -T caIPAserviceCert -v -w

Stop dirsrv

Edit dse.ldif and replace nsSSLPersonalitySSL with Server-Cert, or whatever nickname was chosen, then restart dirsrv.

Done and the certs are replaced and tracked now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


def replace_http_cert(self):
"""
Replace the current HTTP cert-key pair with another one
from a PKCS#12 file
"""
from ipaserver.plugins.service import revoke_certs

# pass in `host_name` to perform
# `NSSDatabase.verify_server_cert_validity()``
cert, key, ca_cert = self.load_pkcs12(
Expand All @@ -184,6 +200,18 @@ def replace_http_cert(self):
req_id, 'HTTP/{host}'.format(host=api.env.host))
certmonger.add_subject(req_id, str(DN(cert.subject)))

http = httpinstance.HTTPInstance()
http.suffix = api.env.basedn
http.realm = api.env.realm
http.fqdn = api.env.host
http.service_prefix = 'HTTP'
http.cert = cert
name = f'{http.service_prefix}/{http.fqdn}'
oldcerts = api.Command.cert_find(service=name)['result']
http.add_cert_to_service(append=False)

revoke_certs(oldcerts)

def replace_kdc_cert(self):
# pass in `realm` to perform `NSSDatabase.verify_kdc_cert_validity()`
cert, key, ca_cert = self.load_pkcs12(
Expand Down Expand Up @@ -314,7 +342,8 @@ def import_cert(self, dirname, pkcs12_passwd, old_cert, principal, command):
cdb.import_pkcs12(pkcs12_file.name, pin)
news = cdb.find_server_certs()
server_certs = [item for item in news if item not in prevs]
server_cert = server_certs[0][0]
nickname = server_certs[0][0]
cert = cdb.get_cert_from_db(nickname)

if ca_enabled:
# Start tracking only if the cert was issued by IPA CA
Expand All @@ -323,11 +352,11 @@ def import_cert(self, dirname, pkcs12_passwd, old_cert, principal, command):
get_ca_nickname(api.env.realm))
# And compare with the CA which signed this certificate
if ca_cert == ipa_ca_cert:
cdb.track_server_cert(server_cert,
cdb.track_server_cert(nickname,
principal,
cdb.passwd_fname,
command)
except RuntimeError as e:
raise admintool.ScriptError(str(e))

return server_cert
return nickname, cert
7 changes: 5 additions & 2 deletions ipaserver/install/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ def add_autobind_entry(self, user, group, principal):

return dn

def add_cert_to_service(self):
def add_cert_to_service(self, append=True):
"""
Add a certificate to a service

Expand All @@ -513,7 +513,10 @@ def add_cert_to_service(self):
raise ValueError("{} has no cert".format(self.service_name))
dn = self.get_principal_dn()
entry = api.Backend.ldap2.get_entry(dn)
entry.setdefault('userCertificate', []).append(self.cert)
if append:
entry.setdefault('userCertificate', []).append(self.cert)
else:
entry['userCertificate'] = self.cert
try:
api.Backend.ldap2.update_entry(entry)
except Exception as e:
Expand Down
56 changes: 56 additions & 0 deletions ipatests/test_integration/test_caless.py
Original file line number Diff line number Diff line change
Expand Up @@ -1545,6 +1545,62 @@ def test_anon_pkinit_with_external_CA(self):
result = self.master.run_command(['kinit', '-n'])
assert result.returncode == 0

# The previous install will be lost with this test
def test_services_are_updated(self):
"Test that IPA service entries get updated & old certs revoked"

tasks.uninstall_master(self.master, clean=False)
ipa_certs_cleanup(self.master)
tasks.install_master(self.master, setup_dns=False)

test_dir = self.master.config.test_dir
for filename in ('ca1.crt', 'ca1/server.crt'):
self.master.transport.put_file(
os.path.join(self.cert_dir, filename),
os.path.join(test_dir, os.path.basename(filename))
)
result = self.master.run_command(
['ipa-cacert-manage', 'install',
os.path.join(self.master.config.test_dir,'ca1.crt')]
)
assert result.returncode == 0
result = self.master.run_command(['ipa-certupdate'])
assert result.returncode == 0

result = self.master.run_command([
'openssl', 'x509', '-serial', '-noout', '-in',
os.path.join(self.master.config.test_dir, 'server.crt')
])
new_serial = str(int(result.stdout_text.split('=')[1].strip()))

serials = {}
for service in ('HTTP', 'ldap',):
result = self.master.run_command(
'ipa service-show %s/%s --raw | grep serial_number:' %
(service, self.master.hostname)
)
serial_number = result.stdout_text.split(':')[1].strip()
serials[service] = serial_number

# import pdb; pdb.set_trace()
for service in ('w', 'd',):
result = self.certinstall(service, 'ca1/server')
assert result.returncode == 0

for service in ('HTTP', 'ldap',):
result = self.master.run_command(
'ipa service-show %s/%s --raw | grep serial_number:' %
(service, self.master.hostname)
)
serial_number = result.stdout_text.split(':')[1].strip()
assert serial_number == new_serial

result = self.master.run_command(
['ipa', 'cert-show', serials[service], '--raw']
)
assert 'revocation_reason:' in result.stdout_text



def verify_kdc_cert_perms(host):
"""Verify that the KDC cert pem file has 0644 perms"""
Expand Down