Настройка HTTPS и собственных сертификатов

Vite поддерживает запуск dev-сервера по HTTPS через встроенный сервер на базе Node.js. HTTPS особенно важен при разработке приложений, использующих:

  • Service Workers
  • Web Push API
  • HTTP/2
  • Secure Cookies
  • WebAuthn
  • OAuth-авторизацию
  • API браузера, доступные только в защищённом контексте

По умолчанию Vite запускает сервер через HTTP:

vite

или:

npm run dev

Адрес сервера обычно выглядит так:

http://localhost:5173

Для включения HTTPS используется параметр server.https.


Базовое включение HTTPS

Минимальная конфигурация:

// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  server: {
    https: true
  }
})

После запуска адрес изменится:

https://localhost:5173

В этом режиме Vite создаёт временный self-signed сертификат.


Проблемы self-signed сертификатов

Самоподписанные сертификаты подходят для локальной разработки, но имеют ограничения:

  • браузер показывает предупреждение безопасности;
  • сертификат не считается доверенным;
  • некоторые API могут работать нестабильно;
  • мобильные устройства часто отклоняют такие сертификаты;
  • корпоративные политики безопасности могут блокировать соединение.

Поэтому в реальных проектах обычно используют собственные сертификаты.


Настройка собственных сертификатов

Vite позволяет передавать полноценные TLS-сертификаты через объект https.

Наиболее распространённый вариант:

// vite.config.js
import { defineConfig } from 'vite'
import fs from 'node:fs'

export default defineConfig({
  server: {
    https: {
      key: fs.readFileSync('./certs/server.key'),
      cert: fs.readFileSync('./certs/server.crt')
    }
  }
})

Структура сертификатов

Обычно используются следующие файлы:

certs/
├── server.key
├── server.crt

Где:

  • server.key — приватный ключ;
  • server.crt — сертификат.

Иногда дополнительно применяется цепочка сертификатов:

certs/
├── server.key
├── server.crt
├── ca.crt

Использование CA-сертификата

При наличии собственного центра сертификации можно указать ca:

import { defineConfig } from 'vite'
import fs from 'node:fs'

export default defineConfig({
  server: {
    https: {
      key: fs.readFileSync('./certs/server.key'),
      cert: fs.readFileSync('./certs/server.crt'),
      ca: fs.readFileSync('./certs/ca.crt')
    }
  }
})

Это особенно важно для:

  • корпоративных сетей;
  • внутренних API;
  • локальной инфраструктуры;
  • Docker-сред;
  • Kubernetes-кластеров.

Генерация сертификатов через OpenSSL

Создание приватного ключа:

openssl genrsa -out server.key 2048

Создание сертификата:

openssl req -new -x509 \
  -key server.key \
  -out server.crt \
  -days 365

После генерации появятся:

server.key
server.crt

SAN и современные браузеры

Современные браузеры требуют наличие Subject Alternative Name (SAN). Без него сертификат считается некорректным.

Пример конфигурации OpenSSL:

