1
0
mirror of https://github.com/laurivosandi/certidude synced 2024-12-22 16:25:17 +00:00

Allow provisioning as subordinate CA and add offline install docs

This commit is contained in:
Lauri Võsandi 2018-05-07 11:18:29 +00:00
parent c01cd279c3
commit f4627b3bd6
7 changed files with 175 additions and 134 deletions

View File

@ -60,6 +60,7 @@ Features
Common: Common:
* Standard request, sign, revoke workflow via web interface. * Standard request, sign, revoke workflow via web interface.
* RSA and Elliptic Curve Cryptography both supported, use ``certidude setup authority --elliptic-curve`` for the second
* `OCSP <https://tools.ietf.org/html/rfc4557>`_ and `SCEP <https://tools.ietf.org/html/draft-nourse-scep-23>`_ support. * `OCSP <https://tools.ietf.org/html/rfc4557>`_ and `SCEP <https://tools.ietf.org/html/draft-nourse-scep-23>`_ support.
* PAM and Active Directory compliant authentication backends: Kerberos single sign-on, LDAP simple bind. * PAM and Active Directory compliant authentication backends: Kerberos single sign-on, LDAP simple bind.
* POSIX groups and Active Directory (LDAP) group membership based authorization. * POSIX groups and Active Directory (LDAP) group membership based authorization.
@ -93,12 +94,8 @@ System dependencies for Ubuntu 16.04:
.. code:: bash .. code:: bash
apt install -y \ apt install -y python3-click python3-jinja2 python3-markdown \
python3-click \ python3-pip python3-mysql.connector python3-requests python3-pyxattr
python3-jinja2 python3-markdown \
python3-pip \
python3-mysql.connector python3-requests \
python3-pyxattr
System dependencies for Fedora 25+: System dependencies for Fedora 25+:
@ -122,7 +119,6 @@ You can check it with:
hostname -f hostname -f
The command should return ``ca.example.com``. The command should return ``ca.example.com``.
If necessary tweak machine's fully qualified hostname in ``/etc/hosts``: If necessary tweak machine's fully qualified hostname in ``/etc/hosts``:
.. code:: .. code::
@ -130,8 +126,15 @@ If necessary tweak machine's fully qualified hostname in ``/etc/hosts``:
127.0.0.1 localhost 127.0.0.1 localhost
127.0.1.1 ca.example.com ca 127.0.1.1 ca.example.com ca
Certidude will submit e-mail notifications to locally running MTA.
Install Postfix and configure it as Satellite system:
.. code:: bash
apt install postfix
Certidude can set up certificate authority relatively easily. Certidude can set up certificate authority relatively easily.
Following will set up certificate authority in ``/var/lib/certidude/hostname.domain.tld``, Following will set up certificate authority in ``/var/lib/certidude/``,
configure systemd service for your platform, configure systemd service for your platform,
nginx in ``/etc/nginx/sites-available/certidude.conf``, nginx in ``/etc/nginx/sites-available/certidude.conf``,
cronjobs in ``/etc/cron.hourly/certidude`` and much more: cronjobs in ``/etc/cron.hourly/certidude`` and much more:
@ -140,20 +143,13 @@ cronjobs in ``/etc/cron.hourly/certidude`` and much more:
certidude setup authority certidude setup authority
Tweak the configuration in ``/etc/certidude/server.conf`` until you meet your requirements Tweak the configuration in ``/etc/certidude/server.conf`` until you meet your requirements,
and start the services: to apply changes run:
.. code:: bash .. code:: bash
systemctl restart certidude systemctl restart certidude
Certidude will submit e-mail notifications to locally running MTA.
Install Postfix and configure it as Satellite system:
.. code:: bash
apt install postfix
Setting up PAM authentication Setting up PAM authentication
----------------------------- -----------------------------
@ -247,7 +243,6 @@ Setting up services
------------------- -------------------
Set up services as usual (OpenVPN, Strongswan, etc), when setting up certificates Set up services as usual (OpenVPN, Strongswan, etc), when setting up certificates
generate signing request with TLS server flag set.
See Certidude admin interface how to submit CSR-s and retrieve signed certificates. See Certidude admin interface how to submit CSR-s and retrieve signed certificates.
@ -262,26 +257,31 @@ Configure Certidude client in ``/etc/certidude/client.conf``:
.. code:: ini .. code:: ini
[ca.example.com] [ca.example.com]
insecure = true
trigger = interface up trigger = interface up
hostname = $HOSTNAME
Configure services in ``/etc/certidude/services.conf``: Configure services in ``/etc/certidude/services.conf``:
.. code:: bash .. code:: bash
[gateway.example.com] [OpenVPN to gateway.example.com]
authority = ca.example.com authority = ca.example.com
service = network-manager/openvpn service = network-manager/openvpn
remote = gateway.example.com remote = gateway.example.com
[IPSec to gateway.example.com]
authority = ca.example.com
service = network-manager/strongswan
remote = gateway.example.com
To request certificate: To request certificate:
.. code:: bash .. code:: bash
certidude request certidude enroll
The keys, signing requests, certificates and CRL-s are placed under The keys, signing requests, certificates and CRL-s are placed under
/var/lib/certidude/ca.example.com/ /etc/certidude/authority/ca.example.com/
The VPN connection should immideately become available under network connections. The VPN connection should immideately become available under network connections.
@ -293,10 +293,8 @@ To use dependencies from pip:
.. code:: bash .. code:: bash
apt install \ apt install build-essential python-dev cython libffi-dev libssl-dev \
build-essential python-dev cython libffi-dev libssl-dev libkrb5-dev \ libkrb5-dev ldap-utils krb5-user libsasl2-modules-gssapi-mit \
ldap-utils krb5-user \
libsasl2-modules-gssapi-mit \
libsasl2-dev libldap2-dev libsasl2-dev libldap2-dev
Clone the repository: Clone the repository:
@ -324,7 +322,7 @@ To run tests and measure code coverage grab a clean VM or container:
pip3 install codecov pytest-cov pip3 install codecov pytest-cov
rm .coverage* rm .coverage*
TRAVIS=1 coverage run --parallel-mode --source certidude -m py.test tests COVERAGE_FILE=/tmp/.coverage TRAVIS=1 coverage run --parallel-mode --source certidude -m py.test tests --capture=sys
coverage combine coverage combine
coverage report coverage report
@ -335,23 +333,34 @@ To uninstall:
pip3 uninstall certidude pip3 uninstall certidude
Certificate attributes Offline install
---------------------- ---------------
Certificates have a lot of fields that can be filled in. To set up certificate authority in an isolated environment use a
In any case country, state, locality, organization, organizational unit are not filled in vanilla Ubuntu 16.04 or container to collect the artifacts:
as this information will already exist in AD and duplicating it in the certificate management
doesn't make sense. Additionally the information will get out of sync if
attributes are changed in AD but certificates won't be updated.
If machine is enrolled, eg by running ``certidude request`` as root on Ubuntu/Fedora/Mac OS X: .. code:: bash
* If Kerberos credentials are presented machine can be automatically enrolled depending on the ``machine enrollment`` setting add-apt-repository -y ppa:nginx/stable
* Common name is set to short ``hostname`` apt-get update -q
* It is tricky to determine user who is triggering the action so given name, surname and e-mail attributes are not filled in rm -fv /var/cache/apt/archives/*.deb /var/cache/certidude/wheels/*.whl
apt install --download-only python3-markdown python3-pyxattr python3-jinja2 python3-cffi software-properties-common libnginx-mod-nchan nginx-full
pip3 wheel --wheel-dir=/var/cache/certidude/wheels -r requirements.txt
pip3 wheel --wheel-dir=/var/cache/certidude/wheels falcon humanize ipaddress simplepam user-agents python-ldap gssapi
pip3 wheel --wheel-dir=/var/cache/certidude/wheels .
tar -cf certidude-assets.tar /var/lib/certidude/assets/ /var/cache/apt/archives/ /var/cache/certidude/wheels
If user enrolls, eg by clicking generate bundle button in the web interface: Transfer certidude-artifacts.tar to the target machine and execute:
* Common name is either set to ``username`` or ``username@device-identifier`` depending on the ``user enrollment`` setting .. code:: bash
* Given name and surname are not filled in because Unicode characters cause issues in OpenVPN Connect app
* E-mail is not filled in because it might change in AD rm -fv /var/cache/apt/archives/*.deb /var/cache/certidude/wheels/*.whl
tar -xvf certidude-artifacts.tar -C /
dpkg -i /var/cache/apt/archives/*.deb
pip3 install --use-wheel --no-index --find-links /var/cache/certidude/wheels/*.whl
Proceed to bootstrap authority without installing packages or assembling assets:
certidude setup authority --skip-packages --skip-assets [--elliptic-curve] [--organization "Mycorp LLC"]
Note it's highly recommended to enable nginx PPA in the target machine

View File

@ -53,17 +53,15 @@ with open(config.AUTHORITY_PRIVATE_KEY_PATH, "rb") as fh:
def self_enroll(skip_notify=False): def self_enroll(skip_notify=False):
assert os.getuid() == 0 and os.getgid() == 0, "Can self-enroll only as root" assert os.getuid() == 0 and os.getgid() == 0, "Can self-enroll only as root"
from certidude import const from certidude import const, config
common_name = const.FQDN common_name = const.FQDN
directory = os.path.join("/var/lib/certidude", const.FQDN)
self_key_path = os.path.join(directory, "self_key.pem")
try: try:
path, buf, cert, signed, expires = get_signed(common_name) path, buf, cert, signed, expires = get_signed(common_name)
self_public_key = asymmetric.load_public_key(path) self_public_key = asymmetric.load_public_key(path)
private_key = asymmetric.load_private_key(self_key_path) private_key = asymmetric.load_private_key(config.SELF_KEY_PATH)
except FileNotFoundError: # certificate or private key not found except FileNotFoundError: # certificate or private key not found
with open(self_key_path, 'wb') as fh: with open(config.SELF_KEY_PATH, 'wb') as fh:
if public_key.algorithm == "ec": if public_key.algorithm == "ec":
self_public_key, private_key = asymmetric.generate_pair("ec", curve=public_key.curve) self_public_key, private_key = asymmetric.generate_pair("ec", curve=public_key.curve)
elif public_key.algorithm == "rsa": elif public_key.algorithm == "rsa":
@ -81,11 +79,11 @@ def self_enroll(skip_notify=False):
request = builder.build(private_key) request = builder.build(private_key)
pid = os.fork() pid = os.fork()
if not pid: if not pid:
from certidude import authority from certidude import authority, config
from certidude.common import drop_privileges from certidude.common import drop_privileges
drop_privileges() drop_privileges()
assert os.getuid() != 0 and os.getgid() != 0 assert os.getuid() != 0 and os.getgid() != 0
path = os.path.join(directory, "requests", common_name + ".pem") path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
click.echo("Writing request to %s" % path) click.echo("Writing request to %s" % path)
with open(path, "wb") as fh: with open(path, "wb") as fh:
fh.write(pem_armor_csr(request)) # Write CSR with certidude permissions fh.write(pem_armor_csr(request)) # Write CSR with certidude permissions
@ -93,10 +91,7 @@ def self_enroll(skip_notify=False):
sys.exit(0) sys.exit(0)
else: else:
os.waitpid(pid, 0) os.waitpid(pid, 0)
if os.path.exists("/etc/systemd"): os.system("systemctl reload nginx")
os.system("systemctl reload nginx")
else:
os.system("service nginx reload")
def get_request(common_name): def get_request(common_name):

View File

@ -1001,12 +1001,14 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat
@click.option("--organization", "-o", default=None, help="Company or organization name") @click.option("--organization", "-o", default=None, help="Company or organization name")
@click.option("--organizational-unit", "-ou", default="Certificate Authority") @click.option("--organizational-unit", "-ou", default="Certificate Authority")
@click.option("--push-server", help="Push server, by default http://%s" % const.FQDN) @click.option("--push-server", help="Push server, by default http://%s" % const.FQDN)
@click.option("--directory", help="Directory for authority files") @click.option("--directory", default="/var/lib/certidude", help="Directory for authority files")
@click.option("--outbox", default="smtp://smtp.%s" % const.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % const.DOMAIN) @click.option("--outbox", default="smtp://smtp.%s" % const.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % const.DOMAIN)
@click.option("--skip-assets", is_flag=True, help="Don't attempt to assemble JS/CSS/font assets")
@click.option("--skip-packages", is_flag=True, help="Don't attempt to install apt/pip/npm packages") @click.option("--skip-packages", is_flag=True, help="Don't attempt to install apt/pip/npm packages")
@click.option("--elliptic-curve", "-e", is_flag=True, help="Generate EC instead of RSA keypair") @click.option("--elliptic-curve", "-e", is_flag=True, help="Generate EC instead of RSA keypair")
@click.option("--subordinate", is_flag=True, help="Set up subordinate CA instead of root CA")
@fqdn_required @fqdn_required
def certidude_setup_authority(username, kerberos_keytab, nginx_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, title, skip_packages, elliptic_curve): def certidude_setup_authority(username, kerberos_keytab, nginx_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, title, skip_assets, skip_packages, elliptic_curve, subordinate):
assert subprocess.check_output(["/usr/bin/lsb_release", "-cs"]) in (b"trusty\n", b"xenial\n", b"bionic\n"), "Only Ubuntu 16.04 supported at the moment" assert subprocess.check_output(["/usr/bin/lsb_release", "-cs"]) in (b"trusty\n", b"xenial\n", b"bionic\n"), "Only Ubuntu 16.04 supported at the moment"
assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root" assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root"
@ -1052,8 +1054,6 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates", "profile") template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates", "profile")
click.echo("Using templates from %s" % template_path) click.echo("Using templates from %s" % template_path)
if not directory:
directory = os.path.join("/var/lib/certidude", common_name)
click.echo("Placing authority files in %s" % directory) click.echo("Placing authority files in %s" % directory)
certificate_url = "http://%s/api/certificate/" % common_name certificate_url = "http://%s/api/certificate/" % common_name
@ -1065,8 +1065,11 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
# Expand variables # Expand variables
assets_dir = os.path.join(directory, "assets") assets_dir = os.path.join(directory, "assets")
ca_key = os.path.join(directory, "ca_key.pem") ca_key = os.path.join(directory, "ca_key.pem")
ca_req = os.path.join(directory, "ca_req.pem")
ca_cert = os.path.join(directory, "ca_cert.pem") ca_cert = os.path.join(directory, "ca_cert.pem")
self_key = os.path.join(directory, "self_key.pem")
sqlite_path = os.path.join(directory, "meta", "db.sqlite") sqlite_path = os.path.join(directory, "meta", "db.sqlite")
distinguished_name = cn_to_dn("Certidude at %s" % common_name, common_name, o=organization, ou=organizational_unit)
# Builder variables # Builder variables
dhgroup = "ecp384" if elliptic_curve else "modp2048" dhgroup = "ecp384" if elliptic_curve else "modp2048"
@ -1164,35 +1167,38 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
click.echo("Installing JavaScript packages: %s" % cmd) click.echo("Installing JavaScript packages: %s" % cmd)
if os.system(cmd): sys.exit(230) if os.system(cmd): sys.exit(230)
# Copy fonts if skip_assets:
click.echo("Copying fonts...") click.echo("Not attempting to assemble assets as requested...")
if os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir): sys.exit(229) else:
# Copy fonts
click.echo("Copying fonts...")
if os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir): sys.exit(229)
# Compile nunjucks templates # Compile nunjucks templates
cmd = 'nunjucks-precompile --include ".html$" --include ".ps1$" --include ".sh$" --include ".svg" %s > %s.part' % (static_path, bundle_js) cmd = 'nunjucks-precompile --include ".html$" --include ".ps1$" --include ".sh$" --include ".svg" %s > %s.part' % (static_path, bundle_js)
click.echo("Compiling templates: %s" % cmd) click.echo("Compiling templates: %s" % cmd)
if os.system(cmd): sys.exit(228) if os.system(cmd): sys.exit(228)
# Assemble bundle.js # Assemble bundle.js
click.echo("Assembling %s" % bundle_js) click.echo("Assembling %s" % bundle_js)
with open(bundle_js + ".part", "a") as fh: with open(bundle_js + ".part", "a") as fh:
for pkg in "qrcode-svg/dist/qrcode.min.js", "jquery/dist/jquery.min.js", "timeago/*.js", "nunjucks/browser/nunjucks-slim.min.js", "tether/dist/js/*.min.js", "bootstrap/dist/js/*.min.js": for pkg in "qrcode-svg/dist/qrcode.min.js", "jquery/dist/jquery.min.js", "timeago/*.js", "nunjucks/browser/nunjucks-slim.min.js", "tether/dist/js/*.min.js", "bootstrap/dist/js/*.min.js":
for j in glob(os.path.join("/usr/local/lib/node_modules", pkg)): for j in glob(os.path.join("/usr/local/lib/node_modules", pkg)):
click.echo("- Merging: %s" % j) click.echo("- Merging: %s" % j)
with open(j) as ih: with open(j) as ih:
fh.write(ih.read()) fh.write(ih.read())
# Assemble bundle.css # Assemble bundle.css
click.echo("Assembling %s" % bundle_css) click.echo("Assembling %s" % bundle_css)
with open(bundle_css + ".part", "w") as fh: with open(bundle_css + ".part", "w") as fh:
for pkg in "tether/dist/css/*.min.css", "bootstrap/dist/css/*.min.*css", "font-awesome/css/font-awesome.min.css": for pkg in "tether/dist/css/*.min.css", "bootstrap/dist/css/*.min.*css", "font-awesome/css/font-awesome.min.css":
for j in glob(os.path.join("/usr/local/lib/node_modules", pkg)): for j in glob(os.path.join("/usr/local/lib/node_modules", pkg)):
click.echo("- Merging: %s" % j) click.echo("- Merging: %s" % j)
with open(j) as ih: with open(j) as ih:
fh.write(ih.read()) fh.write(ih.read())
os.rename(bundle_css + ".part", bundle_css) os.rename(bundle_css + ".part", bundle_css)
os.rename(bundle_js + ".part", bundle_js) os.rename(bundle_js + ".part", bundle_js)
assert os.getuid() == 0 and os.getgid() == 0 assert os.getuid() == 0 and os.getgid() == 0
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude")
@ -1203,7 +1209,8 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
click.echo("Creating %s" % const.CONFIG_DIR) click.echo("Creating %s" % const.CONFIG_DIR)
os.makedirs(const.CONFIG_DIR) os.makedirs(const.CONFIG_DIR)
os.umask(0o137) # 640 os.umask(0o177) # 600
if os.path.exists(const.SERVER_CONFIG_PATH): if os.path.exists(const.SERVER_CONFIG_PATH):
click.echo("Configuration file %s already exists, remove to regenerate" % const.SERVER_CONFIG_PATH) click.echo("Configuration file %s already exists, remove to regenerate" % const.SERVER_CONFIG_PATH)
else: else:
@ -1250,7 +1257,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
pass pass
# Generate and sign CA key # Generate and sign CA key
if not os.path.exists(ca_key): if not os.path.exists(ca_key) or subordinate and not os.path.exists(ca_req):
if elliptic_curve: if elliptic_curve:
click.echo("Generating %s EC key for CA ..." % const.CURVE_NAME) click.echo("Generating %s EC key for CA ..." % const.CURVE_NAME)
public_key, private_key = asymmetric.generate_pair("ec", curve=const.CURVE_NAME) public_key, private_key = asymmetric.generate_pair("ec", curve=const.CURVE_NAME)
@ -1258,12 +1265,38 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
click.echo("Generating %d-bit RSA key for CA ..." % const.KEY_SIZE) click.echo("Generating %d-bit RSA key for CA ..." % const.KEY_SIZE)
public_key, private_key = asymmetric.generate_pair("rsa", bit_size=const.KEY_SIZE) public_key, private_key = asymmetric.generate_pair("rsa", bit_size=const.KEY_SIZE)
# Set permission bits to 600
os.umask(0o177)
with open(ca_key, 'wb') as f:
f.write(asymmetric.dump_private_key(private_key, None))
if subordinate:
builder = CSRBuilder(distinguished_name, public_key)
request = builder.build(private_key)
with open(ca_req + ".part", 'wb') as f:
f.write(pem_armor_csr(request))
os.rename(ca_req + ".part", ca_req)
if not os.path.exists(ca_cert):
if subordinate:
click.echo("Request has been written to %s" % ca_req)
click.echo()
click.echo(open(ca_req).read())
click.echo()
click.echo("Get it signed and insert signed certificate into %s" % ca_cert)
click.echo()
click.echo(" cat > %s" % ca_cert)
click.echo()
click.echo("Paste contents and press Ctrl-D, adjust permissions:")
click.echo()
click.echo(" chown root:root %s" % ca_cert)
click.echo(" chmod 0644 %s" % ca_cert)
click.echo()
click.echo("To finish setup procedure run 'certidude setup authority' again")
sys.exit(1)
# https://technet.microsoft.com/en-us/library/aa998840(v=exchg.141).aspx # https://technet.microsoft.com/en-us/library/aa998840(v=exchg.141).aspx
builder = CertificateBuilder( builder = CertificateBuilder(distinguished_name, public_key)
cn_to_dn("Certidude at %s" % common_name, common_name,
o=organization, ou=organizational_unit),
public_key
)
builder.self_signed = True builder.self_signed = True
builder.ca = True builder.ca = True
builder.serial_number = random.randint( builder.serial_number = random.randint(
@ -1280,22 +1313,20 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
with open(ca_cert, 'wb') as f: with open(ca_cert, 'wb') as f:
f.write(pem_armor_certificate(certificate)) f.write(pem_armor_certificate(certificate))
# Set permission bits to 600
os.umask(0o177)
with open(ca_key, 'wb') as f:
f.write(asymmetric.dump_private_key(private_key, None))
sys.exit(0) # stop this fork here sys.exit(0) # stop this fork here
assert os.stat(sqlite_path).st_mode == 0o100640
assert os.stat(ca_cert).st_mode == 0o100640
assert os.stat(ca_key).st_mode == 0o100600
assert os.stat("/etc/nginx/sites-available/certidude.conf").st_mode == 0o100640
else: else:
os.waitpid(bootstrap_pid, 0) _, exitcode = os.waitpid(bootstrap_pid, 0)
if exitcode:
return 0
from certidude import authority from certidude import authority
authority.self_enroll(skip_notify=True) authority.self_enroll(skip_notify=True)
assert os.getuid() == 0 and os.getgid() == 0, "Enroll contaminated environment" assert os.getuid() == 0 and os.getgid() == 0, "Enroll contaminated environment"
assert os.stat(sqlite_path).st_mode == 0o100660
assert os.stat(ca_cert).st_mode == 0o100640
assert os.stat(ca_key).st_mode == 0o100600
assert os.stat("/etc/nginx/sites-available/certidude.conf").st_mode == 0o100600
assert os.stat("/etc/certidude/server.conf").st_mode == 0o100600
click.echo("Enabling and starting Certidude backend") click.echo("Enabling and starting Certidude backend")
os.system("systemctl enable certidude") os.system("systemctl enable certidude")
os.system("systemctl restart certidude") os.system("systemctl restart certidude")

View File

@ -50,6 +50,7 @@ KERBEROS_SUBNETS = set([ipaddress.ip_network(j) for j in
AUTHORITY_DIR = "/var/lib/certidude" AUTHORITY_DIR = "/var/lib/certidude"
AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path") AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path")
AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate path") AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate path")
SELF_KEY_PATH = cp.get("authority", "self key path")
REQUESTS_DIR = cp.get("authority", "requests dir") REQUESTS_DIR = cp.get("authority", "requests dir")
SIGNED_DIR = cp.get("authority", "signed dir") SIGNED_DIR = cp.get("authority", "signed dir")
SIGNED_BY_SERIAL_DIR = os.path.join(SIGNED_DIR, "by-serial") SIGNED_BY_SERIAL_DIR = os.path.join(SIGNED_DIR, "by-serial")

View File

@ -23,8 +23,8 @@ send_timeout 600;
nchan_message_buffer_length 0; nchan_message_buffer_length 0;
# To use CA-s own certificate for frontend and mutually authenticated connections # To use CA-s own certificate for frontend and mutually authenticated connections
ssl_certificate /var/lib/certidude/{{ common_name }}/signed/{{ common_name }}.pem; ssl_certificate {{ directory }}/signed/{{ common_name }}.pem;
ssl_certificate_key /var/lib/certidude/{{common_name}}/self_key.pem; ssl_certificate_key {{ directory }}/self_key.pem;
server { server {
# Section for serving insecure HTTP, note that this is suitable for # Section for serving insecure HTTP, note that this is suitable for
@ -138,7 +138,7 @@ server {
# Allow client authentication with certificate, # Allow client authentication with certificate,
# backend must still check if certificate was used for TLS handshake # backend must still check if certificate was used for TLS handshake
ssl_verify_client optional; ssl_verify_client optional;
ssl_client_certificate /var/lib/certidude/{{ common_name }}/ca_cert.pem; ssl_client_certificate {{ directory }}/ca_cert.pem;
# Proxy pass to backend # Proxy pass to backend
location /api/ { location /api/ {

View File

@ -25,9 +25,9 @@ kerberos realm = EXAMPLE.LAN
{% if domain %} {% if domain %}
# LDAP URI derived from /etc/samba/smb.conf # LDAP URI derived from /etc/samba/smb.conf
ldap uri = ldap://dc1.{{ domain }} ldap uri = ldaps://dc1.{{ domain }}
{% else %} {% else %}
# LDAP URI # Placeholder LDAP URI
ldap uri = ldaps://dc1.example.lan ldap uri = ldaps://dc1.example.lan
{% endif %} {% endif %}
@ -223,9 +223,14 @@ request submission allowed = false
;user enrollment = single allowed ;user enrollment = single allowed
user enrollment = multiple allowed user enrollment = multiple allowed
# Certificate authority keypair
private key path = {{ ca_key }} private key path = {{ ca_key }}
certificate path = {{ ca_cert }} certificate path = {{ ca_cert }}
# Private key used by nginx frontend
self key path = {{ self_key }}
# Directories for requests, signed, revoked and expired certificates
requests dir = {{ directory }}/requests/ requests dir = {{ directory }}/requests/
signed dir = {{ directory }}/signed/ signed dir = {{ directory }}/signed/
revoked dir = {{ directory }}/revoked/ revoked dir = {{ directory }}/revoked/

View File

@ -96,8 +96,8 @@ def clean_server():
pass pass
if os.path.exists("/var/lib/certidude/ca.example.lan"): if os.path.exists("/var/lib/certidude"):
shutil.rmtree("/var/lib/certidude/ca.example.lan") shutil.rmtree("/var/lib/certidude")
if os.path.exists("/run/certidude"): if os.path.exists("/run/certidude"):
shutil.rmtree("/run/certidude") shutil.rmtree("/run/certidude")
@ -230,13 +230,13 @@ def test_cli_setup_authority():
assert authority.public_key.algorithm == "ec" assert authority.public_key.algorithm == "ec"
# Generate garbage # Generate garbage
with open("/var/lib/certidude/ca.example.lan/bla", "w") as fh: with open("/var/lib/certidude/bla", "w") as fh:
pass pass
with open("/var/lib/certidude/ca.example.lan/requests/bla", "w") as fh: with open("/var/lib/certidude/requests/bla", "w") as fh:
pass pass
with open("/var/lib/certidude/ca.example.lan/signed/bla", "w") as fh: with open("/var/lib/certidude/signed/bla", "w") as fh:
pass pass
with open("/var/lib/certidude/ca.example.lan/revoked/bla", "w") as fh: with open("/var/lib/certidude/revoked/bla", "w") as fh:
pass pass
# Start server before any signing operations are performed # Start server before any signing operations are performed
@ -255,7 +255,7 @@ def test_cli_setup_authority():
# Test CA certificate fetch # Test CA certificate fetch
buf = open("/var/lib/certidude/ca.example.lan/ca_cert.pem").read() buf = open("/var/lib/certidude/ca_cert.pem").read()
r = requests.get("http://ca.example.lan/api/certificate") r = requests.get("http://ca.example.lan/api/certificate")
assert r.status_code == 200 assert r.status_code == 200
assert r.headers.get('content-type') == "application/x-x509-ca-cert" assert r.headers.get('content-type') == "application/x-x509-ca-cert"
@ -308,7 +308,7 @@ def test_cli_setup_authority():
headers={"content-type":"application/pkcs10"}) headers={"content-type":"application/pkcs10"})
assert r.status_code == 202 # success assert r.status_code == 202 # success
assert "Stored request " in inbox.pop(), inbox assert "Stored request " in inbox.pop(), inbox
assert os.path.exists("/var/lib/certidude/ca.example.lan/requests/test.pem") assert os.path.exists("/var/lib/certidude/requests/test.pem")
# Test request deletion # Test request deletion
r = client().simulate_delete("/api/request/test/") r = client().simulate_delete("/api/request/test/")
@ -319,7 +319,7 @@ def test_cli_setup_authority():
r = client().simulate_delete("/api/request/test/", r = client().simulate_delete("/api/request/test/",
headers={"User-Agent":UA_FEDORA_FIREFOX, "Authorization":admintoken}) headers={"User-Agent":UA_FEDORA_FIREFOX, "Authorization":admintoken})
assert r.status_code == 403, r.text # CSRF prevented assert r.status_code == 403, r.text # CSRF prevented
assert os.path.exists("/var/lib/certidude/ca.example.lan/requests/test.pem") assert os.path.exists("/var/lib/certidude/requests/test.pem")
r = client().simulate_delete("/api/request/test/", r = client().simulate_delete("/api/request/test/",
headers={"Authorization":admintoken}) headers={"Authorization":admintoken})
assert r.status_code == 200, r.text assert r.status_code == 200, r.text
@ -507,19 +507,19 @@ def test_cli_setup_authority():
r = client().simulate_post("/api/lease/", r = client().simulate_post("/api/lease/",
query_string = "client=test&inner_address=127.0.0.1&outer_address=8.8.8.8", query_string = "client=test&inner_address=127.0.0.1&outer_address=8.8.8.8",
headers={"X-SSL-CERT":open("/var/lib/certidude/ca.example.lan/signed/ca.example.lan.pem").read() }) headers={"X-SSL-CERT":open("/var/lib/certidude/signed/ca.example.lan.pem").read() })
assert r.status_code == 200, r.text # lease update ok assert r.status_code == 200, r.text # lease update ok
# Attempt to fetch and execute default.sh script # Attempt to fetch and execute default.sh script
from xattr import listxattr, getxattr from xattr import listxattr, getxattr
assert not [j for j in listxattr("/var/lib/certidude/ca.example.lan/signed/test.pem") if j.startswith(b"user.machine.")] assert not [j for j in listxattr("/var/lib/certidude/signed/test.pem") if j.startswith(b"user.machine.")]
#os.system("curl http://ca.example.lan/api/signed/test/script | bash") #os.system("curl http://ca.example.lan/api/signed/test/script | bash")
r = client().simulate_post("/api/signed/test/attr", body="cpu=i5&mem=512M&dist=Ubunt", r = client().simulate_post("/api/signed/test/attr", body="cpu=i5&mem=512M&dist=Ubunt",
headers={"content-type": "application/x-www-form-urlencoded"}) headers={"content-type": "application/x-www-form-urlencoded"})
assert r.status_code == 200, r.text assert r.status_code == 200, r.text
assert getxattr("/var/lib/certidude/ca.example.lan/signed/test.pem", "user.machine.cpu") == b"i5" assert getxattr("/var/lib/certidude/signed/test.pem", "user.machine.cpu") == b"i5"
assert getxattr("/var/lib/certidude/ca.example.lan/signed/test.pem", "user.machine.mem") == b"512M" assert getxattr("/var/lib/certidude/signed/test.pem", "user.machine.mem") == b"512M"
assert getxattr("/var/lib/certidude/ca.example.lan/signed/test.pem", "user.machine.dist") == b"Ubunt" assert getxattr("/var/lib/certidude/signed/test.pem", "user.machine.dist") == b"Ubunt"
# Test tagging integration in scripting framework # Test tagging integration in scripting framework
r = client().simulate_get("/api/signed/test/script/") r = client().simulate_get("/api/signed/test/script/")
@ -572,11 +572,11 @@ def test_cli_setup_authority():
# Test lease update # Test lease update
r = client().simulate_post("/api/lease/", r = client().simulate_post("/api/lease/",
query_string = "client=test&inner_address=127.0.0.1&outer_address=8.8.8.8&serial=0", query_string = "client=test&inner_address=127.0.0.1&outer_address=8.8.8.8&serial=0",
headers={"X-SSL-CERT":open("/var/lib/certidude/ca.example.lan/signed/ca.example.lan.pem").read() }) headers={"X-SSL-CERT":open("/var/lib/certidude/signed/ca.example.lan.pem").read() })
assert r.status_code == 403, r.text # invalid serial number supplied assert r.status_code == 403, r.text # invalid serial number supplied
r = client().simulate_post("/api/lease/", r = client().simulate_post("/api/lease/",
query_string = "client=test&inner_address=1.2.3.4&outer_address=8.8.8.8", query_string = "client=test&inner_address=1.2.3.4&outer_address=8.8.8.8",
headers={"X-SSL-CERT":open("/var/lib/certidude/ca.example.lan/signed/ca.example.lan.pem").read() }) headers={"X-SSL-CERT":open("/var/lib/certidude/signed/ca.example.lan.pem").read() })
assert r.status_code == 200, r.text # lease update ok assert r.status_code == 200, r.text # lease update ok
@ -717,11 +717,11 @@ def test_cli_setup_authority():
assert not result.exception, result.output assert not result.exception, result.output
assert "(autosign not requested)" in result.output, result.output assert "(autosign not requested)" in result.output, result.output
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/vpn.example.lan.pem") assert not os.path.exists("/var/lib/certidude/signed/vpn.example.lan.pem")
child_pid = os.fork() child_pid = os.fork()
if not child_pid: if not child_pid:
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/vpn.example.lan.pem") assert not os.path.exists("/var/lib/certidude/signed/vpn.example.lan.pem")
result = runner.invoke(cli, ["sign", "vpn.example.lan", "--profile", "srv"]) result = runner.invoke(cli, ["sign", "vpn.example.lan", "--profile", "srv"])
assert not result.exception, result.output assert not result.exception, result.output
assert "overwrit" not in result.output, result.output assert "overwrit" not in result.output, result.output
@ -912,20 +912,20 @@ def test_cli_setup_authority():
# Setup gateway # Setup gateway
clean_client() clean_client()
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem") assert not os.path.exists("/var/lib/certidude/signed/ipsec.example.lan.pem")
result = runner.invoke(cli, ['setup', 'strongswan', 'server', "-cn", "ipsec", "ca.example.lan"]) result = runner.invoke(cli, ['setup', 'strongswan', 'server', "-cn", "ipsec", "ca.example.lan"])
assert result.exception, result.output # FQDN required assert result.exception, result.output # FQDN required
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem") assert not os.path.exists("/var/lib/certidude/signed/ipsec.example.lan.pem")
result = runner.invoke(cli, ['setup', 'strongswan', 'server', "-cn", "ipsec.example.lan", "ca.example.lan"]) result = runner.invoke(cli, ['setup', 'strongswan', 'server', "-cn", "ipsec.example.lan", "ca.example.lan"])
assert not result.exception, result.output assert not result.exception, result.output
assert open("/etc/ipsec.secrets").read() == ": RSA /etc/certidude/authority/ca.example.lan/server_key.pem\n" assert open("/etc/ipsec.secrets").read() == ": RSA /etc/certidude/authority/ca.example.lan/server_key.pem\n"
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem") assert not os.path.exists("/var/lib/certidude/signed/ipsec.example.lan.pem")
result = runner.invoke(cli, ['setup', 'strongswan', 'server', "-cn", "ipsec.example.lan", "ca.example.lan"]) result = runner.invoke(cli, ['setup', 'strongswan', 'server', "-cn", "ipsec.example.lan", "ca.example.lan"])
assert not result.exception, result.output # client conf already exists, remove to regenerate assert not result.exception, result.output # client conf already exists, remove to regenerate
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem") assert not os.path.exists("/var/lib/certidude/signed/ipsec.example.lan.pem")
with open("/etc/certidude/client.conf", "a") as fh: with open("/etc/certidude/client.conf", "a") as fh:
fh.write("autosign = false\n") fh.write("autosign = false\n")
@ -934,11 +934,11 @@ def test_cli_setup_authority():
assert not result.exception, result.output assert not result.exception, result.output
assert "(autosign not requested)" in result.output, result.output assert "(autosign not requested)" in result.output, result.output
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem") assert not os.path.exists("/var/lib/certidude/signed/ipsec.example.lan.pem")
child_pid = os.fork() child_pid = os.fork()
if not child_pid: if not child_pid:
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem") assert not os.path.exists("/var/lib/certidude/signed/ipsec.example.lan.pem")
result = runner.invoke(cli, ["sign", "ipsec.example.lan", "--profile", "srv"]) result = runner.invoke(cli, ["sign", "ipsec.example.lan", "--profile", "srv"])
assert not result.exception, result.output assert not result.exception, result.output
assert "overwrit" not in result.output, result.output assert "overwrit" not in result.output, result.output
@ -1024,13 +1024,13 @@ def test_cli_setup_authority():
assert r.status_code == 400 assert r.status_code == 400
assert os.system("openssl ocsp -issuer /var/lib/certidude/ca.example.lan/ca_cert.pem -CAfile /var/lib/certidude/ca.example.lan/ca_cert.pem -cert /var/lib/certidude/ca.example.lan/signed/roadwarrior2.pem -text -url http://ca.example.lan/api/ocsp/ -out /tmp/ocsp1.log") == 0 assert os.system("openssl ocsp -issuer /var/lib/certidude/ca_cert.pem -CAfile /var/lib/certidude/ca_cert.pem -cert /var/lib/certidude/signed/roadwarrior2.pem -text -url http://ca.example.lan/api/ocsp/ -out /tmp/ocsp1.log") == 0
assert os.system("openssl ocsp -issuer /var/lib/certidude/ca.example.lan/ca_cert.pem -CAfile /var/lib/certidude/ca.example.lan/ca_cert.pem -cert /var/lib/certidude/ca.example.lan/ca_cert.pem -text -url http://ca.example.lan/api/ocsp/ -out /tmp/ocsp2.log") == 0 assert os.system("openssl ocsp -issuer /var/lib/certidude/ca_cert.pem -CAfile /var/lib/certidude/ca_cert.pem -cert /var/lib/certidude/ca_cert.pem -text -url http://ca.example.lan/api/ocsp/ -out /tmp/ocsp2.log") == 0
for filename in os.listdir("/var/lib/certidude/ca.example.lan/revoked"): for filename in os.listdir("/var/lib/certidude/revoked"):
if not filename.endswith(".pem"): if not filename.endswith(".pem"):
continue continue
assert os.system("openssl ocsp -issuer /var/lib/certidude/ca.example.lan/ca_cert.pem -CAfile /var/lib/certidude/ca.example.lan/ca_cert.pem -cert /var/lib/certidude/ca.example.lan/revoked/%s -text -url http://ca.example.lan/api/ocsp/ -out /tmp/ocsp3.log" % filename) == 0 assert os.system("openssl ocsp -issuer /var/lib/certidude/ca_cert.pem -CAfile /var/lib/certidude/ca_cert.pem -cert /var/lib/certidude/revoked/%s -text -url http://ca.example.lan/api/ocsp/ -out /tmp/ocsp3.log" % filename) == 0
break break
with open("/tmp/ocsp1.log") as fh: with open("/tmp/ocsp1.log") as fh:
@ -1108,7 +1108,7 @@ def test_cli_setup_authority():
# Bootstrap authority # Bootstrap authority
assert not os.path.exists("/var/lib/certidude/ca.example.lan/ca_key.pem") assert not os.path.exists("/var/lib/certidude/ca_key.pem")
assert os.system("certidude setup authority --skip-packages") == 0 assert os.system("certidude setup authority --skip-packages") == 0