En privat certificate authority lar deg signere dine egne TLS-sertifikater uten å betale for hvert enkelt, og uten å stole på en tredjepart. For interne API-er, mikrotjenester, utviklingsmiljøer og IoT-flåter er dette standardtilnærmingen i 2026. I denne guiden setter du opp en fullstendig PKI-hierarki fra rot-CA til sluttsertifikat med OpenSSL 3.5 og integrerer det i Node.js på under 30 minutter.

Hva er en Certificate Authority og hvorfor lage din egen?

En certificate authority (CA) er en enhet som signerer digitale sertifikater og dermed bekrefter identiteten til en server, klient eller person. Nettleseren din stoler på omtrent 150 offentlige CA-er som er forhåndsinstallert i operativsystemet. Disse passer til offentlige nettsider, men for intern infrastruktur har de tre store ulemper: de koster penger per sertifikat, de krever at domenet er offentlig resolverbart, og de eksponerer intern infrastruktur mot offentlig Certificate Transparency-logging.

En privat CA løser alle tre: du signerer sertifikater gratis, for vilkårlige interne domener som api.intern.local, og uten at de havner i offentlige CT-logger. Prisen er at du selv må distribuere rot-sertifikatet til alle klienter og vedlikeholde nøkkelinfrastrukturen.

I 2026 er dette standardpraksis for norske teknologiselskaper, banker og offentlige etater som håndterer intern tjenestekommunikasjon. Alternativet, Let’s Encrypt, krever offentlig DNS og fungerer ikke for private nett. HashiCorp Vault PKI og AWS Private CA gir mer funksjonalitet, men for de fleste brukstilfeller er OpenSSL mer enn nok.

Forutsetninger og versjoner

Før du starter, sørg for at du har følgende på plass:

  • OpenSSL 3.5.x (LTS, støttet til april 2030). OpenSSL 4.0.1 er den nyeste utgivelsen per juni 2026, men 3.5 er LTS-linjen som anbefales for langlivet CA-infrastruktur. Sjekk versjon med openssl version.
  • Node.js 22 LTS eller nyere. TLS-modulen i Node.js bruker OpenSSL internt og er fullt kompatibel med sertifikater generert av OpenSSL 3.5.
  • Linux eller macOS med tilgang til terminalen. Windows-brukere kan bruke WSL 2.
  • Root-tilgang eller sudo-rettigheter for å installere rot-sertifikatet i systemets truststore.
  • Grunnleggende kjennskap til kommandolinjen og konseptet offentlig-privat nøkkelpar.

Installer OpenSSL 3.5 på Ubuntu/Debian:

# Sjekk eksisterende versjon
openssl version
# Output: OpenSSL 3.x.x  (versjon avhengig av distro)

# Oppdater til 3.5 via kompilering (Ubuntu 24.04 har 3.0 som standard)
# Alternativt, bruk snap: snap install openssl --classic

# Ubuntu 24.04+ pakkeversjon
sudo apt update && sudo apt install openssl libssl-dev
openssl version

PKI-hierarki: Rot-CA, mellom-CA og sluttsertifikat

Før du skriver en eneste kommando, er det viktig å forstå strukturen du skal sette opp. Et riktig PKI-hierarki består av tre lag:

LagNavnFunksjonNøkkelstørrelseGyldighetsperiode
1Rot-CA (Root CA)Signerer mellom-CA. Holdes frakoblet (offline).RSA 4096 eller P-38420 år
2Mellom-CA (Intermediate CA)Signerer sluttsertifikater. Er online.RSA 4096 eller P-25610 år
3SluttsertifikatIdentifiserer server eller klient. Distribueres med applikasjon.RSA 2048 eller P-2561 år

Grunnen til at rot-CA holdes offline er enkel: hvis mellom-CA-nøkkelen kompromitteres, kan du tilbakekalle mellom-CA-en ved hjelp av rot-CA og utstede en ny. Hadde rot-CA-nøkkelen blitt kompromittert, måtte du startet helt fra scratch og distribuert et nytt rot-sertifikat til alle klienter. Dette er den grunnleggende sikkerhetsfordelingen i et veldefinert PKI-hierarki.

I denne guiden lager vi et to-lags hierarki (rot-CA og mellom-CA) og viser deretter hvordan mellom-CA signerer sluttsertifikater for dine tjenester.

Steg 1: Opprett mappestruktur for CA

OpenSSL sin CA-funksjonalitet forutsetter en bestemt mappestruktur for å holde orden på sertifikater, private nøkler, serienumre og tilbakekallelseslister. Opprett denne strukturen:

mkdir -p ~/myca/{root-ca/{certs,crl,newcerts,private},intermediate/{certs,crl,csr,newcerts,private},endcerts}
cd ~/myca

# Root CA filer
touch root-ca/index.txt
echo 1000 > root-ca/serial
echo 1000 > root-ca/crlnumber

