{"id":137,"date":"2026-06-20T16:51:43","date_gmt":"2026-06-20T16:51:43","guid":{"rendered":"https:\/\/shattered.io\/fi\/2026\/06\/20\/nodejs-api-avainten-hallinta\/"},"modified":"2026-06-20T16:53:09","modified_gmt":"2026-06-20T16:53:09","slug":"nodejs-api-avainten-hallinta","status":"publish","type":"post","link":"https:\/\/shattered.io\/fi\/nodejs-api-avainten-hallinta\/","title":{"rendered":"Node.js API-avainten hallinta: 12 vaihetta, 30 min [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Node.js-sovellusten API-avainten hallinta on yksi kriittisimmist\u00e4 tietoturvakohdista, jonka kehitt\u00e4j\u00e4t j\u00e4tt\u00e4v\u00e4t usein sivuun kiireen vuoksi. Vuoden 2025 GitGuardian-raportti l\u00f6ysi yli 12,8 miljoonaa kovakoodattua salaista tunnusta julkisista GitHub-repositorioista, ja Node.js-projektit muodostivat niist\u00e4 merkitt\u00e4v\u00e4n osan. Yksi vuotanut API-avain voi johtaa tietoturvaloukkaukseen, jonka kustannukset ylt\u00e4v\u00e4t satoihin tuhansiin euroihin.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">EU:n Cyber Resilience Act (CRA) astuu voimaan syyskuussa 2026, ja sen my\u00f6t\u00e4 suomalaisilla yrityksill\u00e4 on 24 tunnin raportointivelvoite kriittisist\u00e4 tietoturvaloukkauksista. Huono API-avainten hallinta voi tarkoittaa suoraa CRA-rikkomusta. T\u00e4ss\u00e4 oppaassa rakennat Node.js-sovellukseen kattavan API-avainten hallintaj\u00e4rjestelm\u00e4n 12 vaiheessa, noin 30 minuutissa.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"miksi-api-avainten-hallinta-on-kriittinen\">Miksi API-avainten hallinta on kriittinen osa Node.js-tietoturvaa<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">API-avainten vuotaminen on yleisempi ongelma kuin moni kehitt\u00e4j\u00e4 haluaa my\u00f6nt\u00e4\u00e4. Tyypillisin skenaario: kehitt\u00e4j\u00e4 lis\u00e4\u00e4 API-avaimen suoraan kooditiedostoon testausta varten, committaa vahingossa koko kansion versionhallintaan, ja avain p\u00e4\u00e4tyy GitHubiin. GitHub Advanced Securityn skannerit havaitsevat yleens\u00e4 vuodot muutamassa minuutissa, mutta haitalliset toimijat ovat usein nopeampia.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">OpenSSF:n (Open Source Security Foundation) vuoden 2025 raportin mukaan 68 prosenttia Node.js-projekteissa havaituista tuotantoymp\u00e4rist\u00f6n tietoturvahaavoittuvuuksista liittyy salaisuuksien v\u00e4\u00e4r\u00e4\u00e4n k\u00e4sittelyyn. T\u00e4h\u00e4n lukeutuvat kovakoodatut API-avaimet, selkotekstiset salasanat ja versionhallintaan p\u00e4\u00e4tyneet <code>.env<\/code>-tiedostot. Oikein toteutettu API-avainten hallinta Node.js:ss\u00e4 koostuu viidest\u00e4 kerroksesta: turvallinen tallennus, validointi, rotaatio, audit-lokitus ja salaus.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Vuototapa<\/th><th>Osuus tapauksista<\/th><th>Keskim\u00e4\u00e4r\u00e4inen vahinko<\/th><\/tr><\/thead><tbody><tr><td>Kovakoodattu avain Git-repositoriossa<\/td><td>42 %<\/td><td>38 000 \u20ac<\/td><\/tr><tr><td>API-avain lokitiedostossa<\/td><td>21 %<\/td><td>12 000 \u20ac<\/td><\/tr><tr><td>Avain HTTP-pyynn\u00f6iss\u00e4 selkotekstin\u00e4<\/td><td>18 %<\/td><td>55 000 \u20ac<\/td><\/tr><tr><td>.env-tiedosto tuotantopalvelimella julkisena<\/td><td>11 %<\/td><td>22 000 \u20ac<\/td><\/tr><tr><td>Avain client-side JavaScript-koodissa<\/td><td>8 %<\/td><td>18 000 \u20ac<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"esivaatimukset-ja-versiot\">Esivaatimukset ja versiot<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Tarvitset seuraavat ty\u00f6kalut asennettuna ennen kuin aloitat t\u00e4m\u00e4n oppaan:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>Node.js 22.x LTS<\/strong> (suositellaan, minimiversio 20.x)<\/li><li><strong>npm 10.x<\/strong> tai uudempi (tulee Node.js 22:n mukana)<\/li><li><strong>Express 5.x<\/strong> (asennetaan oppaan aikana)<\/li><li><strong>dotenv 16.x<\/strong> (ymp\u00e4rist\u00f6muuttujien lataamiseen)<\/li><li><strong>envalid 8.x<\/strong> (ymp\u00e4rist\u00f6muuttujien validointiin)<\/li><li><strong>better-sqlite3 11.x<\/strong> (avainten tallennukseen)<\/li><li><strong>Git<\/strong> (versiohallinnan .gitignore-konfigurointiin)<\/li><li>Tekstieditori (VS Code suositeltava)<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Tarkista Node.js-versio ennen aloittamista:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node --version\n# v22.12.0\nnpm --version\n# 10.9.0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Jos Node.js ei ole asennettu tai versio on vanhempi kuin 20.x, lataa uusin LTS-versio <a href=\"https:\/\/nodejs.org\/api\/crypto.html\" target=\"_blank\" rel=\"noopener\">nodejs.org<\/a>-sivustolta tai k\u00e4yt\u00e4 nvm-versiohallintaty\u00f6kalua (<code>nvm install --lts<\/code>). Node.js 22 on suositeltava versio, koska se sis\u00e4lt\u00e4\u00e4 paranneltu WebCrypto-rajapinta ja pitk\u00e4aikainen tuki vuoteen 2027 saakka.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-1-projektin-alustus-ja-kansiorakenne\">Vaihe 1: Projektin alustus ja kansiorakenne<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Luo uusi Node.js-projekti selke\u00e4ll\u00e4 kansiorakenteella, joka erottaa konfiguraation sovelluskoodista. Hyv\u00e4 rakenne ehk\u00e4isee tulevat virheet ja helpottaa yll\u00e4pitoa.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir nodejs-api-key-demo\ncd nodejs-api-key-demo\nnpm init -y\n\nmkdir -p src\/middleware src\/database config tests logs\ntouch src\/app.js\ntouch src\/middleware\/apiKeyAuth.js\ntouch src\/middleware\/auditLogger.js\ntouch src\/database\/apiKeyStore.js\ntouch src\/apiKeyManager.js\ntouch config\/config.js\ntouch .env\ntouch .env.example\ntouch .gitignore<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Kansiorakenne n\u00e4ytt\u00e4\u00e4 t\u00e4lt\u00e4:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>nodejs-api-key-demo\/\n\u251c\u2500\u2500 src\/\n\u2502   \u251c\u2500\u2500 app.js\n\u2502   \u251c\u2500\u2500 apiKeyManager.js\n\u2502   \u251c\u2500\u2500 middleware\/\n\u2502   \u2502   \u251c\u2500\u2500 apiKeyAuth.js\n\u2502   \u2502   \u2514\u2500\u2500 auditLogger.js\n\u2502   \u2514\u2500\u2500 database\/\n\u2502       \u2514\u2500\u2500 apiKeyStore.js\n\u251c\u2500\u2500 config\/\n\u2502   \u2514\u2500\u2500 config.js\n\u251c\u2500\u2500 tests\/\n\u251c\u2500\u2500 logs\/             # EI versionhallintaan\n\u251c\u2500\u2500 .env              # EI versionhallintaan\n\u251c\u2500\u2500 .env.example      # Versionhallintaan, ei oikeita arvoja\n\u251c\u2500\u2500 .gitignore\n\u2514\u2500\u2500 package.json<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">T\u00e4m\u00e4 rakenne on t\u00e4rke\u00e4: <code>.env<\/code>-tiedosto sis\u00e4lt\u00e4\u00e4 todelliset salaisuudet eik\u00e4 koskaan p\u00e4\u00e4dy versionhallintaan. <code>.env.example<\/code>-tiedosto dokumentoi tarvittavat ymp\u00e4rist\u00f6muuttujat ilman oikeita arvoja ja lis\u00e4t\u00e4\u00e4n versionhallintaan. <code>logs\/<\/code>-kansio s\u00e4ilytet\u00e4\u00e4n paikallisesti mutta ei vied\u00e4 repositorioon, koska lokitiedostot voivat sis\u00e4lt\u00e4\u00e4 arkaluonteista tietoa.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-2-gitignore-ja-env-konfigurointi\">Vaihe 2: .gitignore-konfigurointi ja .env-tiedoston rakenne<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ensimm\u00e4inen ja t\u00e4rkein askel on varmistaa, ett\u00e4 <code>.env<\/code>-tiedosto ei koskaan p\u00e4\u00e4dy versionhallintaan. T\u00e4m\u00e4 on yksinkertaisin mutta useimmin unohdettava turvatoimenpide. Lis\u00e4\u00e4 <code>.gitignore<\/code>-tiedostoon kaikki sensitiiviset tiedostotyypit:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># .gitignore\n.env\n.env.local\n.env.production\n.env.staging\nnode_modules\/\n*.log\nlogs\/\n*.pem\n*.key\n*.sqlite\nsecrets\/\napi-keys.sqlite<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Luo <code>.env.example<\/code>-tiedosto, joka n\u00e4ytt\u00e4\u00e4 kehitt\u00e4jille mit\u00e4 muuttujia tarvitaan mutta ei sis\u00e4ll\u00e4 oikeita arvoja:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># .env.example - Kopioi t\u00e4m\u00e4 tiedosto nimell\u00e4 .env ja t\u00e4yt\u00e4 oikeat arvot\n# \u00c4L\u00c4 koskaan tallenna oikeita avaimia t\u00e4h\u00e4n tiedostoon\nNODE_ENV=development\nPORT=3000\n\n# Ulkoiset API-avaimet (palveluntarjoajilta haettavat)\nDATABASE_API_KEY=your-database-api-key-here\nPAYMENT_API_KEY=your-payment-api-key-here\nSMTP_API_KEY=your-smtp-api-key-here\nEXTERNAL_SERVICE_KEY=your-external-service-key-here\n\n# API-avainten konfiguraatio\nAPI_KEY_MIN_LENGTH=32\nAPI_KEY_ROTATION_DAYS=90<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Luo varsinainen <code>.env<\/code>-tiedosto kehitysymp\u00e4rist\u00f6\u00e4 varten. Huomaa: n\u00e4m\u00e4 ovat esimerkkiarvoja, ei oikeita avaimia:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># .env - T\u00c4T\u00c4 EI LIS\u00c4T\u00c4 VERSIONHALLINTAAN KOSKAAN\nNODE_ENV=development\nPORT=3000\n\nDATABASE_API_KEY=dev-db-key-8f3k2m9p4x7q1n5w8r4v\nPAYMENT_API_KEY=dev-payment-key-2n5w8r1v6y3t4j7c9h2m\nSMTP_API_KEY=dev-smtp-key-9j4c7h2m5b8x1a4e7i0l3\nEXTERNAL_SERVICE_KEY=dev-ext-key-1a4e7i0l3o6r9u2s5b8\n\nAPI_KEY_MIN_LENGTH=32\nAPI_KEY_ROTATION_DAYS=90<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-3-dotenv-paketin-asennus-ja-kaytto\">Vaihe 3: Dotenv-paketin asennus ja k\u00e4ytt\u00f6<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/github.com\/motdotla\/dotenv\" target=\"_blank\" rel=\"noopener\">Dotenv<\/a> on Node.js-ekosysteemin suosituin kirjasto ymp\u00e4rist\u00f6muuttujien lataamiseen. Se lataa <code>.env<\/code>-tiedoston muuttujat <code>process.env<\/code>-objektiin sovelluksen k\u00e4ynnistyksen yhteydess\u00e4. Asennetaan tarvittavat paketit:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install dotenv express envalid better-sqlite3\nnpm install --save-dev jest supertest<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">T\u00e4rke\u00e4 huomio: dotenv t\u00e4ytyy ladata <strong>ensimm\u00e4isen\u00e4<\/strong> sovelluksessa, ennen muita <code>require<\/code>-kutsuja, jotka voivat tarvita ymp\u00e4rist\u00f6muuttujia. Luo konfiguraatiotiedosto, joka validoi kaikki muuttujat k\u00e4ynnistysvaiheessa:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ config\/config.js\nrequire('dotenv').config();\n\nconst { cleanEnv, str, num } = require('envalid');\n\n\/\/ Validoi kaikki ymp\u00e4rist\u00f6muuttujat sovelluksen k\u00e4ynnistyksess\u00e4.\n\/\/ Jos jokin pakollinen muuttuja puuttuu, sovellus kaatuu heti selke\u00e4ll\u00e4 virheviestill\u00e4.\nconst env = cleanEnv(process.env, {\n  NODE_ENV: str({ choices: ['development', 'test', 'production'] }),\n  PORT: num({ default: 3000 }),\n  DATABASE_API_KEY: str({ docs: 'Haetaan tietokantapalveluntarjoajalta' }),\n  PAYMENT_API_KEY: str({ docs: 'Haetaan maksupalveluntarjoajalta' }),\n  SMTP_API_KEY: str({ docs: 'Haetaan s\u00e4hk\u00f6postipalveluntarjoajalta' }),\n  EXTERNAL_SERVICE_KEY: str(),\n  API_KEY_MIN_LENGTH: num({ default: 32 }),\n  API_KEY_ROTATION_DAYS: num({ default: 90 }),\n});\n\nmodule.exports = env;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Envalid-kirjasto tekee kaksi t\u00e4rke\u00e4\u00e4 asiaa: se varmistaa, ett\u00e4 kaikki pakolliset muuttujat on asetettu (ja kaataa sovelluksen k\u00e4ynnistysvaiheessa jos jokin puuttuu), ja se tyypitt\u00e4\u00e4 muuttujat oikeiksi JavaScript-tyypeiksi. T\u00e4m\u00e4 est\u00e4\u00e4 tilanteen, jossa sovellus k\u00e4ynnistyy puutteellisilla asetuksilla ja aiheuttaa my\u00f6hemmin hankalasti j\u00e4ljitett\u00e4vi\u00e4 ajonaikaisia virheit\u00e4.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-4-api-avainten-validointi-middleware\">Vaihe 4: API-avainten validointi-middleware<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Kun sovelluksesi tarjoaa API-rajapinnan ulkopuolisille asiakkaille, tarvitset middlewaren, joka tarkistaa jokaisen pyynn\u00f6n API-avaimen. T\u00e4m\u00e4 middleware on sovelluksesi ensimm\u00e4inen puolustuslinja. Kriittisin yksityiskohta on vakioaikainen vertailu (<code>crypto.timingSafeEqual<\/code>), joka est\u00e4\u00e4 timing-hy\u00f6kk\u00e4ykset:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/middleware\/apiKeyAuth.js\nconst crypto = require('crypto');\nconst env = require('..\/..\/config\/config');\n\n\/\/ Kehitysymp\u00e4rist\u00f6n testitestaimet in-memory-rakenteessa.\n\/\/ Tuotannossa t\u00e4m\u00e4 data tulee tietokannasta (ks. vaihe 7).\nconst validApiKeys = new Map([\n  ['client-key-a7f3k9m2p5x8q1n5w8r4v', { name: 'Client A', permissions: ['read'] }],\n  ['client-key-b2w6r4v8y1t7j5c9h2m0p', { name: 'Client B', permissions: ['read', 'write'] }],\n]);\n\n\/\/ Vakioaikainen vertailu est\u00e4\u00e4 timing-hy\u00f6kk\u00e4ykset.\n\/\/ Tavallinen === vuotaa tietoa: se palauttaa false heti ensimm\u00e4isen virheellisen merkin kohdalla,\n\/\/ jolloin hy\u00f6kk\u00e4\u00e4j\u00e4 voi mitata vasteaikaa ja p\u00e4\u00e4tell\u00e4 montako merkki\u00e4 on oikein.\nfunction safeCompare(a, b) {\n  if (typeof a !== 'string' || typeof b !== 'string') return false;\n  \/\/ Normalisoi pituus ennen vertailua\n  const aBuf = Buffer.from(a.padEnd(64, '\\0'));\n  const bBuf = Buffer.from(b.padEnd(64, '\\0'));\n  const equal = crypto.timingSafeEqual(aBuf, bBuf);\n  \/\/ Pituusero on aina ep\u00e4onnistuminen, vaikka vertailu kest\u00e4\u00e4kin vakioajan\n  return equal && a.length === b.length;\n}\n\nfunction apiKeyAuth(req, res, next) {\n  const authHeader = req.headers['authorization'];\n  const queryKey = req.query.api_key;\n\n  let apiKey = null;\n\n  if (authHeader && authHeader.startsWith('Bearer ')) {\n    apiKey = authHeader.slice(7).trim();\n  } else if (queryKey) {\n    \/\/ URL-parametri on v\u00e4hemm\u00e4n turvallinen: p\u00e4\u00e4tyy palvelinlokeihin ja selaimen historiaan\n    console.warn('[SECURITY] API-avain l\u00e4hetettiin URL-parametrina - k\u00e4yt\u00e4 Authorization-headeria');\n    apiKey = queryKey.trim();\n  }\n\n  if (!apiKey) {\n    return res.status(401).json({\n      error: 'API-avain puuttuu',\n      hint: 'Lis\u00e4\u00e4 Authorization: Bearer <api-key> -header'\n    });\n  }\n\n  if (apiKey.length < env.API_KEY_MIN_LENGTH) {\n    return res.status(401).json({ error: 'Virheellinen API-avain' });\n  }\n\n  let clientInfo = null;\n  for (const [key, info] of validApiKeys) {\n    if (safeCompare(apiKey, key)) {\n      clientInfo = info;\n      break;\n    }\n  }\n\n  if (!clientInfo) {\n    console.warn(`[SECURITY] Ep\u00e4onnistunut API-avain-autentikointi IP:lt\u00e4 ${req.ip} polkuun ${req.path}`);\n    return res.status(401).json({ error: 'Virheellinen API-avain' });\n  }\n\n  req.apiClient = clientInfo;\n  next();\n}\n\nmodule.exports = apiKeyAuth;<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-5-express-sovelluksen-rakentaminen\">Vaihe 5: Express-sovelluksen rakentaminen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Rakennetaan nyt p\u00e4\u00e4sovellus, joka yhdist\u00e4\u00e4 konfiguraation ja middlewaret yhteen toimivaksi kokonaisuudeksi. Express 5.x tuo mukanaan paranneltu virheenk\u00e4sittelyn async-funktioille:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/app.js\nconst express = require('express');\nconst env = require('..\/config\/config');\nconst apiKeyAuth = require('.\/middleware\/apiKeyAuth');\nconst { auditMiddleware } = require('.\/middleware\/auditLogger');\n\nconst app = express();\napp.use(express.json());\napp.use(auditMiddleware); \/\/ Lokita kaikki pyynn\u00f6t\n\n\/\/ Ei paljasteta teknologiapinoa hy\u00f6kk\u00e4\u00e4jille\napp.disable('x-powered-by');\n\n\/\/ Julkinen terveystarkistusreitti - ei vaadi autentikointia\napp.get('\/health', (req, res) => {\n  res.json({ status: 'ok', timestamp: new Date().toISOString() });\n});\n\n\/\/ Suojattu lukureitti - vaatii API-avaimen\napp.get('\/api\/data', apiKeyAuth, (req, res) => {\n  res.json({\n    message: `Hei, ${req.apiClient.name}!`,\n    data: { items: ['tuote-1', 'tuote-2', 'tuote-3'] },\n    permissions: req.apiClient.permissions,\n  });\n});\n\n\/\/ Kirjoitusoikeutta vaativa reitti\napp.post('\/api\/data', apiKeyAuth, (req, res) => {\n  if (!req.apiClient.permissions.includes('write')) {\n    return res.status(403).json({ error: 'Kirjoitusoikeus puuttuu t\u00e4lt\u00e4 API-avaimelta' });\n  }\n  res.status(201).json({ message: 'Data tallennettu onnistuneesti' });\n});\n\n\/\/ Globaali virheenk\u00e4sittely\napp.use((err, req, res, _next) => {\n  console.error('[ERROR]', err.message);\n  res.status(500).json({ error: 'Sis\u00e4inen palvelinvirhe' });\n});\n\nconst PORT = env.PORT;\napp.listen(PORT, () => {\n  console.log(`Palvelin k\u00e4ynnistetty portissa ${PORT} (${env.NODE_ENV})`);\n});\n\nmodule.exports = app;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">K\u00e4ynnist\u00e4 sovellus ja testaa se curlilla:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node src\/app.js\n# Palvelin k\u00e4ynnistetty portissa 3000 (development)\n\n# Testaa julkinen reitti\ncurl http:\/\/localhost:3000\/health\n# {\"status\":\"ok\",\"timestamp\":\"2026-06-20T10:30:00.000Z\"}\n\n# Testaa ilman API-avainta\ncurl http:\/\/localhost:3000\/api\/data\n# {\"error\":\"API-avain puuttuu\",\"hint\":\"Lis\u00e4\u00e4 Authorization: Bearer <api-key> -header\"}\n\n# Testaa oikealla API-avaimella\ncurl -H \"Authorization: Bearer client-key-a7f3k9m2p5x8q1n5w8r4v\" http:\/\/localhost:3000\/api\/data\n# {\"message\":\"Hei, Client A!\",\"data\":{\"items\":[\"tuote-1\",\"tuote-2\",\"tuote-3\"]},\"permissions\":[\"read\"]}\n\n# Testaa lukuoikeudella kirjoitusreitti\u00e4\ncurl -X POST -H \"Authorization: Bearer client-key-a7f3k9m2p5x8q1n5w8r4v\" \\\n  -H \"Content-Type: application\/json\" -d '{}' http:\/\/localhost:3000\/api\/data\n# {\"error\":\"Kirjoitusoikeus puuttuu t\u00e4lt\u00e4 API-avaimelta\"}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-6-api-avainten-generointi-ja-rotaatio\">Vaihe 6: API-avainten generointi ja rotaatio<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">API-avainten s\u00e4\u00e4nn\u00f6llinen rotaatio on yksi t\u00e4rkeimmist\u00e4 tietoturvak\u00e4yt\u00e4nn\u00f6ist\u00e4. Jos avain vuotaa eik\u00e4 sit\u00e4 ole koskaan rotaattu, hy\u00f6kk\u00e4\u00e4j\u00e4ll\u00e4 on rajoittamaton p\u00e4\u00e4sy ikuisesti. <a href=\"https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Secrets_Management_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener\">OWASP:n Secrets Management -suositukset<\/a> m\u00e4\u00e4rittelev\u00e4t rotaatiov\u00e4liksi 90 p\u00e4iv\u00e4\u00e4 normaalik\u00e4yt\u00f6ss\u00e4. Luo erillinen moduuli avainten hallintaan:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/apiKeyManager.js\nconst crypto = require('crypto');\nconst env = require('..\/config\/config');\n\n\/**\n * Generoi kryptografisesti turvallisen API-avaimen.\n * crypto.randomBytes k\u00e4ytt\u00e4\u00e4 k\u00e4ytt\u00f6j\u00e4rjestelm\u00e4n entropial\u00e4hdett\u00e4 (CSPRNG),\n * joten avain on aidosti satunnainen eik\u00e4 ennustettavissa.\n *\/\nfunction generateApiKey(prefix = 'key', byteLength = 32) {\n  const randomBytes = crypto.randomBytes(byteLength);\n  const keyBody = randomBytes.toString('base64url');\n  return `${prefix}_${keyBody}`;\n}\n\n\/**\n * Hajautaa API-avaimen tallennusta varten SHA-256:lla.\n * Tallennetaan vain hajautearvo, ei koskaan selkoteksti.\n *\/\nfunction hashApiKey(apiKey) {\n  return crypto.createHash('sha256').update(apiKey).digest('hex');\n}\n\n\/**\n * Tarkistaa, onko avain vanhentunut.\n *\/\nfunction isApiKeyExpired(createdAt, maxAgeDays = env.API_KEY_ROTATION_DAYS) {\n  const ageMs = Date.now() - new Date(createdAt).getTime();\n  const ageDays = ageMs \/ (1000 * 60 * 60 * 24);\n  return ageDays > maxAgeDays;\n}\n\n\/**\n * Rotaatiologiikka: generoi uusi avain ja asettaa vanhalle grace period -ajan,\n * jonka aikana asiakkaat voivat siirty\u00e4 uuteen avaimeen.\n *\/\nfunction rotateApiKey(gracePeriodDays = 7) {\n  const newKey = generateApiKey('key');\n  const expiryDate = new Date();\n  expiryDate.setDate(expiryDate.getDate() + gracePeriodDays);\n\n  return {\n    newKey,\n    newKeyHash: hashApiKey(newKey),\n    oldKeyExpiresAt: expiryDate.toISOString(),\n    rotatedAt: new Date().toISOString(),\n  };\n}\n\n\/\/ Esimerkkituloste kehitysymp\u00e4rist\u00f6ss\u00e4\nif (require.main === module) {\n  const newKey = generateApiKey('prod');\n  console.log('Uusi API-avain (n\u00e4ytet\u00e4\u00e4n vain kerran):', newKey);\n  console.log('Hajautearvo (tallennetaan tietokantaan):', hashApiKey(newKey));\n  console.log('Rotaatiotulos:', rotateApiKey());\n}\n\nmodule.exports = { generateApiKey, hashApiKey, isApiKeyExpired, rotateApiKey };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Esimerkkituloste:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node src\/apiKeyManager.js\n\nUusi API-avain (n\u00e4ytet\u00e4\u00e4n vain kerran): prod_8f3k2m9p4X7q1N5w8R4vBY2T7j3C1hZ\nHajautearvo (tallennetaan tietokantaan): a3f9e2b7c4d8f1a5e9c3b7d2f6a0e4b8a1c5f9e3...\nRotaatiotulos: {\n  newKey: 'key_2N5w8R1V6y3T4J7c9H2M5b8X1A4E0L',\n  newKeyHash: 'e7b3a9f2c5d8f1a4e7c0b4d8f2a5e9c3...',\n  oldKeyExpiresAt: '2026-06-27T10:30:00.000Z',\n  rotatedAt: '2026-06-20T10:30:00.000Z'\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Avain t\u00e4ytyy tallentaa tietokantaan <strong>ainoastaan hajautettuna<\/strong>. Selkotekstiavain n\u00e4ytet\u00e4\u00e4n asiakkaalle vain kerran luontivaiheessa, aivan kuten salasanat toimivat oikein toteutetuissa j\u00e4rjestelmiss\u00e4.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-7-api-avainten-hallinta-tietokannassa\">Vaihe 7: API-avainten hallinta tietokannassa<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Oikeassa tuotantosovelluksessa API-avaimet tallennettaan tietokantaan, ei muistiin. T\u00e4ss\u00e4 esimerkiss\u00e4 k\u00e4yt\u00e4mme SQLite-tietokantaa havainnollistamaan rakennetta, mutta sama logiikka toimii PostgreSQL:n tai MySQL:n kanssa muuttamatta arkkitehtuuria:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/database\/apiKeyStore.js\nconst Database = require('better-sqlite3');\nconst { generateApiKey, hashApiKey, isApiKeyExpired } = require('..\/apiKeyManager');\n\nconst db = new Database('.\/api-keys.sqlite');\n\n\/\/ Alusta tietokantarakenne ensimm\u00e4isell\u00e4 k\u00e4ynnistyskerralla\ndb.exec(`\n  CREATE TABLE IF NOT EXISTS api_keys (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    key_hash TEXT UNIQUE NOT NULL,\n    key_prefix TEXT NOT NULL,\n    client_name TEXT NOT NULL,\n    permissions TEXT NOT NULL DEFAULT '[\"read\"]',\n    created_at TEXT NOT NULL,\n    expires_at TEXT,\n    last_used_at TEXT,\n    use_count INTEGER DEFAULT 0,\n    is_active INTEGER DEFAULT 1,\n    rotated_from_id INTEGER REFERENCES api_keys(id)\n  );\n\n  CREATE TABLE IF NOT EXISTS api_key_audit (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    key_id INTEGER REFERENCES api_keys(id),\n    event_type TEXT NOT NULL,\n    ip_address TEXT,\n    user_agent TEXT,\n    resource TEXT,\n    timestamp TEXT NOT NULL\n  );\n`);\n\n\/**\n * Luo uusi API-avain asiakkaalle ja palauttaa selkotekstin\u00e4 vain kerran.\n *\/\nfunction createApiKey(clientName, permissions = ['read']) {\n  const rawKey = generateApiKey('key');\n  const keyHash = hashApiKey(rawKey);\n  const keyPrefix = rawKey.substring(0, 12) + '...';\n\n  db.prepare(`\n    INSERT INTO api_keys (key_hash, key_prefix, client_name, permissions, created_at)\n    VALUES (?, ?, ?, ?, ?)\n  `).run(keyHash, keyPrefix, clientName, JSON.stringify(permissions), new Date().toISOString());\n\n  \/\/ Palautetaan selkotekstiavain VAIN KERRAN - sen j\u00e4lkeen sit\u00e4 ei en\u00e4\u00e4 voi hakea\n  return { rawKey, keyPrefix, clientName, permissions };\n}\n\n\/**\n * Validoi API-avain pyynnoissa ja p\u00e4ivitt\u00e4\u00e4 k\u00e4ytt\u00f6tilastot.\n *\/\nfunction validateApiKey(rawKey, requestInfo = {}) {\n  const keyHash = hashApiKey(rawKey);\n\n  const key = db.prepare(`\n    SELECT * FROM api_keys WHERE key_hash = ? AND is_active = 1\n  `).get(keyHash);\n\n  if (!key) return null;\n\n  \/\/ Tarkista vanheneminen jos expires_at on asetettu\n  if (key.expires_at && new Date(key.expires_at) < new Date()) {\n    return null;\n  }\n\n  \/\/ P\u00e4ivit\u00e4 k\u00e4ytt\u00f6tiedot transaktiona\n  const updateAndAudit = db.transaction(() => {\n    db.prepare(`\n      UPDATE api_keys SET last_used_at = ?, use_count = use_count + 1 WHERE id = ?\n    `).run(new Date().toISOString(), key.id);\n\n    db.prepare(`\n      INSERT INTO api_key_audit (key_id, event_type, ip_address, user_agent, resource, timestamp)\n      VALUES (?, 'api_call', ?, ?, ?, ?)\n    `).run(key.id, requestInfo.ip, requestInfo.userAgent, requestInfo.resource, new Date().toISOString());\n  });\n\n  updateAndAudit();\n\n  return {\n    id: key.id,\n    clientName: key.client_name,\n    permissions: JSON.parse(key.permissions),\n    keyPrefix: key.key_prefix,\n  };\n}\n\n\/**\n * Poistaa vanhan avaimen k\u00e4yt\u00f6st\u00e4 rotaation yhteydess\u00e4.\n *\/\nfunction deactivateApiKey(keyId, gracePeriodDays = 7) {\n  const expiryDate = new Date();\n  expiryDate.setDate(expiryDate.getDate() + gracePeriodDays);\n\n  db.prepare(`\n    UPDATE api_keys SET expires_at = ? WHERE id = ?\n  `).run(expiryDate.toISOString(), keyId);\n}\n\nmodule.exports = { createApiKey, validateApiKey, deactivateApiKey, db };<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-8-audit-lokitus-ja-valvonta\">Vaihe 8: Audit-lokitus ja valvonta<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">EU:n Cyber Resilience Act edellytt\u00e4\u00e4, ett\u00e4 kriittisiss\u00e4 j\u00e4rjestelmiss\u00e4 on kattava lokitus tietoturvatoimista. <a href=\"https:\/\/hpp.fi\/blog\/the-cra-countdown-a-practical-guide-for-finnish-companies-on-the-2026-cybersecurity-deadlines\/\" target=\"_blank\" rel=\"noopener\">CRA:n 24 tunnin raportointivelvoite<\/a> on mahdoton t\u00e4ytt\u00e4\u00e4 ilman kattavaa audit-lokia. API-avainten k\u00e4yt\u00f6n lokittaminen on keskeinen osa t\u00e4t\u00e4 vaatimusta:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/middleware\/auditLogger.js\nconst fs = require('fs');\nconst path = require('path');\n\nconst LOG_DIR = path.join(__dirname, '..\/..\/logs');\nif (!fs.existsSync(LOG_DIR)) {\n  fs.mkdirSync(LOG_DIR, { recursive: true });\n}\n\nfunction writeAuditLog(event) {\n  const logEntry = JSON.stringify({\n    timestamp: new Date().toISOString(),\n    ...event,\n  }) + '\\n';\n\n  const today = new Date().toISOString().split('T')[0];\n  const logFile = path.join(LOG_DIR, `api-audit-${today}.jsonl`);\n\n  fs.appendFile(logFile, logEntry, (err) => {\n    if (err) console.error('[AUDIT] Kirjoitus ep\u00e4onnistui:', err.message);\n  });\n\n  if (process.env.NODE_ENV !== 'production') {\n    console.log('[AUDIT]', JSON.stringify(event));\n  }\n}\n\nfunction auditMiddleware(req, res, next) {\n  const startTime = Date.now();\n\n  res.on('finish', () => {\n    \/\/ Suodata sensitiiviset headerit pois lokista\n    const safeHeaders = Object.fromEntries(\n      Object.entries(req.headers).filter(([k]) =>\n        !['authorization', 'cookie', 'x-api-key'].includes(k.toLowerCase())\n      )\n    );\n\n    writeAuditLog({\n      event: 'http_request',\n      method: req.method,\n      path: req.path,\n      status: res.statusCode,\n      ip: req.ip,\n      userAgent: req.headers['user-agent'],\n      client: req.apiClient ? req.apiClient.name : 'anonymous',\n      durationMs: Date.now() - startTime,\n      authSuccess: res.statusCode !== 401 && res.statusCode !== 403,\n    });\n  });\n\n  next();\n}\n\nmodule.exports = { auditMiddleware, writeAuditLog };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Esimerkki audit-lokimerkinn\u00f6ist\u00e4:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\"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}\n{\"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}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Hyv\u00e4 audit-loki sis\u00e4lt\u00e4\u00e4 v\u00e4hint\u00e4\u00e4n: aikaleiman (UTC), pyynn\u00f6n tehneen asiakkaan tunnisteen, IP-osoitteen, pyynnetyn resurssin ja HTTP-metodin, autentikoinnin tuloksen ja vasteajan millisekunteina. Mit\u00e4 lokiin <strong>ei<\/strong> saa kirjoittaa: selkotekstiset API-avaimet, salasanat tai muut sensitiiviset kent\u00e4t.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-9-docker-ja-salaisuuksien-hallinta\">Vaihe 9: Docker-kontit ja salaisuuksien hallinta<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Konttipohjainen tuotantoonvienti on vakiintunut k\u00e4yt\u00e4nt\u00f6, mutta se tuo mukanaan erityisi\u00e4 haasteita API-avainten hallintaan. Salaisuudet ei saa olla Docker-kuvassa (image) eik\u00e4 Dockerfilessa. Luo ensin <code>.dockerignore<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># .dockerignore\n.env\n.env.*\n!.env.example\nnode_modules\n*.log\nlogs\/\nsecrets\/\n*.sqlite\n.git\n.gitignore<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Dockerfile ilman kovakoodattuja salaisuuksia, tuotantoa varten optimoituna:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Dockerfile\nFROM node:22-alpine AS base\nWORKDIR \/app\n\n# Kopioi vain package.json ensin (parempi Docker-v\u00e4limuistitus)\nCOPY package*.json .\/\nRUN npm ci --only=production\n\n# Kopioi l\u00e4hdekoodi - .dockerignore est\u00e4\u00e4 .env-tiedoston kopioinnin\nCOPY . .\n\n# K\u00e4yt\u00e4 ei-root-k\u00e4ytt\u00e4j\u00e4\u00e4 tietoturvasyist\u00e4\nRUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001\nUSER nodejs\n\nEXPOSE 3000\n# Ei hardkoodattuja muuttujia - ne annetaan ajoaikana\nCMD [\"node\", \"src\/app.js\"]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">K\u00e4ynnist\u00e4 kontti ymp\u00e4rist\u00f6muuttujilla ajoaikana, ei kuvaan sis\u00e4llytettyn\u00e4:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Kehitysymp\u00e4rist\u00f6ss\u00e4 - k\u00e4yt\u00e4 --env-file\ndocker build -t nodejs-api-demo .\ndocker run -p 3000:3000 --env-file .env nodejs-api-demo\n\n# Tuotannossa Docker Swarmilla\ndocker secret create database_api_key - <<< \"prod-db-key-xyz123abc456\"\ndocker service create \\\n  --name api-service \\\n  --secret database_api_key \\\n  --publish 3000:3000 \\\n  nodejs-api-demo\n\n# Tuotannossa Kubernetes Secretill\u00e4\nkubectl create secret generic api-keys \\\n  --from-literal=DATABASE_API_KEY=\"prod-db-key-xyz123abc456\" \\\n  --from-literal=PAYMENT_API_KEY=\"prod-payment-key-def789ghi\"\n\n# Kubernetes Pod -konfiguraatio\n# (env-muuttuja tulee secretist\u00e4, ei kovakoodattuna)\n# env:\n#   - name: DATABASE_API_KEY\n#     valueFrom:\n#       secretKeyRef:\n#         name: api-keys\n#         key: DATABASE_API_KEY<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vaihe-10-12-testaus-ja-tuotantoonvienti\">Vaihe 10-12: Testaus, tietoturvatarkistus ja tuotantoonvienti<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Vaihe 10 on yksikk\u00f6testien kirjoittaminen API-avainten validointilogiikalle. Vaihe 11 on tietoturvatarkistus ennen tuotantoonvienti\u00e4. Vaihe 12 on jatkuva valvonta tuotannossa.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ tests\/apiKeyAuth.test.js\nconst request = require('supertest');\nconst app = require('..\/src\/app');\n\ndescribe('API-avainten autentikointi', () => {\n  test('palauttaa 401 ilman API-avainta', async () => {\n    const res = await request(app).get('\/api\/data');\n    expect(res.status).toBe(401);\n    expect(res.body.error).toBe('API-avain puuttuu');\n  });\n\n  test('palauttaa 401 virheellisell\u00e4 API-avaimella', async () => {\n    const res = await request(app)\n      .get('\/api\/data')\n      .set('Authorization', 'Bearer v\u00e4\u00e4r\u00e4-avain-123');\n    expect(res.status).toBe(401);\n  });\n\n  test('palauttaa 200 oikealla API-avaimella', async () => {\n    const res = await request(app)\n      .get('\/api\/data')\n      .set('Authorization', 'Bearer client-key-a7f3k9m2p5x8q1n5w8r4v');\n    expect(res.status).toBe(200);\n    expect(res.body.message).toContain('Client A');\n  });\n\n  test('est\u00e4\u00e4 kirjoittamisen pelk\u00e4ll\u00e4 lukuoikeudella', async () => {\n    const res = await request(app)\n      .post('\/api\/data')\n      .set('Authorization', 'Bearer client-key-a7f3k9m2p5x8q1n5w8r4v')\n      .send({ data: 'testi' });\n    expect(res.status).toBe(403);\n  });\n\n  test('sallii kirjoittamisen kirjoitusoikeudella', async () => {\n    const res = await request(app)\n      .post('\/api\/data')\n      .set('Authorization', 'Bearer client-key-b2w6r4v8y1t7j5c9h2m0p')\n      .send({ data: 'testi' });\n    expect(res.status).toBe(201);\n  });\n});\n\n\/\/ Suorita testit: npx jest<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Tietoturvatarkistuslista ennen tuotantoonvienti\u00e4:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Tarkistuspiste<\/th><th>Prioriteetti<\/th><th>Toimenpide jos puuttuu<\/th><\/tr><\/thead><tbody><tr><td>.env listattu .gitignoressa<\/td><td>Kriittinen<\/td><td>Lis\u00e4\u00e4 v\u00e4litt\u00f6m\u00e4sti, py\u00f6rit\u00e4 kaikki mahdollisesti vuotaneet avaimet<\/td><\/tr><tr><td>API-avaimet hajautettuna tietokannassa<\/td><td>Kriittinen<\/td><td>Migroi hajautukseen, poista selkotekstit<\/td><\/tr><tr><td>Vakioaikainen vertailu k\u00e4yt\u00f6ss\u00e4<\/td><td>Kriittinen<\/td><td>Korvaa === crypto.timingSafeEqual-kutsulla<\/td><\/tr><tr><td>HTTPS pakollinen tuotannossa<\/td><td>Kriittinen<\/td><td>Ohjaa HTTP -&gt; HTTPS, lis\u00e4\u00e4 HSTS-header<\/td><\/tr><tr><td>Rate limiting API-p\u00e4\u00e4tepisteihin<\/td><td>T\u00e4rke\u00e4<\/td><td>Lis\u00e4\u00e4 express-rate-limit middleware<\/td><\/tr><tr><td>Audit-lokitus aktiivisena<\/td><td>T\u00e4rke\u00e4<\/td><td>Lis\u00e4\u00e4 auditMiddleware kaikkiin reitteihin<\/td><\/tr><tr><td>Avainten rotaatioaikataulu m\u00e4\u00e4ritelty<\/td><td>Suositeltava<\/td><td>Aseta muistutukset 90 p\u00e4iv\u00e4n rotaatiolle<\/td><\/tr><tr><td>H\u00e4lytykset ep\u00e4onnistuneille autentikoinneille<\/td><td>Suositeltava<\/td><td>Lis\u00e4\u00e4 kynnys h\u00e4lytyslogiikkaan (esim. &gt;10 ep\u00e4onnistumista\/min)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"yleiset-sudenkuopat\">5 yleist\u00e4 sudenkuoppaa Node.js API-avainten hallinnassa<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">N\u00e4m\u00e4 virheet toistuvat projekteissa jatkuvasti, ja niist\u00e4 jokainen voi johtaa tietoturvaloukkaukseen. <a href=\"https:\/\/owasp.org\/www-project-top-ten\/\" target=\"_blank\" rel=\"noopener\">OWASP Top 10<\/a> listaa puutteellisen salaisuuksienhallintaan liittyv\u00e4t ongelmat osana haavoittuvuuskategoriaa A02 (Cryptographic Failures).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sudenkuoppa-1-kovakoodatut-avaimet\">Sudenkuoppa 1: Kovakoodatut API-avaimet l\u00e4hdekoodissa<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yleisin virhe on API-avaimen kirjoittaminen suoraan kooditiedostoon, usein \"vain tilap\u00e4isesti\". Tilap\u00e4inen muuttuu pysyv\u00e4ksi, avain committaan, ja ongelma on olemassa. GitHub Advanced Security skannaa kaikki public-repositoriot automaattisesti ja ilmoittaa palveluntarjoajalle vuotaneesta avaimesta muutamassa minuutissa.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ V\u00c4\u00c4RIN - EI KOSKAAN N\u00c4IN\nconst stripeApiKey = 'sk_live_8f3k2m9p4x7q1n5w';\n\n\/\/ OIKEIN - ymp\u00e4rist\u00f6muuttujasta\nrequire('dotenv').config();\nconst stripeApiKey = process.env.PAYMENT_API_KEY;\nif (!stripeApiKey) {\n  throw new Error('PAYMENT_API_KEY ymp\u00e4rist\u00f6muuttuja puuttuu - tarkista .env-tiedosto');\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Jos ep\u00e4ilet vahingossa committanneesi avaimen: py\u00f6rit\u00e4 avain palveluntarjoajalta v\u00e4litt\u00f6m\u00e4sti (t\u00e4rkeint\u00e4), puhdista Git-historia BFG Repo-Cleanerilla, tarkista GitHub Security -h\u00e4lytykset repositoriosta.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sudenkuoppa-2-avaimet-lokitiedostoissa\">Sudenkuoppa 2: API-avaimet lokitiedostoissa<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Pyynt\u00f6loggaus tulostaa usein kaikki headerit, mukaan lukien Authorization-headerin. Kehityksess\u00e4 t\u00e4m\u00e4 menee konsoliin, tuotannossa lokitiedostoihin, jotka voivat olla useiden ihmisten luettavissa tai p\u00e4\u00e4ty\u00e4 ulkopuolisiin lokipalveluihin.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ V\u00c4\u00c4RIN - Loggaa kaikki headerit mukaan lukien authorization-avaimen\napp.use((req, res, next) => {\n  console.log('Pyynto:', req.method, req.path, req.headers); \/\/ Vuotaa API-avaimen!\n  next();\n});\n\n\/\/ OIKEIN - Suodata sensitiiviset headerit ennen loggausta\napp.use((req, res, next) => {\n  const { authorization, cookie, 'x-api-key': apiKey, ...safeHeaders } = req.headers;\n  console.log('Pyynto:', req.method, req.path, safeHeaders);\n  next();\n});<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sudenkuoppa-3-api-avaimet-url-parametreissa\">Sudenkuoppa 3: API-avaimet URL-parametreissa<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">URL-parametreihin (<code>?api_key=xyz<\/code>) lis\u00e4tty API-avain p\u00e4\u00e4tyy palvelinlokeihin, selaimen historiaan, v\u00e4lityspalvelimien lokeihin, CDN:n lokeihin ja selaimen v\u00e4limuistiin. K\u00e4yt\u00e4 aina Authorization-headeria: <code>Authorization: Bearer &lt;avain&gt;<\/code>. Jos integraatiosi ei tue custom-headereita, k\u00e4yt\u00e4 POST-pyynt\u00f6\u00e4 ja l\u00e4het\u00e4 avain pyynn\u00f6n bodyssa HTTPS-yhteyden kautta.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sudenkuoppa-4-tavallinen-merkkijono-vertailu\">Sudenkuoppa 4: Tavallinen merkkijonovertailu API-avainten tarkistuksessa<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Tavallinen <code>===<\/code>-vertailu on altis timing-hy\u00f6kk\u00e4yksille. Hy\u00f6kk\u00e4\u00e4j\u00e4 voi mitata vertailun kestoa tuhansien pyynt\u00f6jen avulla ja p\u00e4\u00e4tell\u00e4, kuinka monta merkki\u00e4 h\u00e4nen testaamansa avain on oikein:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ V\u00c4\u00c4RIN - Altis timing-hy\u00f6kk\u00e4yksille\nif (providedKey === storedKey) { \/* ... *\/ }\n\n\/\/ OIKEIN - Vakioaikainen vertailu, kest\u00e4\u00e4 aina saman ajan\nconst crypto = require('crypto');\nfunction safeCompare(a, b) {\n  \/\/ Padataan samaan pituuteen ennen vertailua\n  const maxLen = Math.max(a.length, b.length, 32);\n  const aBuf = Buffer.alloc(maxLen);\n  const bBuf = Buffer.alloc(maxLen);\n  Buffer.from(a).copy(aBuf);\n  Buffer.from(b).copy(bBuf);\n  \/\/ timingSafeEqual kest\u00e4\u00e4 aina vakioajan riippumatta sis\u00e4ll\u00f6st\u00e4\n  return crypto.timingSafeEqual(aBuf, bBuf) && a.length === b.length;\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"sudenkuoppa-5-selkoteksti-tietokannassa\">Sudenkuoppa 5: Selkotekstiset API-avaimet tietokannassa<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Jos tietokanta vuotaa ja siell\u00e4 on selkotekstiset API-avaimet, hy\u00f6kk\u00e4\u00e4j\u00e4ll\u00e4 on v\u00e4lit\u00f6n p\u00e4\u00e4sy kaikkiin integraatioihin. Tallenna aina vain SHA-256-hajautearvo. N\u00e4yt\u00e4 selkotekstiavain asiakkaalle vain kerran (luomishetkell\u00e4) ja tallenna sen j\u00e4lkeen vain hajautearvo. Sama periaate kuin salasanojen hajautuksessa, mutta bcrypt\/Argon2:n sijaan SHA-256 riitt\u00e4\u00e4 API-avaimille, koska ne ovat jo pitki\u00e4 ja satunnaisia (toisin kuin k\u00e4ytt\u00e4jien valitsemat salasanat).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"vianmaaritys\">Vianm\u00e4\u00e4ritys: 8 yleist\u00e4 ongelmaa ratkaisuineen<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Ongelma<\/th><th>Oire<\/th><th>Ratkaisu<\/th><\/tr><\/thead><tbody><tr><td>process.env.API_KEY on undefined<\/td><td>Sovellus kaatuu k\u00e4ynnistyksess\u00e4 tai arvo on undefined<\/td><td>Tarkista dotenv-kutsu ennen muita require-kutsuja, tarkista .env-tiedoston sijainti suhteessa process.cwd()<\/td><\/tr><tr><td>.env-muutokset eiv\u00e4t n\u00e4y<\/td><td>Vanha arvo k\u00e4yt\u00f6ss\u00e4 muutoksen j\u00e4lkeen<\/td><td>Dotenv lukee vain k\u00e4ynnistyksess\u00e4, k\u00e4ynnist\u00e4 sovellus uudelleen<\/td><\/tr><tr><td>401 oikealla API-avaimella<\/td><td>Autentikointi ep\u00e4onnistuu jatkuvasti<\/td><td>Tarkista v\u00e4lily\u00f6nnit avaimessa (trim()), tarkista encoding (kopioi avain suoraan, ei k\u00e4sin kirjoita)<\/td><\/tr><tr><td>timingSafeEqual heitt\u00e4\u00e4 TypeError<\/td><td>TypeError: Input buffers must have the same length<\/td><td>K\u00e4yt\u00e4 safeCompare-wrapperia, joka normalisoi pituuden ennen vertailua<\/td><\/tr><tr><td>Docker-kontti ei l\u00f6yd\u00e4 muuttujia<\/td><td>envalid heitt\u00e4\u00e4 MissingEnvVarsError kontissa<\/td><td>Varmista --env-file tai -e flagien k\u00e4ytt\u00f6, tarkista .dockerignore ei sulje pois .env.example:a<\/td><\/tr><tr><td>API-avaimet lokitiedostoissa<\/td><td>Authorization-header selkotekstin\u00e4 lokissa<\/td><td>Suodata authorization-header ennen loggausta, tarkista middleware-j\u00e4rjestys<\/td><\/tr><tr><td>Rotaatio rikkoo asiakasintegraatioita<\/td><td>Asiakkaat saavat 401 rotaation j\u00e4lkeen<\/td><td>Toteuta grace period -k\u00e4yt\u00e4nt\u00f6 (vanha avain toimii viel\u00e4 7 p\u00e4iv\u00e4\u00e4), ilmoita rotaatiosta etuk\u00e4teen<\/td><\/tr><tr><td>envalid MissingEnvVarsError k\u00e4ynnistyksess\u00e4<\/td><td>Sovellus ei k\u00e4ynnisty selke\u00e4ll\u00e4 virheilmoituksella<\/td><td>Vertaa .env.example ja .env sis\u00e4lt\u00f6j\u00e4, lis\u00e4\u00e4 puuttuvat muuttujat .env-tiedostoon<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Diagnostiikkakomennot yleisimpiin ongelmiin:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Tarkista l\u00f6ytyyk\u00f6 .env-tiedosto oikeasta paikasta\nls -la .env && echo \"L\u00f6ytyi\" || echo \"EI L\u00d6YDY\"\n\n# Tarkista dotenv lataa muuttujan oikein\nnode -e \"require('dotenv').config(); console.log(process.env.DATABASE_API_KEY ? 'OK' : 'PUUTTUU')\"\n\n# Tarkista dotenv:n mahdolliset virheet\nnode -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')\"\n\n# Tarkista ymp\u00e4rist\u00f6muuttujat Docker-kontissa\ndocker exec -it <container-id> sh -c \"env | grep -E 'API_KEY|NODE_ENV|PORT'\"\n\n# Tarkista SQLite-tietokanta ja API-avainten tilat\nnode -e \"\nconst db = require('better-sqlite3')('.\/api-keys.sqlite');\nconst keys = db.prepare('SELECT id, key_prefix, client_name, is_active, use_count, last_used_at FROM api_keys').all();\nconsole.table(keys);\n\"<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"edistyneet-tekniikat\">Edistyneet tekniikat: HashiCorp Vault, AWS Secrets Manager ja scoped avaimet<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Dotenv soveltuu kehitysymp\u00e4rist\u00f6\u00f6n ja pieniin tuotantosovelluksiin. Suuremmissa yrityssovelluksissa, joissa on useita mikrosovelluksia tai tiimej\u00e4, kannattaa harkita dedikoidun salaisuuksienhallintaratkaisun k\u00e4ytt\u00f6\u00e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"hashicorp-vault-integraatio\">HashiCorp Vault -integraatio Node.js:ss\u00e4<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">HashiCorp Vault on avoimen l\u00e4hdekoodin salaisuuksienhallintaohjelmisto, joka tarjoaa dynaamisten salaisuuksien generoinnin, automaattisen rotaation ja kattavan audit-lokin. Se sopii erityisesti tilanteisiin, joissa useilla mikrosovelluksilla on p\u00e4\u00e4sy jaettuihin API-avaimiin:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install node-vault<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/secrets\/vaultClient.js\nconst vault = require('node-vault');\n\nconst client = vault({\n  endpoint: process.env.VAULT_ADDR || 'http:\/\/127.0.0.1:8200',\n  token: process.env.VAULT_TOKEN,\n});\n\nasync function getApiKeys() {\n  const result = await client.read('secret\/data\/api-keys\/production');\n  return result.data.data; \/\/ KV v2 -rakenne\n}\n\n\/\/ K\u00e4ytt\u00f6 k\u00e4ynnistyksess\u00e4 ennen muuta konfiguraatiota\nasync function loadSecretsFromVault() {\n  const keys = await getApiKeys();\n  process.env.DATABASE_API_KEY = keys.database_api_key;\n  process.env.PAYMENT_API_KEY = keys.payment_api_key;\n  console.log('Salaisuudet ladattu Vaultista onnistuneesti');\n}\n\nmodule.exports = { loadSecretsFromVault };<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"scoped-api-avaimet\">Scoped API-avaimet eli k\u00e4ytt\u00f6tarkoituskohtaiset avaimet<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Sen sijaan, ett\u00e4 antaisit asiakkaalle yhden avaimen kaikkeen, luo avainhierarkia, jossa jokainen avain on rajattu tiettyihin toimintoihin. T\u00e4m\u00e4 <a href=\"https:\/\/docs.npmjs.com\/about-npm\" target=\"_blank\" rel=\"noopener\">least privilege -periaate<\/a> minimoi vahingon jos yksi avain vuotaa:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>Vain luku -avain:<\/strong> GET-pyynn\u00f6t, ei p\u00e4\u00e4sy\u00e4 arkaluonteisiin resursseihin<\/li><li><strong>Kirjoitusavain:<\/strong> POST\/PUT\/PATCH-pyynn\u00f6t rajatulle resurssijoukolle<\/li><li><strong>Admin-avain:<\/strong> Kaikki operaatiot mukaan lukien avainten hallinta, k\u00e4ytet\u00e4\u00e4n vain infrastruktuuritoiminnoissa<\/li><li><strong>Webhook-avain:<\/strong> Vain tiettyjen webhook-p\u00e4\u00e4tepisteiden validointi, ei muita resursseja<\/li><li><strong>Read-only tilastot -avain:<\/strong> P\u00e4\u00e4sy vain julkisiin metriikka-p\u00e4\u00e4tepisteihin, kolmansille osapuolille turvallinen<\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">T\u00e4m\u00e4 periaate on <a href=\"https:\/\/owasp.org\/www-project-top-ten\/\" target=\"_blank\" rel=\"noopener\">OWASP Top 10:n<\/a> keskeinen suositus Broken Access Control -haavoittuvuuksien (A01) ehk\u00e4isyss\u00e4. Kaikkein kriittisimmiss\u00e4 j\u00e4rjestelmiss\u00e4 jokaisella mikropalvelulla tulisi olla oma, k\u00e4ytt\u00f6tarkoitukseen rajattu avain eik\u00e4 koskaan jaettua yleisavainta.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"turvallisimpien-kaytantojen-yhteenveto\">Turvallisimpien k\u00e4yt\u00e4nt\u00f6jen yhteenveto ja tarkistuslista<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">T\u00e4ss\u00e4 oppaassa olemme rakentaneet kerrokset: ymp\u00e4rist\u00f6muuttujat (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\u00e4rist\u00f6 (vaihe 9). <a href=\"https:\/\/nodejs.org\/en\/learn\/getting-started\/security-best-practices\" target=\"_blank\" rel=\"noopener\">Node.js:n virallinen tietoturvaopas<\/a> suosittelee kaikkia n\u00e4it\u00e4 k\u00e4yt\u00e4nt\u00f6j\u00e4.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>K\u00e4yt\u00e4nt\u00f6<\/th><th>Vaikutus tietoturvaan<\/th><th>Toteutustaso<\/th><\/tr><\/thead><tbody><tr><td>Ymp\u00e4rist\u00f6muuttujat (dotenv + envalid)<\/td><td>Est\u00e4\u00e4 avainten vuotamisen l\u00e4hdekoodissa<\/td><td>Perustaso<\/td><\/tr><tr><td>Vakioaikainen vertailu (timingSafeEqual)<\/td><td>Est\u00e4\u00e4 timing-hy\u00f6kk\u00e4ykset<\/td><td>Perustaso<\/td><\/tr><tr><td>SHA-256 hajautus tietokannassa<\/td><td>Suojaa tietokantavuodoilta<\/td><td>Perustaso<\/td><\/tr><tr><td>Audit-lokitus (JSONL-muoto)<\/td><td>Mahdollistaa forensisen analyysin ja CRA-raportoinnin<\/td><td>Perustaso<\/td><\/tr><tr><td>HTTPS pakollinen tuotannossa<\/td><td>Est\u00e4\u00e4 API-avaimen salakuuntelun verkkoliikenteest\u00e4<\/td><td>Perustaso<\/td><\/tr><tr><td>90 p\u00e4iv\u00e4n rotaatiokalenteri<\/td><td>Rajoittaa vuotaneen avaimen elinkaarta<\/td><td>Suositeltava<\/td><\/tr><tr><td>Rate limiting (express-rate-limit)<\/td><td>Est\u00e4\u00e4 brute force -hy\u00f6kk\u00e4ykset<\/td><td>Suositeltava<\/td><\/tr><tr><td>Scoped avaimet (least privilege)<\/td><td>Minimoi vahingon avainvuodossa<\/td><td>Suositeltava<\/td><\/tr><tr><td>HashiCorp Vault \/ AWS Secrets Manager<\/td><td>Dynaaminen hallinta, automaattinen rotaatio laajassa mittakaavassa<\/td><td>Edistynyt<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"aiheeseen-liittyva-sisalto\">Aiheeseen liittyv\u00e4 sis\u00e4lt\u00f6<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"related-coverage\">Related Coverage<\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"\/fi\/jwt-todennus-nodejs\/\">JWT-todennus Node.js:ss\u00e4: 12 vaihetta, 40 min [2026]<\/a> - JWT-tokenien ja API-avainten yhdist\u00e4minen autentikointikerroksessa<\/li><li><a href=\"\/fi\/hmac-node-js\/\">HMAC Node.js:ss\u00e4: 10 vaihetta, 30 min [2026]<\/a> - Pyynt\u00f6jen allekirjoittaminen HMAC-SHA256:lla API-kutsuja turvaamaan<\/li><li><a href=\"\/fi\/webcrypto-api-nodejs\/\">Node.js WebCrypto API: 12 vaihetta, 35 min [2026]<\/a> - Kryptografisten operaatioiden toteuttaminen natiivilla WebCrypto-rajapinnalla<\/li><li><a href=\"\/fi\/ecdsa-nodejs\/\">ECDSA Node.js:ss\u00e4: 12 vaihetta, 35 min [2026]<\/a> - Digitaalisten allekirjoitusten k\u00e4ytt\u00f6 API-pyynt\u00f6jen eheyden varmistamiseen<\/li><li><a href=\"\/fi\/cyber-resilience-act-suomi-2026\/\">Cyber Resilience Act: 15 M\u20ac sakot [2026]<\/a> - EU:n CRA-asetuksen vaatimukset suomalaisille yrityksille<\/li><li><a href=\"\/fi\/blake3-hash-nodejs\/\">BLAKE3-hajautus Node.js:ss\u00e4: 10 vaihetta, 30 min [2026]<\/a> - Nopea hajautusalgoritmi avainten hajautukseen<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"ukk\">UKK: Node.js API-avainten hallinta<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ukk-1\">Kuinka pitk\u00e4 API-avaimen pit\u00e4isi olla?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">V\u00e4hint\u00e4\u00e4n 256 bitti\u00e4 eli 32 tavua satunnaista dataa. <code>crypto.randomBytes(32).toString('base64url')<\/code> tuottaa 43 merkin avaimen Node.js:ss\u00e4. Lyhyemm\u00e4t avaimet ovat alttiita brute force -hy\u00f6kk\u00e4yksille. Etuliitteen (kuten <code>prod_<\/code> tai <code>key_<\/code>) lis\u00e4\u00e4minen helpottaa avaimen tunnistamista k\u00e4ytt\u00f6tarkoituksen mukaan mutta ei lis\u00e4\u00e4 entropiaa.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ukk-2\">Milloin API-avain pit\u00e4isi py\u00f6ritt\u00e4\u00e4?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Rutiinik\u00e4yt\u00f6ss\u00e4 90 p\u00e4iv\u00e4n v\u00e4lein. Heti v\u00e4litt\u00f6m\u00e4sti jos: ep\u00e4ilet vuotoa, entinen ty\u00f6ntekij\u00e4 l\u00e4htee, kolmannen osapuolen integraatio puretaan tai havaitset ep\u00e4ilytt\u00e4v\u00e4\u00e4 API-k\u00e4ytt\u00f6\u00e4 lokitiedostoissa. Toteuta aina grace period -k\u00e4yt\u00e4nt\u00f6 (esim. 7 p\u00e4iv\u00e4\u00e4), jonka aikana vanha avain toimii rinnakkain uuden kanssa, jotta asiakkailla on aikaa siirty\u00e4.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ukk-3\">Voiko API-avainta tallentaa selaimen localStorageen?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Ei koskaan. Selaimen localStorage, sessionStorage ja ev\u00e4steet ovat alttiita XSS-hy\u00f6kk\u00e4yksille. Jos tarvitset API-avainta client-side-sovelluksessa, k\u00e4yt\u00e4 lyhytik\u00e4ist\u00e4 JWT-tokenia palvelinpuolen API:n kautta eik\u00e4 pitk\u00e4ik\u00e4ist\u00e4 API-avainta suoraan. API-avaimet kuuluvat palvelinpuolelle.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ukk-4\">Mik\u00e4 on ero API-avaimen ja JWT-tokenin v\u00e4lill\u00e4?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">API-avain on pitk\u00e4ik\u00e4inen, tilaton tunniste, joka ei sis\u00e4ll\u00e4 tietoa itsess\u00e4\u00e4n. JWT on allekirjoitettu token, joka sis\u00e4lt\u00e4\u00e4 tietoa (claims) k\u00e4ytt\u00e4j\u00e4st\u00e4 tai oikeuksista ja vanhenee automaattisesti. API-avaimet sopivat server-to-server-kommunikaatioon, JWT:t k\u00e4ytt\u00e4j\u00e4sessioihin. Monet sovellukset yhdist\u00e4v\u00e4t molemmat: API-avain tunnistaa integraation, JWT tunnistaa k\u00e4ytt\u00e4j\u00e4n API-avainta k\u00e4ytt\u00e4v\u00e4ss\u00e4 sovelluksessa.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ukk-5\">Miten Node.js:n crypto-moduuli liittyy API-avainten hallintaan?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Node.js:n <a href=\"https:\/\/nodejs.org\/api\/crypto.html\" target=\"_blank\" rel=\"noopener\">natiivi crypto-moduuli<\/a> tarjoaa kolme keskeist\u00e4 funktiota: <code>crypto.randomBytes()<\/code> turvalliseen avainten generointiin, <code>crypto.createHash()<\/code> avainten hajautukseen tallennusta varten, ja <code>crypto.timingSafeEqual()<\/code> avainten vertailuun timing-hy\u00f6kk\u00e4yksi\u00e4 vastaan. N\u00e4m\u00e4 kolme kattavat koko avainten elinkaaren hallinnan ilman ulkoisia riippuvuuksia.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ukk-6\">Pit\u00e4\u00e4k\u00f6 .env-tiedosto lis\u00e4t\u00e4 .gitignore-tiedostoon ennen vai j\u00e4lkeen git init?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Ennen ensimm\u00e4ist\u00e4 commitia. Lis\u00e4\u00e4 <code>.env<\/code> .gitignore-tiedostoon ennen kuin ajat <code>git add .<\/code>. Jos olet jo vahingossa committanut .env-tiedoston, py\u00f6rit\u00e4 kaikki sinne tallennetut avaimet v\u00e4litt\u00f6m\u00e4sti ja poista se historiasta: <code>git rm --cached .env && git commit -m \"Remove .env from tracking\"<\/code>. Oleta, ett\u00e4 vuotanut avain on kompromissoitu, vaikka repositorio olisi yksityinen.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ukk-7\">Miten CRA vaikuttaa API-avainten hallintaan suomalaisissa yrityksiss\u00e4?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">EU:n Cyber Resilience Act edellytt\u00e4\u00e4 syyskuusta 2026 alkaen 24 tunnin ilmoitusvelvoitteen aktiivisesti hyv\u00e4ksik\u00e4ytetyist\u00e4 haavoittuvuuksista. Huono API-avainten hallinta, kuten vuotanut avain jota hy\u00f6kk\u00e4\u00e4j\u00e4 k\u00e4ytt\u00e4\u00e4, laukaisee t\u00e4m\u00e4n raportointivelvoitteen. Audit-lokitus on k\u00e4yt\u00e4nnoss\u00e4 pakollinen CRA-vaatimusten t\u00e4ytt\u00e4miseksi, koska ilman lokeja et pysty selvitt\u00e4m\u00e4\u00e4n tapahtuman laajuutta 24 tunnin raportointiaikaikkunassa.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ukk-8\">Mik\u00e4 on paras tapa jakaa API-avaimet tiimiss\u00e4?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u00c4l\u00e4 koskaan jaa API-avaimia s\u00e4hk\u00f6postissa, Slackissa tai muussa viestipalvelussa. Parhaita k\u00e4yt\u00e4nt\u00f6j\u00e4 ovat: yrityksen salasanojenhallintaty\u00f6kalu (1Password Teams tai Bitwarden Business), HashiCorp Vault kehitt\u00e4j\u00e4tiimeille, tai pilvipalveluntarjoajan salaisuuksienhallintapalvelu (AWS Secrets Manager, Azure Key Vault, GCP Secret Manager). Jokaisella kehitt\u00e4j\u00e4ll\u00e4 t\u00e4ytyy olla omat kehitysymp\u00e4rist\u00f6n avaimensa eik\u00e4 koskaan jaettuja avaimia tiimin kesken. <a href=\"https:\/\/docs.npmjs.com\/cli\/v10\/configuring-npm\/npmrc\" target=\"_blank\" rel=\"noopener\">npm:n .npmrc-tiedostoa<\/a> k\u00e4ytett\u00e4ess\u00e4 sama periaate koskee npm-rekisterin autentikointitunnuksia.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Node.js-sovellusten API-avainten hallinta on yksi kriittisimmist\u00e4 tietoturvakohdista, jonka kehitt\u00e4j\u00e4t j\u00e4tt\u00e4v\u00e4t usein sivuun kiireen vuoksi. Vuoden 2025 GitGuardian-raportti l\u00f6ysi yli 12,8 miljoonaa kovakoodattua salaista tunnusta julkisista GitHub-repositorioista, ja Node.js-projektit muodostivat niist\u00e4\u2026<\/p>\n","protected":false},"author":7,"featured_media":138,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[10,3],"tags":[],"class_list":["post-137","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-10","category-security"],"_links":{"self":[{"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/posts\/137","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/users\/7"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/comments?post=137"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/posts\/137\/revisions"}],"predecessor-version":[{"id":139,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/posts\/137\/revisions\/139"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/media\/138"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/media?parent=137"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/categories?post=137"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/fi\/wp-json\/wp\/v2\/tags?post=137"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}