Install npm using apt:
$ sudo apt install nodejs npm
$ git clone https://github.com/zenetys/zpki.git
$ cd zpki
Once the dependencies are installed, build and start the project using the following command:
$ npm run build
This will install dependencies and start the API.
If you want to simply install all dependencies without building the project, you can use:
$ npm install
The interface should now be accessible on port 3000. You can verify this by opening a web browser and navigating to localhost:3000 or 127.0.0.1:3000.
Now, you can create the first certificate authority (CA), see next section to get started.
$ zpki [options] ACTION [parameters]
-h, --helpDisplay help message-V, --versionView version-C, --caSet current CA base directory-q, --quietSet verbose level to 0-y, --yesValidate all responses-v, --verboseDefine verbose level (must be repeat)-c, --cipher [CIPHER]Define cipher for key (none for no encryption)--force-crtRegenerate CRT even if it exists (e.g., change in SANs)--force-csrRegenerate CSR even if it exists (e.g., change in SANs)--jsonFormat using JSON--no-utf8Disable default UTF8 encoding--x-debugEnable bash debug mode
create-cnf: Generate a default OpenSSL configuration filecreate-key [CN|SUBJ]: Create a key filecreate-csr [CN|SUBJ] <ALTNAMES>: Generate a certificate signing request (CSR) filecreate-self [CN|SUBJ] <ALTNAMES>: Create a self-signed certificatecreate-ca [CN|SUBJ]: Create a Certificate Authority (CA) and its storageca-create-crt [CN|SUBJ] <ALTNAMES>: Create a certificateca-update-crl: Generate the CRLca-update-db: Reload all certificates in theca.idzfileca-list: List certificates stored in the CAca-sign-csr [CN|SUBJ|CSRFILE]: Sign a CSR file using the CAca-update-crt [CN|SUBJ|CRTFILE]: Update a certificateca-revoke-crt [CN|SUBJ|CRTFILE]: Revoke a certificateca-disable-crt [CN|SUBJ|CRTFILE]: Disable a certificate permanentlyca-display-crt [CRTFILE]: Display an entire certificate file (.crt)ca-update-dump-crt [CRTFILE]: Update and dump the content of a certificate file (.crt)ca-dump-crt [CRTFILE]: Dump the content of a certificate (.crt) fileca-dump-csr [CSRFILE]: Dump the content of a certificate signing request (.csr) fileca-dump-key [KEYFILE]: Dump the content of a private key (.key) fileca-dump-pkcs12 [KEYFILE]: Dump the content of a certificate in the pkcs12 formatca-test-password: Test if the CA passphrase is correct
For Subject Alternative Names (SANs), add address types like: DNS:<FQDN>, IP:ADDR.
$ zpki -C ZPKI-Demo-CA -y create-ca "ZPKI Demo Certificate Authority"
: openssl genrsa -out ca.key -aes256 4096
Enter PEM pass phrase: *********
Verifying - Enter PEM pass phrase: *********
ca.cnf: already exists, bypass
ca.key: already exists, bypass
: openssl req -batch -new -x509 -days 366 -utf8 -out ca.crt -key ca.key -subj '/CN=ZPKI Demo CA' -config ca.cnf -extensions ca_ext
Enter pass phrase for ca.key: *********
: openssl ca -gencrl -config ca.cnf -out ca.crl -batch
Enter pass phrase for ./ca.key: *********In the following command, DNS and IP are used to specify the Subject Alternative Names (SANs).
$ zpki -C ZPKI-Demo-CA -y -c none ca-create-crt "zpki.acme.loc" DNS:zpki.acme.loc IP:10.109.42.104
: openssl genrsa -out private/zpki_acme_loc.key 4096
: openssl req -batch -new -utf8 -out certs/zpki_acme_loc.csr -key private/zpki_acme_loc.key -subj /CN=zpki.acme.loc -addext 'subjectAltName=DNS:zpki.acme.loc,IP:10.109.42.104'
: openssl ca -config ca.cnf -batch -in certs/zpki_acme_loc.csr -out certs/zpki_acme_loc.crt -days 366 -extensions server_ext
Enter pass phrase for ./ca.key: *********
: openssl ca -updatedb -config ca.cnf -batch
Enter pass phrase for ./ca.key: *********
Updated ca.idz fileIn the following command, ZPKI_EXT and ZPKI_CA_PASSWORD are used to define the certificate extension and the CA password respectively.
$ ZPKI_EXT=server_ext ZPKI_CA_PASSWORD=x9ZAyX289 zpki -C ZPKI-Demo-CA -y -c none ca-update-crt "zpki.acme.loc" DNS:zpki.acme.loc IP:10.109.42.104
: openssl ca -config ca.cnf -batch -revoke certs/zpki_acme_loc.crt -passin 'env:ZPKI_CA_PASSWORD'
: openssl ca -gencrl -config ca.cnf -out ca.crl -batch -passin 'env:ZPKI_CA_PASSWORD'
certs/zpki_acme_loc.csr: already exists, bypass
: openssl ca -config ca.cnf -batch -in certs/zpki_acme_loc.csr -out certs/zpki_acme_loc.crt -days 366 -extensions server_ext -passin 'env:ZPKI_CA_PASSWORD'
: openssl ca -updatedb -config ca.cnf -batch -passin 'env:ZPKI_CA_PASSWORD'
Updated ca.idz file$ zpki -C ZPKI-Demo-CA -y -c none ca-revoke-crt "zpki.acme.loc"
: openssl ca -config ca.cnf -batch -revoke certs/zpki_acme_loc.crt
Enter pass phrase for ./ca.key: *********
: openssl ca -gencrl -config ca.cnf -out ca.crl -batch
Enter pass phrase for ./ca.key: *********$ zpki -C ZPKI-Demo-CA ca-list --json | jq
[
{
"status": "R",
"expiration": "2026-01-18T15:50:49Z",
"revocation": "2025-01-17T15:52:07Z",
"serial": "D2AFB6054BD8",
"id": "zpki.acme.loc",
"hash": "ae0d1e17",
"issuer": "/CN=ZPKI Demo CA",
"cn": "zpki.acme.loc",
"subject": "/CN=zpki.acme.loc",
"startDate": "2025-01-17T15:50:49Z",
"endDate": "2026-01-18T15:50:49Z",
"keyStatus": "plain",
"type": "server_ext"
}
].
├── data/
│ └── zpki/
│ └── CA.zpki/
│ ├── CA-Example-1/ # First Certificate Authority
│ └── CA-Example-2/ # Second Certificate Authority
├── opt/
│ └── zpki/
│ ├── api.js # Node.js API
│ ├── ca-folders # List CA folders script
│ ├── icons/ # Icons folder
│ ├── index.html # Main page
│ ├── main.css # Main CSS
│ ├── main.js # Main Js
│ ├── node_modules/ # Node.js modules
│ ├── package.json # Node.js package
│ ├── package-lock.json # Node.js package lock
│ └── zpki # Main script
└── etc/
├── sudoers.d/
│ └── zpki # Sudoers configuration file
└── systemd/system/
└── zpki-core.service # Systemd service configuration fileAdd a user that will read and write to the /data/zpki directory:
$ groupadd -r zpki-data
$ useradd -r -g zpki-data -d /data/CA.zpki -s /sbin/nologin zpki-data
Add a user that will run the server:
$ groupadd -r zpki-core
$ useradd -r -g zpki-core -d /opt/zpki -s /sbin/nologin zpki-core
Edit the /etc/sudoers.d/zpki file:
Cmnd_Alias ZPKI=/opt/zpki/zpki, /opt/zpki/ca-folders
Defaults!ZPKI env_reset, env_keep="ZPKI_*", !requiretty, !pam_session
zpki-core ALL=(zpki-data) NOPASSWD: ZPKI
Edit the /etc/systemd/system/zpki-core.service file:
[Unit]
Description=zpki-core
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/node /opt/zpki/api.js
User=zpki-core
UMask=0007
Restart=always
SyslogIdentifier=zpki-core
Environment="LISTEN_ADDRESS=127.0.0.1"
Environment="LISTEN_PORT=3000"
Environment="PASSWORD_EXPIRE_MS=600000"
Environment="COOKIE_MAX_AGE_MS=86400000"
Environment="LOG_HTTP_REQUESTS=1"
Environment="CA_BASEDIR=/data/CA.zpki"
Environment="CA_FOLDERS_CMD=sudo -n -u zpki-data /opt/zpki/ca-folders"
Environment="ZPKI_CMD=sudo -n -u zpki-data /opt/zpki/zpki"
Environment="ZPKI_OPENSSL_CMD=/usr/bin/openssl11"
# Environment="TRUST_PROXY=loopback,{IP Adresses}"
[Install]
WantedBy=multi-user.target
$ systemctl daemon-reload
Sample reverse proxy configuration for apache:
Redirect /zpki /zpki/
ProxyPass /zpki/ http://127.0.0.1:3000/ connectiontimeout=5 timeout=30
ProxyPassReverse /zpki/ http://127.0.0.1:3000/If apache configuration is updated, restart the service with:
$ systemctl reload httpd
To enable and start the service, use the following commands:
$ systemctl enable zpki-core
$ systemctl start zpki-core
To restart the service, use:
$ systemctl restart zpki-core
Check if the service is running:
$ systemctl status zpki-core
Check logs for errors or verify the service is running:
$ journalctl -u zpki-core -f