# Intermediate CA filer
touch intermediate/index.txt
echo 1000 > intermediate/serial
echo 1000 > intermediate/crlnumber

# Sett riktige tilganger på private mapper
chmod 700 root-ca/private intermediate/private

# Bekreft strukturen
find ~/myca -type d | sort

index.txt er CA-ens database over utstedte sertifikater. serial holder løpende serienummer som økes for hvert nytt sertifikat. crlnumber brukes for Certificate Revocation Lists. Del aldri innholdet i private/-mappene med andre systemer eller prosesser.

Steg 2: Konfigurer OpenSSL for rot-CA

OpenSSL bruker en konfigurasjonsfil (openssl.cnf) for å styre CA-atferd. Lag én for rot-CA. Legg merke til pathlen:0 i v3_intermediate_ca som hindrer mellom-CA fra å utstede nye CA-sertifikater:

cat > ~/myca/root-ca/openssl.cnf << 'EOF'
[ ca ]
default_ca = CA_default

[ CA_default ]
dir               = /root/myca/root-ca
certs             = $dir/certs
crl_dir           = $dir/crl
new_certs_dir     = $dir/newcerts
database          = $dir/index.txt
serial            = $dir/serial
RANDFILE          = $dir/private/.rand
private_key       = $dir/private/ca.key.pem
certificate       = $dir/certs/ca.cert.pem
crlnumber         = $dir/crlnumber
crl               = $dir/crl/ca.crl.pem
crl_extensions    = crl_ext
default_crl_days  = 30
default_md        = sha256
name_opt          = ca_default
cert_opt          = ca_default
default_days      = 375
preserve          = no
policy            = policy_strict

[ policy_strict ]
countryName             = match
stateOrProvinceName     = match
organizationName        = match
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ policy_loose ]
countryName             = optional
stateOrProvinceName     = optional
localityName            = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ req ]
default_bits        = 4096
distinguished_name  = req_distinguished_name
string_mask         = utf8only
default_md          = sha256
x509_extensions     = v3_ca

[ req_distinguished_name ]
countryName                     = Landkode (2 bokstaver)
stateOrProvinceName             = Fylke
localityName                    = By
0.organizationName              = Organisasjon
organizationalUnitName          = Avdeling
commonName                      = Felles navn
emailAddress                    = E-postadresse
countryName_default             = NO
stateOrProvinceName_default     = Oslo
0.organizationName_default      = MinBedrift CA

[ v3_ca ]
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid:always,issuer
basicConstraints        = critical, CA:true
keyUsage                = critical, digitalSignature, cRLSign, keyCertSign

[ v3_intermediate_ca ]
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid:always,issuer
basicConstraints        = critical, CA:true, pathlen:0
keyUsage                = critical, digitalSignature, cRLSign, keyCertSign

[ usr_cert ]
basicConstraints        = CA:FALSE
nsCertType              = client, email
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid,issuer
keyUsage                = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage        = clientAuth, emailProtection

[ server_cert ]
basicConstraints        = CA:FALSE
nsCertType              = server
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid,issuer:always
keyUsage                = critical, digitalSignature, keyEncipherment
extendedKeyUsage        = serverAuth

[ crl_ext ]
authorityKeyIdentifier  = keyid:always

[ ocsp ]
basicConstraints        = CA:FALSE
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid,issuer
keyUsage                = critical, digitalSignature
extendedKeyUsage        = critical, OCSPSigning
EOF

Steg 3: Generer rot-CA-nøkkel og selvsignert sertifikat

Nå lager vi selve hjertet i PKI-en: rot-CA-nøkkelen og det tilhørende selvsignerte sertifikatet. Bruk RSA 4096 for maksimal kompatibilitet, eller P-384 (ECDSA) for bedre ytelse. Begge gir tilsvarende sikkerhetsnivå i 2026:

cd ~/myca

# Alternativ A: RSA 4096 (høyest kompatibilitet)
openssl genpkey -algorithm RSA \
  -pkeyopt rsa_keygen_bits:4096 \
  -aes256 \
  -out root-ca/private/ca.key.pem
chmod 400 root-ca/private/ca.key.pem

# Alternativ B: ECDSA P-384 (raskere, mindre nøkler)
# openssl genpkey -algorithm EC \
#   -pkeyopt ec_paramgen_curve:P-384 \
#   -aes256 \
#   -out root-ca/private/ca.key.pem

# Selvsignert rot-CA-sertifikat (20 år = 7300 dager)
openssl req -config root-ca/openssl.cnf \
  -key root-ca/private/ca.key.pem \
  -new -x509 -days 7300 \
  -sha256 \
  -extensions v3_ca \
  -out root-ca/certs/ca.cert.pem
chmod 444 root-ca/certs/ca.cert.pem

# Verifiser rot-CA-sertifikatet
openssl x509 -noout -text -in root-ca/certs/ca.cert.pem | grep -E "(Issuer|Subject|Not Before|Not After|CA:)"

