This commit is contained in:
Mateusz Gruszczyński committed 2026-10-08 11:27:18 +02:00
1 parent d940f9995f
commit 65e082b9d1
61 files changed
+1225 -581

No files matched your search

+1
View File
@@ -36,6 +36,7 @@ function syncCertificateFileSource() {
const helpKey = external ? "certificateSourceExternalHelp" : "certificateSourceManagedHelp";
$("certificate-file-source-help").dataset.i18n = helpKey;
$("certificate-file-source-help").textContent = t(helpKey);
$("certificate-external-acl-example").hidden = !external;
}
$("certificate-file-source").addEventListener("change", syncCertificateFileSource);
+13 -6
View File
@@ -1186,6 +1186,7 @@
<option data-i18n="certificateSourceExternal" value="external"></option>
</select>
<p class="fine" id="certificate-file-source-help" data-i18n="certificateSourceManagedHelp"></p>
<pre class="metrics-example" data-i18n="certificateExternalAclExample" hidden id="certificate-external-acl-example"></pre>
<label data-i18n="certificateFile" for="certificate-file"></label><input
id="certificate-file" maxlength="96" pattern="[A-Za-z0-9_.-]+"
placeholder="app.pem" required="" />
@@ -2728,14 +2729,20 @@
class="pill live">SCOPED TOKENS</span>
</div>
<p class="fine" data-i18n="certificateApiHelp">Separate Key ID + Bearer Token
credentials can only deploy or reload externally managed certificates. They cannot
read configuration or use other administration APIs.</p>
credentials are reload-only by default and restricted to explicit certificate IDs.
Deploy is optional; tokens cannot read configuration or use other administration APIs.</p>
<p class="fine" id="certificate-api-status" role="status"></p>
<label><span data-i18n="certificateApiKeyId">Key ID</span><input
id="certificate-api-key-id" maxlength="64" placeholder="certbot-main" /></label>
<label><span data-i18n="certificateApiComment">Comment</span><input
id="certificate-api-comment" maxlength="256"
placeholder="Certbot on edge-01" /></label>
<label><span data-i18n="certificateApiCertificateIds">Allowed certificate IDs</span><textarea
id="certificate-api-certificate-ids" maxlength="2048" placeholder="shared-cert" required rows="3"></textarea></label>
<p class="fine" data-i18n="certificateApiCertificateIdsHelp">List one or more certificate IDs separated by commas or new lines. The token can act only on these IDs.</p>
<label class="check"><input id="certificate-api-allow-deploy" type="checkbox" /><span
data-i18n="certificateApiAllowDeploy">Also allow deploy</span></label>
<p class="fine" data-i18n="certificateApiAllowDeployHelp">Leave disabled for Certbot renewal hooks. Reload-only is the recommended least-privilege mode.</p>
<label><span data-i18n="certificateApiToken">Bearer token (optional custom
value)</span><input autocomplete="off" id="certificate-api-token"
maxlength="128" minlength="32"
@@ -2752,8 +2759,8 @@
</div>
<div class="certificate-api-credentials" id="certificate-api-credentials"></div>
<p class="fine" data-i18n="certificateApiCurlHelp">External automation sends the Key ID
header and Bearer token to the certificate deploy/reload endpoints over HTTPS (or
loopback HTTP).</p>
header and Bearer token over HTTPS (or loopback HTTP). Reload tokens use the targeted
/api/certificates/{id}/reload endpoint; token access cannot trigger global reload.</p>
<pre class="metrics-example" id="certificate-api-curl"></pre>
</form>
</div>
@@ -2884,7 +2891,7 @@
</section>
</div>
</section>
<footer class="page-footer"><span>RustProxy <b>0.28.1</b></span><span id="footer-status">API - /
<footer class="page-footer"><span>RustProxy <b>0.28.2</b></span><span id="footer-status">API - /
dropped: 0</span></footer>
</div>
</main>
@@ -3628,7 +3635,7 @@ max_execution_time = 30</code></pre>
<article class="card about-card">
<div class="about-product">
<span aria-hidden="true" class="brand-mark">r<span>/</span>p</span>
<div><span class="small-label">RustProxy</span><strong>0.28.1</strong></div>
<div><span class="small-label">RustProxy</span><strong>0.28.2</strong></div>
</div>
<dl class="about-meta">
<div>
+26 -11
View File
@@ -88,18 +88,25 @@
}
function certificateApiExample() {
const key = $("certificate-api-key-id").value.trim() || certificateApiCredentials()[0]?.key_id || "certbot-main";
$("certificate-api-curl").textContent = `curl --fail --show-error --request POST \
const enteredIds = $("certificate-api-certificate-ids").value.split(/[\n,]+/).map(value => value.trim()).filter(Boolean);
const selected = enteredIds[0] || certificateApiCredentials()[0]?.certificate_ids?.find(value => value !== "*") || "CERTIFICATE_ID";
const reload = `curl --fail --show-error --request POST \
--header 'Authorization: Bearer $RUSTPROXY_CERT_TOKEN' \
--header 'X-RustProxy-Certificate-Key: ${key}' \
${location.origin}/api/certificates/${selected}/reload`;
if (!$("certificate-api-allow-deploy").checked) {
$("certificate-api-curl").textContent = reload;
return;
}
$("certificate-api-curl").textContent = `${reload}
# Optional higher-privilege deploy for the same allowlisted certificate IDs:
curl --fail --show-error --request POST \
--header 'Authorization: Bearer $RUSTPROXY_CERT_TOKEN' \
--header 'X-RustProxy-Certificate-Key: ${key}' \
--header 'Content-Type: application/json' \
--data @deploy.json \
${location.origin}/api/certificates/deploy
# Reload one manual/file-backed certificate without changing its configuration:
curl --fail --show-error --request POST \
--header 'Authorization: Bearer $RUSTPROXY_CERT_TOKEN' \
--header 'X-RustProxy-Certificate-Key: ${key}' \
${location.origin}/api/certificates/CERTIFICATE_ID/reload`;
${location.origin}/api/certificates/deploy`;
}
function renderCertificateApi() {
const state = certificateApiState, target = $("certificate-api-credentials");
@@ -116,6 +123,9 @@ curl --fail --show-error --request POST \
const info = el("div", undefined, "certificate-api-credential-info");
info.append(el("b", credential.key_id || "-", "mono"));
if (credential.comment) info.append(el("span", credential.comment, "fine"));
const scope = credential.allow_deploy ? t("certificateApiScopeDeployReload") : t("certificateApiScopeReloadOnly");
const ids = Array.isArray(credential.certificate_ids) ? credential.certificate_ids.join(", ") : "-";
info.append(el("span", `${scope} · ${t("certificateApiAllowedIds")}: ${ids}`, "fine mono"));
const lastUsed = credential.last_used_at ? formatDateTime(Number(credential.last_used_at) * 1000) : t("certificateApiNeverUsed");
info.append(el("span", `${t("certificateApiLastUsed")}: ${lastUsed}`, "fine"));
const remove = action(t("certificateApiRevoke"), async () => {
@@ -267,9 +277,12 @@ curl --fail --show-error --request POST \
event.preventDefault(); const button = event.submitter || $("certificate-api-form").querySelector("button[type=submit]"); button.disabled = true;
try {
const keyId = $("certificate-api-key-id").value.trim(), token = $("certificate-api-token").value.trim(), comment = $("certificate-api-comment").value.trim();
const payload = await api("/api/certificate-api", { method: "PUT", body: JSON.stringify({ key_id: keyId || null, token: token || null, comment }) });
const certificateIds = $("certificate-api-certificate-ids").value.split(/[\n,]+/).map(value => value.trim()).filter(Boolean);
if (!certificateIds.length) throw fieldError($("certificate-api-certificate-ids"), t("certificateApiCertificateIdsRequired"));
const allowDeploy = $("certificate-api-allow-deploy").checked;
const payload = await api("/api/certificate-api", { method: "PUT", body: JSON.stringify({ key_id: keyId || null, token: token || null, comment, certificate_ids: certificateIds, allow_deploy: allowDeploy }) });
const clearToken = payload.token || ""; certificateApiState = { ...payload }; delete certificateApiState.token; delete certificateApiState.credential;
$("certificate-api-key-id").value = ""; $("certificate-api-comment").value = ""; $("certificate-api-token").value = ""; $("certificate-api-secret").textContent = clearToken; $("certificate-api-secret-wrap").hidden = !clearToken; renderCertificateApi(); toast(t("certificateApiRotated"));
$("certificate-api-key-id").value = ""; $("certificate-api-comment").value = ""; $("certificate-api-certificate-ids").value = ""; $("certificate-api-allow-deploy").checked = false; $("certificate-api-token").value = ""; $("certificate-api-secret").textContent = clearToken; $("certificate-api-secret-wrap").hidden = !clearToken; renderCertificateApi(); toast(t("certificateApiRotated"));
} catch (error) { toast(error.message, true); } finally { button.disabled = false; }
});
$("certificate-api-revoke-all").addEventListener("click", async () => {
@@ -279,6 +292,8 @@ curl --fail --show-error --request POST \
catch (error) { toast(error.message, true); } finally { button.disabled = false; }
});
$("certificate-api-key-id").addEventListener("input", certificateApiExample);
$("certificate-api-certificate-ids").addEventListener("input", certificateApiExample);
$("certificate-api-allow-deploy").addEventListener("change", certificateApiExample);
$("admin-tls").addEventListener("change", () => { $("admin-certificate").disabled = !$("admin-tls").checked; });
$("prometheus-form").addEventListener("input", example);
$("prom-preview").addEventListener("click", async () => {
@@ -288,6 +303,6 @@ curl --fail --show-error --request POST \
const text = await response.text(); if (epoch !== authEpoch || !authenticated) return; $("prom-preview-output").textContent = text.slice(0, 262144); $("prom-preview-output").hidden = false;
} catch (error) { if (epoch === authEpoch) toast(error.message, true); } finally { button.disabled = false; }
});
window.rustProxySettings = { select, selectHttpTab, syncLocation, render, refreshSearch: applySettingsSearch, current: () => active, reset() { if (search) search.value = ""; applySettingsSearch(); select("listeners", false, false); selectCacheTab("general"); selectLoggingTab("outputs"); selectHttpTab("http2"); certificateApiState = null; certificateApiLoading = false; $("certificate-api-key-id").value = ""; $("certificate-api-comment").value = ""; $("certificate-api-token").value = ""; $("certificate-api-secret").textContent = ""; $("certificate-api-credentials").replaceChildren(); $("certificate-api-secret-wrap").hidden = true; $("prom-preview-output").textContent = ""; $("prom-preview-output").hidden = true; } };
window.rustProxySettings = { select, selectHttpTab, syncLocation, render, refreshSearch: applySettingsSearch, current: () => active, reset() { if (search) search.value = ""; applySettingsSearch(); select("listeners", false, false); selectCacheTab("general"); selectLoggingTab("outputs"); selectHttpTab("http2"); certificateApiState = null; certificateApiLoading = false; $("certificate-api-key-id").value = ""; $("certificate-api-comment").value = ""; $("certificate-api-certificate-ids").value = ""; $("certificate-api-allow-deploy").checked = false; $("certificate-api-token").value = ""; $("certificate-api-secret").textContent = ""; $("certificate-api-credentials").replaceChildren(); $("certificate-api-secret-wrap").hidden = true; $("prom-preview-output").textContent = ""; $("prom-preview-output").hidden = true; } };
select(active); render();
})();