Loading config/proxyplus.yaml +6 −3 Changes for config/proxyplus.yaml: 6 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -29,9 +29,12 @@ proxyplus: # # POST /api/v1/tls/profiles to have plugins/letsencrypt keep it renewed # - address: "0.0.0.0" # SNI virtual hosting: several profiles on one :443 listener, # port: 443 # each presented only for its own TlsProfileRecord.domains -- # tls_profiles: # mutually exclusive with the single tls_profile above. # - "www" # e.g. domains: ["example.com", "www.example.com"] # - "shop" # e.g. domains: ["shop.example.com"], can be a wholly separate cert # secure_connection: true # mutually exclusive with the single tls_profile above. # tls_profiles: # optional self-check: if set, must agree with # - "www" # tls_profile/tls_profiles being non-empty (see # - "shop" # BootstrapConfig.cpp) -- omit it and nothing changes. # # e.g. "www" -> domains: ["example.com", "www.example.com"], # # "shop" -> domains: ["shop.example.com"], a wholly separate cert smtp: listen: Loading src/config/BootstrapConfig.cpp +12 −0 Changes for src/config/BootstrapConfig.cpp: 12 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -33,6 +33,8 @@ const std::set<std::string> &knownLeafPatterns() { "/proxyplus/http/listen/#/port", "/proxyplus/http/listen/#/tls_profile", "/proxyplus/http/listen/#/tls_profiles", "/proxyplus/http/listen/#/secure_connection", "/proxyplus/http/listen/#/tls_autogen_self_signed", "/proxyplus/smtp/listen/#/address", "/proxyplus/smtp/listen/#/port", "/proxyplus/smtp/listen/#/tls_profile", Loading Loading @@ -140,6 +142,16 @@ std::vector<ListenerConfig> readListeners(confplus::Config &conf, const std::str if (!lc.tlsProfile.empty() && !lc.tlsProfiles.empty()) { throw ConfigValidationError(pfx + ": set either tls_profile or tls_profiles, not both"); } lc.secureConnection = readBool(conf, pfx + "/secure_connection", false); if (lc.secureConnection && lc.tlsProfile.empty() && lc.tlsProfiles.empty()) { throw ConfigValidationError(pfx + ": secure_connection is true but neither tls_profile " "nor tls_profiles is set"); } lc.tlsAutogenSelfSigned = readBool(conf, pfx + "/tls_autogen_self_signed", false); if (lc.tlsAutogenSelfSigned && lc.tlsProfile.empty() && lc.tlsProfiles.empty()) { throw ConfigValidationError(pfx + ": tls_autogen_self_signed is true but neither " "tls_profile nor tls_profiles is set"); } out.push_back(lc); } return out; Loading src/config/BootstrapConfig.h +18 −0 Changes for src/config/BootstrapConfig.h: 18 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -21,6 +21,24 @@ struct ListenerConfig { // Mutually exclusive with tlsProfile above -- set at most one of the two (see // BootstrapConfig.cpp's readListeners()); empty = single-profile listener as before. std::vector<std::string> tlsProfiles; // Optional, purely a self-documenting guard rail: if set, must agree with whether a // profile is actually configured above (see readListeners()'s validation) -- a config // author's explicit statement "this listener is meant to be HTTPS", checked rather than // silently trusted. Never required: an existing config with no secure_connection field at // all keeps working exactly as before, inferring TLS-on-ness from tlsProfile/tlsProfiles // alone, same as always. bool secureConnection = false; // First-boot convenience: if true, main.cpp registers a self-signed TlsProfileRecord // (domains = {profile name}, see CertificateManager::generateSelfSigned's CN fallback) // for every name in tlsProfile/tlsProfiles that doesn't already exist in the ConfigStore, // before CertificateManager::provisionSelfSigned() generates the actual cert file -- // otherwise a brand-new deployment has no way to create that first record at all (the // admin API needed to POST /api/v1/tls/profiles isn't up yet, since it's this same // process's own listener still waiting to bind). A profile that already exists (however // it got there -- ACME, manual, or a previous autogen run) is left untouched. bool tlsAutogenSelfSigned = false; }; enum class NodeMode { Single, Cluster }; Loading src/main.cpp +33 −0 Changes for src/main.cpp: 33 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -61,6 +61,37 @@ std::map<std::string, netplus::ssl::CertificateBundle> loadSniCertMap( } return out; } // First-boot convenience for ListenerConfig::tlsAutogenSelfSigned: registers a self-signed // TlsProfileRecord for every tls_profile/tls_profiles name that doesn't already exist, so // CertificateManager::provisionSelfSigned() (called right after this) has something to // generate a cert for. Domain defaults to the profile's own name -- generateSelfSigned() // already falls back to the profile name as CN when domains is empty, so naming a profile // after the hostname it's meant to serve (e.g. "shop.example.com") is enough on its own. void registerAutogenSelfSignedProfiles(proxyplus::store::ConfigStore &configStore, const std::vector<proxyplus::config::ListenerConfig> &listeners) { auto existing = configStore.listTlsProfiles(); auto hasProfile = [&](const std::string &name) { return std::find_if(existing.begin(), existing.end(), [&](const auto &p) { return p.profile == name; }) != existing.end(); }; for (const auto &listener : listeners) { if (!listener.tlsAutogenSelfSigned) continue; std::vector<std::string> names = !listener.tlsProfile.empty() ? std::vector<std::string>{listener.tlsProfile} : listener.tlsProfiles; for (const auto &name : names) { if (hasProfile(name)) continue; proxyplus::store::TlsProfileRecord rec; rec.profile = name; rec.domains = {name}; rec.selfSigned = true; configStore.putTlsProfile(rec); std::cerr << "[main] autogenerated self-signed TLS profile '" << name << "'" << std::endl; existing.push_back(rec); } } } } // namespace #ifndef PROXYPLUS_DEFAULT_STATIC_DIR Loading Loading @@ -163,6 +194,8 @@ int main(int argc, char **argv) { proxyplus::tls::AcmeChallengeStore acmeChallenges; proxyplus::tls::CertificateManager certManager(cfg, configStore, pluginManager, acmeChallenges); registerAutogenSelfSignedProfiles(configStore, cfg.httpListeners); // Self-signed profiles must have a cert on disk before any listener binds below -- // HttpD/SmtpD's constructor throws if tls_profile resolves to a missing file, and // (unlike ACME) there's no reason to defer this to the background checkLoop() since Loading src/tls/CertificateManager.cpp +35 −0 Changes for src/tls/CertificateManager.cpp: 35 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -139,10 +139,31 @@ void CertificateManager::writeCertKeyAndNotify(const std::string &profile, << " listener(s)" << std::endl; } bool CertificateManager::certPemStillValid(const std::string &certPem) const { if (certPem.empty()) return false; std::string tmpPath = _cfg.dataPath + "/tls/.certcheck.tmp"; if (!writeFileAtomic(tmpPath, certPem)) return false; netplus::x509cert cert; bool ok = cert.loadFromFile(tmpPath) && cert.checkValidity(); ::remove(tmpPath.c_str()); return ok; } // Self-signed generation is local and instant (no network round-trip, no rate limit like // ACME), so unlike renew() it's also called synchronously at startup via // provisionSelfSigned() -- see that method and docs/ARCHITECTURE.md. void CertificateManager::generateSelfSigned(const store::TlsProfileRecord &rec) { // In cluster mode, a peer (or an earlier run of this same node, before this node's local // file existed/expired) may already have published a still-valid cert for this profile -- // reuse it verbatim rather than minting a second, different self-signed identity for the // same domain. Consistency matters far more here than for ACME's per-node-issued certs: a // client that pins/TOFUs a self-signed cert would see it change on every request that // happens to land on a different node otherwise. if (_cfg.clusterEnabled && certPemStillValid(rec.certPem)) { materializeManualCert(rec); return; } netplus::CertGenConfig genCfg; genCfg.commonName = rec.domains.empty() ? rec.profile : rec.domains.front(); genCfg.subjectAltNames = rec.domains; Loading @@ -157,6 +178,20 @@ void CertificateManager::generateSelfSigned(const store::TlsProfileRecord &rec) std::cerr << "[tls] generated self-signed certificate for '" << rec.profile << "' (CN=" << genCfg.commonName << ")" << std::endl; if (_cfg.clusterEnabled) { // Publish it into the replicated ConfigStore so every other node materializes this // exact cert (via materializeManualCert() above) instead of each generating its own. auto clusterKey = cluster::ClusterManager::loadOrCreateSharedKey(_cfg.dataPath); auto sealed = sealPrivateKey(clusterKey, kp.keyPem); store::TlsProfileRecord published = rec; published.certPem = kp.certPem; published.encryptedKeyPem = sealed.ciphertext; published.keyNonce = sealed.nonce; published.keyTag = sealed.tag; _configStore.putTlsProfile(published); } writeCertKeyAndNotify(rec.profile, kp.certPem, kp.keyPem); } Loading Loading
config/proxyplus.yaml +6 −3 Changes for config/proxyplus.yaml: 6 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -29,9 +29,12 @@ proxyplus: # # POST /api/v1/tls/profiles to have plugins/letsencrypt keep it renewed # - address: "0.0.0.0" # SNI virtual hosting: several profiles on one :443 listener, # port: 443 # each presented only for its own TlsProfileRecord.domains -- # tls_profiles: # mutually exclusive with the single tls_profile above. # - "www" # e.g. domains: ["example.com", "www.example.com"] # - "shop" # e.g. domains: ["shop.example.com"], can be a wholly separate cert # secure_connection: true # mutually exclusive with the single tls_profile above. # tls_profiles: # optional self-check: if set, must agree with # - "www" # tls_profile/tls_profiles being non-empty (see # - "shop" # BootstrapConfig.cpp) -- omit it and nothing changes. # # e.g. "www" -> domains: ["example.com", "www.example.com"], # # "shop" -> domains: ["shop.example.com"], a wholly separate cert smtp: listen: Loading
src/config/BootstrapConfig.cpp +12 −0 Changes for src/config/BootstrapConfig.cpp: 12 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -33,6 +33,8 @@ const std::set<std::string> &knownLeafPatterns() { "/proxyplus/http/listen/#/port", "/proxyplus/http/listen/#/tls_profile", "/proxyplus/http/listen/#/tls_profiles", "/proxyplus/http/listen/#/secure_connection", "/proxyplus/http/listen/#/tls_autogen_self_signed", "/proxyplus/smtp/listen/#/address", "/proxyplus/smtp/listen/#/port", "/proxyplus/smtp/listen/#/tls_profile", Loading Loading @@ -140,6 +142,16 @@ std::vector<ListenerConfig> readListeners(confplus::Config &conf, const std::str if (!lc.tlsProfile.empty() && !lc.tlsProfiles.empty()) { throw ConfigValidationError(pfx + ": set either tls_profile or tls_profiles, not both"); } lc.secureConnection = readBool(conf, pfx + "/secure_connection", false); if (lc.secureConnection && lc.tlsProfile.empty() && lc.tlsProfiles.empty()) { throw ConfigValidationError(pfx + ": secure_connection is true but neither tls_profile " "nor tls_profiles is set"); } lc.tlsAutogenSelfSigned = readBool(conf, pfx + "/tls_autogen_self_signed", false); if (lc.tlsAutogenSelfSigned && lc.tlsProfile.empty() && lc.tlsProfiles.empty()) { throw ConfigValidationError(pfx + ": tls_autogen_self_signed is true but neither " "tls_profile nor tls_profiles is set"); } out.push_back(lc); } return out; Loading
src/config/BootstrapConfig.h +18 −0 Changes for src/config/BootstrapConfig.h: 18 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -21,6 +21,24 @@ struct ListenerConfig { // Mutually exclusive with tlsProfile above -- set at most one of the two (see // BootstrapConfig.cpp's readListeners()); empty = single-profile listener as before. std::vector<std::string> tlsProfiles; // Optional, purely a self-documenting guard rail: if set, must agree with whether a // profile is actually configured above (see readListeners()'s validation) -- a config // author's explicit statement "this listener is meant to be HTTPS", checked rather than // silently trusted. Never required: an existing config with no secure_connection field at // all keeps working exactly as before, inferring TLS-on-ness from tlsProfile/tlsProfiles // alone, same as always. bool secureConnection = false; // First-boot convenience: if true, main.cpp registers a self-signed TlsProfileRecord // (domains = {profile name}, see CertificateManager::generateSelfSigned's CN fallback) // for every name in tlsProfile/tlsProfiles that doesn't already exist in the ConfigStore, // before CertificateManager::provisionSelfSigned() generates the actual cert file -- // otherwise a brand-new deployment has no way to create that first record at all (the // admin API needed to POST /api/v1/tls/profiles isn't up yet, since it's this same // process's own listener still waiting to bind). A profile that already exists (however // it got there -- ACME, manual, or a previous autogen run) is left untouched. bool tlsAutogenSelfSigned = false; }; enum class NodeMode { Single, Cluster }; Loading
src/main.cpp +33 −0 Changes for src/main.cpp: 33 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -61,6 +61,37 @@ std::map<std::string, netplus::ssl::CertificateBundle> loadSniCertMap( } return out; } // First-boot convenience for ListenerConfig::tlsAutogenSelfSigned: registers a self-signed // TlsProfileRecord for every tls_profile/tls_profiles name that doesn't already exist, so // CertificateManager::provisionSelfSigned() (called right after this) has something to // generate a cert for. Domain defaults to the profile's own name -- generateSelfSigned() // already falls back to the profile name as CN when domains is empty, so naming a profile // after the hostname it's meant to serve (e.g. "shop.example.com") is enough on its own. void registerAutogenSelfSignedProfiles(proxyplus::store::ConfigStore &configStore, const std::vector<proxyplus::config::ListenerConfig> &listeners) { auto existing = configStore.listTlsProfiles(); auto hasProfile = [&](const std::string &name) { return std::find_if(existing.begin(), existing.end(), [&](const auto &p) { return p.profile == name; }) != existing.end(); }; for (const auto &listener : listeners) { if (!listener.tlsAutogenSelfSigned) continue; std::vector<std::string> names = !listener.tlsProfile.empty() ? std::vector<std::string>{listener.tlsProfile} : listener.tlsProfiles; for (const auto &name : names) { if (hasProfile(name)) continue; proxyplus::store::TlsProfileRecord rec; rec.profile = name; rec.domains = {name}; rec.selfSigned = true; configStore.putTlsProfile(rec); std::cerr << "[main] autogenerated self-signed TLS profile '" << name << "'" << std::endl; existing.push_back(rec); } } } } // namespace #ifndef PROXYPLUS_DEFAULT_STATIC_DIR Loading Loading @@ -163,6 +194,8 @@ int main(int argc, char **argv) { proxyplus::tls::AcmeChallengeStore acmeChallenges; proxyplus::tls::CertificateManager certManager(cfg, configStore, pluginManager, acmeChallenges); registerAutogenSelfSignedProfiles(configStore, cfg.httpListeners); // Self-signed profiles must have a cert on disk before any listener binds below -- // HttpD/SmtpD's constructor throws if tls_profile resolves to a missing file, and // (unlike ACME) there's no reason to defer this to the background checkLoop() since Loading
src/tls/CertificateManager.cpp +35 −0 Changes for src/tls/CertificateManager.cpp: 35 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -139,10 +139,31 @@ void CertificateManager::writeCertKeyAndNotify(const std::string &profile, << " listener(s)" << std::endl; } bool CertificateManager::certPemStillValid(const std::string &certPem) const { if (certPem.empty()) return false; std::string tmpPath = _cfg.dataPath + "/tls/.certcheck.tmp"; if (!writeFileAtomic(tmpPath, certPem)) return false; netplus::x509cert cert; bool ok = cert.loadFromFile(tmpPath) && cert.checkValidity(); ::remove(tmpPath.c_str()); return ok; } // Self-signed generation is local and instant (no network round-trip, no rate limit like // ACME), so unlike renew() it's also called synchronously at startup via // provisionSelfSigned() -- see that method and docs/ARCHITECTURE.md. void CertificateManager::generateSelfSigned(const store::TlsProfileRecord &rec) { // In cluster mode, a peer (or an earlier run of this same node, before this node's local // file existed/expired) may already have published a still-valid cert for this profile -- // reuse it verbatim rather than minting a second, different self-signed identity for the // same domain. Consistency matters far more here than for ACME's per-node-issued certs: a // client that pins/TOFUs a self-signed cert would see it change on every request that // happens to land on a different node otherwise. if (_cfg.clusterEnabled && certPemStillValid(rec.certPem)) { materializeManualCert(rec); return; } netplus::CertGenConfig genCfg; genCfg.commonName = rec.domains.empty() ? rec.profile : rec.domains.front(); genCfg.subjectAltNames = rec.domains; Loading @@ -157,6 +178,20 @@ void CertificateManager::generateSelfSigned(const store::TlsProfileRecord &rec) std::cerr << "[tls] generated self-signed certificate for '" << rec.profile << "' (CN=" << genCfg.commonName << ")" << std::endl; if (_cfg.clusterEnabled) { // Publish it into the replicated ConfigStore so every other node materializes this // exact cert (via materializeManualCert() above) instead of each generating its own. auto clusterKey = cluster::ClusterManager::loadOrCreateSharedKey(_cfg.dataPath); auto sealed = sealPrivateKey(clusterKey, kp.keyPem); store::TlsProfileRecord published = rec; published.certPem = kp.certPem; published.encryptedKeyPem = sealed.ciphertext; published.keyNonce = sealed.nonce; published.keyTag = sealed.tag; _configStore.putTlsProfile(published); } writeCertKeyAndNotify(rec.profile, kp.certPem, kp.keyPem); } Loading