Du vil bli bedt om å angi et passord for nøkkelen (bruk et sterkt passord og lagre det sikkert), og deretter fylle ut Distinguished Name-feltene. Bruk NO for landkode og ditt organisasjonsnavn. Forventet output fra verifikasjonskommandoen:

# Forventet output:
# Issuer: C=NO, ST=Oslo, O=MinBedrift CA, CN=MinBedrift Root CA
# Subject: C=NO, ST=Oslo, O=MinBedrift CA, CN=MinBedrift Root CA
# Not Before: Jun 20 10:00:00 2026 GMT
# Not After : Jun 18 10:00:00 2046 GMT
# CA:TRUE

Steg 4: Opprett og signer mellom-CA

Mellom-CA er den operative CA-en som utsteder sertifikater til daglig. Rot-CA brukes bare for å signere mellom-CA og holdes ellers offline. Dette lagdelingen er kritisk for sikkerheten:

# Generer mellom-CA-nøkkel
openssl genpkey -algorithm RSA \
  -pkeyopt rsa_keygen_bits:4096 \
  -aes256 \
  -out intermediate/private/intermediate.key.pem
chmod 400 intermediate/private/intermediate.key.pem

# Lag Certificate Signing Request (CSR) for mellom-CA
openssl req -config root-ca/openssl.cnf \
  -new -sha256 \
  -key intermediate/private/intermediate.key.pem \
  -out intermediate/csr/intermediate.csr.pem

# Signer mellom-CA med rot-CA (10 år = 3650 dager)
openssl ca -config root-ca/openssl.cnf \
  -extensions v3_intermediate_ca \
  -days 3650 \
  -notext \
  -md sha256 \
  -in intermediate/csr/intermediate.csr.pem \
  -out intermediate/certs/intermediate.cert.pem
chmod 444 intermediate/certs/intermediate.cert.pem

# Lag sertifikatkjede (chain) - brukes av klienter
cat intermediate/certs/intermediate.cert.pem \
    root-ca/certs/ca.cert.pem > \
    intermediate/certs/ca-chain.cert.pem
chmod 444 intermediate/certs/ca-chain.cert.pem

# Verifiser mellom-CA mot rot-CA
openssl verify -CAfile root-ca/certs/ca.cert.pem \
  intermediate/certs/intermediate.cert.pem
# Forventet: intermediate/certs/intermediate.cert.pem: OK

Steg 5: Konfigurer mellom-CA for sertifikatutstedelse

Mellom-CA trenger sin egen konfigurasjonsfil som peker til riktige stier og bruker policy_loose slik at den kan utstede sertifikater til ulike organisasjoner og domener:

cat > ~/myca/intermediate/openssl.cnf << 'EOF'
[ ca ]
default_ca = CA_default

[ CA_default ]
dir               = /root/myca/intermediate
certs             = $dir/certs
crl_dir           = $dir/crl
new_certs_dir     = $dir/newcerts
database          = $dir/index.txt
serial            = $dir/serial
RANDFILE          = $dir/private/.rand
private_key       = $dir/private/intermediate.key.pem
certificate       = $dir/certs/intermediate.cert.pem
crlnumber         = $dir/crlnumber
crl               = $dir/crl/intermediate.crl.pem
crl_extensions    = crl_ext
default_crl_days  = 30
default_md        = sha256
name_opt          = ca_default
cert_opt          = ca_default
default_days      = 375
preserve          = no
policy            = policy_loose

[ policy_loose ]
countryName             = optional
stateOrProvinceName     = optional
localityName            = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ req ]
default_bits        = 2048
distinguished_name  = req_distinguished_name
string_mask         = utf8only
default_md          = sha256

[ req_distinguished_name ]
countryName                     = Landkode
stateOrProvinceName             = Fylke
localityName                    = By
0.organizationName              = Organisasjon
commonName                      = Felles navn

[ server_cert ]
basicConstraints        = CA:FALSE
nsCertType              = server
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid,issuer:always
keyUsage                = critical, digitalSignature, keyEncipherment
extendedKeyUsage        = serverAuth

[ usr_cert ]
basicConstraints        = CA:FALSE
nsCertType              = client, email
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid,issuer
keyUsage                = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage        = clientAuth, emailProtection

[ crl_ext ]
authorityKeyIdentifier  = keyid:always
EOF

Steg 6: Utsted et serversertifikat med Subject Alternative Names

Subject Alternative Names (SAN) er obligatorisk siden 2017 og kreves av alle moderne nettlesere og Node.js. Sertifikater som bare bruker Common Name (CN) uten SAN avvises. Her er det korrekte oppsettet for api.intern.local:

# Generer servernøkkel (RSA 2048 er tilstrekkelig for sluttsertifikater)
openssl genpkey -algorithm RSA \
  -pkeyopt rsa_keygen_bits:2048 \
  -out endcerts/api.intern.local.key.pem

