הרמת דוקר למכונת node שתומך ב-https

הרמת שרת node\express שתומך ב-https באמצעות docker compose

במאמרים הקודמים על דוקר הראיתי והדגמתי איך מרימים קונטיינרים של דוקר על מנת להרים וורדפרס. במאמר הזה אני מדגים איך אני מרים מכונת node\express ומכין אותה לעבודה עם https כדי לדמות לחלוטין שרת node. כמובן שאפשר להשתמש בעקרונות האלו כדי להקים כל שרת אחר ולאפשר לו לעבוד עם https.

אז קודם כל, בואו ונרים שרת express. איך עושים את זה? עם קובץ compose שמכיל קונטיינר. איזה קונטיינר? ובכן. בחרתח באחד פופולרי של אקספרס וביצעתי בו התאמות מסוימות – כמו להעיף את mongo למשל שאותו אני לא צריך.

version: '2'

services:
  myapp:
    tty: true # Enables debugging capabilities when attached to this container.
    image: 'bitnami/express:latest'
    labels:
      kompose.service.type: nodeport
    command: npm run development
    environment:
      - PORT=3000
      - NODE_ENV=development

      - SKIP_DB_WAIT=0
      - SKIP_DB_MIGRATION=0
      - SKIP_NPM_INSTALL=0
      - SKIP_BOWER_INSTALL=0
    ports:
      - 80:3000
      - 443:8000
      - 3000:3000
    volumes:
      - .:/app

כדאי לשים לכמה דברים חשובים פה. הדבר החשוב ביותר הוא שיש לי כאן מיפויים ב-ports. יש לנו את המיפוי והכנסתי את 443 שימופה לפורט 8000 שהוא הפורט של האפליקציה שלי. כשיש לנו node\express. הוא עובד מאחורי נתב שמעביר את כל התנועה מהפורטים השונים (80, 8080 ו-443) לפורט 3000 או לכל פורט אחר שבחרנו. פורט 443 הוא חשוב במיוחד כי הוא הפורט של הפרוטוקול המאובטח HTTPS ואני מפנה אותו לפורט הפנימי של 8000.

מיפוי פורטים בדוקר - פורט 80 הולך לפורט 3000 ופורט 443, שמשמש את ה-https, הולך לפורט 8000.
מיפוי פורטים בדוקר – פורט 80 הולך לפורט 3000 ופורט 443, שמשמש את ה-https, הולך לפורט 8000.

אני אשמור את הקובץ הזה בתיקיה ריקה ואריץ בטרמינל של docker:

docker-compose up

האפליקציה תיבנה הישר בתיקית app ואני בעצם אוכל לראות אותה אם אכנס ל-IP. אבל אני רוצה תמיכה מלאה ב-https. איך עושים את זה? מבחינת דוקר עשיתי את כל מה שאני יכול לעשות. עכשיו צריך לטפל באפליקציה.

בניגוד ל-Apache\NginX וחבריהם שיודעים לעבוד עם HTTPS ישר מהקופסה, עם node אני צריך לגרום ל-https לעבוד. איך? קודם כל אני צריך ליצור לעצמי מפתח.

למי שלא יודע, תקשורת https עובדת עם RSA (החיבור הראשוני עובד כך, התקשורת עצמה עובדת בהצפנה סימטרית) שמחייבת מפתח פומבי ומפתח פרטי. אני צריך ליצור אותם. זה לא כזה מסובך. אני אצור תיקיה בפרויקט שלי שתקרא sslcert ואריץ בה את הפקודה הזו:

openssl req -newkey rsa:2048 -new -nodes -keyout key.pem -out csr.pem

מה שהפקודה הזו עושה זה ליצור מפתח הצפנה פרטי ופומבי. מפתח ההצפנה הפרטי הוא key.pem ואם תפתחו אותו תוכלו לראות שיש בו private key. מפתח ההצפנה הפומבי הוא csr.pem שמשמש אותנו לסרטיפיקייט. זה כל מה שאנחנו צריכים.

