We have all been there. That newly installed application required confidential material to function. Where should that material be securely stored? Or, you just took over ownership of a system where the database credentials are stored in plain text! We all know (or should know) that protecting secrets is important. Just about anyone, intentional or not, could be a threat actor. Our trust and integrity depend on securing our secrets.
To that end, for my immediate needs, I wanted a way to store confidential infrastructure material for:
- Application API token long term storage
- CI (continuous integration) and workflow-type scenarios where API tokens are required
Initially, I looked at Bitwarden Secrets Manager as a hosted solution. But I read that for their free tier, there was an undocumented API rate limit, that I did not want to be bothered with. Maybe in the future.
I then turned to Hashicorp’s Vault to fulfill my needs. I can run it on my hardware, and it costs me nothing.
Vault is described as:
- HashiCorp Vault is an identity-based secrets and encryption management system. It provides encryption services that are gated by authentication and authorization methods to ensure secure, auditable and restricted access to secrets.
Here is what I did to get it installed.
To complete this how-to, I used three Raspberry Pi running Ubuntu 24.04. A PKI solution is also required to generate certificates. I use step-ca, so adjust to your circumstance.
Certificates#
- On the certificate server, create folders for each server and add the root certificate to them
sudo -i
mkdir -p ~/certs/prod-core-services0{1,2,3}
step ca root ~/certs/prod-core-services01/root_ca.crt \
--fingerprint $(step certificate fingerprint /etc/step-ca/certs/root_ca.crt) \
--ca-url https://ca.lab.howto.engineer
cp -v ~/certs/prod-core-services01/root_ca.crt certs/prod-core-services02/
cp -v ~/certs/prod-core-services01/root_ca.crt certs/prod-core-services03/- Create certificates with appropriate SAN entries for each server and with 30 days of lifetime before they expire
step ca certificate prod-core-services01.lab.howto.engineer \
~/certs/prod-core-services01/vault.crt \
~/certs/prod-core-services01/vault.key \
--ca-url https://ca.lab.howto.engineer \
--root /etc/step-ca/certs/root_ca.crt \
--not-after="720h" \
--san="prod-core-services01" \
--san="192.168.100.10"
step ca certificate prod-core-services02.lab.howto.engineer \
~/certs/prod-core-services02/vault.crt \
~/certs/prod-core-services02/vault.key \
--ca-url https://ca.lab.howto.engineer \
--root /etc/step-ca/certs/root_ca.crt \
--not-after="720h" \
--san="prod-core-services02" \
--san="192.168.100.11"
step ca certificate prod-core-services03.lab.howto.engineer \
~/certs/prod-core-services03/vault.crt \
~/certs/prod-core-services03/vault.key \
--ca-url https://ca.lab.howto.engineer \
--root /etc/step-ca/certs/root_ca.crt \
--not-after="720h" \
--san="prod-core-services03" \
--san="192.168.100.12"Move the certs directory to a location where it can be copied to destination servers
From another terminal, copy the node specific folder to each destination server
scp -r ca:~/certs/prod-core-services01 prod-core-services01:
scp -r ca:~/certs/prod-core-services02 prod-core-services02:
scp -r ca:~/certs/prod-core-services03 prod-core-services03:- Remove the certificates from the certificate server
find certs -type f -exec shred -u {} \;
rm -rf certsPreparation#
- On the Vault servers, create a directory for the certificate files
sudo mkdir -p /etc/step/certs/vault- Copy the certificates to the directory created in the prior step
# On prod-core-services01
sudo mv -v ~/prod-core-services01/* /etc/step/certs/vault/
rm -rf ~/prod-core-services01
# On prod-core-services02
sudo mv -v ~/prod-core-services02/* /etc/step/certs/vault/
rm -rf ~/prod-core-services02
# On prod-core-services03
sudo mv -v ~/prod-core-services03/* /etc/step/certs/vault/
rm -rf ~/prod-core-services03- Modify the ownership/permissions on the certificate files
sudo chown root:root /etc/step/certs/vault/vault.crt \
/etc/step/certs/vault/root_ca.crt
sudo chown root:vault /etc/step/certs/vault/vault.key
sudo chmod 0644 /etc/step/certs/vault/vault.crt \
/etc/step/certs/vault/root_ca.crt
sudo chmod 0640 /etc/step/certs/vault/vault.key- If not completed prior, I needed to configure Ubuntu’s APT for Hashicorp’s software sources on all nodes.
sudo apt update && sudo apt install --yes wget gpg coreutils
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
wget --quiet -O - https://apt.releases.hashicorp.com/gpg | \
sudo gpg --dearmor --output \
/usr/share/keyrings/hashicorp-archive-keyring.gpg- Now let me install vault on all nodes.
sudo apt update && sudo apt install --yes vault- Grant the vault user ownership of its working directory on all nodes.
sudo chown -R vault:vault /opt/vault/I now have everything required to configure the vault. Let’s proceed to the next section.
Configuring Hashicorp Vault#
- Backup the default Vault configuration file on all servers.
sudo mv /etc/vault.d/vault.hcl /etc/vault.d/vault.hcl.bak- Now, I am going to create a unique configuration file per server.
# Node 1
sudo tee /etc/vault.d/vault.hcl << EOF > /dev/null
ui = true
cluster_addr = "https://prod-core-services01:8201"
api_addr = "https://prod-core-services01:8200"
disable_mlock = true
storage "raft" {
path = "/opt/vault/data"
retry_join {
leader_tls_servername = "prod-core-services02"
leader_api_addr = "https://prod-core-services02:8200"
leader_ca_cert_file = "/etc/step/certs/vault/root_ca.crt"
leader_client_cert_file = "/etc/step/certs/vault/vault.crt"
leader_client_key_file = "/etc/step/certs/vault/vault.key"
}
retry_join {
leader_tls_servername = "prod-core-services03"
leader_api_addr = "https://prod-core-services03:8200"
leader_ca_cert_file = "/etc/step/certs/vault/root_ca.crt"
leader_client_cert_file = "/etc/step/certs/vault/vault.crt"
leader_client_key_file = "/etc/step/certs/vault/vault.key"
}
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_cert_file = "/etc/step/certs/vault/vault.crt"
tls_key_file = "/etc/step/certs/vault/vault.key"
tls_client_ca_file = "/etc/step/certs/vault/root_ca.crt"
}
service_registration "consul" {
address = "http://127.0.0.1:8500"
}
EOF# Node 2
sudo tee /etc/vault.d/vault.hcl << EOF > /dev/null
ui = true
cluster_addr = "https://prod-core-services02:8201"
api_addr = "https://prod-core-services02:8200"
disable_mlock = true
storage "raft" {
path = "/opt/vault/data"
retry_join {
leader_tls_servername = "prod-core-services01"
leader_api_addr = "https://prod-core-services01:8200"
leader_ca_cert_file = "/etc/step/certs/vault/root_ca.crt"
leader_client_cert_file = "/etc/step/certs/vault/vault.crt"
leader_client_key_file = "/etc/step/certs/vault/vault.key"
}
retry_join {
leader_tls_servername = "prod-core-services03"
leader_api_addr = "https://prod-core-services03:8200"
leader_ca_cert_file = "/etc/step/certs/vault/root_ca.crt"
leader_client_cert_file = "/etc/step/certs/vault/vault.crt"
leader_client_key_file = "/etc/step/certs/vault/vault.key"
}
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_cert_file = "/etc/step/certs/vault/vault.crt"
tls_key_file = "/etc/step/certs/vault/vault.key"
tls_client_ca_file = "/etc/step/certs/vault/root_ca.crt"
}
service_registration "consul" {
address = "http://127.0.0.1:8500"
}
EOF# Node 3
sudo tee /etc/vault.d/vault.hcl << EOF > /dev/null
ui = true
cluster_addr = "https://prod-core-services03:8201"
api_addr = "https://prod-core-services03:8200"
disable_mlock = true
storage "raft" {
path = "/opt/vault/data"
retry_join {
leader_tls_servername = "prod-core-services01"
leader_api_addr = "https://prod-core-services01:8200"
leader_ca_cert_file = "/etc/step/certs/vault/root_ca.crt"
leader_client_cert_file = "/etc/step/certs/vault/vault.crt"
leader_client_key_file = "/etc/step/certs/vault/vault.key"
}
retry_join {
leader_tls_servername = "prod-core-services02"
leader_api_addr = "https://prod-core-services02:8200"
leader_ca_cert_file = "/etc/step/certs/vault/root_ca.crt"
leader_client_cert_file = "/etc/step/certs/vault/vault.crt"
leader_client_key_file = "/etc/step/certs/vault/vault.key"
}
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_cert_file = "/etc/step/certs/vault/vault.crt"
tls_key_file = "/etc/step/certs/vault/vault.key"
tls_client_ca_file = "/etc/step/certs/vault/root_ca.crt"
}
service_registration "consul" {
address = "http://127.0.0.1:8500"
}
EOF- To work with vault from the terminal, a few variables are required to be set.
export VAULT_ADDR="https://$(hostname):8200"
export VAULT_CACERT="/etc/step/certs/vault/vault.crt"
export CA_CERT=$(sudo cat /etc/step/certs/vault/certs/root_ca.crt)
export DBUS_SESSION_BUS_ADDRESS=/dev/null- Start the Vault service and monitor its startup logs.
sudo systemctl enable vault
sudo systemctl start vault
sudo systemctl status vault- Check the vault’s status. Note the
SealedandUnseal Progresslines.
vault status- In its current state, the vault is running but sealed or locked. I will require unlock key(s) (something like a password) to unlock it. I’ll initialize the vault to get these unlock key(s). Here I am overridding the default values for
recovery-sharesandrecovery-threshold. For more details, please review the documentation for vault operator init.
vault operator init -recovery-shares=1 -recovery-threshold=1Note the output of the previous command. It will generate a list of unlock key(s) and a root token. Once unsealed, the root token is used to log into vault. Save this information in a secure location.
- Now, to unseal the vault, execute the following and enter one of the Vault unseal key(s).
vault operator unsealIf required, repeat the prior step with additional unseal keys until the vault is unsealed.
If all went well, log into Vault with the root token by pointing your browser to one of the Vault servers (https://
:8200/ui/) or by using the terminal.
vault loginCertificate maintenance#
As noted earlier when I created the certificates, I opted for 30 days before they need to be renewed. To make renewals easier, let me use the step cli and systemd to automate the process.
- Install the step cli on all nodes
wget https://dl.smallstep.com/gh-release/cli/gh-release-header/v0.27.2/step-cli_0.27.2_arm64.deb
sudo dpkg -i step-cli_0.27.2_arm64.deb
rm step-cli_0.27.2_arm64.deb- Write out a systemd service and timer files for scheduling
step
# Service
sudo tee /etc/systemd/system/[email protected] << EOF > /dev/null
[Unit]
Description=Certificate renewer for %I
After=network-online.target
Documentation=https://smallstep.com/docs/step-ca/certificate-authority-server-production
StartLimitIntervalSec=0
PartOf=cert-renewer.target
[Service]
Type=oneshot
User=root
Environment=STEPPATH=/etc/step \
CERT_LOCATION=/etc/step/certs/%i/%i.crt \
KEY_LOCATION=/etc/step/certs/%i/%i.key
; ExecCondition checks if the certificate is ready for renewal,
; based on the exit status of the command.
; (In systemd <242, you can use ExecStartPre= here.)
ExecCondition=/usr/bin/step certificate needs-renewal ${CERT_LOCATION}
; ExecStart renews the certificate, if ExecStartPre was successful.
ExecStart=/usr/bin/step ca renew --ca-url https://ca.lab.howto.engineer --force ${CERT_LOCATION} ${KEY_LOCATION}
; Try to reload or restart the systemd service that relies on this cert-renewer
; If the relying service doesn't exist, forge ahead.
; (In systemd <229, use `reload-or-try-restart` instead of `try-reload-or-restart`)
ExecStartPost=/usr/bin/env sh -c "! systemctl --quiet is-active %i.service || systemctl try-reload-or-restart %i"
[Install]
WantedBy=multi-user.target
EOF
# Timer
sudo tee /etc/systemd/system/[email protected] << EOF > /dev/null
[Unit]
Description=Timer for certificate renewal of %I
Documentation=https://smallstep.com/docs/step-ca/certificate-authority-server-production
PartOf=cert-renewer.target
[Timer]
Persistent=true
; Run the timer unit every 15 minutes.
OnCalendar=*:1/15
; Always run the timer on time.
AccuracySec=1us
; Add jitter to prevent a "thundering herd" of simultaneous certificate renewals.
RandomizedDelaySec=5m
[Install]
WantedBy=timers.target
EOF- Enable the timer service
sudo systemctl enable --now [email protected]- List the active timers
systemctl list-timers | grep cert-renewerFrom here on out, step will check every 15 minutes to query the remaining lifetime of the vault certificate. When the default 66% of lifetime has passed, step will start trying to renew the certificate.
Conclusion#
In this article, I showed how to:
- Create certificates using step-ca
- Installing Hashicorp Vault
- Configuring Hashicorp Vault using the generated certificates
- Providing a way to automate certificate renewals
Thanks