# Lag CSR med Subject Alternative Names
openssl req -new -sha256 \
  -key endcerts/api.intern.local.key.pem \
  -subj "/C=NO/ST=Oslo/O=MinBedrift/CN=api.intern.local" \
  -addext "subjectAltName=DNS:api.intern.local,DNS:*.api.intern.local,IP:192.168.1.10" \
  -out endcerts/api.intern.local.csr.pem

# Signer med mellom-CA
openssl ca -config intermediate/openssl.cnf \
  -extensions server_cert \
  -days 365 \
  -notext \
  -md sha256 \
  -in endcerts/api.intern.local.csr.pem \
  -out endcerts/api.intern.local.cert.pem

# Verifiser sertifikatkjeden
openssl verify -CAfile intermediate/certs/ca-chain.cert.pem \
  endcerts/api.intern.local.cert.pem

# Forventet output:
# endcerts/api.intern.local.cert.pem: OK

# Vis SAN-feltet for å bekrefte
openssl x509 -noout -text -in endcerts/api.intern.local.cert.pem | grep -A1 "Subject Alternative"

Alternativet -addext krever OpenSSL 1.1.1 eller nyere. Med eldre versjoner må du bruke en ekstern konfigurasjonsfil med [SAN]-seksjonen. OpenSSL 3.5 støtter -addext fullt ut.

Steg 7: Installer rot-sertifikatet i systemets truststore

For at klienter skal stole på sertifikatene du utsteder, må rot-CA-sertifikatet installeres i systemets truststore. Dette er et engangssteg per klientmaskin:

# Ubuntu / Debian
sudo cp root-ca/certs/ca.cert.pem /usr/local/share/ca-certificates/minbedrift-ca.crt
sudo update-ca-certificates
# Output: 1 added, 0 removed; done.

# RHEL / Fedora / CentOS
sudo cp root-ca/certs/ca.cert.pem /etc/pki/ca-trust/source/anchors/minbedrift-ca.crt
sudo update-ca-trust

# macOS
sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain \
  root-ca/certs/ca.cert.pem

# Verifiser at systemet stoler på CA-en
echo | openssl s_client -connect api.intern.local:8443 \
  -CApath /etc/ssl/certs/ 2>&1 | grep "Verify return code"
# Forventet: Verify return code: 0 (ok)

# Firefox bruker egen truststore
# Innstillinger > Personvern og sikkerhet > Vis sertifikater > Importer
# Velg rot-CA/certs/ca.cert.pem og huk av for "Stol på denne CA for nettsteder"

Steg 8: Konfigurer Node.js TLS med privat CA

Node.js sitt TLS-lag aksepterer egendefinerte CA-sertifikater via ca-parameteret i tls.createSecureContext(). Du har to tilnærminger: direkte i koden via ca-parameteret, eller via miljøvariabelen NODE_EXTRA_CA_CERTS som er den enkleste metoden for containerbaserte miljøer.

HTTPS-server med privat CA-sertifikat

// server.js - HTTPS-server med sertifikat fra privat CA
const https = require('https');
const fs = require('fs');

const options = {
  // Serversertifikat og nøkkel fra privat CA
  cert: fs.readFileSync('/root/myca/endcerts/api.intern.local.cert.pem'),
  key: fs.readFileSync('/root/myca/endcerts/api.intern.local.key.pem'),
  // Tilbud CA-kjeden for klientvalidering
  ca: fs.readFileSync('/root/myca/intermediate/certs/ca-chain.cert.pem'),
  // For mTLS: krev klientsertifikat
  requestCert: false,
  rejectUnauthorized: false,
  // Minste TLS-versjon: 1.2
  minVersion: 'TLSv1.2',
};

const server = https.createServer(options, (req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ status: 'OK', tls: req.socket.getCipher() }));
});

server.listen(8443, () => {
  console.log('HTTPS-server kjører på https://api.intern.local:8443');
});

HTTPS-klient som stoler på privat CA

// client.js - Koble til intern API med privat CA
const https = require('https');
const fs = require('fs');
const tls = require('tls');

// Metode 1: Via createSecureContext (anbefalt for gjenbruk)
const secureContext = tls.createSecureContext({
  ca: fs.readFileSync('/root/myca/intermediate/certs/ca-chain.cert.pem'),
});

const options = {
  hostname: 'api.intern.local',
  port: 8443,
  path: '/health',
  method: 'GET',
  secureContext: secureContext,
  rejectUnauthorized: true, // ALDRI false i produksjon
};

const req = https.request(options, (res) => {
  console.log(`Status: ${res.statusCode}`);
  let data = '';
  res.on('data', (chunk) => data += chunk);
  res.on('end', () => console.log(JSON.parse(data)));
});

req.on('error', (err) => {
  // Vanlige feil:
  // CERT_HAS_EXPIRED: Sertifikatet er utløpt - forny det
  // UNABLE_TO_VERIFY_LEAF_SIGNATURE: CA-kjeden er ufullstendig
  // ERR_TLS_CERT_ALTNAME_INVALID: SAN mangler hostnavn
  console.error('TLS-feil:', err.code, err.message);
});

