This commit is contained in:
2026-08-21 11:56:14 +05:00
parent 271f79d6f0
commit eb142e339c
4 changed files with 131 additions and 0 deletions

View File

@@ -50,6 +50,12 @@ ENCRYPT_TRAFFIC=false
# LDAP_USE_POWERSHELL_PASSWORD=true
# LDAP_POWERSHELL_EXE=powershell.exe
# ============ Kerberos (password setting) ============
# Additional domain controllers for the generated /etc/krb5.conf
# (comma-separated). Default: only the host from LDAP_URL is used,
# the rest is discovered via DNS SRV (_kerberos._tcp.<domain>).
LDAP_KDC_SERVERS=dc.school25.ru,dc3.school25.ru,srv.school25.ru
# ============ Email (registration) ============
# SMTP server used to send verification codes and registration result letters.
# If EMAIL_HOST is empty the registration endpoints return 503.

30
Dockerfile.example Normal file
View File

@@ -0,0 +1,30 @@
# Example Dockerfile for running the service in a Linux container.
# Password setting works via Kerberos (kinit + ldapmodify -Y GSSAPI):
# - openldap-clients provides ldapmodify
# - cyrus-sasl-gssapiv2 provides the SASL GSSAPI plugin used by ldapmodify
# - krb5 provides kinit / kdestroy (there is no krb5-client
# package in Alpine v3.24 — the tools are in krb5)
#
# /etc/krb5.conf is regenerated from .env at every start by
# scripts/generate-krb5-conf.js (the static file below is only a fallback).
# Make sure the container environment does NOT contain
# LDAP_USE_POWERSHELL_PASSWORD=true (remove it from the .env you mount).
FROM node:22-alpine
WORKDIR /app
RUN apk add --no-cache git build-base python3 \
openldap-clients cyrus-sasl-gssapiv2 krb5
COPY config/krb5.conf /etc/krb5.conf
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 8080
# Regenerate /etc/krb5.conf from .env at every start, then launch the server
CMD ["sh", "-c", "node scripts/generate-krb5-conf.js && node server.js"]

View File

@@ -26,6 +26,7 @@ const config = {
caFile: getEnv('LDAP_CA_FILE', ''),
passwordMethod: getEnv('LDAP_PASSWORD_METHOD', passwordMethodDefault),
ldapRealm: getEnv('LDAP_REALM', String(getEnv('LDAP_DOMAIN', 'school25.ru')).toUpperCase()),
kdcServers: getEnv('LDAP_KDC_SERVERS', ''),
powerShellExe: getEnv('LDAP_POWERSHELL_EXE', 'powershell.exe'),
emailHost: getEnv('EMAIL_HOST', ''),
emailPort: parseInt(getEnv('EMAIL_PORT', '587'), 10),

View File

@@ -0,0 +1,94 @@
'use strict';
// Generates /etc/krb5.conf from .env values (realm, DC list, domain).
// Run at every container start before server.js:
// node scripts/generate-krb5-conf.js
// Output path defaults to /etc/krb5.conf; override with KRB5_CONF_PATH
// (useful for tests or when the process has no write access to /etc).
const fs = require('fs');
const path = require('path');
const config = require('../lib/config');
const DEFAULT_OUTPUT = '/etc/krb5.conf';
function hostFromUrl(url) {
const match = /^[a-z0-9+.-]+:\/\/([^/:]+)(?::\d+)?/i.exec(String(url || '').trim());
return match ? match[1].toLowerCase() : '';
}
function collectKdcServers() {
const hosts = [];
const push = (host) => {
const value = String(host || '').trim().split(':')[0].toLowerCase();
if (value && !hosts.some((h) => h.toLowerCase() === value)) hosts.push(value);
};
push(hostFromUrl(config.ldapUrl));
for (const item of String(config.kdcServers || '').split(/[\s,]+/)) {
if (item) push(item);
}
return hosts;
}
function buildKrb5Conf({ realm, domain, kdcServers }) {
const lines = [
'# Generated by scripts/generate-krb5-conf.js from .env — do not edit manually.',
'# Overrides: LDAP_REALM (realm), LDAP_KDC_SERVERS (comma-separated DC list),',
'# LDAP_URL (primary DC host is always included).',
'',
'[libdefaults]',
` default_realm = ${realm}`,
' dns_lookup_kdc = true',
' dns_lookup_realm = false',
' rdns = false',
' udp_preference_limit = 1',
' kdc_timeout = 5',
' max_retries = 2',
' forwardable = true',
' ticket_lifetime = 10h',
''
];
if (kdcServers.length > 0) {
lines.push('[realms]', ` ${realm} = {`);
for (const kdc of kdcServers) lines.push(` kdc = ${kdc}`);
lines.push(' }', '');
}
if (domain) {
lines.push('[domain_realm]', ` .${domain} = ${realm}`, ` ${domain} = ${realm}`);
}
return lines.join('\n') + '\n';
}
function writeKrb5Conf(content, outputPath) {
const target = outputPath || DEFAULT_OUTPUT;
const dir = path.dirname(target);
fs.mkdirSync(dir, { recursive: true });
const tmp = `${target}.tmp-${process.pid}`;
fs.writeFileSync(tmp, content, { encoding: 'utf8', mode: 0o644 });
fs.renameSync(tmp, target);
return target;
}
function main() {
try {
const realm = config.ldapRealm;
const domain = config.domain;
const kdcServers = collectKdcServers();
const outputPath = process.env.KRB5_CONF_PATH || DEFAULT_OUTPUT;
const content = buildKrb5Conf({ realm, domain, kdcServers });
const target = writeKrb5Conf(content, outputPath);
console.log(`krb5.conf generated -> ${target} (realm ${realm}, kdc: ${kdcServers.join(', ') || '(DNS SRV)'})`);
} catch (err) {
console.error(`Не удалось сгенерировать krb5.conf: ${err.message}`);
process.exit(1);
}
}
module.exports = { buildKrb5Conf, collectKdcServers, hostFromUrl, writeKrb5Conf, main, DEFAULT_OUTPUT };
if (require.main === module) {
main();
}