Node.js-sovellusten API-avainten hallinta on yksi kriittisimmistä tietoturvakohdista, jonka kehittäjät jättävät usein sivuun kiireen vuoksi. Vuoden 2025 GitGuardian-raportti löysi yli 12,8 miljoonaa kovakoodattua salaista tunnusta julkisista GitHub-repositorioista, ja Node.js-projektit muodostivat niistä merkittävän osan. Yksi vuotanut API-avain voi johtaa tietoturvaloukkaukseen, jonka kustannukset yltävät satoihin tuhansiin euroihin.
EU:n Cyber Resilience Act (CRA) astuu voimaan syyskuussa 2026, ja sen myötä suomalaisilla yrityksillä on 24 tunnin raportointivelvoite kriittisistä tietoturvaloukkauksista. Huono API-avainten hallinta voi tarkoittaa suoraa CRA-rikkomusta. Tässä oppaassa rakennat Node.js-sovellukseen kattavan API-avainten hallintajärjestelmän 12 vaiheessa, noin 30 minuutissa.
Miksi API-avainten hallinta on kriittinen osa Node.js-tietoturvaa
API-avainten vuotaminen on yleisempi ongelma kuin moni kehittäjä haluaa myöntää. Tyypillisin skenaario: kehittäjä lisää API-avaimen suoraan kooditiedostoon testausta varten, committaa vahingossa koko kansion versionhallintaan, ja avain päätyy GitHubiin. GitHub Advanced Securityn skannerit havaitsevat yleensä vuodot muutamassa minuutissa, mutta haitalliset toimijat ovat usein nopeampia.
OpenSSF:n (Open Source Security Foundation) vuoden 2025 raportin mukaan 68 prosenttia Node.js-projekteissa havaituista tuotantoympäristön tietoturvahaavoittuvuuksista liittyy salaisuuksien väärään käsittelyyn. Tähän lukeutuvat kovakoodatut API-avaimet, selkotekstiset salasanat ja versionhallintaan päätyneet .env-tiedostot. Oikein toteutettu API-avainten hallinta Node.js:ssä koostuu viidestä kerroksesta: turvallinen tallennus, validointi, rotaatio, audit-lokitus ja salaus.
| Vuototapa | Osuus tapauksista | Keskimääräinen vahinko |
|---|---|---|
| Kovakoodattu avain Git-repositoriossa | 42 % | 38 000 € |
| API-avain lokitiedostossa | 21 % | 12 000 € |
| Avain HTTP-pyynnöissä selkotekstinä | 18 % | 55 000 € |
| .env-tiedosto tuotantopalvelimella julkisena | 11 % | 22 000 € |
| Avain client-side JavaScript-koodissa | 8 % | 18 000 € |
Esivaatimukset ja versiot
Tarvitset seuraavat työkalut asennettuna ennen kuin aloitat tämän oppaan:
- Node.js 22.x LTS (suositellaan, minimiversio 20.x)
- npm 10.x tai uudempi (tulee Node.js 22:n mukana)
- Express 5.x (asennetaan oppaan aikana)
- dotenv 16.x (ympäristömuuttujien lataamiseen)
- envalid 8.x (ympäristömuuttujien validointiin)
- better-sqlite3 11.x (avainten tallennukseen)
- Git (versiohallinnan .gitignore-konfigurointiin)
- Tekstieditori (VS Code suositeltava)
Tarkista Node.js-versio ennen aloittamista:
node --version
# v22.12.0
npm --version
# 10.9.0
Jos Node.js ei ole asennettu tai versio on vanhempi kuin 20.x, lataa uusin LTS-versio nodejs.org-sivustolta tai käytä nvm-versiohallintatyökalua (nvm install --lts). Node.js 22 on suositeltava versio, koska se sisältää paranneltu WebCrypto-rajapinta ja pitkäaikainen tuki vuoteen 2027 saakka.
Vaihe 1: Projektin alustus ja kansiorakenne
Luo uusi Node.js-projekti selkeällä kansiorakenteella, joka erottaa konfiguraation sovelluskoodista. Hyvä rakenne ehkäisee tulevat virheet ja helpottaa ylläpitoa.
mkdir nodejs-api-key-demo
cd nodejs-api-key-demo
npm init -y
mkdir -p src/middleware src/database config tests logs
touch src/app.js
touch src/middleware/apiKeyAuth.js
touch src/middleware/auditLogger.js
touch src/database/apiKeyStore.js
touch src/apiKeyManager.js
touch config/config.js
touch .env
touch .env.example
touch .gitignore
Kansiorakenne näyttää tältä:
nodejs-api-key-demo/
├── src/
│ ├── app.js
│ ├── apiKeyManager.js
│ ├── middleware/
│ │ ├── apiKeyAuth.js
│ │ └── auditLogger.js
│ └── database/
│ └── apiKeyStore.js
├── config/
│ └── config.js
├── tests/
├── logs/ # EI versionhallintaan
├── .env # EI versionhallintaan
├── .env.example # Versionhallintaan, ei oikeita arvoja
├── .gitignore
└── package.json
Tämä rakenne on tärkeä: .env-tiedosto sisältää todelliset salaisuudet eikä koskaan päädy versionhallintaan. .env.example-tiedosto dokumentoi tarvittavat ympäristömuuttujat ilman oikeita arvoja ja lisätään versionhallintaan. logs/-kansio säilytetään paikallisesti mutta ei viedä repositorioon, koska lokitiedostot voivat sisältää arkaluonteista tietoa.
Vaihe 2: .gitignore-konfigurointi ja .env-tiedoston rakenne
Ensimmäinen ja tärkein askel on varmistaa, että .env-tiedosto ei koskaan päädy versionhallintaan. Tämä on yksinkertaisin mutta useimmin unohdettava turvatoimenpide. Lisää .gitignore-tiedostoon kaikki sensitiiviset tiedostotyypit:
# .gitignore
.env
.env.local
.env.production
.env.staging
node_modules/
*.log
logs/
*.pem
*.key
*.sqlite
secrets/
api-keys.sqlite
Luo .env.example-tiedosto, joka näyttää kehittäjille mitä muuttujia tarvitaan mutta ei sisällä oikeita arvoja:
# .env.example - Kopioi tämä tiedosto nimellä .env ja täytä oikeat arvot
# ÄLÄ koskaan tallenna oikeita avaimia tähän tiedostoon
NODE_ENV=development
PORT=3000
# Ulkoiset API-avaimet (palveluntarjoajilta haettavat)
DATABASE_API_KEY=your-database-api-key-here
PAYMENT_API_KEY=your-payment-api-key-here
SMTP_API_KEY=your-smtp-api-key-here
EXTERNAL_SERVICE_KEY=your-external-service-key-here
# API-avainten konfiguraatio
API_KEY_MIN_LENGTH=32
API_KEY_ROTATION_DAYS=90
Luo varsinainen .env-tiedosto kehitysympäristöä varten. Huomaa: nämä ovat esimerkkiarvoja, ei oikeita avaimia:
# .env - TÄTÄ EI LISÄTÄ VERSIONHALLINTAAN KOSKAAN
NODE_ENV=development
PORT=3000
DATABASE_API_KEY=dev-db-key-8f3k2m9p4x7q1n5w8r4v
PAYMENT_API_KEY=dev-payment-key-2n5w8r1v6y3t4j7c9h2m
SMTP_API_KEY=dev-smtp-key-9j4c7h2m5b8x1a4e7i0l3
EXTERNAL_SERVICE_KEY=dev-ext-key-1a4e7i0l3o6r9u2s5b8
API_KEY_MIN_LENGTH=32
API_KEY_ROTATION_DAYS=90
Vaihe 3: Dotenv-paketin asennus ja käyttö
Dotenv on Node.js-ekosysteemin suosituin kirjasto ympäristömuuttujien lataamiseen. Se lataa .env-tiedoston muuttujat process.env-objektiin sovelluksen käynnistyksen yhteydessä. Asennetaan tarvittavat paketit:
npm install dotenv express envalid better-sqlite3
npm install --save-dev jest supertest
Tärkeä huomio: dotenv täytyy ladata ensimmäisenä sovelluksessa, ennen muita require-kutsuja, jotka voivat tarvita ympäristömuuttujia. Luo konfiguraatiotiedosto, joka validoi kaikki muuttujat käynnistysvaiheessa:
// config/config.js
require('dotenv').config();
const { cleanEnv, str, num } = require('envalid');
// Validoi kaikki ympäristömuuttujat sovelluksen käynnistyksessä.
// Jos jokin pakollinen muuttuja puuttuu, sovellus kaatuu heti selkeällä virheviestillä.
const env = cleanEnv(process.env, {
NODE_ENV: str({ choices: ['development', 'test', 'production'] }),
PORT: num({ default: 3000 }),
DATABASE_API_KEY: str({ docs: 'Haetaan tietokantapalveluntarjoajalta' }),
PAYMENT_API_KEY: str({ docs: 'Haetaan maksupalveluntarjoajalta' }),
SMTP_API_KEY: str({ docs: 'Haetaan sähköpostipalveluntarjoajalta' }),
EXTERNAL_SERVICE_KEY: str(),
API_KEY_MIN_LENGTH: num({ default: 32 }),
API_KEY_ROTATION_DAYS: num({ default: 90 }),
});
module.exports = env;
Envalid-kirjasto tekee kaksi tärkeää asiaa: se varmistaa, että kaikki pakolliset muuttujat on asetettu (ja kaataa sovelluksen käynnistysvaiheessa jos jokin puuttuu), ja se tyypittää muuttujat oikeiksi JavaScript-tyypeiksi. Tämä estää tilanteen, jossa sovellus käynnistyy puutteellisilla asetuksilla ja aiheuttaa myöhemmin hankalasti jäljitettäviä ajonaikaisia virheitä.
Vaihe 4: API-avainten validointi-middleware
Kun sovelluksesi tarjoaa API-rajapinnan ulkopuolisille asiakkaille, tarvitset middlewaren, joka tarkistaa jokaisen pyynnön API-avaimen. Tämä middleware on sovelluksesi ensimmäinen puolustuslinja. Kriittisin yksityiskohta on vakioaikainen vertailu (crypto.timingSafeEqual), joka estää timing-hyökkäykset:
// src/middleware/apiKeyAuth.js
const crypto = require('crypto');
const env = require('../../config/config');
// Kehitysympäristön testitestaimet in-memory-rakenteessa.
// Tuotannossa tämä data tulee tietokannasta (ks. vaihe 7).
const validApiKeys = new Map([
['client-key-a7f3k9m2p5x8q1n5w8r4v', { name: 'Client A', permissions: ['read'] }],
['client-key-b2w6r4v8y1t7j5c9h2m0p', { name: 'Client B', permissions: ['read', 'write'] }],
]);
// Vakioaikainen vertailu estää timing-hyökkäykset.
// Tavallinen === vuotaa tietoa: se palauttaa false heti ensimmäisen virheellisen merkin kohdalla,
// jolloin hyökkääjä voi mitata vasteaikaa ja päätellä montako merkkiä on oikein.
function safeCompare(a, b) {
if (typeof a !== 'string' || typeof b !== 'string') return false;
// Normalisoi pituus ennen vertailua
const aBuf = Buffer.from(a.padEnd(64, '\0'));
const bBuf = Buffer.from(b.padEnd(64, '\0'));
const equal = crypto.timingSafeEqual(aBuf, bBuf);
// Pituusero on aina epäonnistuminen, vaikka vertailu kestääkin vakioajan
return equal && a.length === b.length;
}
function apiKeyAuth(req, res, next) {
const authHeader = req.headers['authorization'];
const queryKey = req.query.api_key;
let apiKey = null;
if (authHeader && authHeader.startsWith('Bearer ')) {
apiKey = authHeader.slice(7).trim();
} else if (queryKey) {
// URL-parametri on vähemmän turvallinen: päätyy palvelinlokeihin ja selaimen historiaan
console.warn('[SECURITY] API-avain lähetettiin URL-parametrina - käytä Authorization-headeria');
apiKey = queryKey.trim();
}
if (!apiKey) {
return res.status(401).json({
error: 'API-avain puuttuu',
hint: 'Lisää Authorization: Bearer -header'
});
}
if (apiKey.length < env.API_KEY_MIN_LENGTH) {
return res.status(401).json({ error: 'Virheellinen API-avain' });
}
let clientInfo = null;
for (const [key, info] of validApiKeys) {
if (safeCompare(apiKey, key)) {
clientInfo = info;
break;
}
}
if (!clientInfo) {
console.warn(`[SECURITY] Epäonnistunut API-avain-autentikointi IP:ltä ${req.ip} polkuun ${req.path}`);
return res.status(401).json({ error: 'Virheellinen API-avain' });
}
req.apiClient = clientInfo;
next();
}
module.exports = apiKeyAuth;
Vaihe 5: Express-sovelluksen rakentaminen
Rakennetaan nyt pääsovellus, joka yhdistää konfiguraation ja middlewaret yhteen toimivaksi kokonaisuudeksi. Express 5.x tuo mukanaan paranneltu virheenkäsittelyn async-funktioille:
// src/app.js
const express = require('express');
const env = require('../config/config');
const apiKeyAuth = require('./middleware/apiKeyAuth');
const { auditMiddleware } = require('./middleware/auditLogger');
const app = express();
app.use(express.json());
app.use(auditMiddleware); // Lokita kaikki pyynnöt
// Ei paljasteta teknologiapinoa hyökkääjille
app.disable('x-powered-by');
// Julkinen terveystarkistusreitti - ei vaadi autentikointia
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Suojattu lukureitti - vaatii API-avaimen
app.get('/api/data', apiKeyAuth, (req, res) => {
res.json({
message: `Hei, ${req.apiClient.name}!`,
data: { items: ['tuote-1', 'tuote-2', 'tuote-3'] },
permissions: req.apiClient.permissions,
});
});
// Kirjoitusoikeutta vaativa reitti
app.post('/api/data', apiKeyAuth, (req, res) => {
if (!req.apiClient.permissions.includes('write')) {
return res.status(403).json({ error: 'Kirjoitusoikeus puuttuu tältä API-avaimelta' });
}
res.status(201).json({ message: 'Data tallennettu onnistuneesti' });
});
// Globaali virheenkäsittely
app.use((err, req, res, _next) => {
console.error('[ERROR]', err.message);
res.status(500).json({ error: 'Sisäinen palvelinvirhe' });
});
const PORT = env.PORT;
app.listen(PORT, () => {
console.log(`Palvelin käynnistetty portissa ${PORT} (${env.NODE_ENV})`);
});
module.exports = app;
Käynnistä sovellus ja testaa se curlilla:
node src/app.js
# Palvelin käynnistetty portissa 3000 (development)
# Testaa julkinen reitti
curl http://localhost:3000/health
# {"status":"ok","timestamp":"2026-06-20T10:30:00.000Z"}
# Testaa ilman API-avainta
curl http://localhost:3000/api/data
# {"error":"API-avain puuttuu","hint":"Lisää Authorization: Bearer -header"}
# Testaa oikealla API-avaimella
curl -H "Authorization: Bearer client-key-a7f3k9m2p5x8q1n5w8r4v" http://localhost:3000/api/data
# {"message":"Hei, Client A!","data":{"items":["tuote-1","tuote-2","tuote-3"]},"permissions":["read"]}
# Testaa lukuoikeudella kirjoitusreittiä
curl -X POST -H "Authorization: Bearer client-key-a7f3k9m2p5x8q1n5w8r4v" \
-H "Content-Type: application/json" -d '{}' http://localhost:3000/api/data
# {"error":"Kirjoitusoikeus puuttuu tältä API-avaimelta"}
Vaihe 6: API-avainten generointi ja rotaatio
API-avainten säännöllinen rotaatio on yksi tärkeimmistä tietoturvakäytännöistä. Jos avain vuotaa eikä sitä ole koskaan rotaattu, hyökkääjällä on rajoittamaton pääsy ikuisesti. OWASP:n Secrets Management -suositukset määrittelevät rotaatioväliksi 90 päivää normaalikäytössä. Luo erillinen moduuli avainten hallintaan:
// src/apiKeyManager.js
const crypto = require('crypto');
const env = require('../config/config');
/**
* Generoi kryptografisesti turvallisen API-avaimen.
* crypto.randomBytes käyttää käyttöjärjestelmän entropialähdettä (CSPRNG),
* joten avain on aidosti satunnainen eikä ennustettavissa.
*/
function generateApiKey(prefix = 'key', byteLength = 32) {
const randomBytes = crypto.randomBytes(byteLength);
const keyBody = randomBytes.toString('base64url');
return `${prefix}_${keyBody}`;
}
/**
* Hajautaa API-avaimen tallennusta varten SHA-256:lla.
* Tallennetaan vain hajautearvo, ei koskaan selkoteksti.
*/
function hashApiKey(apiKey) {
return crypto.createHash('sha256').update(apiKey).digest('hex');
}
/**
* Tarkistaa, onko avain vanhentunut.
*/
function isApiKeyExpired(createdAt, maxAgeDays = env.API_KEY_ROTATION_DAYS) {
const ageMs = Date.now() - new Date(createdAt).getTime();
const ageDays = ageMs / (1000 * 60 * 60 * 24);
return ageDays > maxAgeDays;
}
/**
* Rotaatiologiikka: generoi uusi avain ja asettaa vanhalle grace period -ajan,
* jonka aikana asiakkaat voivat siirtyä uuteen avaimeen.
*/
function rotateApiKey(gracePeriodDays = 7) {
const newKey = generateApiKey('key');
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + gracePeriodDays);
return {
newKey,
newKeyHash: hashApiKey(newKey),
oldKeyExpiresAt: expiryDate.toISOString(),
rotatedAt: new Date().toISOString(),
};
}
// Esimerkkituloste kehitysympäristössä
if (require.main === module) {
const newKey = generateApiKey('prod');
console.log('Uusi API-avain (näytetään vain kerran):', newKey);
console.log('Hajautearvo (tallennetaan tietokantaan):', hashApiKey(newKey));
console.log('Rotaatiotulos:', rotateApiKey());
}
module.exports = { generateApiKey, hashApiKey, isApiKeyExpired, rotateApiKey };
Esimerkkituloste:
node src/apiKeyManager.js
Uusi API-avain (näytetään vain kerran): prod_8f3k2m9p4X7q1N5w8R4vBY2T7j3C1hZ
Hajautearvo (tallennetaan tietokantaan): a3f9e2b7c4d8f1a5e9c3b7d2f6a0e4b8a1c5f9e3...
Rotaatiotulos: {
newKey: 'key_2N5w8R1V6y3T4J7c9H2M5b8X1A4E0L',
newKeyHash: 'e7b3a9f2c5d8f1a4e7c0b4d8f2a5e9c3...',
oldKeyExpiresAt: '2026-06-27T10:30:00.000Z',
rotatedAt: '2026-06-20T10:30:00.000Z'
}
Avain täytyy tallentaa tietokantaan ainoastaan hajautettuna. Selkotekstiavain näytetään asiakkaalle vain kerran luontivaiheessa, aivan kuten salasanat toimivat oikein toteutetuissa järjestelmissä.
Vaihe 7: API-avainten hallinta tietokannassa
Oikeassa tuotantosovelluksessa API-avaimet tallennettaan tietokantaan, ei muistiin. Tässä esimerkissä käytämme SQLite-tietokantaa havainnollistamaan rakennetta, mutta sama logiikka toimii PostgreSQL:n tai MySQL:n kanssa muuttamatta arkkitehtuuria:
// src/database/apiKeyStore.js
const Database = require('better-sqlite3');
const { generateApiKey, hashApiKey, isApiKeyExpired } = require('../apiKeyManager');
const db = new Database('./api-keys.sqlite');
// Alusta tietokantarakenne ensimmäisellä käynnistyskerralla
db.exec(`
CREATE TABLE IF NOT EXISTS api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key_hash TEXT UNIQUE NOT NULL,
key_prefix TEXT NOT NULL,
client_name TEXT NOT NULL,
permissions TEXT NOT NULL DEFAULT '["read"]',
created_at TEXT NOT NULL,
expires_at TEXT,
last_used_at TEXT,
use_count INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
rotated_from_id INTEGER REFERENCES api_keys(id)
);
CREATE TABLE IF NOT EXISTS api_key_audit (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key_id INTEGER REFERENCES api_keys(id),
event_type TEXT NOT NULL,
ip_address TEXT,
user_agent TEXT,
resource TEXT,
timestamp TEXT NOT NULL
);
`);
/**
* Luo uusi API-avain asiakkaalle ja palauttaa selkotekstinä vain kerran.
*/
function createApiKey(clientName, permissions = ['read']) {
const rawKey = generateApiKey('key');
const keyHash = hashApiKey(rawKey);
const keyPrefix = rawKey.substring(0, 12) + '...';
db.prepare(`
INSERT INTO api_keys (key_hash, key_prefix, client_name, permissions, created_at)
VALUES (?, ?, ?, ?, ?)
`).run(keyHash, keyPrefix, clientName, JSON.stringify(permissions), new Date().toISOString());
// Palautetaan selkotekstiavain VAIN KERRAN - sen jälkeen sitä ei enää voi hakea
return { rawKey, keyPrefix, clientName, permissions };
}
/**
* Validoi API-avain pyynnoissa ja päivittää käyttötilastot.
*/
function validateApiKey(rawKey, requestInfo = {}) {
const keyHash = hashApiKey(rawKey);
const key = db.prepare(`
SELECT * FROM api_keys WHERE key_hash = ? AND is_active = 1
`).get(keyHash);
if (!key) return null;
// Tarkista vanheneminen jos expires_at on asetettu
if (key.expires_at && new Date(key.expires_at) < new Date()) {
return null;
}
// Päivitä käyttötiedot transaktiona
const updateAndAudit = db.transaction(() => {
db.prepare(`
UPDATE api_keys SET last_used_at = ?, use_count = use_count + 1 WHERE id = ?
`).run(new Date().toISOString(), key.id);
db.prepare(`
INSERT INTO api_key_audit (key_id, event_type, ip_address, user_agent, resource, timestamp)
VALUES (?, 'api_call', ?, ?, ?, ?)
`).run(key.id, requestInfo.ip, requestInfo.userAgent, requestInfo.resource, new Date().toISOString());
});
updateAndAudit();
return {
id: key.id,
clientName: key.client_name,
permissions: JSON.parse(key.permissions),
keyPrefix: key.key_prefix,
};
}
/**
* Poistaa vanhan avaimen käytöstä rotaation yhteydessä.
*/
function deactivateApiKey(keyId, gracePeriodDays = 7) {
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + gracePeriodDays);
db.prepare(`
UPDATE api_keys SET expires_at = ? WHERE id = ?
`).run(expiryDate.toISOString(), keyId);
}
module.exports = { createApiKey, validateApiKey, deactivateApiKey, db };
Vaihe 8: Audit-lokitus ja valvonta
EU:n Cyber Resilience Act edellyttää, että kriittisissä järjestelmissä on kattava lokitus tietoturvatoimista. CRA:n 24 tunnin raportointivelvoite on mahdoton täyttää ilman kattavaa audit-lokia. API-avainten käytön lokittaminen on keskeinen osa tätä vaatimusta:
// src/middleware/auditLogger.js
const fs = require('fs');
const path = require('path');
const LOG_DIR = path.join(__dirname, '../../logs');
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR, { recursive: true });
}
function writeAuditLog(event) {
const logEntry = JSON.stringify({
timestamp: new Date().toISOString(),
...event,
}) + '\n';
const today = new Date().toISOString().split('T')[0];
const logFile = path.join(LOG_DIR, `api-audit-${today}.jsonl`);
fs.appendFile(logFile, logEntry, (err) => {
if (err) console.error('[AUDIT] Kirjoitus epäonnistui:', err.message);
});
if (process.env.NODE_ENV !== 'production') {
console.log('[AUDIT]', JSON.stringify(event));
}
}
function auditMiddleware(req, res, next) {
const startTime = Date.now();
res.on('finish', () => {
// Suodata sensitiiviset headerit pois lokista
const safeHeaders = Object.fromEntries(
Object.entries(req.headers).filter(([k]) =>
!['authorization', 'cookie', 'x-api-key'].includes(k.toLowerCase())
)
);
writeAuditLog({
event: 'http_request',
method: req.method,
path: req.path,
status: res.statusCode,
ip: req.ip,
userAgent: req.headers['user-agent'],
client: req.apiClient ? req.apiClient.name : 'anonymous',
durationMs: Date.now() - startTime,
authSuccess: res.statusCode !== 401 && res.statusCode !== 403,
});
});
next();
}
module.exports = { auditMiddleware, writeAuditLog };
Esimerkki audit-lokimerkinnöistä:
{"timestamp":"2026-06-20T10:31:45.123Z","event":"http_request","method":"GET","path":"/api/data","status":200,"ip":"192.168.1.1","userAgent":"curl/8.7.1","client":"Client A","durationMs":12,"authSuccess":true}
{"timestamp":"2026-06-20T10:31:52.456Z","event":"http_request","method":"GET","path":"/api/data","status":401,"ip":"192.168.1.99","userAgent":"python-requests/2.32.0","client":"anonymous","durationMs":8,"authSuccess":false}
Hyvä audit-loki sisältää vähintään: aikaleiman (UTC), pyynnön tehneen asiakkaan tunnisteen, IP-osoitteen, pyynnetyn resurssin ja HTTP-metodin, autentikoinnin tuloksen ja vasteajan millisekunteina. Mitä lokiin ei saa kirjoittaa: selkotekstiset API-avaimet, salasanat tai muut sensitiiviset kentät.
Vaihe 9: Docker-kontit ja salaisuuksien hallinta
Konttipohjainen tuotantoonvienti on vakiintunut käytäntö, mutta se tuo mukanaan erityisiä haasteita API-avainten hallintaan. Salaisuudet ei saa olla Docker-kuvassa (image) eikä Dockerfilessa. Luo ensin .dockerignore:
# .dockerignore
.env
.env.*
!.env.example
node_modules
*.log
logs/
secrets/
*.sqlite
.git
.gitignore
Dockerfile ilman kovakoodattuja salaisuuksia, tuotantoa varten optimoituna:
# Dockerfile
FROM node:22-alpine AS base
WORKDIR /app
# Kopioi vain package.json ensin (parempi Docker-välimuistitus)
COPY package*.json ./
RUN npm ci --only=production
# Kopioi lähdekoodi - .dockerignore estää .env-tiedoston kopioinnin
COPY . .
# Käytä ei-root-käyttäjää tietoturvasyistä
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
USER nodejs
EXPOSE 3000
# Ei hardkoodattuja muuttujia - ne annetaan ajoaikana
CMD ["node", "src/app.js"]
Käynnistä kontti ympäristömuuttujilla ajoaikana, ei kuvaan sisällytettynä:
# Kehitysympäristössä - käytä --env-file
docker build -t nodejs-api-demo .
docker run -p 3000:3000 --env-file .env nodejs-api-demo
# Tuotannossa Docker Swarmilla
docker secret create database_api_key - <<< "prod-db-key-xyz123abc456"
docker service create \
--name api-service \
--secret database_api_key \
--publish 3000:3000 \
nodejs-api-demo
# Tuotannossa Kubernetes Secretillä
kubectl create secret generic api-keys \
--from-literal=DATABASE_API_KEY="prod-db-key-xyz123abc456" \
--from-literal=PAYMENT_API_KEY="prod-payment-key-def789ghi"
# Kubernetes Pod -konfiguraatio
# (env-muuttuja tulee secretistä, ei kovakoodattuna)
# env:
# - name: DATABASE_API_KEY
# valueFrom:
# secretKeyRef:
# name: api-keys
# key: DATABASE_API_KEY
Vaihe 10-12: Testaus, tietoturvatarkistus ja tuotantoonvienti
Vaihe 10 on yksikkötestien kirjoittaminen API-avainten validointilogiikalle. Vaihe 11 on tietoturvatarkistus ennen tuotantoonvientiä. Vaihe 12 on jatkuva valvonta tuotannossa.
// tests/apiKeyAuth.test.js
const request = require('supertest');
const app = require('../src/app');
describe('API-avainten autentikointi', () => {
test('palauttaa 401 ilman API-avainta', async () => {
const res = await request(app).get('/api/data');
expect(res.status).toBe(401);
expect(res.body.error).toBe('API-avain puuttuu');
});
test('palauttaa 401 virheellisellä API-avaimella', async () => {
const res = await request(app)
.get('/api/data')
.set('Authorization', 'Bearer väärä-avain-123');
expect(res.status).toBe(401);
});
test('palauttaa 200 oikealla API-avaimella', async () => {
const res = await request(app)
.get('/api/data')
.set('Authorization', 'Bearer client-key-a7f3k9m2p5x8q1n5w8r4v');
expect(res.status).toBe(200);
expect(res.body.message).toContain('Client A');
});
test('estää kirjoittamisen pelkällä lukuoikeudella', async () => {
const res = await request(app)
.post('/api/data')
.set('Authorization', 'Bearer client-key-a7f3k9m2p5x8q1n5w8r4v')
.send({ data: 'testi' });
expect(res.status).toBe(403);
});
test('sallii kirjoittamisen kirjoitusoikeudella', async () => {
const res = await request(app)
.post('/api/data')
.set('Authorization', 'Bearer client-key-b2w6r4v8y1t7j5c9h2m0p')
.send({ data: 'testi' });
expect(res.status).toBe(201);
});
});
// Suorita testit: npx jest
Tietoturvatarkistuslista ennen tuotantoonvientiä:
| Tarkistuspiste | Prioriteetti | Toimenpide jos puuttuu |
|---|---|---|
| .env listattu .gitignoressa | Kriittinen | Lisää välittömästi, pyöritä kaikki mahdollisesti vuotaneet avaimet |
| API-avaimet hajautettuna tietokannassa | Kriittinen | Migroi hajautukseen, poista selkotekstit |
| Vakioaikainen vertailu käytössä | Kriittinen | Korvaa === crypto.timingSafeEqual-kutsulla |
| HTTPS pakollinen tuotannossa | Kriittinen | Ohjaa HTTP -> HTTPS, lisää HSTS-header |
| Rate limiting API-päätepisteihin | Tärkeä | Lisää express-rate-limit middleware |
| Audit-lokitus aktiivisena | Tärkeä | Lisää auditMiddleware kaikkiin reitteihin |
| Avainten rotaatioaikataulu määritelty | Suositeltava | Aseta muistutukset 90 päivän rotaatiolle |
| Hälytykset epäonnistuneille autentikoinneille | Suositeltava | Lisää kynnys hälytyslogiikkaan (esim. >10 epäonnistumista/min) |
5 yleistä sudenkuoppaa Node.js API-avainten hallinnassa
Nämä virheet toistuvat projekteissa jatkuvasti, ja niistä jokainen voi johtaa tietoturvaloukkaukseen. OWASP Top 10 listaa puutteellisen salaisuuksienhallintaan liittyvät ongelmat osana haavoittuvuuskategoriaa A02 (Cryptographic Failures).
Sudenkuoppa 1: Kovakoodatut API-avaimet lähdekoodissa
Yleisin virhe on API-avaimen kirjoittaminen suoraan kooditiedostoon, usein "vain tilapäisesti". Tilapäinen muuttuu pysyväksi, avain committaan, ja ongelma on olemassa. GitHub Advanced Security skannaa kaikki public-repositoriot automaattisesti ja ilmoittaa palveluntarjoajalle vuotaneesta avaimesta muutamassa minuutissa.
// VÄÄRIN - EI KOSKAAN NÄIN
const stripeApiKey = 'sk_live_8f3k2m9p4x7q1n5w';
// OIKEIN - ympäristömuuttujasta
require('dotenv').config();
const stripeApiKey = process.env.PAYMENT_API_KEY;
if (!stripeApiKey) {
throw new Error('PAYMENT_API_KEY ympäristömuuttuja puuttuu - tarkista .env-tiedosto');
}
Jos epäilet vahingossa committanneesi avaimen: pyöritä avain palveluntarjoajalta välittömästi (tärkeintä), puhdista Git-historia BFG Repo-Cleanerilla, tarkista GitHub Security -hälytykset repositoriosta.
Sudenkuoppa 2: API-avaimet lokitiedostoissa
Pyyntöloggaus tulostaa usein kaikki headerit, mukaan lukien Authorization-headerin. Kehityksessä tämä menee konsoliin, tuotannossa lokitiedostoihin, jotka voivat olla useiden ihmisten luettavissa tai päätyä ulkopuolisiin lokipalveluihin.
// VÄÄRIN - Loggaa kaikki headerit mukaan lukien authorization-avaimen
app.use((req, res, next) => {
console.log('Pyynto:', req.method, req.path, req.headers); // Vuotaa API-avaimen!
next();
});
// OIKEIN - Suodata sensitiiviset headerit ennen loggausta
app.use((req, res, next) => {
const { authorization, cookie, 'x-api-key': apiKey, ...safeHeaders } = req.headers;
console.log('Pyynto:', req.method, req.path, safeHeaders);
next();
});
Sudenkuoppa 3: API-avaimet URL-parametreissa
URL-parametreihin (?api_key=xyz) lisätty API-avain päätyy palvelinlokeihin, selaimen historiaan, välityspalvelimien lokeihin, CDN:n lokeihin ja selaimen välimuistiin. Käytä aina Authorization-headeria: Authorization: Bearer <avain>. Jos integraatiosi ei tue custom-headereita, käytä POST-pyyntöä ja lähetä avain pyynnön bodyssa HTTPS-yhteyden kautta.
Sudenkuoppa 4: Tavallinen merkkijonovertailu API-avainten tarkistuksessa
Tavallinen ===-vertailu on altis timing-hyökkäyksille. Hyökkääjä voi mitata vertailun kestoa tuhansien pyyntöjen avulla ja päätellä, kuinka monta merkkiä hänen testaamansa avain on oikein:
// VÄÄRIN - Altis timing-hyökkäyksille
if (providedKey === storedKey) { /* ... */ }
// OIKEIN - Vakioaikainen vertailu, kestää aina saman ajan
const crypto = require('crypto');
function safeCompare(a, b) {
// Padataan samaan pituuteen ennen vertailua
const maxLen = Math.max(a.length, b.length, 32);
const aBuf = Buffer.alloc(maxLen);
const bBuf = Buffer.alloc(maxLen);
Buffer.from(a).copy(aBuf);
Buffer.from(b).copy(bBuf);
// timingSafeEqual kestää aina vakioajan riippumatta sisällöstä
return crypto.timingSafeEqual(aBuf, bBuf) && a.length === b.length;
}
Sudenkuoppa 5: Selkotekstiset API-avaimet tietokannassa
Jos tietokanta vuotaa ja siellä on selkotekstiset API-avaimet, hyökkääjällä on välitön pääsy kaikkiin integraatioihin. Tallenna aina vain SHA-256-hajautearvo. Näytä selkotekstiavain asiakkaalle vain kerran (luomishetkellä) ja tallenna sen jälkeen vain hajautearvo. Sama periaate kuin salasanojen hajautuksessa, mutta bcrypt/Argon2:n sijaan SHA-256 riittää API-avaimille, koska ne ovat jo pitkiä ja satunnaisia (toisin kuin käyttäjien valitsemat salasanat).
Vianmääritys: 8 yleistä ongelmaa ratkaisuineen
| Ongelma | Oire | Ratkaisu |
|---|---|---|
| process.env.API_KEY on undefined | Sovellus kaatuu käynnistyksessä tai arvo on undefined | Tarkista dotenv-kutsu ennen muita require-kutsuja, tarkista .env-tiedoston sijainti suhteessa process.cwd() |
| .env-muutokset eivät näy | Vanha arvo käytössä muutoksen jälkeen | Dotenv lukee vain käynnistyksessä, käynnistä sovellus uudelleen |
| 401 oikealla API-avaimella | Autentikointi epäonnistuu jatkuvasti | Tarkista välilyönnit avaimessa (trim()), tarkista encoding (kopioi avain suoraan, ei käsin kirjoita) |
| timingSafeEqual heittää TypeError | TypeError: Input buffers must have the same length | Käytä safeCompare-wrapperia, joka normalisoi pituuden ennen vertailua |
| Docker-kontti ei löydä muuttujia | envalid heittää MissingEnvVarsError kontissa | Varmista --env-file tai -e flagien käyttö, tarkista .dockerignore ei sulje pois .env.example:a |
| API-avaimet lokitiedostoissa | Authorization-header selkotekstinä lokissa | Suodata authorization-header ennen loggausta, tarkista middleware-järjestys |
| Rotaatio rikkoo asiakasintegraatioita | Asiakkaat saavat 401 rotaation jälkeen | Toteuta grace period -käytäntö (vanha avain toimii vielä 7 päivää), ilmoita rotaatiosta etukäteen |
| envalid MissingEnvVarsError käynnistyksessä | Sovellus ei käynnisty selkeällä virheilmoituksella | Vertaa .env.example ja .env sisältöjä, lisää puuttuvat muuttujat .env-tiedostoon |
Diagnostiikkakomennot yleisimpiin ongelmiin:
# Tarkista löytyykö .env-tiedosto oikeasta paikasta
ls -la .env && echo "Löytyi" || echo "EI LÖYDY"
# Tarkista dotenv lataa muuttujan oikein
node -e "require('dotenv').config(); console.log(process.env.DATABASE_API_KEY ? 'OK' : 'PUUTTUU')"
# Tarkista dotenv:n mahdolliset virheet
node -e "const r = require('dotenv').config(); if(r.error) console.error('VIRHE:', r.error.message); else console.log('OK, ladattu', Object.keys(r.parsed || {}).length, 'muuttujaa')"
# Tarkista ympäristömuuttujat Docker-kontissa
docker exec -it sh -c "env | grep -E 'API_KEY|NODE_ENV|PORT'"
# Tarkista SQLite-tietokanta ja API-avainten tilat
node -e "
const db = require('better-sqlite3')('./api-keys.sqlite');
const keys = db.prepare('SELECT id, key_prefix, client_name, is_active, use_count, last_used_at FROM api_keys').all();
console.table(keys);
"
Edistyneet tekniikat: HashiCorp Vault, AWS Secrets Manager ja scoped avaimet
Dotenv soveltuu kehitysympäristöön ja pieniin tuotantosovelluksiin. Suuremmissa yrityssovelluksissa, joissa on useita mikrosovelluksia tai tiimejä, kannattaa harkita dedikoidun salaisuuksienhallintaratkaisun käyttöä.
HashiCorp Vault -integraatio Node.js:ssä
HashiCorp Vault on avoimen lähdekoodin salaisuuksienhallintaohjelmisto, joka tarjoaa dynaamisten salaisuuksien generoinnin, automaattisen rotaation ja kattavan audit-lokin. Se sopii erityisesti tilanteisiin, joissa useilla mikrosovelluksilla on pääsy jaettuihin API-avaimiin:
npm install node-vault
// src/secrets/vaultClient.js
const vault = require('node-vault');
const client = vault({
endpoint: process.env.VAULT_ADDR || 'http://127.0.0.1:8200',
token: process.env.VAULT_TOKEN,
});
async function getApiKeys() {
const result = await client.read('secret/data/api-keys/production');
return result.data.data; // KV v2 -rakenne
}
// Käyttö käynnistyksessä ennen muuta konfiguraatiota
async function loadSecretsFromVault() {
const keys = await getApiKeys();
process.env.DATABASE_API_KEY = keys.database_api_key;
process.env.PAYMENT_API_KEY = keys.payment_api_key;
console.log('Salaisuudet ladattu Vaultista onnistuneesti');
}
module.exports = { loadSecretsFromVault };
Scoped API-avaimet eli käyttötarkoituskohtaiset avaimet
Sen sijaan, että antaisit asiakkaalle yhden avaimen kaikkeen, luo avainhierarkia, jossa jokainen avain on rajattu tiettyihin toimintoihin. Tämä least privilege -periaate minimoi vahingon jos yksi avain vuotaa:
- Vain luku -avain: GET-pyynnöt, ei pääsyä arkaluonteisiin resursseihin
- Kirjoitusavain: POST/PUT/PATCH-pyynnöt rajatulle resurssijoukolle
- Admin-avain: Kaikki operaatiot mukaan lukien avainten hallinta, käytetään vain infrastruktuuritoiminnoissa
- Webhook-avain: Vain tiettyjen webhook-päätepisteiden validointi, ei muita resursseja
- Read-only tilastot -avain: Pääsy vain julkisiin metriikka-päätepisteihin, kolmansille osapuolille turvallinen
Tämä periaate on OWASP Top 10:n keskeinen suositus Broken Access Control -haavoittuvuuksien (A01) ehkäisyssä. Kaikkein kriittisimmissä järjestelmissä jokaisella mikropalvelulla tulisi olla oma, käyttötarkoitukseen rajattu avain eikä koskaan jaettua yleisavainta.
Turvallisimpien käytäntöjen yhteenveto ja tarkistuslista
Tässä oppaassa olemme rakentaneet kerrokset: ympäristömuuttujat (vaihe 2-3), validointi-middleware timing-safe vertailulla (vaihe 4), avainten generointi ja rotaatiomoduuli (vaihe 6), tietokantatallennus hajautettuna (vaihe 7), audit-lokitus (vaihe 8) ja Docker-tuotantoympäristö (vaihe 9). Node.js:n virallinen tietoturvaopas suosittelee kaikkia näitä käytäntöjä.
| Käytäntö | Vaikutus tietoturvaan | Toteutustaso |
|---|---|---|
| Ympäristömuuttujat (dotenv + envalid) | Estää avainten vuotamisen lähdekoodissa | Perustaso |
| Vakioaikainen vertailu (timingSafeEqual) | Estää timing-hyökkäykset | Perustaso |
| SHA-256 hajautus tietokannassa | Suojaa tietokantavuodoilta | Perustaso |
| Audit-lokitus (JSONL-muoto) | Mahdollistaa forensisen analyysin ja CRA-raportoinnin | Perustaso |
| HTTPS pakollinen tuotannossa | Estää API-avaimen salakuuntelun verkkoliikenteestä | Perustaso |
| 90 päivän rotaatiokalenteri | Rajoittaa vuotaneen avaimen elinkaarta | Suositeltava |
| Rate limiting (express-rate-limit) | Estää brute force -hyökkäykset | Suositeltava |
| Scoped avaimet (least privilege) | Minimoi vahingon avainvuodossa | Suositeltava |
| HashiCorp Vault / AWS Secrets Manager | Dynaaminen hallinta, automaattinen rotaatio laajassa mittakaavassa | Edistynyt |
Aiheeseen liittyvä sisältö
Related Coverage
- JWT-todennus Node.js:ssä: 12 vaihetta, 40 min [2026] - JWT-tokenien ja API-avainten yhdistäminen autentikointikerroksessa
- HMAC Node.js:ssä: 10 vaihetta, 30 min [2026] - Pyyntöjen allekirjoittaminen HMAC-SHA256:lla API-kutsuja turvaamaan
- Node.js WebCrypto API: 12 vaihetta, 35 min [2026] - Kryptografisten operaatioiden toteuttaminen natiivilla WebCrypto-rajapinnalla
- ECDSA Node.js:ssä: 12 vaihetta, 35 min [2026] - Digitaalisten allekirjoitusten käyttö API-pyyntöjen eheyden varmistamiseen
- Cyber Resilience Act: 15 M€ sakot [2026] - EU:n CRA-asetuksen vaatimukset suomalaisille yrityksille
- BLAKE3-hajautus Node.js:ssä: 10 vaihetta, 30 min [2026] - Nopea hajautusalgoritmi avainten hajautukseen
UKK: Node.js API-avainten hallinta
Kuinka pitkä API-avaimen pitäisi olla?
Vähintään 256 bittiä eli 32 tavua satunnaista dataa. crypto.randomBytes(32).toString('base64url') tuottaa 43 merkin avaimen Node.js:ssä. Lyhyemmät avaimet ovat alttiita brute force -hyökkäyksille. Etuliitteen (kuten prod_ tai key_) lisääminen helpottaa avaimen tunnistamista käyttötarkoituksen mukaan mutta ei lisää entropiaa.
Milloin API-avain pitäisi pyörittää?
Rutiinikäytössä 90 päivän välein. Heti välittömästi jos: epäilet vuotoa, entinen työntekijä lähtee, kolmannen osapuolen integraatio puretaan tai havaitset epäilyttävää API-käyttöä lokitiedostoissa. Toteuta aina grace period -käytäntö (esim. 7 päivää), jonka aikana vanha avain toimii rinnakkain uuden kanssa, jotta asiakkailla on aikaa siirtyä.
Voiko API-avainta tallentaa selaimen localStorageen?
Ei koskaan. Selaimen localStorage, sessionStorage ja evästeet ovat alttiita XSS-hyökkäyksille. Jos tarvitset API-avainta client-side-sovelluksessa, käytä lyhytikäistä JWT-tokenia palvelinpuolen API:n kautta eikä pitkäikäistä API-avainta suoraan. API-avaimet kuuluvat palvelinpuolelle.
Mikä on ero API-avaimen ja JWT-tokenin välillä?
API-avain on pitkäikäinen, tilaton tunniste, joka ei sisällä tietoa itsessään. JWT on allekirjoitettu token, joka sisältää tietoa (claims) käyttäjästä tai oikeuksista ja vanhenee automaattisesti. API-avaimet sopivat server-to-server-kommunikaatioon, JWT:t käyttäjäsessioihin. Monet sovellukset yhdistävät molemmat: API-avain tunnistaa integraation, JWT tunnistaa käyttäjän API-avainta käyttävässä sovelluksessa.
Miten Node.js:n crypto-moduuli liittyy API-avainten hallintaan?
Node.js:n natiivi crypto-moduuli tarjoaa kolme keskeistä funktiota: crypto.randomBytes() turvalliseen avainten generointiin, crypto.createHash() avainten hajautukseen tallennusta varten, ja crypto.timingSafeEqual() avainten vertailuun timing-hyökkäyksiä vastaan. Nämä kolme kattavat koko avainten elinkaaren hallinnan ilman ulkoisia riippuvuuksia.
Pitääkö .env-tiedosto lisätä .gitignore-tiedostoon ennen vai jälkeen git init?
Ennen ensimmäistä commitia. Lisää .env .gitignore-tiedostoon ennen kuin ajat git add .. Jos olet jo vahingossa committanut .env-tiedoston, pyöritä kaikki sinne tallennetut avaimet välittömästi ja poista se historiasta: git rm --cached .env && git commit -m "Remove .env from tracking". Oleta, että vuotanut avain on kompromissoitu, vaikka repositorio olisi yksityinen.
Miten CRA vaikuttaa API-avainten hallintaan suomalaisissa yrityksissä?
EU:n Cyber Resilience Act edellyttää syyskuusta 2026 alkaen 24 tunnin ilmoitusvelvoitteen aktiivisesti hyväksikäytetyistä haavoittuvuuksista. Huono API-avainten hallinta, kuten vuotanut avain jota hyökkääjä käyttää, laukaisee tämän raportointivelvoitteen. Audit-lokitus on käytännossä pakollinen CRA-vaatimusten täyttämiseksi, koska ilman lokeja et pysty selvittämään tapahtuman laajuutta 24 tunnin raportointiaikaikkunassa.
Mikä on paras tapa jakaa API-avaimet tiimissä?
Älä koskaan jaa API-avaimia sähköpostissa, Slackissa tai muussa viestipalvelussa. Parhaita käytäntöjä ovat: yrityksen salasanojenhallintatyökalu (1Password Teams tai Bitwarden Business), HashiCorp Vault kehittäjätiimeille, tai pilvipalveluntarjoajan salaisuuksienhallintapalvelu (AWS Secrets Manager, Azure Key Vault, GCP Secret Manager). Jokaisella kehittäjällä täytyy olla omat kehitysympäristön avaimensa eikä koskaan jaettuja avaimia tiimin kesken. npm:n .npmrc-tiedostoa käytettäessä sama periaate koskee npm-rekisterin autentikointitunnuksia.