req.end();

// Metode 2: Via NODE_EXTRA_CA_CERTS (ingen kodeendringer)
// Sett i terminal: export NODE_EXTRA_CA_CERTS=/root/myca/intermediate/certs/ca-chain.cert.pem
// Da bruker Node.js den private CA automatisk for alle HTTPS-forespørsler

Steg 9: mTLS for tjeneste-til-tjeneste-autentisering

Gjensidig TLS (mTLS) krever at både server og klient presenterer gyldige sertifikater fra din private CA. Dette er standard i nulltillitsarkitekturer og Kubernetes-miljøer med service meshes som Istio eller Linkerd. Med din private CA er mTLS enkelt å implementere:

# Utsted klientsertifikat for tjeneste-A
openssl genpkey -algorithm RSA \
  -pkeyopt rsa_keygen_bits:2048 \
  -out endcerts/tjeneste-a.key.pem

openssl req -new -sha256 \
  -key endcerts/tjeneste-a.key.pem \
  -subj "/C=NO/ST=Oslo/O=MinBedrift/CN=tjeneste-a" \
  -out endcerts/tjeneste-a.csr.pem

openssl ca -config intermediate/openssl.cnf \
  -extensions usr_cert \
  -days 365 -notext -md sha256 \
  -in endcerts/tjeneste-a.csr.pem \
  -out endcerts/tjeneste-a.cert.pem
// mtls-server.js - Server som krever klientsertifikat
const https = require('https');
const fs = require('fs');

const server = https.createServer({
  cert: fs.readFileSync('/root/myca/endcerts/api.intern.local.cert.pem'),
  key: fs.readFileSync('/root/myca/endcerts/api.intern.local.key.pem'),
  ca: fs.readFileSync('/root/myca/intermediate/certs/ca-chain.cert.pem'),
  requestCert: true,       // Krev klientsertifikat
  rejectUnauthorized: true, // Avvis klienter uten gyldig sertifikat
}, (req, res) => {
  const cert = req.socket.getPeerCertificate();
  if (!cert || !cert.subject) {
    res.writeHead(401, { 'Content-Type': 'application/json' });
    return res.end(JSON.stringify({ error: 'Klientsertifikat mangler' }));
  }
  // Sjekk at klienten er fra din egen CA
  console.log('Autentisert klient:', cert.subject.CN);
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ message: `Velkommen, ${cert.subject.CN}` }));
});

server.listen(8443, () => console.log('mTLS-server klar på port 8443'));

Steg 10: Sertifikatfornying og tilbakekalling

Sertifikater med 365 dagers levetid må fornyes. Sett opp automatisk overvåking og CRL-generering:

# Sjekk utløpsdato
openssl x509 -noout -enddate -in endcerts/api.intern.local.cert.pem
# Forventet: notAfter=Jun 20 10:00:00 2027 GMT

# Sjekk antall dager til utløp
python3 -c "
from datetime import datetime
import subprocess
result = subprocess.run(['openssl','x509','-noout','-enddate','-in',
  'endcerts/api.intern.local.cert.pem'], capture_output=True, text=True)
date_str = result.stdout.strip().split('=')[1]
exp = datetime.strptime(date_str, '%b %d %H:%M:%S %Y %Z')
days = (exp - datetime.utcnow()).days
print(f'Utløper om {days} dager')
"

# Tilbakekall et kompromittert sertifikat
openssl ca -config intermediate/openssl.cnf \
  -revoke intermediate/newcerts/1000.pem

# Generer ny Certificate Revocation List (CRL)
openssl ca -config intermediate/openssl.cnf \
  -gencrl \
  -out intermediate/crl/intermediate.crl.pem

# Valider sertifikat mot CRL
cat intermediate/certs/ca-chain.cert.pem \
    intermediate/crl/intermediate.crl.pem > /tmp/chain-crl.pem
openssl verify -crl_check \
  -CAfile /tmp/chain-crl.pem \
  endcerts/api.intern.local.cert.pem

CRL er gyldig i 30 dager som standard (satt i default_crl_days). Distribuer CRL til et internt endepunkt og oppdater den automatisk via cron. For sanntidsstatus er OCSP en bedre løsning enn CRL, men krever en OCSP-responder-tjeneste.

Steg 11: Automatiser med bash-skript

For produksjonsbruk lønner det seg å automatisere sertifikatutstedelse. Her er et skript som håndterer nøkkel, CSR og sertifikat i én operasjon:

#!/bin/bash
# issue-cert.sh - Automatisk sertifikatutstedelse fra privat CA
# Bruk: ./issue-cert.sh  [ip-adresse]

set -euo pipefail

DOMAIN="${1:?Bruk: $0  [ip]}"
IP="${2:-}"
CA_DIR="/root/myca"
CERT_DIR="$CA_DIR/endcerts"
DAYS=365

