FreeIPA is pretty cool but it is a complex beast with a lot of moving parts. Its documentation is alright but there are many things that were (as least to me) not obvious about it.
Contents
- Official Documentation
- Directory suffix
- Connecting to the directory via UNIX sockets
- Root DSE attributes
- Password storage
- Logging
- Replication status monitoring
- Automating host enrollment with PKINIT
- Exposing to the Internet
- Certificate storage locations
- Host Aliases
- CA renewal server/CRL publisher promotion
- PKI topology goes out of sync with LDAP server topology
- Extending FreeIPA
- DNS: long TXT records
Official Documentation
The best place to start when looking for reference documentation is Red Hat's Red Hat Identity Management Documentation index; then the FreeIPA workshop.
The FreeIPA wiki has a documentation index, although many pages are out of date. Google will often take you here, or to various other outdated sources of information, so always check the above documentation first!
Documentation for components
FreeIPA configures its underlying components in an opinionated way. Keep their documentation to hand as well:
Red Hat Directory Server; upstream documentation: 389 Directory Server Documentation
Red Hat Certificate System; upstream documentation: Dogtag PKI Wiki
Directory suffix
The examples below assume a FreeIPA domain of ipa.example.com. When naming directory entries, replace SUFFIX, with dc=ipa,dc=example,dc=com.
The domain is also used to derive the instance of the directory server mangaed by FreeIPA. This is used, among other places, with the dsctl and dsconf utilities. In our examples it will be IPA-EXAMPLE-COM.
If you use the integrated CA feature, then Dogtag's state will be stored in the directory at another suffix: o=ipaca. You mostly don't have to worry about that detail except when configuring and monitoring replication status.
Connecting to the directory via UNIX sockets
To administer the directory server, you have to use Simple Authentication, specifying cn=Directory Manager as the password. Having to keep the password handy is a bit annoying.
There is an alternative: a process connecting via a UNIX socket can use SASL EXTERNAL authentication in order to be identified by their UID/GID. On the command line:
# ldapwhoami -H ldapi://%2frun%2fslapd-IPA-EXAMPLE-COM.socket -Y EXTERNAL SASL/EXTERNAL authentication started SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth SASL SSF: 0 dn: cn=Directory Manager
Note that we weren't prompted for a password--the root user is mapped to cn=Directory Manager by default.
This can be made the default for the root user by putting the following in /root/.ldaprc:
URI ldapi://%2frun%2fslapd-IPA-EXAMPLE-COM.socket SASL_MECH EXTERNAL
Root DSE attributes
As with all LDAP directories, some interesting details can be queried anonymously by performing a search for the root entry:
$ ldapsearch -LLL -x -H ldaps://ipa0.ipa.exampe.com -s base -b '' vendorname vendorversion netscapemdsuffix namingcontexts defaultnamingcontext dn: vendorname: 389 Project vendorversion: 389-Directory/1.4.0.2010 B2019.175.2029 netscapemdsuffix: cn=ldap://dc=ipa0,dc=ipa,dc=example,dc=com namingcontexts: cn=changelog namingcontexts: dc=ipa,dc=example,dc=com namingcontexts: o=ipaca defaultnamingcontext: dc=ipa,dc=example,dc=com
See Root DSE Attributes for an explanation of what they mean.
Password storage
The cn=Directory Manager password is stored on the nsslapd-rootpw attribute of cn=config.
The easiest way to change the password if the directory server is up is by running dsconf IPA-EXAMPLE-COM directory_manager password_change as root, which should use EXTERNAL authentication as described above (i.e., run it as root and it won't prompt for the current rootdn password).
If the directory server is not up, edit dse.ldif as desribed at How to Reset the Directory Manager Password and How to reset IPA Directory Manager password in RHEL 7,8,9.
Password hashes are stored in the userPassword attribute.
The default hash format is (now) {PBKDF2_SHA256}; it used to be the (weak) {SSHA512}.
The hash format for newly-changed passwords can be get/set via dsconf IPA-EXAMPLE-COM pwpolicy ...
To list all enabled formats: ldapsearch -b 'cn=Password Storage Schemes,cn=plugins,cn=config' nsslapd-pluginenabled=on cn nsslapd-plugindescription nsslapd-pluginenabled
The AllowNThash password plugin policy is enabled by default. If ipa-adtrust-install has been run, then the NT hash (unsalted MD4, gotta love that security) will be written to the sambaNTPassword and/or ipaNTHash attributes during password change.
This may also require the AD-SUPPORT or AD-SUPPORT-LEGACY crypto-policies to be enabled?
Kerberos keys are stored in the krbPrincipalKey attribute.
These are encrypted with the 'master key' found at cn=IPA.EXAMPLE.COM,cn=kerberos,SUFFIX.
Logging
Red Hat have a knowledge base article detailing how to add debug logging on an IPA server. I'll not repeat _all_ of that article in the information below, so read the article if you want more logging for a particular FreeIPA component.
Audit logs
Newer version of FreeIPA have an audit logging feature.
Messages are logged to the journal. They can be viewed with journalctl -g '^\[IPA\.API\]'.
CLI
The ipa command logs to ~/.ipa/log/cli.conf.
Verbosity can be increased by placing debug = true in the [global] section of /etc/ipa/cli.conf (or ~/.ipa/default.conf). This can also be done for a single ipa invocation with the -e debug=true command line option.
Directory server
Under /var/log/dirsrv/slapd-IPA-EXAMPLE-COM you'll find access, security and errors.
Additional audit and audit-failure logs can be enabled:
# dsconf IPA-EXAMPLE-COM config replace nsslapd-auditlog-logging-enabled=on # dsconf IPA-EXAMPLE-COM config replace nsslapd-auditfaillog-logging-enabled=on
The audit logs will contain sensitive information, such as kerberos keys, vault contents and password hashes.
Kerberos
KDC logs are found at /var/log/krb5kdc.log
kadmin (password change) are at /var/log/kadmind.log.
Web server
The FreeIPA web console and also API logs are found in /var/log/httpd.
Verbosity can be increased by placing debug = true in the [global] section of /etc/ipa/server.conf and restarting httpd.service.
PKI server
Has a lot of log files some of which are not purged by default, so they can grow very large if not removed manually.
By the way, the PKI server is hosted by a Tomcat web server on ports 8080 and 8443. https://lists.fedorahosted.org/archives/list/freeipa-users@lists.fedorahosted.org/thread/R7AXDSRRHI2ZCYPM3SPFDEHHL2EQYJWX/, so it will be accessible from the network unless firewalled off. (Once I have created a separate section that descibes all the components of FreeIPA, this information will move there).
Tomcat logs
These are:
/var/log/pki/pki-tomcat/catalina.*.log
/var/log/pki/pki-tomcat/host-manager.*.log
/var/log/pki/pki-tomcat/localhost.*.log
/var/log/pki/pki-tomcat/manager.*.log
They're documented in pki-server-logging(5).
They are rotated daily but old log files are not purged by default.
To enable purging, replace the /etc/pki/pki-tomcat/logging.properties symlink with a copy of the target file (see the man page for details) and then set:
1catalina.org.apache.juli.FileHandler.maxDays = 6 2localhost.org.apache.juli.FileHandler.maxDays = 6 3manager.org.apache.juli.FileHandler.maxDays = 6 4host-manager.org.apache.juli.FileHandler.maxDays = 6
Tomcat access logs
This is found at /var/log/pki/pki-tomcat/localhost_access_log.*.txt. It's rotated daily old log files are not purged by default.
To enable purging, edit /etc/pki/pki-tomcat/server.xml and find the Valve element for org.apache.catalina.valves.AccessLogValve. Add an attribute maxDays="7".
Subsystem logs
These are /var/log/pki/pki-tomcat/*/{selftests.log,system,transactions,signedAudit/*_audit}.
system and transactions are obsolete; as of v11.2.0-beta3-656-g043515bd9d they are no longer created.
They are rotated after 30 days or after they grow to 2000 KiB. This can be figured in the subsystem's corresponding CS.cfg file; however this file only exists for the ca and kra subsystems; there's no corresponding file for acme and pki. Old log files are not purged automtaically.
There is an expirationDate property for these log files in CS.cfg but a comment in RollingLogFile.java says that it is not supported.
I've written a script which can be run purge these log files automatically.
Subsystem debug logs
These are /var/log/pki/pki-tomcat/*/debug.*.log.
They're documented at Configuring Subsystem Debug Log.
They're rotated daily and old log files should be removed after 7 days, but this doesn't seem to work.
- My script (linked above) also purges these log files.
Name server logs
These are found in /var/named/data/*.log. They are rotated automatically and old log files are purged, however not very often.
This can be adjusted within /etc/named/ipa-logging-ext.conf.
Custodia
This is the component that transfers the directory manager password, the CA and system certificates & private keys, the Kerberos master key, and so on between FreeIPA servers when installing a new replica.
Audit log is found at /var/log/ipa-custodia.audit.log.
Doesn't seem to produce operational logs to a file, nor to the journal.
OTP
Logs to the journal. Because this is a per-connection socket-activated service, the easiest way to filter for its messages is journalctl -t ipa-otpd.
Replication status monitoring
There's a general purpose dsctl IPA-EXAMPLE-COM healthcheck command. This outputs a list of problems detected with the directory server. It doesn't seem to be called by ipa-healthcheck, maybe it's a relatively new command. I don't know if it checks for problems with replication status.
Monitoring the Replication Topology in the RHDS documentation leads us to:
dsconf IPA-EXAMPLE-COM replication monitor: port of repl-monitor.pl to Python. Added in #50545 which made it into 389-ds 1.4.2.2; the original Perl script is still shipped in the 389-ds-base-legacy-tools package.
This tool will prompt for a DN and password for each replication agreement configured on the instance. It will then perform a simple bind over an unencrypted channel in order to fetch the status of each agreement from the remote server. If you must use this tool, you'll definitely want to create a separate entry with minimal permissions for replication monitoring rather than using cn=Directory Manager!
Comparing two Directory Server instances describes the ds-replcheck command, which can be use to check if two servers are in-sync. It can also compare replicated suffixes and output any differences found.
This tool is able to use TLS; it will do so if the -Z option is given (which will expect a server URL to use the ldap protocol and port 389; STARTTLS will be used to upgrade to a TLS-protected connection); or if the server URL uses the ldaps protocol (in which case port 636 must be explicitly included in the URL, otherwise it will try to perform a TLS handshake directly after connecting to port 389 🤦)
Here's an alternative that you can run on each server to check its view of each of its replication agreements:
# dsconf -j IPA-EXAMLPE-COM replication list | jq '.items[]' -r | xargs -P8 -i -- dsconf -j EXAMPLE-COM repl-agmt list --suffix={} | jq '.items[].attrs | (.nsds5replicalastupdatestatusjson[0] | fromjson) as $status | [.nsds5replicaroot[0], .cn[0], $status.state, "\((10 * (now - ($status.date | fromdate)) | round) / 10) s", $status.ldap_rc_text, $status.repl_rc_text] | @tsv' -r | sort | column -s$'\t' -t -N SUFFIX,AGREEMENT,STATE,TIME-SINCE,LDAP-STATUS,REPL-STATUS
When things are good, it will look like:SUFFIX AGREEMENT STATE TIME-SINCE LDAP-STATUS REPL-STATUS dc=ipa,dc=example,dc=com ipa6.ipa.example-com-to-ipa3.ipa.example.com green 680 s Success replica acquired dc=ipa,dc=example,dc=com meToipa5.ipa.example.com green 680 s Success replica acquired o=ipaca caToipa5.ipa.example.com green 564.9 s Success replica acquired o=ipaca ipa6.ipa.example.com-to-ipa3.ipa.example.com green 564.9 s Success replica acquired
When things are bad:SUFFIX AGREEMENT STATE TIME-SINCE LDAP-STATUS REPL-STATUS dc=ipa,dc=example,com ipa3.ipa.example.com-to-ipa6.ipa.example.com red 133 s Can't contact LDAP server connection error dc=ipa,dc=example,com meToipa5.ipa.example.com green 21 s Success replica acquired o=ipaca caToipa5.ipa.example.com green 211 s Success replica acquired o=ipaca ipa3.ipa.example.com-to-ipa6.ipa.example.com red 23 s Can't contact LDAP server connection error
The replicaLastUpdateStatusJSON attribute was added in 389-ds 1.4.1.4: #2661. The JSON includes helpful fields such as state which is green, amber, red and is ideal for alerting.
On RHEL 7, there's no dsconf command, nor replicaLastUpdateStatusJSON attribute; so here's a pure-ldapsearch equivalent instead:
# ldapsearch -LLL -o ldif-wrap=no -H "ldapi://%2frun%2fslapd-IPA-EXAMPLE-COM -Q -Y EXTERNAL -b 'cn=mapping tree,cn=config' -s sub objectClass=nsds5replicationagreement nsDS5ReplicaHost nsDS5ReplicaPort nsDS5ReplicaRoot nsds5replicaLastUpdateStatus; done
Check the nsds5replicalastupdatestatus attribute of each nsds5replicationagreemet entry, match ^Error \((\d+)\) and alert if non-zero.
All replication agreements can be 'poked' to force them to send data with:
# dsconf -j IPA-EXAMPLE-COM replication list | jq '.items[]' -r | xargs -P8 -i -- dsconf -j IPA-EXAMPLE-COM repl-agmt list --suffix={} | jq '.items[].attrs | "dsconf IPA-EXAMPLE-COM repl-agmt poke --suffix=\(.nsds5replicaroot[0]) \(.cn[0])"' -r | bash -x
- Run the previous monitor commands after running that to see if the replication agreements are still green.
You can view details for a single replication agreement with:
# dsconf -j IPA-EXAMPLE-COM repl-agmt status --suffix=dc=ipa,dc=example,dc=com meToipa5.example.com
This pretty much shows you the same information as dsconf IPA-EXAMPLE-COM repl-agmt list. It's documented in Displaying the status of a specific replication agreement.
Some low-level details of replication agreements can be viewed with:
dsconf IPA-EXAMPLE-COM repl-agmt get (requires --suffix and agreement name)
dsctl IPA-EXAMPLE-COM get-nsstate (outputs state of all replication agreements)
Those are pretty technical. Probably if you need to interpret this information you're best off asking for help on the mailing lists.
Two other parts of the RHDS documentation are worth pointing out:
But generally the entire Managing Replication chapter is worth a read.
In any case, check the directory server error log to investigate the cause of replication errors.
Adding a dedicated system account for replication status monitoring
This is an account that exists only in the directory. It's not a FreeIPA user, it can't log in to any systems, etc; it can only bind to the directory and query.
I'm using ldapvi instead of ldapadd/ldapmodify; this assumes that /root/.ldaprc has been created as described above.
# ldapvi -Q -A -o account -o simplesecurityobject -b uid=repl-mon,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com
Generate a decent password (e.g., pwqgen random=128) and enter it in the userPassword field.
Optionally uncomment the description field and provide one.
Save and exit, then commit with y.
Test binding as the user:
$ ldapwhoami -x -H ldaps://ipa0.ipa.example.com -D uid=repl-mon,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com -w password dn: uid=repl-mon,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com
Grant the user permission to view replication agreements:
# ldapvi -Q --add -b 'cn=Read Replication Agreements,cn=permissions,cn=pbac,dc=ipa,dc=example,dc=com
add a line member: uid=repl-mon,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com.
Save and exit, then commit with y.
Fetch replication info from a server with the system account:
$ ldapsearch -H ldaps://ipa0.ipa.example.com -x -D uid=repl-mon,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com -w password -s sub -b 'cn=mapping tree,cn=config'
Now use ds-replcheck with the system account:
$ ds-replcheck -v state -Z /etc/ipa/nssdb -m ldapi://ipa0.ipa.example.com -r ldap://ipa1.ipa.example.com -D uid=repl-mon,cn=sysaccounts,cn=etc,dc=ipa,dc=example,dc=com -w password -b dc=ipa,dc=example,dc=com
This fails with Error: Supplier does not have an RUV entry because ds-replcheck tries to retrieve the nsds50ruv attribute from the replicated suffix, rather than the replication agreement under cn=mapping tree,cn=config. I've asked for help...
Adding aci: (targetattr=nsds50ruv)(version 3.0); acl "for ds-replcheck"; allow (read) groupdn = "ldap:///cn=Read Replication Agreements,cn=permissions,cn=pbac,dc=ipa,dc=example,dc=com"; to o=ipaca and dc=ipa,dc=example,dc=com didn't work.
Automating host enrollment with PKINIT
Public Key Cryptography for Initial Authentication (PKINIT) can be used for unattended enrollment of hosts. The advantage over using an admin account (or a dedicated account for enrollment) is that you don't need to manage the credentials of such an account, or enter them on the system being enrolled.
To begin, we need to create a certificate mapping rule.
$ ipa certmaprule-add pkinit-host \ --maprule='(&(krbprincipalname={subject_nt_principal})(fqdn={subject_dns_name}))' \ --matchrule='<ISSUER>^CN=Certificate Authority,O=IPA\.EXAMPLE\.COM$<SAN>^host/' Rule name: pkinit-host Mapping rule: (&(krbprincipalname={subject_nt_principal})(fqdn={subject_dns_name})) Matching rule: <ISSUER>^CN=Certificate Authority,O=IPA\.EXAMPLE\.COM$<SAN>^host/ Enabled: True
When a KDC recieves a certauth request from a client, it will go through all these rules (in priority order, ascending, with no priority sorting last), until it finds one where the matching rule accepts the client's certificate.
In this case, the matching rule has two component rules; both must match in order for the matching rule to accept the certificate.
<ISSUER> matches the client certificate's issuer. It's a regular expression search; note the start/end of string symbols at the beginning and end of the expression, and that . is escaped. Here, we want to match any certificate issued by our IPA domain's own certificate authority.
<SAN> matches the 'subject alternative name' extension, by looking through each name for a Kerberos principal name. It too is a regular expression match. Here we want to match any certificate issued to a host principal.
The syntax of the matching rules is quite obscure; it is documented in krb5.conf(5) and in sss-certmap(5).
homework: add an <EKU>pkinit component rule so that certificates are only matched if they are explicitly intended for use with PKINIT; to issue such a certificate with FreeIPA might require the creation of a custom certificate profile which includes this value in the extended key usage extension...
Once a certificate mapping rule's matching rule accepts the client's certificate, the mapping rule is used as a template, parametized with values taken from the client certificate. The template produces an LDAP filter which will be used to search the directory. If a single entry is found, then it identifies the DN of the identity that will be authenticated.
The available template parameters are listed in sss-certmap(5).Here we're looking for an entry that has both an fqdn matching the DNS-ID from the certificate, and a krbPrincipalName matching the Kerberos principal name from the certificate.
It would be simpler to create a rule that matches the certificate subject alone, but because FreeIPA hosts and services can both request certificates with the same subject and DNS-IDs, such a rule would let these separate principals authenticate as each other via PKINIT requests. This is prevented by a more complex rule that checks the Kerberos principal name as well as the DNS-ID.
Now we create the host object to be enrolled:
$ ipa host-add myhost.ipa.example.com --force ------------------------------------------- Added host "myhost.ipa.example.com" ------------------------------------------- Host name: myhost.ipa.example.com Principal name: host/myhost.ipa.example.com@IPA.EXAMPLE.COM Principal alias: host/myhost.ipa.example.com@IPA.EXAMPLE.COM Password: False Keytab: False Managed by: myhost.ipa.example.com
Next we create a private key and a certificate signing request.
We must consider how we want the Kerberos principal name to be included in the certificate. There are two supported methods. Both involve the addition of an otherName entry to the subject alternative name extension (for details see RFC 52809).
The szOID_NT_PRINCIPAL_NAME OID is followed by a UTF8String encoding the principal name (including realm).
The id-pkinit-san (also known as id-kerberos-san) OID is followed by a KRB5PrincipalName sequence, comprises a realm (a GeneralName string) followed by a 2-element sequence of a name-type (1) and a name-string, which is itself a sequence of GeneralNames, in this case being the primary (host) followed by the instance (the hostname).
Fortunately, the <SAN> component-rule in the matching rule of the certificate mapping rule we created earlier can match OIDs (although I've only tested it with certificates that contain szOID_NT_PRINCIPAL_NAME alone and both szOID_NT_PRINCIPAL_NAME and id-pkinit-san). So the choice comes down to how easy they are to work with:
A szOID_NT_PRINCIPAL_NAME otherName can be added to a certificate signing request with a single option: -addext 'subjectAltName=otherName:msUPN;UTF8:primary/instance@REALM'; and OpenSSL is able to display it back to us. The {subject_nt_principal} mapping rule template parameter is replaced by this value.
id-pkinit-san requires the creation of a config file; and OpenSSL is unable to display it at all! The {subject_pkinit_principal} template parameter is replaced by this value.
Incidentally, there is a {subject_principal} template parameter that takes its value from either value, so we could make our mapping rule more generic by using this parameter instead of the OID-specific ones.
Enough talk, let's create a certificate signing request including the Kerberos principal name:
$ openssl req \ -newkey rsa:3072 \ -noenc -keyout /tmp/myhost.key \ -out myhost.csr \ -subj /CN=myhost.ipa.example.com \ -addext 'subjectAltName=otherName:msUPN;UTF8:host/myhost.ipa.example.com@IPA.EXAMPLE.COM'
OpenSSL maps msUPN and Microsoft User Principal Name to szOID_NT_PRINCIPAL_NAME, so fortunately we can use them in our command, rather than the full OID.
Let's take a moment to examine the CSR's subject alternative name extension. We can use OpenSSL to parse the CSR, identify the offset of the subject alternative name extension data, and then parse the extension data in a second command:
$ openssl asn1parse -in myhost.csr | grep -A1 'Subject Alternative Name' 497:d=7 hl=2 l= 3 prim: OBJECT :X509v3 Subject Alternative Name 502:d=7 hl=2 l= 73 prim: OCTET STRING [HEX DUMP]:ABCDEF0123456789[...]ABCDEF0123456789 $ openssl asn1parse -in myhost.csr -strparse 502 0:d=0 hl=2 l= 71 cons: SEQUENCE 2:d=1 hl=2 l= 69 cons: cont [ 0 ] 4:d=2 hl=2 l= 10 prim: OBJECT :Microsoft User Principal Name 16:d=2 hl=2 l= 55 cons: cont [ 0 ] 18:d=3 hl=2 l= 53 prim: UTF8STRING :host/myhost.ipa.example.com@IPA.EXAMPLE.COM
Now let's issue a certificate:
$ ipa cert-request /tmp/myhost.csr --principal=host/myhost.ipa.example.com --certificate-out=/tmp/myhost.crt Issuing CA: ipa Certificate: [...] Subject: CN=myost.ipa.example.com,O=IPA.EXAMPLE.COM Subject DNS name: myhost.ipa.example.com Subject UPN: host/ipa.example.com@IPA.EXAMPLE.COM Issuer: CN=Certificate Authority,O=IPA.EXAMPLE.COM Not Before: Tue Oct 22 18:18:15 2024 UTC Not After: Mon Feb 24 18:18:15 2025 UTC Serial number: 3053396273 Serial number (hex): 0xB5FF2131
Note that the response includes the 'subject UPN'. Let's double-check that the Kerberos principal name is really present in the issued certificate:
$ openssl x509 -in /tmp/myhost.crt -noout -ext subjectAltName X509v3 Subject Alternative Name: othername: UPN::host/myhost.ipa.example.com@IPA.EXAMPLE.COM, DNS:myhost.ipa.example.com
Now we have a certificate that can be used to authenticate as the host.
Before we try to use the certificate to authenticate, we need to wait 5 minutes. This is because KDCs cache certificate mapping rules for 300 seconds when they first recieve a certauth request, and we don't want a stale ruleset to cause an authentication failure.
While we wait, we can use a handy tool to evaluate our certificate mapping rule against the certificate, and check that it produces the LDAP filter expression that we expect.
$ sssctl cert-eval-rule \ "$(openssl x509 -in /tmp/myhost.crt -outform der | base64 -w 0)" \ -p '(&(krbprincipalname={subject_nt_principal})(fqdn={subject_dns_name}))' \ -t '<ISSUER>^CN=Certificate Authority,O=IPA\.EXAMPLE\.COM$<SAN>^host/' Certificate matches rule. Mapping filter: (&(krbprincipalname=host/myhost.ipa.example.com@IPA.EXAMPLE.COM)(fqdn=myhost.ipa.example.com))
And we can even search the directory with the produced filter to check that it finds the correct entry:
$ ldapsearch -o ldif-wrap=no -LLL -Q \ -b dc=ipa,dc=example,dc=com -s sub \ '(&(krbprincipalname=host/myhost.ipa.example.com@IPA.EXAMPLE.COM)(fqdn=myhost.ipa.example.com))' \ dn dn: fqdn=myhost.ipa.example.com,cn=computers,cn=accounts,dc=ipa,dc=example,dc=com
Ok, enough stalling for time. The KDC certificate mapping rule cache has expired and we're able to authenticate with the certificate:
$ kinit -V \ -c MEMORY: \ -X X509_anchors=FILE:/etc/ipa/ca.crt \ -X X509_user_identity=FILE:/tmp/myhost.crt,/tmp/myhost.key \ host/myhost.ipa.example.com Using specified cache: MEMORY: Using principal: host/myhost.example.com@IPA.EXAMPLE.COM PA Option X509_anchors = FILE:/etc/ipa/ca.crt PA Option X509_user_identity = FILE:/tmp/myost.crt,/tmp/myhost.key Authenticated to Kerberos v5
If kinit prompts for a password, check that the certificate and private key files exist and are readable (if not then it will silently ignore the option and fall back to classic Kerberos authentication). Set KRB5_TRACE=/dev/stderr in the environment for detailed tracing.
In our example, it worked! Now it's time to copy the client certificate, private key and CA certificate to the host and run ipa-client-install!
This will look something like:
# ipa-client-install --unattended --domain=ipa.example.com --hostname=myhost.ipa.example.com --no-ntp --pkinit-identity=FILE:/tmp/myhost.crt,/tmp/myhost.key --pkinit-anchor=FILE:/tmp/ca.crt This program will set up IPA client. Version 4.9.13 Discovery was successful! Client hostname: myhsot.ipa.example.com Realm: IPA.EXAMPLE.COM DNS Domain: ipa.example.com IPA Server: ipa7.ipa.example.com BaseDN: dc=ipa,dc=example,dc=com Skipping chrony configuration Successfully retrieved CA cert Subject: CN=Certificate Authority,O=IPA.EXAMPLE.COM Issuer: CN=Certificate Authority,O=IPA.EXAMPLE.COM Valid From: 2019-03-02 20:18:03 Valid Until: 2039-03-02 20:18:03 Enrolled in IPA realm IPA.EXAMPLE.COM Created /etc/ipa/default.conf Configured /etc/sssd/sssd.conf Systemwide CA database updated. [...] Client configuration complete. The ipa-client-install command was successful
The same certificate and private key can be used again and again, as many times as needed, before the certificate expires.
Aside from the official IdM documentation and the man pages linked above, additional information can be found at:
Automated enrollment of FreeIPA host on the freeipa-users mailing list.
IPA client enrollment with PKINIT design documentation
Matching and Mapping Certificates design documentation
Exposing to the Internet
- Create a CA certificate offline, and use it to sign FreeIPA's own CA certificate
- I'm not sure how to handle revocation of FreeIPA's certificate without creating another set of infrastructure for OCSP and CRL publishing, and even then, are all TLS clients actaully able to make use of them? Certainly not...
Use a firewall and only expose the correct ports (the freeipa-4 service in firewalld is the correct one; freeipa-ldap{{,s},-replication are outdated, do not use them).
- Delegate a zone to your IPA servers properly, so that clients find them via the public DNS.
- Configure each DNS server object to 'forward only' and specify your operator's recursive DNS servers as forwarders
Don't be tempted to set the recursion option to no: BIND provides recursive DNS for the IPA server itself.
The default value of the allow-recursion option is { localhost; localnets; } which is likely OK if you trust other things on localnets.
Configure TLS version min/max for httpd and dirsrv to TLSv1.2/TLSv1.3
RHEL8 uses system-wide crypto-policies settings that might make this un-necessary for httpd, but I don't think dirsrv (which still uses NSS rather than OpenSSL) obeys them yet
Configure sensible TLS ciphers for httpd and nss.
- Originally I also did this for Dogtag's tomcat, but this was before I realised that this is only accessed from the local host so isn't necessary.
Disable access to dirsrv's root user by enabling the rootdn_access plugin and configuring a whitelist of IP addresses able to use it (only ::1/127.0.0.1 is a good start).
repl-monitor.pl won't like this. It can't use TLS or GSSAPI to protect the transport so we definitely don't want it running over the internet. SSH tunnel?
ds-replcheck can use TLS, so the whitelist approach will work.
But let's just use SSH tunnelling which will work for both (as long as the source address on the server for forwarded connections is ::1/127.0.0.1.
Disable the default admin user.
Create a new admin user with a different name and put it in the admins group.
Make sure your HBAC and Sudo rules refer to the admins group rather than the admin user.
Log in as the new admin user, and then disable the original admin user.
It would be neat to dynamically adjust firewall rules to only allow SSH in from hosts within a particular host group. But that relies on Kerberos being up and DNS updates working correctly (i.e., not being intercepted and blocked by your ISP).
- Then again there's always the operator console for troubleshooting.
- Just disable password authentication entirely and rely on GSSAPI or public key authentication... the basic SSH stuff really!
Additional options for dirsrv:
nsslapd-minssf: requires use of a protected transport. Use it.
FreeIPA sets nsslapd-minssf-exclude-rootdse by default, so anonymous users can still retrieve basic information from the root DSE entry (ldapsearch -H ldap://server.example.com -s base -b '').
Also known to break realmd but who uses that to join FreeIPA?
nsslapd-allow-anonymous-access to prevent reading the directory without authentication.
nsslapd-require-secure-binds: require a protected transport for authentication requests
- Can't stop a misconfigured client from blurting out a password unsolicited, via a simple bind on the plaintext port!
- Would be nice to audit when this happens
- We can't just block port 389 because replication requires it (with confidentiality protected by GSSAPI)
- But we could block port 389 from non-IPA servers...
- Can't stop a misconfigured client from blurting out a password unsolicited, via a simple bind on the plaintext port!
Also has the effect of breaking repl-monitor.pl, which is denied from doing a simple bind to localhost - the server isn't able to waive nsslapd-require-secure-binds (and presumably nsslapd-minssf for connections from ::1/127.0.0.0/8.
TODO: file a bug upstream suggesting that connections from localhost should be considered 'secure' for the purposes of nsslapd-require-secure-binds
Certificate storage locations
Trust Store
The list of known CA certificates that the FreeIPA installation trusts is kept in the directory under cn=certificates,cn=ipa,cn=etc,SUFFIX. For a stand-alone CA installation, the store will normally only contain the CA's certificate (IPA.EXAMPLE.COM IPA CA). But for an externally signed CA installation, it will contain the external CA certificate as well. This bug shows examples of the schema.
The ipa-cacert-manage list command will perform an LDAP search, and print an entry for each certificate it finds.
Each entry can have more than one certificate, since the cACertificate and ipaCertIssuerSerial fields are multi-valued. (Note that although LDAP does not preserve the order of multi-valued attributes, the ipaCertIssuerSerial is also present in the caCertificate so the entries can be matched that way)
New certificates can be installed with ipa-cacert-manage install. There's no command to remove them.
The ipa-certupdate can be run on a client to make sure any new or renewed certificates added to the trust store are applied to the local machine. On a server it will additionally ensure that new or renewed certificates added to the trust store are installed into the the KDC, web and directory server certificate databases (or is this done instead by ipa-server-certinstall?)
If you have more than one certificate in the trust store, this bug means that OpenSSL on Debian-based systems won't trust *any* of them. I'm working on a fix for this.
When certmonger starts up, it uses these entries to refresh its cached copies of the CA certificates for the IPA CA (ref: fetch_roots function).
Legacy CAcert container
The entry cn=CAcert,cn=ipa,cn=etc,SUFFIX contains the IPA CA certificate. It's used by:
Older (RHEL6) ipa-certupdate command, which only retrieves this single CA certificate
Older certmonger, which refreshes its copy of the IPA CA certificate when it starts up.
IPA CAs
cn=cas,cn=ca,SUFFIX contains the IPA CA certificates themselves (plural because IPA supports multiple 'lightweight sub-CAs'.
$ ipa ca-find --raw --pkey-only --all ------------ 1 CA matched ------------ dn: cn=ipa,cn=cas,cn=ca,dc=ipa,dc=example,dc=com cn: ipa ---------------------------- Number of entries returned 1 ----------------------------
Host Aliases
Say you have a host, foo.ipa.example.com, with an IP address of 192.0.2.0.
The host is behind a NAT gateway with an address on the Internet of 203.0.113.0.
If you enable dyndns_update, a foo.ipa.example.com will point at 192.0.2.0. Although this leaks information about foo's local network to the public, at least users on foo's network will be able to reach it using that FQDN.
But what if the NAT gateway is provided by Sky Broadband? Sky's routers intercept DNS traffic, preventing their customers from using third-party DNS resolvers, doubtless so that Sky can sell information about the web sites their customers visit to advertisers. As a side effect of this, some more exotic forms of DNS traffic are blocked. This includes the GSS-TSIG messages from nsupdate, which is the mechanism that sssd uses to perform DNS updates. (Debugging this was not fun.)
One alternative is for users on foo's network to make use of mDNS and reach it at foo.local. In order to make sure that they can use Kerberos to log in, we must make use of FreeIPA's Kerberos Principal Alias feature:
$ ipa host-add-principal foo.ipa.example.com host/foo.local ------------------------------------------------------ Added new aliases to host "foo.ipa.example.com" ------------------------------------------------------ Host name: foo.ipa.example.com Principal alias: host/foo.ipa.example.com@IPA.EXAMPLE.COM, host/foo.local@IPA.EXAMPLE.COM
Users can now obtain a ticket for host/foo.local and use it to authenticate to the SSH server running on foo.
However, users will still be prompted to confirm foo's SSH host key; this is because sss_ssh_knownhostsproxy requests the host keys for foo.local, and SSSD's SSH responder isn't able to search the directory for hosts by alias.
Using Kerberos for key agreement (via the gssapi-keyex mechanism) entirely bypasses the use of SSH public keys for host verification & key agreement, making this seamless.
CA renewal server/CRL publisher promotion
The documentation for removing a server from the topology doesn't mention that you need to move the CA renewal server and CRL publisher roles to another server with the CA role.
If you don't do this, it's not fatal, but your CRL file won't be updated.
Both steps are explained in a separate chapter, Decommissioning a server that performs the CA renewal server and CRL publisher role. There is overlap with content from the Managing Replication Topology chapter.
If you want to check the status of your CRL file:
$ curl -sS -L http://ipa-ca.ipa.example.com/ipa/crl/MasterCRL.bin | openssl crl -inform der -noout -lastupdate lastUpdate=Nov 16 15:13:34 2021 GMT
... you can view the whole CRL with -text and other options are available.
The rest of the notes under this heading are obsolete now that I've straightened things up, linked to the docs and filed the above bugs to try to get them cross linked... just skip past them. I'll remove them eventually...
The Starting CRL generation on RHEL 8 chapter of the "Migrating IdM from RHEL 7 to RHEL 8 and keeping it up-to-date" section of the RHEL 8 Installing IdM manual doesn't make much sense. The new replica is running RHEL 8, so why do the prerequisites talk about RHEL 7.6/7.7?
I guess the text was sloppily copied from the previous chapter: https://bugzilla.redhat.com/show_bug.cgi?id=1785595
After following these steps on a CentOS 8.0.1905 machine, the CRL is not available:
$ curl -s -I http://ipa2.ipa.example.com/ipa/crl/MasterCRL.bin | head -n1 HTTP/1.1 404 Not Found
The manual mentions a ipa-crlgen-manage command, which does not exist in CentOS 8, but it does exist in CentOS 7:
centos8$ ipa-crlgen-manage --version -bash: ipa-crlgen-manage: command not found $ rpm -q centos-release ipa-server centos-release-8.0-0.1905.0.9.el8.x86_64 ipa-server-4.7.1-11.module_el8.0.0+79+bbd20d7b.x86_64
$ ipa-crlgen-manage --version 4.6.5 $ rpm -q centos-release ipa-server centos-release-7-7.1908.0.el7.centos.x86_64 ipa-server-4.6.5-11.el7.centos.3.x86_64
Examining the FreeIPA source reveals that this script was added in FreeIPA 4.8:
$ git checkout master $ git log --oneline ./install/tools/ipa-crlgen-manage.in 6d02eddd3 Replace PYTHONSHEBANG with valid shebang 0d23fa927 CRL generation master: new utility to enable|disable $ git describe --contains 0d23fa927 rc_4-8-0-1~126
And backported to FreeIPA 4.7.3 and 4.6.5:
$ git checkout ipa-4-7 $ git log --oneline ./install/tools/ipa-crlgen-manage.in 452fe2fdc Replace PYTHONSHEBANG with valid shebang 52770aa5f CRL generation master: new utility to enable|disable $ git describe --contains 52770aa5f release-4-7-3~202
$ git checkout ipa-4-6 $ git log --oneline ./install/tools/man/ipa-crlgen-manage.1 af5abe0d7 CRL generation master: new utility to enable|disable $ git describe --contains af5abe0d7 release-4-6-5~4
So I guess it will show up in CentOS 8.1. But until then, examining the source code reveals that, after manually enabling CRL generation, there is an additional undocumented step:
1 # make sure a CRL is generated if setup_crl is True 2 if setup_crlgen: 3 logger.info("Forcing CRL update") 4 api.Backend.ra.override_port = 8443 5 result = api.Backend.ra.updateCRL(wait='true') 6 if result.get('crlUpdate', 'Failure') == 'Success': 7 logger.debug("Successfully updated CRL") 8 api.Backend.ra.override_port = None
Unfortunately the updateCRL method was likewise only added in FreeIPA 4.7.3. Fortunately it's not too complex to prevent me from bodging together the following:
1 from pprint import pprint 2 3 from ipalib import api 4 from ipaplatform.paths import paths 5 6 api.bootstrap(in_server=True, confdir=paths.ETC_IPA) 7 api.finalize() 8 9 api.Backend.ldap2.connect() 10 11 pprint(api.Backend.ra._sslget('/ca/agent/ca/updateCRL', 12 8443, 13 crlIssuingPoint='MasterCRL', 14 waitForUpdate=True, 15 xml='true'))
# python3 force.py (200, <http.client.HTTPMessage object at 0x7f8ddd4e9a58>, b'<?xml version="1.0" encoding="UTF-8" standalone="no"?><xml><header><crlIssui' b'ngPoint>MasterCRL</crlIssuingPoint><crlUpdate>Scheduled</crlUpdate></header>' b'<fixed/><records/></xml>')
And behold, we now have a CRL on the new master:
$ curl -s -I http://ipa2.ipa.example.com/ipa/crl/MasterCRL.bin | head -n1 HTTP/1.1 200 OK
Remaining questions
There are still seemingly significant differences in Tomcat's configuration between the old and new masters:
property
ipa0
ipa2
ca.certStatusUpdateInterval
unset
0
ca.listenToCloneModifications
true
false
master.ca.agent.host
unset
ipa0.ipa.example.com
master.ca.agent.port
unset
443
These settings are mentioned in older documentation and on a post to the mailing list:
https://docs.fedoraproject.org/en-US/Fedora/15/html/FreeIPA_Guide/promoting-replica.html
https://www.redhat.com/archives/freeipa-users/2012-May/msg00389.html
So the operation performed by ipa-crlgen-manage is incomplete, it does not manage ca.certStatusUpdateInterval and this setting is absolutely required for functioning CRL generation!
I also wonder whether master.ca.agent.host has to be updated on ipa1...
PKI topology goes out of sync with LDAP server topology
After removing some servers with the CA/KRA roles, the list of servers in Dogtag will get out of sync with the replication topology in the directory.
# pki -d /etc/pki/pki-tomcat/alias/ -n 'subsystemCert cert-pki-ca' -C /etc/pki/pki-tomcat/alias/pwdfile.txt securitydomain-host-find Host ID: CA ipa0.ipa.example.com 443 Hostname: ipa0.ipa.example.com Port: 80 Secure Port: 443 Domain Manager: TRUE Clone: FALSE Host ID: KRA ipa0.ipa.example.com 443 Hostname: ipa0.ipa.example.com Port: 80 Secure Port: 443 Domain Manager: TRUE Clone: TRUE [...]
This will cause ipa-healthcheck to complain about non-contactable CA/KRA servers.
The upstream bug shows how to fix this:
# pki -d /etc/pki/pki-tomcat/alias/ -n 'subsystemCert cert-pki-ca' -C /etc/pki/pki-tomcat/alias/pwdfile.txt securitydomain-host-del 'CA ipa0.ipa.example.com 443' # pki -d /etc/pki/pki-tomcat/alias/ -n 'subsystemCert cert-pki-ca' -C /etc/pki/pki-tomcat/alias/pwdfile.txt securitydomain-host-del 'KRA ipa0.ipa.example.com 443'
... run that for each removed CA/KRA server.
Extending FreeIPA
Extending FreeIPA (also found in PDF form attached to https://www.redhat.com/archives/freeipa-users/2012-February/msg00230.html)
https://www.freeipa.org/images/5/5b/FreeIPA33-extending-freeipa.pdf
Custom subtrees should go in an nsContainer directly under SUFFIX.
DNS: long TXT records
DMARC requires rather long TXT records. Attempting to create one of these in the web UI or on the command line works, but then named-pkcs11 logs:
failed to parse RR entry: resource record DN 'idnsname=0._domainkey,idnsname=example.com.,cn=dns,dc=ipa,dc=example,dc=com': data 'v=DKIM1; t=s; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApIoCiX7nfTcACGVVNxUbxIrhtYrBCDeRFVxGwAbkraMI+6qtVicNu1XB6i26TcVz+azCGl6oZALS32oJJGrcdJlqmHb9egzwUMs9cgksiaLI5D54fT7rWi9JEgsu59PJE9uFPQ2PBnWeDpblQc5q1D0G15/udTKp06QV17qkCemdvLoNMYh+BtPT4cn1AqDvXKHPSt/H5Lrr9ZpNNbw5hhIHAho7zJRUsL4wTImL5WFahX4HUc3XDmTrdw9CJbSU4gZRODfh0G7g28MxybfiltxNWDYPelgxsPFJFifo6vrXyUZQbQwyqvCvpyJbJZ66u2EOtYQ6VCUH6l9CJdkADQIDAQAB': syntax error
The reason for this is not obvious.
So, RFC 1035 describes the data of a TXT RR as simply "One or more <character-string>s". And describes <character-string> as "a single length octet followed by that number of characters. <character-string> is treated as binary information, and can be up to 256 characters in length (including the length octet)."
So our everyday experience of a TXT record is wrong! We naïvely think of it as "just a string", when really it has structure; in this case, one or more arrays of bytes each of which can be up to 255 characters. BIND is choking because it's trying to compile a TXT record consisting of a single <character-string> of well over 300 bytes!
The RFC also defines how to represent a <character-string> in a Zone file, as parsed by BIND:
- Entries are predominantly line-oriented, though parentheses can be used to continue a list of items across a line boundary
and
<character-string> is expressed in one or two ways: as a contiguous set of characters without interior spaces, or as a string beginning with a " and ending with a ". Inside a " delimited string any character can occur, except for a " itself, which must be quoted using \ (back slash).
So it turns out that zone file format is more of a literal transcription of DNS data into a binary format than I had expected. So where you express the following in a zone file:
0._domainkey IN TXT ( "v=DKIM1;" "t=s;" "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq1cxaCim2Tx+HfuB4oGG" "IpQaynGyJV0vi0uQZPBE7/uCVL7/+yJKll+Ec07RHkUg4j3tMltpeuSJd6Hddkj8" "OPDtFYI3EyqexUQe04NsTB9gNAmA1ag4vYb3YCCA2KsbAipIDchkUL31I1XYW8cL" "U/4R2aWYxEJNnPkt/Y0ljy5oKx5HLUs46Miqvz4BvQPKy6MvTEci7yJ5pLWcYGzd" "PjGRZuuBcQGPXB26dVZ7LTEHMrhiJ2uQ7ukoFw//nDU8weypvymSX9AVIWmq9ZXr" "CORBeCspOCupO5jAyGNJjBvs8IoVzis/iICqCgFv7XNgGTEs8nC5L9QXvX/lrSI6" "HwIDAQAB" )
... the compiled RRDATA for the record consists of *nine* length-prefixed byte strings. It's only RFC 7489, sec. 6.1 that determines that they are stitched together into a single string by the entity making use of the records:
- a TXT record can comprise several "character-string" objects. Where this is the case, the module performing DMARC evaluation MUST concatenate these strings by joining together the objects in order and parsing the result as a single string.
Stitching this knowledge together we can now create the record in the web UI or the ipa command, simply by adding spaces to the record data so that no one run of characters lasts for more than 255 bytes. For example:
$ ipa dnsrecord-mod example.com 0._domainkey --txt-rec='"v=DKIM1; t=s; p=" "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApIoCiX7nfTcACGVVNxUb" "xIrhtYrBCDeRFVxGwAbkraMI+6qtVicNu1XB6i26TcVz+azCGl6oZALS32oJJGrc" "dJlqmHb9egzwUMs9cgksiaLI5D54fT7rWi9JEgsu59PJE9uFPQ2PBnWeDpblQc5q" "1D0G15/udTKp06QV17qkCemdvLoNMYh+BtPT4cn1AqDvXKHPSt/H5Lrr9ZpNNbw5" "hhIHAho7zJRUsL4wTImL5WFahX4HUc3XDmTrdw9CJbSU4gZRODfh0G7g28Mxybfi" "ltxNWDYPelgxsPFJFifo6vrXyUZQbQwyqvCvpyJbJZ66u2EOtYQ6VCUH6l9CJdkA" "DQIDAQAB"' Record name: 0._domainkey TXT record: "v=DKIM1; t=s; p=" "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApIoCiX7nfTcACGVVNxUb" "xIrhtYrBCDeRFVxGwAbkraMI+6qtVicNu1XB6i26TcVz+azCGl6oZALS32oJJGrc" "dJlqmHb9egzwUMs9cgksiaLI5D54fT7rWi9JEgsu59PJE9uFPQ2PBnWeDpblQc5q" "1D0G15/udTKp06QV17qkCemdvLoNMYh+BtPT4cn1AqDvXKHPSt/H5Lrr9ZpNNbw5" "hhIHAho7zJRUsL4wTImL5WFahX4HUc3XDmTrdw9CJbSU4gZRODfh0G7g28Mxybfi" "ltxNWDYPelgxsPFJFifo6vrXyUZQbQwyqvCvpyJbJZ66u2EOtYQ6VCUH6l9CJdkA" "DQIDAQAB"
While figuring out the above I ran into RFC 1464, which proposes a way to store structured data within TXT records, by having each <character-string> within the RDATA for a TXT record represent an attribute and value pair, separated by =. e.g.:
host.widgets.com IN TXT "printer=lpr5" sam.widgets.com IN TXT "favorite drink=orange juice"
It does not state, although I presume, that you can have multiple attributes by using multiple <character-string>s within the RDATA:
record.example.com IN TXT "foo=bar" "baz=qux"
... up to the maximum length of the RRDATA for a record which is 65536 bytes. I guess that one of the reasons this never took off is that the length of each value would be limited to 254 - (length of attribute) bytes.