Use a new mechanism for delegating certificate issuance.

Using the client IP address was a rather poor mechanism for controlling
who could request certificates for whom. Instead the client machine will
bind using the host service principal and request the certificate.

In order to do this:
* the service will need to exist
* the machine needs to be in the certadmin rolegroup
* the host needs to be in the managedBy attribute of the service

It might look something like:

admin

ipa host-add client.example.com --password=secret123
ipa service-add HTTP/client.example.com
ipa service-add-host --hosts=client.example.com HTTP/client.example.com
ipa rolegroup-add-member --hosts=client.example.com certadmin

client

ipa-client-install
ipa-join -w secret123
kinit -kt /etc/krb5.keytab host/client.example.com
ipa -d cert-request file://web.csr --principal=HTTP/client.example.com
This commit is contained in:
Rob Crittenden
2009-11-03 09:35:19 -05:00
committed by Jason Gerard DeRose
parent e4c119ed4b
commit bd619adb5c
6 changed files with 52 additions and 45 deletions

View File

@@ -4,9 +4,10 @@ attributeTypes: (2.16.840.1.113730.3.8.3.2 NAME 'ipaClientVersion' DESC 'Text st
attributeTypes: (2.16.840.1.113730.3.8.3.3 NAME 'enrolledBy' DESC 'DN of administrator who performed manual enrollment of the host' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'IPA v2' )
attributeTypes: (2.16.840.1.113730.3.8.3.4 NAME 'enrollmentPwd' DESC 'Password used to bulk enroll machines' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40{128} X-ORIGIN 'IPA v2' )
attributeTypes: (2.16.840.1.113730.3.8.3.43 NAME 'fqdn' DESC 'FQDN' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v2' )
attributeTypes: (2.16.840.1.113730.3.8.3.53 NAME 'managedBy' DESC 'DNs of entries allowed to manage' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v2')
objectClasses: (2.16.840.1.113730.3.8.4.1 NAME 'ipaHost' AUXILIARY MUST ( fqdn ) MAY ( userPassword $ ipaClientVersion $ enrolledBy $ memberOf) X-ORIGIN 'IPA v2' )
objectClasses: (2.16.840.1.113730.3.8.4.44 NAME 'ipaObject' DESC 'IPA objectclass' AUXILIARY MUST ( ipaUniqueId ) X-ORIGIN 'IPA v2' )
objectClasses: (2.16.840.1.113730.3.8.4.2 NAME 'ipaService' DESC 'IPA service objectclass' AUXILIARY MAY ( memberOf ) X-ORIGIN 'IPA v2' )
objectClasses: (2.16.840.1.113730.3.8.4.2 NAME 'ipaService' DESC 'IPA service objectclass' AUXILIARY MAY ( memberOf $ managedBy ) X-ORIGIN 'IPA v2' )
objectClasses: (2.16.840.1.113730.3.8.4.3 NAME 'nestedGroup' DESC 'Group that supports nesting' SUP groupOfNames STRUCTURAL MAY memberOf X-ORIGIN 'IPA v2' )
objectClasses: (2.16.840.1.113730.3.8.4.4 NAME 'ipaUserGroup' DESC 'IPA user group object class' SUP nestedGroup STRUCTURAL X-ORIGIN 'IPA v2' )
objectClasses: (2.16.840.1.113730.3.8.4.5 NAME 'ipaHostGroup' DESC 'IPA host group object class' SUP nestedGroup STRUCTURAL X-ORIGIN 'IPA v2' )

View File

@@ -36,3 +36,10 @@ dn: cn=services,cn=accounts,$SUFFIX
changetype: modify
add: aci
aci: (targetattr="krbPrincipalName || krbUPEnabled || krbPrincipalKey || krbTicketPolicyReference || krbPrincipalExpiration || krbPasswordExpiration || krbPwdPolicyReference || krbPrincipalType || krbPwdHistory || krbLastPwdChange || krbPrincipalAliases || krbExtraData")(version 3.0; acl "KDC System Account"; allow (read, search, compare, write) userdn="ldap:///uid=kdc,cn=sysaccounts,cn=etc,$SUFFIX";)
# Define which hosts can edit services
dn: cn=services,cn=accounts,$SUFFIX
changetype: modify
add: aci
aci: (targetattr=userCertificate)(version 3.0; aci "Hosts can modify service userCertificate"; allow(write) userattr = "parent[0,1].managedby#USERDN";)

View File

@@ -102,6 +102,7 @@ class cert_request(VirtualCommand):
)
def execute(self, csr, **kw):
ldap = self.api.Backend.ldap2
skw = {"all": True}
principal = kw.get('principal')
add = kw.get('add')
@@ -121,35 +122,6 @@ class cert_request(VirtualCommand):
if subject_host.lower() != hostname.lower():
raise errors.ACIError(info="hostname in subject of request '%s' does not match principal hostname '%s'" % (subject_host, hostname))
# Get the IP address of the machine that submitted the request. We
# will compare this to the subjectname of the CSR.
client_ip = getattr(context, 'client_ip')
rhost = None
if client_ip not in (None, ''):
rev = client_ip.split('.')
if len(rev) == 0:
rev = client_ip.split(':')
rev.reverse()
addr = "%s.in-addr.arpa." % ".".join(rev)
else:
rev.reverse()
addr = "%s.in-addr.arpa." % ".".join(rev)
rs = dnsclient.query(addr, dnsclient.DNS_C_IN, dnsclient.DNS_T_PTR)
if len(rs) == 0:
raise errors.ACIError(info='DNS lookup on client failed for IP %s' % client_ip)
for rsn in rs:
if rsn.dns_type == dnsclient.DNS_T_PTR:
rhost = rsn
break
if rhost is None:
raise errors.ACIError(info='DNS lookup on client failed for IP %s' % client_ip)
client_hostname = rhost.rdata.ptrdname[:-1]
if subject_host.lower() != client_hostname.lower():
self.log.debug("IPA: hostname in subject of request '%s' does not match requesting hostname '%s'" % (subject_host, client_hostname))
self.check_access(operation="request certificate different host")
# See if the service exists and punt if it doesn't and we aren't
# going to add it
try:
@@ -157,24 +129,23 @@ class cert_request(VirtualCommand):
if 'usercertificate' in service:
# FIXME, what to do here? Do we revoke the old cert?
raise errors.GenericError(format='entry already has a certificate, serial number %s' % get_serial(service['usercertificate']))
if not can_write(dn, "usercertificate"):
raise errors.ACIError(info='You need to be a member of the serviceadmin role to update services')
except errors.NotFound, e:
if not add:
raise errors.NotFound(reason="The service principal for this request doesn't exist.")
try:
(dn, service) = api.Command['service_add'](principal, **{})
except errors.ACIError:
raise errors.ACIError(info='You need to be a member of the serviceadmin role to add services')
# We got this far so the service entry exists, can we write it?
if not ldap.can_write(dn, "usercertificate"):
raise errors.ACIError(info="Insufficient 'write' privilege to the 'userCertificate' attribute of entry '%s'." % dn)
# Request the certificate
result = self.Backend.ra.request_certificate(csr, **kw)
# Success? Then add it to the service entry. We know that it
# either exists or we should add it.
if result.get('status') == '0':
if service is None:
try:
service = api.Command['service_add'](principal, **{})
except errors.ACIError:
raise errors.ACIError(info='You need to be a member of the serviceadmin role to add services')
# Success? Then add it to the service entry.
if result.get('status') == 0:
skw = {"usercertificate": str(result.get('certificate'))}
api.Command['service_mod'](principal, **skw)

View File

@@ -40,10 +40,12 @@ class rolegroup(LDAPObject):
'cn': 'name',
'member user': 'member users',
'member group': 'member groups',
'member host': 'member hosts',
'member hostgroup': 'member hostgroups',
'memberof taskgroup': 'member of taskgroup',
}
attribute_members = {
'member': ['user', 'group'],
'member': ['user', 'group', 'host', 'hostgroup'],
'memberof': ['taskgroup'],
}
@@ -99,7 +101,7 @@ class rolegroup_show(LDAPRetrieve):
"""
Display rolegroup.
"""
api.register(rolegroup_show)

View File

@@ -103,12 +103,16 @@ class service(LDAPObject):
'krbprincipal', 'krbprincipalaux', 'krbticketpolicyaux', 'ipaobject',
'ipaservice', 'pkiuser'
]
default_attributes = ['krbprincipalname', 'usercertificate']
default_attributes = ['krbprincipalname', 'usercertificate', 'managedby']
uuid_attribute = 'ipauniqueid'
attribute_names = {
'krbprincipalname': 'kerberos principal',
'usercertificate': 'user certificate',
'ipauniqueid': 'unique identifier',
'managedby': 'managed by',
}
attribute_members = {
'managedby': ['host'],
}
takes_params = (
@@ -131,6 +135,7 @@ class service_add(LDAPCreate):
"""
Add new service.
"""
member_attributes = ['managedby']
takes_options = (
Flag('force',
doc='force principal name even if not in DNS',
@@ -176,6 +181,7 @@ class service_del(LDAPDelete):
"""
Delete an existing service.
"""
member_attributes = ['managedby']
def pre_callback(self, ldap, dn, *keys, **options):
if self.api.env.enable_ra:
(dn, entry_attrs) = ldap.get_entry(dn, ['usercertificate'])
@@ -192,6 +198,7 @@ class service_mod(LDAPUpdate):
"""
Modify service.
"""
member_attributes = ['managedby']
def pre_callback(self, ldap, dn, entry_attrs, *keys, **options):
cert = entry_attrs.get('usercertificate')
if cert:
@@ -213,6 +220,7 @@ class service_find(LDAPSearch):
"""
Search for services.
"""
member_attributes = ['managedby']
def pre_callback(self, ldap, filter, attrs_list, base_dn, *args, **options):
# lisp style!
custom_filter = '(&(objectclass=ipaService)' \
@@ -233,6 +241,24 @@ class service_show(LDAPRetrieve):
"""
Display service.
"""
member_attributes = ['managedby']
api.register(service_show)
class service_add_host(LDAPAddMember):
"""
Add members to service.
"""
member_attributes = ['managedby']
api.register(service_add_host)
class service_remove_host(LDAPRemoveMember):
"""
Remove members from service.
"""
member_attributes = ['managedby']
api.register(service_remove_host)

View File

@@ -512,7 +512,7 @@ class ldap2(CrudBackend, Encoder):
attributes and the entryLevelRights for the entry itself.
"""
principal = getattr(context, 'principal')
(binddn, attrs) = self.find_entry_by_attr("krbprincipalname", principal, "posixAccount")
(binddn, attrs) = self.find_entry_by_attr("krbprincipalname", principal, "krbPrincipalAux")
sctrl = [LDAPControl("1.3.6.1.4.1.42.2.27.9.5.2", True, "dn: " + binddn.encode('UTF-8'))]
self.conn.set_option(_ldap.OPT_SERVER_CONTROLS, sctrl)
(dn, attrs) = self.get_entry(dn, entry_attrs)