# Generer nøkkel (ukryptert for automatisering)
openssl genpkey -algorithm RSA \
  -pkeyopt rsa_keygen_bits:2048 \
  -out "$CERT_DIR/$DOMAIN.key.pem" 2>/dev/null
chmod 400 "$CERT_DIR/$DOMAIN.key.pem"

# Bygg SAN-streng
SAN="DNS:$DOMAIN"
[ -n "$IP" ] && SAN="$SAN,IP:$IP"

# Lag CSR med SAN
openssl req -new -sha256 \
  -key "$CERT_DIR/$DOMAIN.key.pem" \
  -subj "/C=NO/ST=Oslo/O=MinBedrift/CN=$DOMAIN" \
  -addext "subjectAltName=$SAN" \
  -out "$CERT_DIR/$DOMAIN.csr.pem" 2>/dev/null

# Signer med mellom-CA (batch-modus, ingen brukerinteraksjon)
openssl ca -config "$CA_DIR/intermediate/openssl.cnf" \
  -extensions server_cert \
  -days "$DAYS" -notext -md sha256 -batch \
  -in "$CERT_DIR/$DOMAIN.csr.pem" \
  -out "$CERT_DIR/$DOMAIN.cert.pem" 2>/dev/null

# Verifiser
openssl verify \
  -CAfile "$CA_DIR/intermediate/certs/ca-chain.cert.pem" \
  "$CERT_DIR/$DOMAIN.cert.pem" || { echo "FEIL: Sertifikatvalidering feilet"; exit 1; }

echo "OK: Sertifikat utstedt for $DOMAIN (gyldig $DAYS dager)"
echo "  Nøkkel:      $CERT_DIR/$DOMAIN.key.pem"
echo "  Sertifikat:  $CERT_DIR/$DOMAIN.cert.pem"
echo "  CA-kjede:    $CA_DIR/intermediate/certs/ca-chain.cert.pem"

Steg 12: Integrer i Docker og Kubernetes

I containerbaserte miljøer er det standard å montere CA-sertifikater som ConfigMap-ressurser eller volumes. Her er de to vanligste tilnærmingene:

# Kubernetes: opprett ConfigMap med CA-kjeden
kubectl create configmap intern-ca \
  --from-file=ca.crt=/root/myca/intermediate/certs/ca-chain.cert.pem

# Bruk ConfigMap i Pod-spesifikasjon
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  template:
    spec:
      containers:
      - name: api
        image: node:22-alpine
        env:
        - name: NODE_EXTRA_CA_CERTS
          value: /etc/ssl/custom/ca.crt
        volumeMounts:
        - name: intern-ca
          mountPath: /etc/ssl/custom
      volumes:
      - name: intern-ca
        configMap:
          name: intern-ca
EOF

# Docker: legg til CA i basis-image
# Dockerfile:
# FROM node:22-alpine
# COPY intermediate/certs/ca-chain.cert.pem /usr/local/share/ca-certificates/intern-ca.crt
# RUN apk add --no-cache ca-certificates && update-ca-certificates
# ENV NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/intern-ca.crt

# Test TLS fra container
docker run --rm -it \
  -v /root/myca/intermediate/certs/ca-chain.cert.pem:/certs/ca.crt \
  -e NODE_EXTRA_CA_CERTS=/certs/ca.crt \
  node:22-alpine \
  node -e "const https=require('https'); https.get('https://api.intern.local:8443', r=>console.log(r.statusCode))"

Vanlige fallgruver og feilsøking

Her er de 8 mest vanlige feilene ved oppsett av privat certificate authority, og nøyaktig hva du gjør med dem:

Feil / FeilkodeÅrsakLøsning
CERT_HAS_EXPIREDSertifikatet er utløptUtsted nytt sertifikat. Sett opp cron-jobb for varsling 30 dager før utløp.
UNABLE_TO_VERIFY_LEAF_SIGNATURECA-kjeden er ufullstendigBruk ca-chain.cert.pem (mellom-CA + rot-CA), ikke bare ett av dem.
ERR_TLS_CERT_ALTNAME_INVALIDSAN mangler domenenavnetLegg til korrekt DNS-navn i subjectAltName ved utstedelse. CN alene holder ikke.
HOSTNAME_MISMATCHHostnavn matcher ikke SANSertifikatet ble utstedt for feil domene. Utsted nytt med riktig SAN.
CA_E_KEYFILE_ERRORFeil passord på CA-nøkkelCA-nøkkelpassordet er case-sensitivt. Bruk en passordbehandler for å lagre det.
Avvist av FirefoxFirefox har egen truststoreImporter CA manuelt: Innstillinger > Personvern > Sertifikater > Importer.
index.txt-konfliktSamme subjekt allerede i databasenRevoke det gamle sertifikatet, eller legg til unique_subject = no i openssl.cnf.
OpenSSL 3.x nøkkelformatUlike standarder fra OpenSSL 1.xBruk openssl pkey i stedet for openssl rsa for nøkkeloperasjoner i 3.x.

