Monday 1st September 2025
Design
CA-Less Mode
The Reason I’m doing CA-Less si because I aim to integrate my domain with an external Certificate Authority. FreeIPAs built-in Dogtag solution is… let’s just say it’s not very flexible. For example, it’s only supports RSA 3072-bit keys and it’s virtually impossible to tune it at all.
But the DNS and Directory services are all good. That’s why I’m aiming to integrate FreeIPA with EJBCA, probably via LDAP Publishing.
My Initial design was to issue certificates from an intermediate CA to the FreeIPA server. However, I discovered some bug with FreeIPA CA-Less mode that made the server only publish the intermediate CA to the clients. Therefore, the FreeIPA server, for the moment, must have certificates issued directly by the Root CA.
Why Podman and not Docker
In my Podman migration post I explain that there is no good way of making FreeIPA work with Docker. Podman has better integration with systemd, which I suspect is the reason why Podman works better with FreeIPA. In addition there is the fact that both Podman and FreeIPA are developed by Redhat.
Configuration
Best Practices
One thing I realized is that this project would have gone much smoother if I read the official documentation on docker hub more granular:
“Note that privileged setup is not supported and will not work — we want the FreeIPA server container to be reasonably isolated from the host and vice versa.”
That means you can’t set the container with sudo privileges (Privileged: Yes)
With podman, normal
podman runis typically enough and works for rootless setups as well.To allow for unprivileged container operation, use the
-h ...option to set the hostname for the FreeIPA server in the container. If it's not possible to set the hostname for the container, specify it withIPA_SERVER_HOSTNAMEenvironment variable, for example withpodman run -e IPA_SERVER_HOSTNAME=.... This might however not work with read-only containers. Do not use theipa-server-install --hostname ...argument.
When running DNS server (the
--setup-dnsargument toipa-server-install) in the FreeIPA container, add--dns=127.0.0.1option to thepodman runordocker runinvocation to allow the FreeIPA server to reach its own DNS server.
Then there are some additional notes on the CA-Less Setup:
To install without a CA, the following options must be provided to ipa-server-install:
–http_pkcs12: a PKCS#12 file containing the HTTP server certificate
–dirsrv_pkcs12: a PKCS#12 file containing the Directory server certificate
–http_pin: password for the file given in –http_pkcs12
–dirsrv_pin: password for the file given in –dirsrv_pkcs12
–root-ca-file: a PEM file containing the root CA certificate
Pre-requisites
Packets Required
Obviously you need Podman and Podman-Compose installed. I would recommend a RHEL-based distribution as well as it comes with Podman pre-installed.
Add a host binding in /etc/hosts on the IPA server
Add the FQDN of the FreeIPA ServerAdd a host binding in /etc/hosts on the IPA server
Add the FQDN of the FreeIPA Server inside /etc/hosts.
# FreeIPA Server
2001:DB8:1234:B001::2 ipa.int.bastuklubben.onlineNote: I’m binding the container to a specific IP on the system.
SELinux tweaking
On SELinux enabled systems like Fedora, it may be necessary to enable running systemd in containers by setting SELinux boolean container_manage_cgroup on the host with:
setsebool -P container_manage_cgroup 1Enable podman to use privileged ports
Unfortunately when using IPv6, port forwarding to some unprivileged ports (1025 - 65535) can’t be done. Something I will explain more detailed in a future post. Therefore, you have to make the system allow rootless podman to publish on privileged ports:
sudo nano /etc/sysctl.d/podman-privileged-ports.conf
# Lowering privileged ports to 52 to allow run rootless Podman containers on lower ports
# default: 1024
net.ipv4.ip_unprivileged_port_start=53Note: Despite the name, the setting applies to IPv6 ports as well.
Then load the new setting:
sudo sysctl -p /etc/sysctl.d/podman-privileged-ports.confFirewall Openings
If you are running firewalld, you need to open these ports:
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv6" destination address="2001:db8:1234:b001::2" port protocol="tcp" port="53" accept'
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv6" destination address="2001:db8:1234:b001::2" port protocol="udp" port="53" accept'
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv6" destination address="2001:db8:1234:b001::2" port protocol="tcp" port="80" accept'
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv6" destination address="2001:db8:1234:b001::2" port protocol="tcp" port="88" accept'
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv6" destination address="2001:db8:1234:b001::2" port protocol="udp" port="88" accept'
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv6" destination address="2001:db8:1234:b001::2" port protocol="udp" port="123" accept'
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv6" destination address="2001:db8:1234:b001::2" port protocol="tcp" port="389" accept'
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv6" destination address="2001:db8:1234:b001::2" port protocol="tcp" port="443" accept'
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv6" destination address="2001:db8:1234:b001::2" port protocol="tcp" port="464" accept'
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv6" destination address="2001:db8:1234:b001::2" port protocol="udp" port="464" accept'
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv6" destination address="2001:db8:1234:b001::2" port protocol="tcp" port="636" accept'Certificates
Make sure that the Root CA .pem file only includes one certificate:
$ cat certs/SAUNA-ROOT-CA.pem
Subject: CN=Bastuklubben Root CA,O=Bastuklubben,C=NO
Issuer: CN=Bastuklubben Root CA,O=Bastuklubben,C=NO
-----BEGIN CERTIFICATE-----
MIIBzTCCAXKgAwIBAgIUQsxzkXVCpO1+O7Hu/h5w4bWpKscwCgYIKoZIzj0EAwQw
QzELMAkGA1UEBhMCTk8xFTATBgNVBAoMDEJhc3R1a2x1YmJlbjEdMBsGA1UEAwwU
QmFzdHVrbHViYmVuIFJvb3QgQ0EwIBcNMjUwODA1MTg0NzEyWhgPMjA1NTA3Mjkx
ODQ3MTFaMEMxCzAJBgNVBAYROOTCAYDVR0TAQH/BAUwAwEB/zAdBgNV
HQ4EFgQUuFHD8vDNBvsiyNL8/dGpTvnJxOMwDgYDVR0PAQH/BAQDAgGGMAoGCCqG
SM49BAMEA0kAMEYCIQCzuDFCiwtcMpWxf4G+//7j06B/oS0FCXHmDSBK48eBwQIh
AJk+Gn/ZzRU6b5HhFqY8wSk7rDrzqvqC3XVUl7O9Lq+Q
-----END CERTIFICATE-----The FreeIPA endpoint certificate is in .p12 format and is issued directly by the Root CA. It must include at least these three extended key usages:
Client Authentication
Server Authentication
Kerberos Client Authentication
The FreeIPA endpoint certificate will be used for both HTTP and Directory Services. In that way I can use the same CN.
Here are screenshots from my EJBCA Endpoint viewer:
Subject Alternative Names is configured to include the container name. that way it can be used by the EJBCA container to establish an LDAP connection on the internal container network later.
Compose Configuration
Project Directory
$ tree -a freeipa/
freeipa/
├── certs
│ ├── ipa.int.bastuklubben.online.p12
│ ├── SAUNA-ROOT-CA.pem
├── compose.yml
├── .envContents of compose.yml
Based on the FreeIPA documentation, I have created this compose file:
services:
freeipa:
container_name: freeipa
image: freeipa/freeipa-server:almalinux-9
restart: unless-stopped
hostname: ipa.int.bastuklubben.online
environment:
IPA_SERVER_HOSTNAME: ipa.int.bastuklubben.online
TZ: "Europe/Oslo"
IPA_SERVER_IP: no-update
read-only: true
volumes:
- /etc/localtime:/etc/localtime:ro
- ./certs:/root
- freeipavol:/data:Z
sysctls:
- net.ipv6.conf.all.disable_ipv6=0
- net.ipv6.conf.lo.disable_ipv6=0
command:
- -U
- --domain=INT.BASTUKLUBBEN.ONLINE
- --realm=int.bastuklubben.online
- --setup-dns
- --no-host-dns
- --forwarder=${DNS_FORWARDER1}
- --forwarder=${DNS_FORWARDER2}
- --http-pin=${IPA_PASSWORD}
- --dirsrv-pin=${IPA_PASSWORD}
- --root-ca-file=${IPA_ROOT_CERT}
- --dirsrv-cert-file=${IPA_DIRSRV_CERT}
- --http-cert-file=${IPA_HTTP_CERT}
- --no-pkinit
- --ds-password=${IPA_PASSWORD}
- --admin-password=${IPA_PASSWORD}
ports:
- "[2001:db8:1234:b001::2]:53:53"
- "[2001:db8:1234:b001::2]:80:80"
- "[2001:db8:1234:b001::2]:88:88"
- "[2001:db8:1234:b001::2]:389:389"
- "[2001:db8:1234:b001::2]:443:443"
- "[2001:db8:1234:b001::2]:464:464"
- "[2001:db8:1234:b001::2]:636:636"
- "[2001:db8:1234:b001::2]:53:53/udp"
- "[2001:db8:1234:b001::2]:88:88/udp"
- "[2001:db8:1234:b001::2]:464:464/udp"
- "[2001:db8:1234:b001::2]:123:123/udp"
dns:
- 127.0.0.1
- ::1
networks:
- internal-services
volumes:
freeipavol:
networks:
internal-services:
external: trueExplanation:
There are many versions of the FreeIPA container. I chose almalinux-9 because it’s one of the recommended stable versions on docker hub.
I have defined both --
hostname flagand the environment variableIPA_SERVER_HOSTNAME,so at least one of them should be functional.“IPA_SERVER_IP: no-update” -I have forgot where I read it but this prevents the A and AAAA records to change everytime you restart the container. Alternatively this can be set to the same address as the host, but it does not work with IPv6 Addresses.Notes about the volumes:
/etc/localtime is necessary for chrony time synchronization
./certs is where i mount the IPA and Root certificates
the :Z after the data volume is only valid for systems using SELinux and makes the content exclusive to this container. Source: https://github.com/compose-spec/compose-spec/issues/191
The Commands are options referring to the default command of “ipa-server-install” that is run when the container starts.
“U” stands for “unattended” and means it’s a non-interactive install process.
no-host-dns skips DNS resolution checks for the server's hostname during installation.
no-pkinit - I don’t know where I read it but I think it’s necessary for CA-Less install to work. At least for me the installation process just halts without explanation if you don’t include it.
The network is externally created because it is shared with the Certificate Authority container.
FreeIPA Compose file with CA
If you want to use the FreeIPA built-in CA functionality, just remove these options from the compose.yml file:
- --http-pin=${IPA_PASSWORD}
- --dirsrv-pin=${IPA_PASSWORD}
- --root-ca-file=${IPA_ROOT_CERT}
- --dirsrv-cert-file=${IPA_DIRSRV_CERT}
- --http-cert-file=${IPA_HTTP_CERT}
- --no-pkinitFor more information about different modes:
Contents of .env
These are the custom environment variables created:
IPA_PASSWORD=foo
IPA_HTTP_CERT=/root/ipa.int.bastuklubben.online.p12
IPA_DIRSRV_CERT=/root/ipa.int.bastuklubben.online.p12
IPA_ROOT_CERT=/root/SAUNA-ROOT-CA.pem
DNS_FORWARDER1=2620:0:ccc::2
DNS_FORWARDER2=2620:0:ccd::2Start the Container and verify the installation
After you start the container with the regular rootless “podman compose up -d”, you can follow along with “podman compose logs -f”
The DNS domain checking will fail but the installation carries on
This is how it should look like when it’s complete:
The last thing to do is to verify that the Root CA has been correctly installed:
You can also verify with ldapsearch that the root CA is the one being published to clients:
First add “TLS_CACERT /etc/ipa/ca.crt” to the bottom of /etc/openldap/ldap.conf
URI ldaps://ipa.int.bastuklubben.online
BASE dc=int,dc=bastuklubben,dc=online
SASL_MECH GSSAPI
TLS_CACERT /etc/ipa/ca.crtSource: https://www.freeipa.org/page/HowTo/LDAP
Then search for this LDAP attribute:
[root@ipa /]# ldapsearch -x cn=CACertIf everything looks as expected, you should be able to reach the webGUI interface.
Appendix
Caveats discovered
Global DNS Forwarders are not visible in the GUI
Even if DNS forwarders are defined and visible during installation, they are not visible in GUI settings afterwards. However, forwarding still somehow works.
Attempted Docker Installation
I tried to install FreeIPA with Docker but it kept failing. The main problem was this:
freeipa-1 | Initializing machine ID from random generator.
freeipa-1 | Failed to create /init.scope control group: Read-only file system
freeipa-1 | Failed to allocate manager object: Read-only file system
freeipa-1 | [!!!!!!] Failed to allocate manager object.
freeipa-1 | Exiting PID 1...The problem has something to do with systemd privileges. There was no good workaround:
The Official FreeIPA documentation suggested to add
{ "userns-remap": "default" }to/etc/docker/daemon.json. While it solved the problem for FreeIPA, it broke my EJBCA container.Another workaround was to add “privileged: yes” to the compose file. It fixed the above problem but caused another problem:
The ipa-client-install command failed, exception: KerberosError: No valid Negotiate header in server responseIt was probably not a good workaround security-wise anyway.
The Solution was not only to use Podman instead of Docker, because it has better integration with systemd, but also use a RHEL-based distribution like Fedora for the underlying operating system (although it might still work with a debian-based system but Podman is just better integrated in RHEL).











