From 4b613513736f3ccdfeacd3bf59820a07e9fa9768 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Tue, 14 Jul 2026 23:54:44 +0200 Subject: [PATCH 1/3] Create and maintain the TLS CA as the confluent service account The CA database under /etc/confluent/tls/ca is typically created by a root context such as osdeploy initialize -t, but the confluent service runs as the owner of /etc/confluent, and openssl ca rewrites the database (index, serial) as the invoking user on every issuance. Certificate issuance through the running service (e.g. the /self/tlscert deployment API) then fails on the root-owned database until packaging happens to repair the ownership. Run the CA creation (full CA and the currently unused simple CA variant) and the openssl ca invocation under normalize_uid, the convention already used when publishing the CA certificate. The issued certificate is staged through a temporary file since the destination may only be writable by the invoking user, e.g. the web server certificate paths. Existing root-owned CA databases are repaired by packaging or manually via: chown -R --reference=/etc/confluent /etc/confluent/tls --- confluent_server/confluent/certutil.py | 187 ++++++++++++++----------- 1 file changed, 109 insertions(+), 78 deletions(-) diff --git a/confluent_server/confluent/certutil.py b/confluent_server/confluent/certutil.py index 0f4f4aa7..a34b7fa5 100644 --- a/confluent_server/confluent/certutil.py +++ b/confluent_server/confluent/certutil.py @@ -207,85 +207,98 @@ def substitute_cfg(setting, key, val, newval, cfgfile, line): return False async def create_full_ca(certout): - mkdirp('/etc/confluent/tls/ca/private') - keyout = '/etc/confluent/tls/ca/private/cakey.pem' - csrout = '/etc/confluent/tls/ca/ca.csr' - mkdirp('/etc/confluent/tls/ca/newcerts') - with open('/etc/confluent/tls/ca/index.txt', 'w') as idx: - pass - with open('/etc/confluent/tls/ca/index.txt.attr', 'w') as idx: - idx.write('unique_subject = no') - with open('/etc/confluent/tls/ca/serial', 'w') as srl: - srl.write('01') - sslcfg = get_openssl_conf_location() - newcfg = '/etc/confluent/tls/ca/openssl.cfg' - settings = { - 'dir': '/etc/confluent/tls/ca', - 'certificate': '$dir/cacert.pem', - 'private_key': '$dir/private/cakey.pem', - 'countryName': 'optional', - 'stateOrProvinceName': 'optional', - 'organizationName': 'optional', - } - subj = '/CN=Confluent TLS Certificate authority ({0})'.format(socket.gethostname()) - if len(subj) > 68: - subj = subj[:68] - with open(sslcfg, 'r') as cfgin: - with open(newcfg, 'w') as cfgfile: - for line in cfgin.readlines(): - cfg = line.split('#')[0] - if '=' in cfg: - key, val = cfg.split('=', 1) - for stg in settings: - if substitute_cfg(stg, key, val, settings[stg], cfgfile, line): - break - else: - cfgfile.write(line.strip() + '\n') - continue - cfgfile.write(line.strip() + '\n') - cfgfile.write('\n[CACert]\nbasicConstraints = critical,CA:true\nkeyUsage = critical,keyCertSign,cRLSign\n[ca_confluent]\n') - await util.check_call( - 'openssl', 'ecparam', '-name', 'secp384r1', '-genkey', '-out', - keyout) - await util.check_call( - 'openssl', 'req', '-new', '-key', keyout, '-out', csrout, '-subj', subj) - await util.check_call( - 'openssl', 'ca', '-config', newcfg, '-batch', '-selfsign', - '-extensions', 'CACert', '-extfile', newcfg, - '-notext', '-md', 'sha384', '-startdate', - '19700101010101Z', '-enddate', '21000101010101Z', '-keyfile', - keyout, '-out', '/etc/confluent/tls/ca/cacert.pem', '-in', csrout - ) - shutil.copy2('/etc/confluent/tls/ca/cacert.pem', certout) + # The CA is used by the confluent service, which runs as the owner of + # /etc/confluent rather than root; create the CA material as that user + # so the service can use the database for issuing certificates + ouid = normalize_uid() + try: + mkdirp('/etc/confluent/tls/ca/private') + keyout = '/etc/confluent/tls/ca/private/cakey.pem' + csrout = '/etc/confluent/tls/ca/ca.csr' + mkdirp('/etc/confluent/tls/ca/newcerts') + with open('/etc/confluent/tls/ca/index.txt', 'w') as idx: + pass + with open('/etc/confluent/tls/ca/index.txt.attr', 'w') as idx: + idx.write('unique_subject = no') + with open('/etc/confluent/tls/ca/serial', 'w') as srl: + srl.write('01') + sslcfg = get_openssl_conf_location() + newcfg = '/etc/confluent/tls/ca/openssl.cfg' + settings = { + 'dir': '/etc/confluent/tls/ca', + 'certificate': '$dir/cacert.pem', + 'private_key': '$dir/private/cakey.pem', + 'countryName': 'optional', + 'stateOrProvinceName': 'optional', + 'organizationName': 'optional', + } + subj = '/CN=Confluent TLS Certificate authority ({0})'.format(socket.gethostname()) + if len(subj) > 68: + subj = subj[:68] + with open(sslcfg, 'r') as cfgin: + with open(newcfg, 'w') as cfgfile: + for line in cfgin.readlines(): + cfg = line.split('#')[0] + if '=' in cfg: + key, val = cfg.split('=', 1) + for stg in settings: + if substitute_cfg(stg, key, val, settings[stg], cfgfile, line): + break + else: + cfgfile.write(line.strip() + '\n') + continue + cfgfile.write(line.strip() + '\n') + cfgfile.write('\n[CACert]\nbasicConstraints = critical,CA:true\nkeyUsage = critical,keyCertSign,cRLSign\n[ca_confluent]\n') + await util.check_call( + 'openssl', 'ecparam', '-name', 'secp384r1', '-genkey', '-out', + keyout) + await util.check_call( + 'openssl', 'req', '-new', '-key', keyout, '-out', csrout, '-subj', subj) + await util.check_call( + 'openssl', 'ca', '-config', newcfg, '-batch', '-selfsign', + '-extensions', 'CACert', '-extfile', newcfg, + '-notext', '-md', 'sha384', '-startdate', + '19700101010101Z', '-enddate', '21000101010101Z', '-keyfile', + keyout, '-out', '/etc/confluent/tls/ca/cacert.pem', '-in', csrout + ) + shutil.copy2('/etc/confluent/tls/ca/cacert.pem', certout) + finally: + os.seteuid(ouid) #openssl ca -config openssl.cnf -selfsign -keyfile cakey.pem -startdate 20150214120000Z -enddate 20160214120000Z #20160107071311Z -enddate 20170106071311Z async def create_simple_ca(keyout, certout): + # As with create_full_ca, the CA material must be owned by the owner + # of /etc/confluent for use by the confluent service + ouid = normalize_uid() try: - os.makedirs('/etc/confluent/tls') - except OSError as e: - if e.errno != 17: - raise - sslcfg = get_openssl_conf_location() - tmphdl, tmpconfig = tempfile.mkstemp() - os.close(tmphdl) - shutil.copy2(sslcfg, tmpconfig) - await util.check_call( - 'openssl', 'ecparam', '-name', 'secp384r1', '-genkey', '-out', - keyout) - try: - subj = '/CN=Confluent TLS Certificate authority ({0})'.format(socket.gethostname()) - if len(subj) > 68: - subj = subj[:68] - with open(tmpconfig, 'a') as cfgfile: - cfgfile.write('\n[CACert]\nbasicConstraints = critical,CA:true\n') + try: + os.makedirs('/etc/confluent/tls') + except OSError as e: + if e.errno != 17: + raise + sslcfg = get_openssl_conf_location() + tmphdl, tmpconfig = tempfile.mkstemp() + os.close(tmphdl) + shutil.copy2(sslcfg, tmpconfig) await util.check_call( - 'openssl', 'req', '-new', '-x509', '-key', keyout, '-days', - '27300', '-out', certout, '-subj', subj, - '-extensions', 'CACert', '-config', tmpconfig - ) + 'openssl', 'ecparam', '-name', 'secp384r1', '-genkey', '-out', + keyout) + try: + subj = '/CN=Confluent TLS Certificate authority ({0})'.format(socket.gethostname()) + if len(subj) > 68: + subj = subj[:68] + with open(tmpconfig, 'a') as cfgfile: + cfgfile.write('\n[CACert]\nbasicConstraints = critical,CA:true\n') + await util.check_call( + 'openssl', 'req', '-new', '-x509', '-key', keyout, '-days', + '27300', '-out', certout, '-subj', subj, + '-extensions', 'CACert', '-config', tmpconfig + ) + finally: + os.remove(tmpconfig) finally: - os.remove(tmpconfig) + os.seteuid(ouid) async def create_certificate(keyout=None, certout=None, csrfile=None, subj=None, san=None, backdate=True, days=None): now_utc = datetime.datetime.now(datetime.timezone.utc) @@ -413,12 +426,30 @@ async def create_certificate(keyout=None, certout=None, csrfile=None, subj=None, shutil.copy2(cacfgfile, tmpcafile) os.close(tmphdl) cacfgfile = tmpcafile - await util.check_call( - 'openssl', 'ca', '-config', cacfgfile, '-rand_serial', - '-in', csrfile, '-out', certout, '-batch', '-notext', - '-startdate', startdate, '-enddate', enddate, '-md', 'sha384', - '-extfile', extconfig, '-subj', subj - ) + os.chmod(cacfgfile, 0o644) + os.chmod(csrfile, 0o644) + # openssl ca rewrites the CA database (index, serial) as the + # invoking user; run it as the owner of /etc/confluent so the + # database remains usable by the confluent service. The chmodded + # temporary inputs hold no secrets, and the certificate is + # written to a temporary path first, as certout may only be + # writable by the original user (e.g. a web server certificate + # path during osdeploy initialize -t) + os.chmod(extconfig, 0o644) + ouid = normalize_uid() + try: + tmphdl, tmpcertout = tempfile.mkstemp() + os.close(tmphdl) + await util.check_call( + 'openssl', 'ca', '-config', cacfgfile, '-rand_serial', + '-in', csrfile, '-out', tmpcertout, '-batch', '-notext', + '-startdate', startdate, '-enddate', enddate, '-md', 'sha384', + '-extfile', extconfig, '-subj', subj + ) + finally: + os.seteuid(ouid) + shutil.copy(tmpcertout, certout) + os.remove(tmpcertout) for keycopy in tlsmateriallocation.get('keys', []): if keycopy != keyout: shutil.copy2(keyout, keycopy) From 86d90281e0aa8bacab60c455f1cd4c94f050b74f Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Wed, 15 Jul 2026 04:27:28 +0200 Subject: [PATCH 2/3] Speed up find Spawn just one find process and stop on first hit --- confluent_server/confluent_server.spec.tmpl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/confluent_server/confluent_server.spec.tmpl b/confluent_server/confluent_server.spec.tmpl index 7c64b467..36ec2f21 100644 --- a/confluent_server/confluent_server.spec.tmpl +++ b/confluent_server/confluent_server.spec.tmpl @@ -68,10 +68,7 @@ chown confluent:confluent /etc/confluent /var/lib/confluent /var/log/confluent / sysctl -p /usr/lib/sysctl.d/confluent.conf >& /dev/null NEEDCHOWN=0 NEEDSTART=0 -find /etc/confluent -uid 0 | grep -E '.*' > /dev/null && NEEDCHOWN=1 -find /var/log/confluent -uid 0 | grep -E '.*' > /dev/null && NEEDCHOWN=1 -find /var/run/confluent -uid 0 | grep -E '.*' > /dev/null && NEEDCHOWN=1 -find /var/cache/confluent -uid 0 | grep -E '.*' > /dev/null && NEEDCHOWN=1 +[ -n "$(find /etc/confluent /var/log/confluent /var/cache/confluent -uid 0 -print -quit 2>/dev/null)" ] && NEEDCHOWN=1 if [ $NEEDCHOWN = 1 ]; then if systemctl is-active confluent > /dev/null; then NEEDSTART=1 From 63a0cd237fb173474325f43e6b33ec295992a6d8 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Wed, 15 Jul 2026 04:30:11 +0200 Subject: [PATCH 3/3] Add missing postinst steps to Ubuntu The following post install steps were missing on Ubuntu builds: - Permission fixes - sysctl load - Service restart - confluent PAM symlink to /etc/pam.d/sshd. It works without it on Ubuntu because it falls back to other which allows login on Ubuntu, but the behaviour should be the same on every OS. Furtheremore, an admin might implement additional steps to sshd PAM and would like to have this in Confluent, too --- confluent_server/builddeb | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/confluent_server/builddeb b/confluent_server/builddeb index 276b4025..0364d9f0 100755 --- a/confluent_server/builddeb +++ b/confluent_server/builddeb @@ -50,12 +50,30 @@ if ! grep wheezy /etc/os-release; then fi head -n -1 debian/control > debian/control1 mv debian/control1 debian/control -cat > debian/postinst << EOF -if ! getent passwd confluent > /dev/null; then +cat > debian/postinst << \EOF +if ! getent passwd confluent > /dev/null; then useradd -r confluent -d /var/lib/confluent -s /usr/sbin/nologin - mkdir -p /etc/confluent - chown confluent /etc/confluent fi +mkdir -p /etc/confluent /var/lib/confluent /var/log/confluent /var/cache/confluent +chown confluent:confluent /etc/confluent /var/lib/confluent /var/log/confluent /var/cache/confluent + +sysctl -p /usr/lib/sysctl.d/confluent.conf > /dev/null 2>&1 +NEEDCHOWN=0 +NEEDSTART=0 +[ -n "$(find /etc/confluent /var/log/confluent /var/cache/confluent -uid 0 -print -quit 2>/dev/null)" ] && NEEDCHOWN=1 +if [ $NEEDCHOWN = 1 ]; then + if systemctl is-active confluent > /dev/null; then + NEEDSTART=1 + systemctl stop confluent + fi + chown -R confluent:confluent /etc/confluent /var/log/confluent /var/cache/confluent +fi +systemctl daemon-reload +if systemctl is-active confluent > /dev/null || [ $NEEDSTART = 1 ]; then systemctl restart confluent > /dev/null 2>&1; fi +if [ ! -e /etc/pam.d/confluent ]; then + ln -s /etc/pam.d/sshd /etc/pam.d/confluent +fi +true EOF echo 'export PYBUILD_INSTALL_ARGS=--install-lib=/opt/confluent/lib/python' >> debian/rules #echo 'Provides: python-'$DPKGNAME >> debian/control