Nyttige feilsøkingskommandoer

# Vis fullstendig sertifikatinfo
openssl x509 -noout -text -in endcerts/api.intern.local.cert.pem

# Test TLS-tilkobling direkte
openssl s_client -connect api.intern.local:8443 \
  -CAfile intermediate/certs/ca-chain.cert.pem \
  -showcerts 2>&1 | head -30

# Sjekk at nøkkel og sertifikat matcher (md5 MÅ være identisk)
openssl x509 -noout -modulus -in endcerts/api.intern.local.cert.pem | md5sum
openssl pkey -noout -modulus -in endcerts/api.intern.local.key.pem | md5sum

# Vis CA-databasen
cat intermediate/index.txt

# Dekod en CSR
openssl req -noout -text -in endcerts/api.intern.local.csr.pem

# Test med curl mot intern server
curl -v --cacert intermediate/certs/ca-chain.cert.pem \
  https://api.intern.local:8443/health

Avanserte tips: OCSP og Step CA

Når basisoppsettet fungerer, er det to avanserte funksjoner som er særlig verdifulle for norske produksjonsmiljøer:

OCSP (Online Certificate Status Protocol) gir sertifikatstatus i sanntid, i motsetning til CRL som bufres og kan være opptil 30 dager gammel. OpenSSL har en innebygd OCSP-responder for test og utvikling:

# Start OCSP-responder (for utvikling)
openssl ocsp \
  -index intermediate/index.txt \
  -CA intermediate/certs/ca-chain.cert.pem \
  -rsigner intermediate/certs/intermediate.cert.pem \
  -rkey intermediate/private/intermediate.key.pem \
  -port 2560 &

# Sjekk status for et sertifikat
openssl ocsp \
  -CAfile intermediate/certs/ca-chain.cert.pem \
  -url http://localhost:2560 \
  -issuer intermediate/certs/intermediate.cert.pem \
  -cert endcerts/api.intern.local.cert.pem

# Forventet output for gyldig sertifikat:
# endcerts/api.intern.local.cert.pem: good
# This Update: Jun 20 10:00:00 2026 GMT

For produksjon anbefales Step CA fra Smallstep som et moderne alternativ. Step CA gir automatisk sertifikatfornying via ACME-protokollen, et REST-API, fullstendig auditlogg og innebygd OCSP. Se Smallstep sin dokumentasjon for step certificate create for oppsett.

Wildcard-sertifikater (*.api.intern.local) støttes fullt ut med privat CA. Legg til DNS:*.api.intern.local i SAN-feltet. Et wildcard dekker bare ett domenelagnivå: *.api.intern.local dekker v1.api.intern.local men ikke v1.v2.api.intern.local.

Sikkerhetshensyn og beste praksis 2026

En privat certificate authority er et kritisk sikkerhetsmål. Kompromittering av rot-CA-nøkkelen krever full re-distribusjon av tillitsankere til alle klienter. Her er de viktigste tiltakene:

TiltakPrioritetBeskrivelse
Hold rot-CA offlineKritiskLagre rot-CA-nøkkelen på kryptert USB eller HSM. Koble til kun for å signere mellom-CA.
Krypter alle private nøklerKritiskBruk alltid -aes256 for CA-nøkler. Ukrypterte CA-nøkler er ikke akseptable.
Bruk OpenSSL 3.5 LTSHøyOpenSSL 3.1 er end-of-life (mars 2025). OpenSSL 3.3 er end-of-life (april 2026). Bruk 3.5 LTS.
Sett korte levetiderHøyMaksimalt 365 dager for sluttsertifikater. 90 dager med autorenewing er ideelt.
Audit CA-databasenMediumSjekk index.txt regelmessig for ukjente sertifikater. Revoke umiddelbart ved mistanke.
Sikkerhetskopier CAHøyKryptert backup av hele CA-katalogen. Test gjenoppretting kvartalsvis.

RFC 5280 (Internet X.509 Public Key Infrastructure Certificate and CRL Profile) er den tekniske referansestandarden for X.509-sertifikater. Bruk alltid sha256 eller sterkere. OpenSSL 3.5 avviser sha1 for ny sertifikatutstedelse som standard. Hold OpenSSL oppdatert ved å følge OpenSSL release-side, der LTS-linjens siste versjon (3.5.7 per juni 2026) dokumenteres.

Privat CA vs Let's Encrypt vs kommersiell CA

FunksjonPrivat CA (OpenSSL)Let's EncryptKommersiell CA
PrisGratisGratis$10–$1.000+/år
Krever offentlig DNSNeiJaJa
GyldighetstidValgfri90 dager1 år (maks)
Automatisk fornyingManuelt/skriptACME (certbot)Avhengig av leverandør
Intern bruk uten DNSJaNeiNei
WildcardJa, gratisJa (DNS-01)Ja (ekstra kostnad)
CT-loggingNeiJa (alltid)Ja (alltid)
Nettlesertillit uten installasjonNeiJaJa
mTLS klientsertifikaterJaNeiAvhengig

