From 64ed4cc8938cad444c002f43382ee0122926e1e9 Mon Sep 17 00:00:00 2001 From: "Endi S. Dewata" Date: Thu, 19 Sep 2024 11:21:13 -0500 Subject: [PATCH] Auto-create SSL server cert for ACME on separate instance pkispawn has been updated to use the ACME's issuer to provide the cert chain and to automatically issue an SSL server cert for ACME on separate instance. The PKIDeployer.import_cert_chain() has been modified to retrieve the cert chain from the ACME issuer. The PKIDeployer.get_ca_signing_cert() has been modified to ignore UNKNOWN_ISSUER error. The PKIDeployer.issue_cert() has been modified to support optional install token, subject DN, and issuer credentials. The test for ACME on separate instance has been modified to automatically issue an SSL server cert. The original test with manual SSL server cert creation has been moved into a separate test. --- .../workflows/acme-existing-nssdb-test.yml | 883 ++++++++++++++++++ .github/workflows/acme-separate-test.yml | 48 +- .github/workflows/acme-tests.yml | 5 + base/server/examples/installation/acme.cfg | 3 + .../python/pki/server/deployment/__init__.py | 160 +++- 5 files changed, 1053 insertions(+), 46 deletions(-) create mode 100644 .github/workflows/acme-existing-nssdb-test.yml diff --git a/.github/workflows/acme-existing-nssdb-test.yml b/.github/workflows/acme-existing-nssdb-test.yml new file mode 100644 index 00000000000..a0d92c5b12c --- /dev/null +++ b/.github/workflows/acme-existing-nssdb-test.yml @@ -0,0 +1,883 @@ +name: ACME with existing NSS database + +on: workflow_call + +env: + DS_IMAGE: ${{ vars.DS_IMAGE || 'quay.io/389ds/dirsrv' }} + +jobs: + # docs/installation/acme/Installing_PKI_ACME_Responder.md + # docs/user/acme/Using_PKI_ACME_Responder_with_Certbot.md + test: + name: Test + runs-on: ubuntu-latest + env: + SHARED: /tmp/workdir/pki + steps: + - name: Clone repository + uses: actions/checkout@v4 + + - name: Retrieve ACME images + uses: actions/cache@v4 + with: + key: acme-images-${{ github.sha }} + path: acme-images.tar + + - name: Load ACME images + run: docker load --input acme-images.tar + + - name: Create network + run: docker network create example + + - name: Set up CA DS container + run: | + tests/bin/ds-create.sh \ + --image=${{ env.DS_IMAGE }} \ + --hostname=cads.example.com \ + --password=Secret.123 \ + --network=example \ + --network-alias=cads.example.com \ + cads + + - name: Set up CA container + run: | + tests/bin/runner-init.sh \ + --hostname=ca.example.com \ + --network=example \ + --network-alias=ca.example.com \ + ca + + - name: Install CA + run: | + docker exec ca pkispawn \ + -f /usr/share/pki/server/examples/installation/ca.cfg \ + -s CA \ + -D pki_ds_url=ldap://cads.example.com:3389 \ + -v + + - name: Install CA admin cert + run: | + docker exec ca pki-server cert-export \ + --cert-file $SHARED/ca_signing.crt \ + ca_signing + + docker exec ca pki nss-cert-import \ + --cert $SHARED/ca_signing.crt \ + --trust CT,C,C \ + ca_signing + + docker exec ca pki pkcs12-import \ + --pkcs12 /root/.dogtag/pki-tomcat/ca_admin_cert.p12 \ + --pkcs12-password Secret.123 + + docker exec ca pki -n caadmin ca-user-show caadmin + + - name: Check initial CA certs + run: | + docker exec ca pki ca-cert-find | tee output + + # there should be 6 certs + echo "6" > expected + grep "Serial Number:" output | wc -l > actual + diff expected actual + + - name: Set up ACME DS container + run: | + tests/bin/ds-create.sh \ + --image=${{ env.DS_IMAGE }} \ + --hostname=acmeds.example.com \ + --password=Secret.123 \ + --network=example \ + --network-alias=acmeds.example.com \ + acmeds + + - name: Set up ACME container + run: | + tests/bin/runner-init.sh \ + --hostname=acme.example.com \ + --network=example \ + --network-alias=acme.example.com \ + acme + + - name: Create PKI server for ACME + run: | + docker exec acme pki-server create + docker exec acme pki-server nss-create --password Secret.123 + + - name: Import CA signing cert for ACME + run: | + docker exec acme pki-server cert-import \ + --input $SHARED/ca_signing.crt \ + ca_signing + + - name: Issue SSL server cert for ACME + run: | + # generate cert request + docker exec acme pki-server cert-request \ + --subject "CN=acme.example.com" \ + --ext /usr/share/pki/server/certs/sslserver.conf \ + sslserver + docker exec acme openssl req \ + -text \ + -noout \ + -in /var/lib/pki/pki-tomcat/conf/certs/sslserver.csr + + # issue cert + docker exec acme pki \ + -d /etc/pki/pki-tomcat/alias \ + -f /etc/pki/pki-tomcat/password.conf \ + -U https://ca.example.com:8443 \ + -u caadmin \ + -w Secret.123 \ + ca-cert-issue \ + --profile caServerCert \ + --csr-file /var/lib/pki/pki-tomcat/conf/certs/sslserver.csr \ + --output-file /var/lib/pki/pki-tomcat/conf/certs/sslserver.crt + docker exec acme openssl x509 \ + -text \ + -noout \ + -in /var/lib/pki/pki-tomcat/conf/certs/sslserver.crt + + # install cert + docker exec acme pki-server cert-import \ + --input /var/lib/pki/pki-tomcat/conf/certs/sslserver.crt \ + sslserver + + - name: Set up ACME database + run: | + docker exec acme ldapmodify \ + -H ldap://acmeds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -f /usr/share/pki/acme/database/ds/schema.ldif + docker exec acme ldapadd \ + -H ldap://acmeds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -f /usr/share/pki/acme/database/ds/create.ldif + docker exec acme ldapadd \ + -H ldap://acmeds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -f /usr/share/pki/acme/realm/ds/create.ldif + + - name: Install ACME + run: | + docker exec acme pkispawn \ + -f /usr/share/pki/server/examples/installation/acme.cfg \ + -s ACME \ + -D acme_database_url=ldap://acmeds.example.com:3389 \ + -D acme_issuer_url=https://ca.example.com:8443 \ + -D acme_realm_url=ldap://acmeds.example.com:3389 \ + -v + + - name: Check ACME server base dir after installation + if: always() + run: | + # check file types, owners, and permissions + docker exec acme ls -l /var/lib/pki/pki-tomcat \ + | sed \ + -e '/^total/d' \ + -e 's/^\(\S*\) *\S* *\(\S*\) *\(\S*\) *\S* *\S* *\S* *\S* *\(.*\)$/\1 \2 \3 \4/' \ + | tee output + + # TODO: review permissions + cat > expected << EOF + drwxrwx--- pkiuser pkiuser acme + lrwxrwxrwx pkiuser pkiuser alias -> /var/lib/pki/pki-tomcat/conf/alias + lrwxrwxrwx pkiuser pkiuser bin -> /usr/share/tomcat/bin + drwxr-x--- pkiuser pkiuser common + lrwxrwxrwx pkiuser pkiuser conf -> /etc/pki/pki-tomcat + lrwxrwxrwx pkiuser pkiuser lib -> /usr/share/pki/server/lib + lrwxrwxrwx pkiuser pkiuser logs -> /var/log/pki/pki-tomcat + drwxr-x--- pkiuser pkiuser temp + drwxr-x--- pkiuser pkiuser webapps + drwxr-x--- pkiuser pkiuser work + EOF + + diff expected output + + - name: Check ACME server conf dir after installation + if: always() + run: | + # check file types, owners, and permissions + docker exec acme ls -l /etc/pki/pki-tomcat \ + | sed \ + -e '/^total/d' \ + -e 's/^\(\S*\) *\S* *\(\S*\) *\(\S*\) *\S* *\S* *\S* *\S* *\(.*\)$/\1 \2 \3 \4/' \ + | tee output + + # TODO: review permissions + cat > expected << EOF + drwxr-x--- pkiuser pkiuser Catalina + drwxrwx--- pkiuser pkiuser acme + drwxrwx--- pkiuser pkiuser alias + -rw-rw---- pkiuser pkiuser catalina.policy + lrwxrwxrwx pkiuser pkiuser catalina.properties -> /usr/share/pki/server/conf/catalina.properties + drwxr-x--- pkiuser pkiuser certs + lrwxrwxrwx pkiuser pkiuser context.xml -> /etc/tomcat/context.xml + lrwxrwxrwx pkiuser pkiuser logging.properties -> /usr/share/pki/server/conf/logging.properties + -rw-rw---- pkiuser pkiuser password.conf + -rw-rw---- pkiuser pkiuser server.xml + -rw-rw---- pkiuser pkiuser tomcat.conf + lrwxrwxrwx pkiuser pkiuser web.xml -> /etc/tomcat/web.xml + EOF + + diff expected output + + - name: Check ACME server logs dir after installation + if: always() + run: | + # check file types, owners, and permissions + docker exec acme ls -l /var/log/pki/pki-tomcat \ + | sed \ + -e '/^total/d' \ + -e 's/^\(\S*\) *\S* *\(\S*\) *\(\S*\) *\S* *\S* *\S* *\S* *\(.*\)$/\1 \2 \3 \4/' \ + | tee output + + DATE=$(date +'%Y-%m-%d') + + # TODO: review permissions + cat > expected << EOF + drwxrwx--- pkiuser pkiuser acme + drwxr-x--- pkiuser pkiuser backup + -rw-r--r-- pkiuser pkiuser catalina.$DATE.log + -rw-r--r-- pkiuser pkiuser host-manager.$DATE.log + -rw-r--r-- pkiuser pkiuser localhost.$DATE.log + -rw-r--r-- pkiuser pkiuser localhost_access_log.$DATE.txt + -rw-r--r-- pkiuser pkiuser manager.$DATE.log + drwxr-xr-x pkiuser pkiuser pki + EOF + + diff expected output + + - name: Check ACME base dir + if: always() + run: | + docker exec acme ls -l /var/lib/pki/pki-tomcat/acme \ + | sed \ + -e '/^total/d' \ + -e 's/^\(\S*\) *\S* *\(\S*\) *\(\S*\) *\S* *\S* *\S* *\S* *\(.*\)$/\1 \2 \3 \4/' \ + | tee output + + # TODO: review permissions + cat > expected << EOF + lrwxrwxrwx pkiuser pkiuser conf -> /var/lib/pki/pki-tomcat/conf/acme + lrwxrwxrwx pkiuser pkiuser logs -> /var/lib/pki/pki-tomcat/logs/acme + EOF + + diff expected output + + - name: Check ACME conf dir + if: always() + run: | + # check file types, owners, and permissions + docker exec acme ls -l /etc/pki/pki-tomcat/acme \ + | sed \ + -e '/^total/d' \ + -e 's/^\(\S*\) *\S* *\(\S*\) *\(\S*\) *\S* *\S* *\S* *\S* *\(.*\)$/\1 \2 \3 \4/' \ + | tee output + + # TODO: review permissions + cat > expected << EOF + -rw-rw---- pkiuser pkiuser database.conf + -rw-rw---- pkiuser pkiuser issuer.conf + -rw-rw---- pkiuser pkiuser realm.conf + EOF + + diff expected output + + - name: Check ACME database config + if: always() + run: | + docker exec acme cat /etc/pki/pki-tomcat/acme/database.conf + + - name: Check ACME issuer config + if: always() + run: | + docker exec acme cat /etc/pki/pki-tomcat/acme/issuer.conf + + - name: Check ACME realm config + if: always() + run: | + docker exec acme cat /etc/pki/pki-tomcat/acme/realm.conf + + - name: Check ACME logs dir + if: always() + run: | + docker exec acme ls -l /var/log/pki/pki-tomcat/acme + + - name: Check ACME system certs + if: always() + run: | + docker exec acme pki \ + -d /etc/pki/pki-tomcat/alias \ + -f /etc/pki/pki-tomcat/password.conf \ + nss-cert-find + + - name: Check initial ACME accounts + run: | + docker exec acmeds ldapsearch \ + -H ldap://acmeds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -b ou=accounts,dc=acme,dc=pki,dc=example,dc=com \ + -s one \ + -o ldif_wrap=no \ + -LLL | tee output + + # there should be no accounts + echo "0" > expected + grep "^dn:" output | wc -l > actual + diff expected actual + + - name: Check initial ACME orders + run: | + docker exec acmeds ldapsearch \ + -H ldap://acmeds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -b ou=orders,dc=acme,dc=pki,dc=example,dc=com \ + -s one \ + -o ldif_wrap=no \ + -LLL | tee output + + # there should be no orders + echo "0" > expected + grep "^dn:" output | wc -l > actual + diff expected actual + + - name: Check initial ACME authorizations + run: | + docker exec acmeds ldapsearch \ + -H ldap://acmeds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -b ou=authorizations,dc=acme,dc=pki,dc=example,dc=com \ + -s one \ + -o ldif_wrap=no \ + -LLL | tee output + + # there should be no authorizations + echo "0" > expected + grep "^dn:" output | wc -l > actual + diff expected actual + + - name: Check initial ACME challenges + run: | + docker exec acmeds ldapsearch \ + -H ldap://acmeds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -b ou=challenges,dc=acme,dc=pki,dc=example,dc=com \ + -s one \ + -o ldif_wrap=no \ + -LLL | tee output + + # there should be no challenges + echo "0" > expected + grep "^dn:" output | wc -l > actual + diff expected actual + + - name: Check initial ACME certs + run: | + docker exec acmeds ldapsearch \ + -H ldap://acmeds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -b ou=certificates,dc=acme,dc=pki,dc=example,dc=com \ + -s one \ + -o ldif_wrap=no \ + -LLL | tee output + + # there should be no certs + echo "0" > expected + grep "^dn:" output | wc -l > actual + diff expected actual + + - name: Check CA certs after ACME installation + run: | + docker exec ca pki ca-cert-find | tee output + + # there should be 7 certs + echo "7" > expected + grep "Serial Number:" output | wc -l > actual + diff expected actual + + - name: Run PKI healthcheck in ACME container + run: docker exec acme pki-healthcheck --failures-only + + - name: Verify ACME in ACME container + run: | + docker exec acme pki nss-cert-import \ + --cert $SHARED/ca_signing.crt \ + --trust CT,C,C \ + ca_signing + + docker exec acme pki acme-info + + - name: Set up client container + run: | + tests/bin/runner-init.sh \ + --hostname=client.example.com \ + --network=example \ + --network-alias=client.example.com \ + client + + - name: Install certbot in client container + run: docker exec client dnf install -y certbot + + - name: Register ACME account + run: | + docker exec client certbot register \ + --server http://acme.example.com:8080/acme/directory \ + --email testuser@example.com \ + --agree-tos \ + --non-interactive + + - name: Check ACME accounts after registration + run: | + docker exec acmeds ldapsearch \ + -H ldap://acmeds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -b ou=accounts,dc=acme,dc=pki,dc=example,dc=com \ + -s one \ + -o ldif_wrap=no \ + -LLL | tee output + + # there should be one account + echo "1" > expected + grep "^dn:" output | wc -l > actual + diff expected actual + + # status should be valid + echo "valid" > expected + sed -n 's/^acmeStatus: *\(.*\)$/\1/p' output > actual + diff expected actual + + # email should be testuser@example.com + echo "mailto:testuser@example.com" > expected + sed -n 's/^acmeAccountContact: *\(.*\)$/\1/p' output > actual + diff expected actual + + - name: Enroll client cert + run: | + docker exec client certbot certonly \ + --server http://acme.example.com:8080/acme/directory \ + -d client.example.com \ + --key-type rsa \ + --standalone \ + --non-interactive + + - name: Check client cert + run: | + docker exec client pki client-cert-import \ + --cert /etc/letsencrypt/live/client.example.com/fullchain.pem \ + client1 + + # store serial number + docker exec client pki nss-cert-show client1 | tee output + sed -n 's/^ *Serial Number: *\(.*\)/\1/p' output > serial1.txt + + # subject should be CN=client.example.com + echo "CN=client.example.com" > expected + sed -n 's/^ *Subject DN: *\(.*\)/\1/p' output > actual + diff expected actual + + - name: Check ACME orders after enrollment + run: | + docker exec acmeds ldapsearch \ + -H ldap://acmeds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -b ou=orders,dc=acme,dc=pki,dc=example,dc=com \ + -s one \ + -o ldif_wrap=no \ + -LLL | tee output + + # there should be one order + echo "1" > expected + grep "^dn:" output | wc -l > actual + diff expected actual + + - name: Check ACME authorizations after enrollment + run: | + docker exec acmeds ldapsearch \ + -H ldap://acmeds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -b ou=authorizations,dc=acme,dc=pki,dc=example,dc=com \ + -s one \ + -o ldif_wrap=no \ + -LLL | tee output + + # there should be one authorization + echo "1" > expected + grep "^dn:" output | wc -l > actual + diff expected actual + + - name: Check ACME challenges after enrollment + run: | + docker exec acmeds ldapsearch \ + -H ldap://acmeds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -b ou=challenges,dc=acme,dc=pki,dc=example,dc=com \ + -s one \ + -o ldif_wrap=no \ + -LLL | tee output + + # there should be one challenge + echo "1" > expected + grep "^dn:" output | wc -l > actual + diff expected actual + + - name: Check ACME certs after enrollment + run: | + docker exec acmeds ldapsearch \ + -H ldap://acmeds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -b ou=certificates,dc=acme,dc=pki,dc=example,dc=com \ + -s one \ + -o ldif_wrap=no \ + -LLL | tee output + + # there should be no certs (they are stored in CA) + echo "0" > expected + grep "^dn:" output | wc -l > actual + diff expected actual + + - name: Check CA certs after enrollment + run: | + docker exec ca pki ca-cert-find | tee output + + # there should be 8 certs + echo "8" > expected + grep "Serial Number:" output | wc -l > actual + diff expected actual + + # check client cert + SERIAL=$(cat serial1.txt) + docker exec ca pki ca-cert-show $SERIAL | tee output + + # subject should be CN=client.example.com + echo "CN=client.example.com" > expected + sed -n 's/^ *Subject DN: *\(.*\)/\1/p' output > actual + diff expected actual + + - name: Renew client cert + run: | + docker exec client certbot renew \ + --server http://acme.example.com:8080/acme/directory \ + --cert-name client.example.com \ + --force-renewal \ + --no-random-sleep-on-renew \ + --non-interactive + + - name: Check renewed client cert + run: | + docker exec client pki client-cert-import \ + --cert /etc/letsencrypt/live/client.example.com/fullchain.pem \ + client2 + + # store serial number + docker exec client pki nss-cert-show client2 | tee output + sed -n 's/^ *Serial Number: *\(.*\)/\1/p' output > serial2.txt + + # subject should be CN=client.example.com + echo "CN=client.example.com" > expected + sed -n 's/^ *Subject DN: *\(.*\)/\1/p' output > actual + diff expected actual + + - name: Check ACME orders after renewal + run: | + docker exec acmeds ldapsearch \ + -H ldap://acmeds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -b ou=orders,dc=acme,dc=pki,dc=example,dc=com \ + -s one \ + -o ldif_wrap=no \ + -LLL | tee output + + # there should be two orders + echo "2" > expected + grep "^dn:" output | wc -l > actual + diff expected actual + + - name: Check ACME authorizations after renewal + run: | + docker exec acmeds ldapsearch \ + -H ldap://acmeds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -b ou=authorizations,dc=acme,dc=pki,dc=example,dc=com \ + -s one \ + -o ldif_wrap=no \ + -LLL | tee output + + # there should be two authorizations + echo "2" > expected + grep "^dn:" output | wc -l > actual + diff expected actual + + - name: Check ACME challenges after renewal + run: | + docker exec acmeds ldapsearch \ + -H ldap://acmeds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -b ou=challenges,dc=acme,dc=pki,dc=example,dc=com \ + -s one \ + -o ldif_wrap=no \ + -LLL | tee output + + # there should be two challenges + echo "2" > expected + grep "^dn:" output | wc -l > actual + diff expected actual + + - name: Check ACME certs after renewal + run: | + docker exec acmeds ldapsearch \ + -H ldap://acmeds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -b ou=certificates,dc=acme,dc=pki,dc=example,dc=com \ + -s one \ + -o ldif_wrap=no \ + -LLL | tee output + + # there should be no certs (they are stored in CA) + echo "0" > expected + grep "^dn:" output | wc -l > actual + diff expected actual + + - name: Check CA certs after renewal + run: | + docker exec ca pki ca-cert-find | tee output + + # there should be 9 certs + echo "9" > expected + grep "Serial Number:" output | wc -l > actual + diff expected actual + + # check renewed client cert + SERIAL=$(cat serial2.txt) + docker exec ca pki ca-cert-show $SERIAL | tee output + + # subject should be CN=client.example.com + echo "CN=client.example.com" > expected + sed -n 's/^ *Subject DN: *\(.*\)/\1/p' output > actual + diff expected actual + + - name: Revoke client cert + run: | + docker exec client certbot revoke \ + --server http://acme.example.com:8080/acme/directory \ + --cert-name client.example.com \ + --non-interactive + + - name: Check CA certs after revocation + run: | + docker exec ca pki ca-cert-find | tee output + + # there should be 9 certs + echo "9" > expected + grep "Serial Number:" output | wc -l > actual + diff expected actual + + # check original client cert + SERIAL=$(cat serial1.txt) + docker exec ca pki ca-cert-show $SERIAL | tee output + + # status should be valid + echo "VALID" > expected + sed -n 's/^ *Status: *\(.*\)/\1/p' output > actual + diff expected actual + + # check renewed-then-revoked client cert + SERIAL=$(cat serial2.txt) + docker exec ca pki ca-cert-show $SERIAL | tee output + + # status should be revoked + echo "REVOKED" > expected + sed -n 's/^ *Status: *\(.*\)/\1/p' output > actual + diff expected actual + + - name: Update ACME account + run: | + docker exec client certbot update_account \ + --server http://acme.example.com:8080/acme/directory \ + --email newuser@example.com \ + --non-interactive + + - name: Check ACME accounts after update + run: | + docker exec acmeds ldapsearch \ + -H ldap://acmeds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -b ou=accounts,dc=acme,dc=pki,dc=example,dc=com \ + -s one \ + -o ldif_wrap=no \ + -LLL | tee output + + # there should be one account + echo "1" > expected + grep "^dn:" output | wc -l > actual + diff expected actual + + # email should be newuser@example.com + echo "mailto:newuser@example.com" > expected + sed -n 's/^acmeAccountContact: *\(.*\)$/\1/p' output > actual + diff expected actual + + - name: Remove ACME account + run: | + docker exec client certbot unregister \ + --server http://acme.example.com:8080/acme/directory \ + --non-interactive + + - name: Check ACME accounts after unregistration + run: | + docker exec acmeds ldapsearch \ + -H ldap://acmeds.example.com:3389 \ + -D "cn=Directory Manager" \ + -w Secret.123 \ + -b ou=accounts,dc=acme,dc=pki,dc=example,dc=com \ + -s one \ + -o ldif_wrap=no \ + -LLL | tee output + + # there should be one account + echo "1" > expected + grep "^dn:" output | wc -l > actual + diff expected actual + + # status should be deactivated + echo "deactivated" > expected + sed -n 's/^acmeStatus: *\(.*\)$/\1/p' output > actual + diff expected actual + + - name: Remove ACME + run: docker exec acme pkidestroy -s ACME -v + + - name: Remove CA + run: docker exec ca pkidestroy -s CA -v + + - name: Check ACME server base dir after removal + run: | + # check file types, owners, and permissions + docker exec acme ls -l /var/lib/pki/pki-tomcat \ + | sed \ + -e '/^total/d' \ + -e 's/^\(\S*\) *\S* *\(\S*\) *\(\S*\) *\S* *\S* *\S* *\S* *\(.*\)$/\1 \2 \3 \4/' \ + | tee output + + # TODO: review permissions + cat > expected << EOF + lrwxrwxrwx pkiuser pkiuser conf -> /etc/pki/pki-tomcat + lrwxrwxrwx pkiuser pkiuser logs -> /var/log/pki/pki-tomcat + EOF + + diff expected output + + - name: Check ACME server conf dir after removal + run: | + # check file types, owners, and permissions + docker exec acme ls -l /etc/pki/pki-tomcat \ + | sed \ + -e '/^total/d' \ + -e 's/^\(\S*\) *\S* *\(\S*\) *\(\S*\) *\S* *\S* *\S* *\S* *\(.*\)$/\1 \2 \3 \4/' \ + | tee output + + # TODO: review permissions + cat > expected << EOF + drwxr-x--- pkiuser pkiuser Catalina + drwxrwx--- pkiuser pkiuser acme + drwxrwx--- pkiuser pkiuser alias + -rw-rw---- pkiuser pkiuser catalina.policy + lrwxrwxrwx pkiuser pkiuser catalina.properties -> /usr/share/pki/server/conf/catalina.properties + drwxr-x--- pkiuser pkiuser certs + lrwxrwxrwx pkiuser pkiuser context.xml -> /etc/tomcat/context.xml + lrwxrwxrwx pkiuser pkiuser logging.properties -> /usr/share/pki/server/conf/logging.properties + -rw-rw---- pkiuser pkiuser password.conf + -rw-rw---- pkiuser pkiuser server.xml + -rw-rw---- pkiuser pkiuser tomcat.conf + lrwxrwxrwx pkiuser pkiuser web.xml -> /etc/tomcat/web.xml + EOF + + diff expected output + + - name: Check ACME server logs dir after removal + run: | + # check file types, owners, and permissions + docker exec acme ls -l /var/log/pki/pki-tomcat \ + | sed \ + -e '/^total/d' \ + -e 's/^\(\S*\) *\S* *\(\S*\) *\(\S*\) *\S* *\S* *\S* *\S* *\(.*\)$/\1 \2 \3 \4/' \ + | tee output + + DATE=$(date +'%Y-%m-%d') + + # TODO: review permissions + cat > expected << EOF + drwxrwx--- pkiuser pkiuser acme + drwxr-x--- pkiuser pkiuser backup + -rw-r--r-- pkiuser pkiuser catalina.$DATE.log + -rw-r--r-- pkiuser pkiuser host-manager.$DATE.log + -rw-r--r-- pkiuser pkiuser localhost.$DATE.log + -rw-r--r-- pkiuser pkiuser localhost_access_log.$DATE.txt + -rw-r--r-- pkiuser pkiuser manager.$DATE.log + drwxr-xr-x pkiuser pkiuser pki + EOF + + diff expected output + + - name: Check CA DS server systemd journal + if: always() + run: | + docker exec cads journalctl -x --no-pager -u dirsrv@localhost.service + + - name: Check CA DS container logs + if: always() + run: | + docker logs cads + + - name: Check CA server systemd journal + if: always() + run: | + docker exec ca journalctl -x --no-pager -u pki-tomcatd@pki-tomcat.service + + - name: Check CA debug log + if: always() + run: | + docker exec ca find /var/lib/pki/pki-tomcat/logs/ca -name "debug.*" -exec cat {} \; + + - name: Check ACME DS server systemd journal + if: always() + run: | + docker exec acmeds journalctl -x --no-pager -u dirsrv@localhost.service + + - name: Check ACME DS container logs + if: always() + run: | + docker logs acmeds + + - name: Check ACME server systemd journal + if: always() + run: | + docker exec acme journalctl -x --no-pager -u pki-tomcatd@pki-tomcat.service + + - name: Check ACME debug log + if: always() + run: | + docker exec acme find /var/lib/pki/pki-tomcat/logs/acme -name "debug.*" -exec cat {} \; + + - name: Check certbot log + if: always() + run: | + docker exec client cat /var/log/letsencrypt/letsencrypt.log diff --git a/.github/workflows/acme-separate-test.yml b/.github/workflows/acme-separate-test.yml index 3a0782c6c96..158965008c7 100644 --- a/.github/workflows/acme-separate-test.yml +++ b/.github/workflows/acme-separate-test.yml @@ -104,36 +104,6 @@ jobs: docker exec acme pki-server create docker exec acme pki-server nss-create --password Secret.123 - - name: Import CA signing cert for ACME - run: | - docker exec acme pki-server cert-import \ - --input $SHARED/ca_signing.crt \ - ca_signing - - - name: Issue SSL server cert for ACME - run: | - # generate cert request - docker exec acme pki-server cert-request \ - --subject "CN=acme.example.com" \ - --ext /usr/share/pki/server/certs/sslserver.conf \ - sslserver - docker exec acme cp /var/lib/pki/pki-tomcat/conf/certs/sslserver.csr $SHARED - docker exec ca openssl req -text -noout -in $SHARED/sslserver.csr - - # issue cert - docker exec ca pki \ - -n caadmin \ - ca-cert-issue \ - --profile caServerCert \ - --csr-file $SHARED/sslserver.csr \ - --output-file $SHARED/sslserver.crt - docker exec ca openssl x509 -text -noout -in $SHARED/sslserver.crt - - # install cert - docker exec acme pki-server cert-import \ - --input $SHARED/sslserver.crt \ - sslserver - - name: Set up ACME database run: | docker exec acme ldapmodify \ @@ -163,6 +133,7 @@ jobs: -v - name: Check ACME server base dir after installation + if: always() run: | # check file types, owners, and permissions docker exec acme ls -l /var/lib/pki/pki-tomcat \ @@ -188,6 +159,7 @@ jobs: diff expected output - name: Check ACME server conf dir after installation + if: always() run: | # check file types, owners, and permissions docker exec acme ls -l /etc/pki/pki-tomcat \ @@ -215,6 +187,7 @@ jobs: diff expected output - name: Check ACME server logs dir after installation + if: always() run: | # check file types, owners, and permissions docker exec acme ls -l /var/log/pki/pki-tomcat \ @@ -232,7 +205,9 @@ jobs: -rw-r--r-- pkiuser pkiuser catalina.$DATE.log -rw-r--r-- pkiuser pkiuser host-manager.$DATE.log -rw-r--r-- pkiuser pkiuser localhost.$DATE.log + -rw-r--r-- pkiuser pkiuser localhost_access_log.$DATE.txt -rw-r--r-- pkiuser pkiuser manager.$DATE.log + drwxr-xr-x pkiuser pkiuser pki EOF diff expected output @@ -255,6 +230,7 @@ jobs: diff expected output - name: Check ACME conf dir + if: always() run: | # check file types, owners, and permissions docker exec acme ls -l /etc/pki/pki-tomcat/acme \ @@ -292,6 +268,14 @@ jobs: run: | docker exec acme ls -l /var/log/pki/pki-tomcat/acme + - name: Check ACME system certs + if: always() + run: | + docker exec acme pki \ + -d /etc/pki/pki-tomcat/alias \ + -f /etc/pki/pki-tomcat/password.conf \ + nss-cert-find + - name: Check initial ACME accounts run: | docker exec acmeds ldapsearch \ @@ -827,7 +811,7 @@ jobs: run: | docker logs cads - - name: Check CA systemd journal + - name: Check CA server systemd journal if: always() run: | docker exec ca journalctl -x --no-pager -u pki-tomcatd@pki-tomcat.service @@ -847,7 +831,7 @@ jobs: run: | docker logs acmeds - - name: Check ACME systemd journal + - name: Check ACME server systemd journal if: always() run: | docker exec acme journalctl -x --no-pager -u pki-tomcatd@pki-tomcat.service diff --git a/.github/workflows/acme-tests.yml b/.github/workflows/acme-tests.yml index ce55f18ddbd..12100abb184 100644 --- a/.github/workflows/acme-tests.yml +++ b/.github/workflows/acme-tests.yml @@ -102,6 +102,11 @@ jobs: needs: build uses: ./.github/workflows/acme-separate-test.yml + acme-existing-nssdb-test: + name: ACME with existing NSS database + needs: build + uses: ./.github/workflows/acme-existing-nssdb-test.yml + acme-switchover-test: name: ACME server switchover needs: build diff --git a/base/server/examples/installation/acme.cfg b/base/server/examples/installation/acme.cfg index 3426740113d..826059cb435 100644 --- a/base/server/examples/installation/acme.cfg +++ b/base/server/examples/installation/acme.cfg @@ -1,7 +1,10 @@ [DEFAULT] pki_server_database_password=Secret.123 +pki_cert_chain_nickname=ca_signing [ACME] +pki_sslserver_nickname=sslserver + acme_database_type=ds acme_database_url=ldap://localhost.localdomain:3389 acme_database_bind_password=Secret.123 diff --git a/base/server/python/pki/server/deployment/__init__.py b/base/server/python/pki/server/deployment/__init__.py index 4458d3af0df..ea07a3beb18 100644 --- a/base/server/python/pki/server/deployment/__init__.py +++ b/base/server/python/pki/server/deployment/__init__.py @@ -2401,6 +2401,16 @@ def import_cert_chain(self, nssdb, subsystem): with open(cert_chain_path, 'r', encoding='utf-8') as f: cert_chain = f.read() + elif subsystem.type == 'ACME': + + # Get cert chain from ACME issuer + + props = subsystem.get_issuer_config() + url = props['url'] + + logger.info('Retrieving cert chain from %s', url) + cert_chain = self.get_ca_signing_cert(url) + elif (subsystem.type == 'CA' and subordinate or subsystem.type != 'CA') \ and not clone \ and not self.mdict['pki_server_pkcs12_path']: @@ -3638,10 +3648,11 @@ def issue_cert( request_type, request_data, profile, - subject, + subject=None, request_format='pem', dns_names=None, - requestor=None): + requestor=None, + credentials=None): tmpdir = tempfile.mkdtemp() try: @@ -3652,22 +3663,41 @@ def issue_cert( with open(request_file, 'w', encoding='utf-8') as f: f.write(request_data) - install_token = os.path.join(tmpdir, 'install-token') - with open(install_token, 'w', encoding='utf-8') as f: - f.write(self.install_token.token) + if self.install_token: + install_token = os.path.join(tmpdir, 'install-token') + with open(install_token, 'w', encoding='utf-8') as f: + f.write(self.install_token.token) cmd = [ 'pki', '-d', self.instance.nssdb_dir, '-f', self.instance.password_conf, '-U', url, + ] + + if credentials: + nickname = credentials.get('nickname') + if nickname: + cmd.extend(['-n', nickname]) + + username = credentials.get('username') + if username: + cmd.extend(['-u', username]) + + password = credentials.get('password') + if password: + cmd.extend(['-w', password]) + + cmd.extend([ '--ignore-banner', 'ca-cert-issue', '--request-type', request_type, '--csr-file', request_file, - '--profile', profile, - '--subject', subject - ] + '--profile', profile + ]) + + if subject: + cmd.extend(['--subject', subject]) if dns_names: cmd.extend(['--dns-names', ','.join(dns_names)]) @@ -3675,10 +3705,10 @@ def issue_cert( if requestor: cmd.extend(['--requestor', requestor]) - cmd.extend([ - '--install-token', install_token, - '--output-format', 'PEM' - ]) + if self.install_token: + cmd.extend(['--install-token', install_token]) + + cmd.extend(['--output-format', 'PEM']) if logger.isEnabledFor(logging.DEBUG): cmd.append('--debug') @@ -4264,7 +4294,7 @@ def get_ca_signing_cert(self, ca_url): '-d', self.instance.nssdb_dir, '-f', self.instance.password_conf, '-U', ca_url, - '--ignore-cert-status', 'UNTRUSTED_ISSUER', + '--ignore-cert-status', 'UNTRUSTED_ISSUER,UNKNOWN_ISSUER', '--ignore-banner', 'ca-cert-signing-export', '--pkcs7' @@ -5336,7 +5366,6 @@ def deploy_acme_webapp(self, subsystem): if len(self.instance.get_subsystems()) == 1: # if this is the first subsystem, deploy the subsystem without waiting subsystem.enable() - self.instance.start() else: # otherwise, deploy the subsystem and wait until it starts subsystem.enable( @@ -5344,6 +5373,98 @@ def deploy_acme_webapp(self, subsystem): max_wait=self.startup_timeout, timeout=self.request_timeout) + def create_acme_sslserver_csr(self, nssdb): + + # TODO: merge with generate_sslserver_request() + + subject_dn = self.mdict.get('pki_sslserver_subject_dn') + + csr_file = self.instance.csr_file('sslserver') + (key_type, key_size, curve, hash_alg) = self.get_key_params('sslserver') + + key_usage_ext = { + 'digitalSignature': True, + 'nonRepudiation': True, + 'keyEncipherment': True, + 'dataEncipherment': True, + 'critical': True + } + + extended_key_usage_ext = { + 'serverAuth': True + } + + nssdb.create_request( + subject_dn=subject_dn, + request_file=csr_file, + key_type=key_type, + key_size=key_size, + curve=curve, + hash_alg=hash_alg, + key_usage_ext=key_usage_ext, + extended_key_usage_ext=extended_key_usage_ext, + use_jss=True) + + with open(csr_file, 'r', encoding='utf-8') as f: + csr_pem = f.read() + + return csr_pem + + def create_acme_sslserver_cert(self, subsystem, request_data): + + props = subsystem.get_issuer_config() + url = props['url'] + credentials = {} + + if 'nickname' in props: + credentials['nickname'] = props['nickname'] + + if 'username' in props: + credentials['username'] = props['username'] + + if 'password' in props: + credentials['password'] = props['password'] + + if 'passwordFile' in props: + credentials['passwordFile'] = props['passwordFile'] + + # TODO: do not hardcode request type and profile name + return self.issue_cert( + url=url, + request_type='pkcs10', + request_data=request_data, + profile='caServerCert', + credentials=credentials) + + def setup_acme_sslserver_cert(self, nssdb, subsystem): + + nickname = self.instance.get_sslserver_cert_nickname() + logger.info('Checking existing SSL server cert: %s', nickname) + + cert_pem = nssdb.get_cert(nickname) + if cert_pem: + # SSL server cert already exists + return + + logger.info('Creating SSL server cert request') + csr_pem = self.create_acme_sslserver_csr(nssdb) + + logger.info('Issuing SSL server cert') + cert_pem = self.create_acme_sslserver_cert(subsystem, csr_pem) + + logger.info('Importing SSL server cert as %s', nickname) + nssdb.add_cert(nickname=nickname, cert_data=cert_pem) + + def setup_acme_certs(self, subsystem): + + nssdb = self.instance.open_nssdb() + try: + self.import_cert_chain(nssdb, subsystem) + self.setup_acme_sslserver_cert(nssdb, subsystem) + + finally: + nssdb.close() + def spawn_acme(self): subsystem = self.create_acme_subsystem() @@ -5355,6 +5476,17 @@ def spawn_acme(self): self.deploy_acme_webapp(subsystem) + if len(self.instance.get_subsystems()) > 1: + # shared instance -> server already set up and running + return + + self.setup_acme_certs(subsystem) + + self.instance.start( + wait=True, + max_wait=self.startup_timeout, + timeout=self.request_timeout) + def undeploy_acme_webapp(self, subsystem): ''' See also pki-server acme-undeploy.