עכשיו רק צריך לגרום לשרת לעבוד עם https וגם זה הרבה יותר פשוט. יש כמה גישות שאפשר לעבוד איתן. אנו נתמקד בגישה הפשוטה יותר, ליצור בקובץ האתחול שרת https נוסף שזהה לחלוטין לזה של http. הוא מאזין ל-8000 ויתפוס את כל התקשורת המאובטחת. נשמע כמו הגיהנום? ממש לא. ככה זה נראה:

#!/usr/bin/env node

/**
 * Module dependencies.
 */

var app = require('../app');
var appSecured = require('../app'); // Creating appSecured
var debug = require('debug')('app:server');
var http = require('http');
var https = require('https'); // require native node's native https module
var fs = require('fs');

/**
 * Get port from environment and store in Express.
 */
var privateKey  = fs.readFileSync('/app/sslcert/key.pem', 'utf8');
var certificate = fs.readFileSync('/app/sslcert/server.crt', 'utf8');

/**
 * Prepare the credentials 
 */
var credentials = {key: privateKey, cert: certificate};

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
var portSecured = normalizePort(process.env.PORT_SECURED || '8000'); // defining port secured
app.set('port', port);
appSecured.set('port', portSecured); // appSecured listen to 8000 port

/**
 * Create HTTP server.
 */

var server = http.createServer(app);

/**
 * Create HTTPS server.
 */
var serverSecured = https.createServer(credentials, appSecured);

/**
 * Listen on provided port, on all network interfaces.
 */
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);


/**
 * HTTPS Listen on provided port, on all network interfaces.
 */
serverSecured.listen(portSecured);
serverSecured.on('error', onError);
serverSecured.on('listening', onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val) {
  var port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }

  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */

function onListening() {
  var addr = server.address();
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}

ממש לא צריך להבהל. זה קובץ ההגדרות הרגיל שמגיע יחד עם הקונטיינר והוספתי לתוכו פשוט את הקוד שיוצר את השרת המאובטח שמאזין לפורט 800. זה כל הקוד שהוספתי:

var appSecured = require('../app'); // Creating appSecured
var https = require('https'); // require native node's native https module
var fs = require('fs');

/**
 * Get port from environment and store in Express.
 */
var privateKey  = fs.readFileSync('/app/sslcert/key.pem', 'utf8');
var certificate = fs.readFileSync('/app/sslcert/server.crt', 'utf8');

/**
 * Prepare the credentials 
 */
var credentials = {key: privateKey, cert: certificate};
var portSecured = normalizePort(process.env.PORT_SECURED || '8000'); // defining port secured
appSecured.set('port', portSecured); // appSecured listen to 8000 port

/**
 * Create HTTPS server.
 */
var serverSecured = https.createServer(credentials, appSecured);

/**
 * HTTPS Listen on provided port, on all network interfaces.
 */
serverSecured.listen(portSecured);
serverSecured.on('error', onError);
serverSecured.on('listening', onListening);

כשאני אכנס לכתובת של המכונה שלי עם https, אני אקבל שגיאת https כמובן שמראה שהכל תקין. למה? כי יש חיבור. מן הסתם הדפדפן שלי לא מכיר ולא סומך על הסקרטיפקט שלי, אבל אם אני אעשה proceed אני יכול להתחיל לעבוד.

כניסה לשרת מאובטח בפעם הראשונה, אפשר לראות שאני מקבל הודעת שגיאה (שאפשר לעקוף אותה).
כניסה לשרת מאובטח בפעם הראשונה, אפשר לראות שאני מקבל הודעת שגיאה (שאפשר לעקוף אותה).

אפשר לגרום להודעה המרגיזה להעלם ולאפשר לדפדפן שלי לקבל את האישור הזה. אבל זה סיפור אחר ולמאמר אחר.

חשוב לי להדגיש משהו חשוב: המאמר הזה נוגע אך ורק להתקנה של אתר/שרת בסביבה המקומית שלכם. לא לפרודקשן.

פוסטים נוספים שכדאי לקרוא

פתרונות ומאמרים על פיתוח אינטרנט

יישום של nonce על מנת להגן מפני התקפות injection

בפוסט הקודם הסברתי על hash עם CSP על משאבי inline – שזה נחמד ומעולה אבל פחות ישים בעולם האמיתי שבו בדרך כלל התוכן ה-inline (בין

גלילה לראש העמוד