For utviklingsmiljøer er mkcert et alternativ til full OpenSSL-oppsett. mkcert oppretter automatisk en lokal CA og installerer den i systemets truststore med én kommando. Les om Let's Encrypt sine anbefalinger for localhost-sertifikater for å se når privat CA er bedre enn Let's Encrypt for lokalt utviklingsarbeid.

Node.js TLS API: nøkkelparametere

Her er en komplett oversikt over de viktigste TLS-parameterne i Node.js for arbeid med privat CA. Se den offisielle Node.js TLS-dokumentasjonen for fullstendige detaljer:

ParameterTypeBruk
castring/Buffer/arrayPEM-kodet CA-sertifikat(er). Overskriver Node.js sine innebygde CA-er hvis angitt.
certstring/Buffer/arrayPEM-kodet sertifikat for serveren eller klienten.
keystring/Buffer/arrayPEM-kodet privat nøkkel. Separate fra sertifikatet.
passphrasestringPassord for kryptert privat nøkkel.
rejectUnauthorizedbooleanAvvis tilkoblinger med ugyldig sertifikat. Alltid true i produksjon.
requestCertbooleanKrev klientsertifikat (mTLS). Kombineres med rejectUnauthorized: true.
minVersionstringMinimum TLS-versjon. Sett til 'TLSv1.2' eller 'TLSv1.3'.
secureContextobjectGjenbrukbar TLS-kontekst fra tls.createSecureContext() for ytelsesoptimering.

Relatert innhold

Relatert dekning

Ofte stilte spørsmål

Kan jeg bruke en privat CA for offentlige nettsider?

Nei. Sertifikater fra en privat certificate authority er bare betrodd av klienter der du har installert rot-sertifikatet manuelt. For offentlige nettsider trenger du en offentlig betrodd CA som Let's Encrypt, DigiCert eller Sectigo. Privat CA egner seg utelukkende for intern infrastruktur.

Hvilken OpenSSL-versjon bør jeg bruke i 2026?

Bruk OpenSSL 3.5 (LTS), støttet til april 2030. OpenSSL 4.0.1 er den nyeste utgivelsen fra juni 2026, men er ikke LTS. Unngå OpenSSL 3.0 (end-of-life september 2026), 3.1 (end-of-life mars 2025) og 3.3 (end-of-life april 2026) for ny CA-infrastruktur.

Hva er forskjellen på RSA og ECDSA for CA-nøkler?

RSA 4096 gir høyest kompatibilitet med eldre systemer og er det sikreste valget for heterogene klientflåter. ECDSA P-384 gir tilsvarende sikkerhet med 3–4 ganger raskere operasjoner og markant mindre nøkler og sertifikater. For nye CA-installasjoner i 2026 med moderne klienter er ECDSA P-384 foretrukket.

Hvor lenge bør sertifikater være gyldige?

Sluttsertifikater: maks 365 dager, ideelt 90 dager med automatisk fornying. Mellom-CA: 10 år er standard. Rot-CA: 20 år er vanlig. Kortere gyldighetsperioder reduserer skadeomfanget ved kompromittering. CA-browser Forum vurderer å senke maksimum til 90 dager for offentlige CA-er i 2026.

Kan Node.js bruke privat CA uten kodeendringer?

Ja. Sett miljøvariabelen NODE_EXTRA_CA_CERTS=/sti/til/ca-chain.cert.pem før du starter Node.js. Da legges CA-en automatisk til Node.js sine tillitsankere uten å endre koden. Alternativt kan du installere CA i systemets truststore via update-ca-certificates.

Hva gjør jeg hvis rot-CA-nøkkelen kompromitteres?

Du må distribuere et nytt rot-sertifikat til alle klienter, tilbakekalle alle eksisterende sertifikater og starte PKI-hierarkiet på nytt. Nettopp derfor skal rot-CA alltid holdes offline, og mellom-CA-laget er kritisk for å begrense eksponering.

Er det trygt å bruke rejectUnauthorized: false i Node.js?

Aldri i produksjon. Dette deaktiverer all TLS-sertifikatvalidering og gjør applikasjonen sårbar for man-in-the-middle-angrep. Bruk det kun i isolerte utviklingsmiljøer, og fjern det alltid før koden går til produksjon. Den korrekte løsningen er NODE_EXTRA_CA_CERTS eller ca-parameteret.

Fungerer wildcard-sertifikater med privat CA?

Ja, og gratis. Legg til DNS:*.domene.intern i subjectAltName ved utstedelse. Et wildcard dekker bare ett domenelagnivå. *.api.intern dekker v1.api.intern men ikke v1.v2.api.intern. For dypere hierarkier trenger du separate SAN-oppføringer.