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)