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, --help
Display help message-V, --version
View version-C, --ca
Set current CA base directory-q, --quiet
Set verbose level to 0-y, --yes
Validate all responses-v, --verbose
Define verbose level (must be repeat)-c, --cipher [CIPHER]
Define cipher for key (none for no encryption)--force-crt
Regenerate CRT even if it exists (e.g., change in SANs)--force-csr
Regenerate CSR even if it exists (e.g., change in SANs)--json
Format using JSON--no-utf8
Disable default UTF8 encoding--x-debug
Enable 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.idz
fileca-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 file
In 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 file
Add 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