Commit f31b7724 authored by jan.koester's avatar jan.koester
Browse files

test

parent f75f9c4d
Loading
Loading
Loading
Loading
+6 −3
Changes for config/proxyplus.yaml: 6 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -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:
+12 −0
Changes for src/config/BootstrapConfig.cpp: 12 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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",
@@ -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;
+18 −0
Changes for src/config/BootstrapConfig.h: 18 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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 };
+33 −0
Changes for src/main.cpp: 33 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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
@@ -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
+35 −0
Changes for src/tls/CertificateManager.cpp: 35 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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;
@@ -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