[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
x509_extensions = v3_req

[dn]
CN = localhost

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost
IP.1 = 127.0.0.1

Создание сертификата:

openssl req -x509 \
  -nodes \
  -days 365 \
  -newkey rsa:2048 \
  -keyout server.key \
  -out server.crt \
  -config openssl.cnf

Использование mkcert

Наиболее удобный инструмент для локальной разработки — mkcert.

Он создаёт локальный доверенный CA и автоматически добавляет его в систему.

Установка:

mkcert -install

Создание сертификатов:

mkcert localhost 127.0.0.1 ::1

Результат:

localhost+2.pem
localhost+2-key.pem

Подключение в Vite:

import { defineConfig } from 'vite'
import fs from 'node:fs'

export default defineConfig({
  server: {
    https: {
      key: fs.readFileSync('./localhost+2-key.pem'),
      cert: fs.readFileSync('./localhost+2.pem')
    }
  }
})

Преимущества mkcert:

  • сертификаты доверенные;
  • отсутствуют предупреждения браузера;
  • корректная работа Secure Context;
  • поддержка мобильных устройств;
  • удобная интеграция с локальными API.

HTTPS и HMR

Vite использует WebSocket-соединение для Hot Module Replacement.

При HTTPS HMR автоматически переключается на WSS:

wss://localhost:5173

Иногда требуется ручная настройка:

export default defineConfig({
  server: {
    https: true,
    hmr: {
      protocol: 'wss',
      host: 'localhost'
    }
  }
})

Это особенно важно при:

  • reverse proxy;
  • Docker;
  • nginx;
  • удалённой разработке;
  • нестандартных доменах.

Настройка HTTPS для кастомного домена

Часто локальная разработка ведётся через домен:

https://myapp.local

Для этого:

  1. Добавляется запись в hosts.
  2. Генерируется сертификат.
  3. Настраивается host в Vite.

Пример:

export default defineConfig({
  server: {
    host: 'myapp.local',
    https: {
      key: fs.readFileSync('./certs/myapp.key'),
      cert: fs.readFileSync('./certs/myapp.crt')
    }
  }
})

Использование HTTPS с Docker

При запуске внутри контейнера сертификаты обычно монтируются как volume:

services:
  vite:
    volumes:
      - ./certs:/app/certs

Конфигурация:

https: {
  key: fs.readFileSync('/app/certs/server.key'),
  cert: fs.readFileSync('/app/certs/server.crt')
}

HTTPS и reverse proxy

В production-like окружениях HTTPS часто завершается на nginx или Traefik.

Схема:

Browser
   ↓ HTTPS
Nginx
   ↓ HTTP
Vite Dev Server

В таком случае HTTPS внутри Vite может не использоваться.

Конфигурация nginx:

server {
    listen 443 ssl;

    ssl_certificate     /etc/nginx/cert.pem;
    ssl_certificate_key /etc/nginx/key.pem;

    location / {
        proxy_pass http://localhost:5173;
    }
}

Использование HTTP/2

Node.js HTTPS-сервер поддерживает HTTP/2 через дополнительные настройки TLS.

Некоторые reverse proxy автоматически активируют HTTP/2:

listen 443 ssl http2;

Для Vite это может быть полезно при тестировании:

  • multiplexing;
  • server push;
  • производительности HTTPS.

Настройка clientPort при HTTPS

Если HMR работает через отдельный порт, используется:

export default defineConfig({
  server: {
    https: true,
    hmr: {
      clientPort: 443
    }
  }
})

Особенно полезно при:

  • проксировании;
  • cloud-средах;
  • HTTPS reverse proxy;
  • нестандартной сетевой топологии.

Использование PFX-сертификатов

Node.js поддерживает формат .pfx.

Пример:

import fs from 'node:fs'

export default defineConfig({
  server: {
    https: {
      pfx: fs.readFileSync('./certs/server.pfx'),
      passphrase: 'secret'
    }
  }
})

Такой формат распространён в Windows-инфраструктуре.


Настройка cipher suites

Node.js позволяет управлять TLS-параметрами:

https: {
  key: fs.readFileSync('./server.key'),
  cert: fs.readFileSync('./server.crt'),
  ciphers: `
    TLS_AES_256_GCM_SHA384:
    TLS_CHACHA20_POLY1305_SHA256:
    TLS_AES_128_GCM_SHA256
  `.replace(/\s+/g, '')
}

Подобная конфигурация используется редко, но может потребоваться для:

  • тестирования безопасности;
  • совместимости;
  • корпоративных стандартов.

Настройка минимальной версии TLS

https: {
  key: fs.readFileSync('./server.key'),
  cert: fs.readFileSync('./server.crt'),
  minVersion: 'TLSv1.2'
}

Поддерживаются:

  • TLSv1
  • TLSv1.1
  • TLSv1.2
  • TLSv1.3

Ошибка ERR_CERT_AUTHORITY_INVALID

Одна из самых распространённых ошибок:

NET::ERR_CERT_AUTHORITY_INVALID

Причины:

  • сертификат не доверенный;
  • отсутствует CA;
  • сертификат повреждён;
  • SAN настроен неверно;
  • hostname не совпадает.

Чаще всего проблема решается через mkcert.


Ошибка ERR_SSL_PROTOCOL_ERROR

Причины:

  • неправильный ключ;
  • несоответствие cert/key;
  • битый сертификат;
  • неправильный proxy;
  • попытка HTTPS к HTTP-серверу.

Проверка сертификата:

openssl x509 -in server.crt -text -noout

Проверка ключа:

openssl rsa -in server.key -check

Проверка HTTPS через curl

Проверка сертификата:

curl -v https://localhost:5173

Игнорирование ошибок:

curl -k https://localhost:5173

Использование переменных окружения

Часто HTTPS включается только для development:

export default defineConfig(({ mode }) => ({
  server: {
    https: mode === 'development'
  }
}))

Или:

https: process.env.VITE_HTTPS === 'true'

Загрузка сертификатов через path.resolve

Для корректной работы путей:

import path from 'node:path'
import fs from 'node:fs'

https: {
  key: fs.readFileSync(
    path.resolve(__dirname, 'certs/server.key')
  ),
  cert: fs.readFileSync(
    path.resolve(__dirname, 'certs/server.crt')
  )
}

Исключение сертификатов из Git

Приватные ключи нельзя хранить в репозитории.

.gitignore:

certs/
*.key
*.pem
*.pfx

Использование разных сертификатов для окружений

Пример:

certs/
├── dev/
├── stage/
├── local/

Конфигурация:

const certDir = `./certs/${process.env.APP_ENV}`

https: {
  key: fs.readFileSync(`${certDir}/server.key`),
  cert: fs.readFileSync(`${certDir}/server.crt`)
}

HTTPS и WebSocket API

При HTTPS браузер блокирует небезопасные WebSocket-соединения:

ws://

Требуется использовать:

wss://

Это касается:

  • Socket.IO
  • native WebSocket
  • GraphQL subscriptions
  • HMR

HTTPS и Service Workers

Service Worker работает только в secure context:

https://

Исключение:

localhost

При разработке PWA HTTPS становится обязательным.


HTTPS и Secure Cookies

Cookie с флагом:

Secure

не передаются по HTTP.

Для тестирования авторизации HTTPS обязателен:

Set-Cookie: token=123; Secure; HttpOnly

HTTPS и WebAuthn

WebAuthn API требует защищённого соединения.

Без HTTPS не работают:

  • аппаратные ключи;
  • биометрическая авторизация;
  • Passkeys.

HTTPS и CORS

Переход с HTTP на HTTPS меняет origin:

http://localhost:5173
https://localhost:5173

Из-за этого могут возникать ошибки CORS.

Иногда требуется обновление whitelist:

origin: [
  'https://localhost:5173'
]

HTTPS и mixed content

Браузеры блокируют HTTP-ресурсы внутри HTTPS-страницы.

Проблемный пример:

<script src="http://localhost/api.js"></script>

Корректный вариант:

<script src="https://localhost/api.js"></script>

Или:

<script src="//localhost/api.js"></script>

Полная конфигурация HTTPS

import { defineConfig } from 'vite'
import fs from 'node:fs'
import path from 'node:path'

export default defineConfig({
  server: {
    host: 'localhost',
    port: 5173,

    https: {
      key: fs.readFileSync(
        path.resolve(__dirname, './certs/server.key')
      ),

      cert: fs.readFileSync(
        path.resolve(__dirname, './certs/server.crt')
      ),

      minVersion: 'TLSv1.2'
    },

    hmr: {
      protocol: 'wss',
      host: 'localhost',
      clientPort: 5173
    }
  }
})