1 Commits

Author SHA1 Message Date
ae4cc8b420 Fix web UI download links for CA certs, SSH CA pubkey, and KRL
Templates linked to /v1/ API server routes which don't exist on the
web server (separate binary). Add web server handlers that fetch data
via gRPC and serve the downloads directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 19:02:15 -07:00
4 changed files with 63 additions and 3 deletions

View File

@@ -50,6 +50,8 @@ func (ws *WebServer) registerRoutes(r chi.Router) {
r.Route("/sshca", func(r chi.Router) { r.Route("/sshca", func(r chi.Router) {
r.Get("/", ws.requireAuth(ws.handleSSHCA)) r.Get("/", ws.requireAuth(ws.handleSSHCA))
r.Get("/ca", ws.requireAuth(ws.handleSSHCADownload))
r.Get("/krl", ws.requireAuth(ws.handleSSHCAKRLDownload))
r.Post("/sign-user", ws.requireAuth(ws.handleSSHCASignUser)) r.Post("/sign-user", ws.requireAuth(ws.handleSSHCASignUser))
r.Post("/sign-host", ws.requireAuth(ws.handleSSHCASignHost)) r.Post("/sign-host", ws.requireAuth(ws.handleSSHCASignHost))
r.Get("/cert/{serial}", ws.requireAuth(ws.handleSSHCACertDetail)) r.Get("/cert/{serial}", ws.requireAuth(ws.handleSSHCACertDetail))
@@ -91,6 +93,7 @@ func (ws *WebServer) registerRoutes(r chi.Router) {
r.Route("/pki", func(r chi.Router) { r.Route("/pki", func(r chi.Router) {
r.Get("/", ws.requireAuth(ws.handlePKI)) r.Get("/", ws.requireAuth(ws.handlePKI))
r.Get("/ca", ws.requireAuth(ws.handlePKIRootCA))
r.Post("/import-root", ws.requireAuth(ws.handleImportRoot)) r.Post("/import-root", ws.requireAuth(ws.handleImportRoot))
r.Post("/create-issuer", ws.requireAuth(ws.handleCreateIssuer)) r.Post("/create-issuer", ws.requireAuth(ws.handleCreateIssuer))
r.Post("/issue", ws.requireAuth(ws.handleIssueCert)) r.Post("/issue", ws.requireAuth(ws.handleIssueCert))
@@ -475,6 +478,25 @@ func (ws *WebServer) handleCreateIssuer(w http.ResponseWriter, r *http.Request)
http.Redirect(w, r, "/pki", http.StatusFound) http.Redirect(w, r, "/pki", http.StatusFound)
} }
func (ws *WebServer) handlePKIRootCA(w http.ResponseWriter, r *http.Request) {
token := extractCookie(r)
mountName, err := ws.findCAMount(r, token)
if err != nil {
http.Error(w, "no CA engine mounted", http.StatusNotFound)
return
}
certPEM, err := ws.vault.GetRootCert(r.Context(), mountName)
if err != nil || len(certPEM) == 0 {
http.Error(w, "root CA not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/x-pem-file")
w.Header().Set("Content-Disposition", "attachment; filename=root-ca.pem")
_, _ = w.Write(certPEM) //nolint:gosec
}
func (ws *WebServer) handlePKIIssuer(w http.ResponseWriter, r *http.Request) { func (ws *WebServer) handlePKIIssuer(w http.ResponseWriter, r *http.Request) {
token := extractCookie(r) token := extractCookie(r)
mountName, err := ws.findCAMount(r, token) mountName, err := ws.findCAMount(r, token)

View File

@@ -40,6 +40,44 @@ func (ws *WebServer) handleSSHCA(w http.ResponseWriter, r *http.Request) {
ws.renderTemplate(w, "sshca.html", data) ws.renderTemplate(w, "sshca.html", data)
} }
func (ws *WebServer) handleSSHCADownload(w http.ResponseWriter, r *http.Request) {
token := extractCookie(r)
mountName, err := ws.findSSHCAMount(r, token)
if err != nil {
http.Error(w, "no SSH CA engine mounted", http.StatusNotFound)
return
}
pubkey, err := ws.vault.GetSSHCAPublicKey(r.Context(), mountName)
if err != nil || pubkey == nil {
http.Error(w, "CA public key not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Content-Disposition", "attachment; filename=ca.pub")
_, _ = w.Write([]byte(pubkey.PublicKey)) //nolint:gosec
}
func (ws *WebServer) handleSSHCAKRLDownload(w http.ResponseWriter, r *http.Request) {
token := extractCookie(r)
mountName, err := ws.findSSHCAMount(r, token)
if err != nil {
http.Error(w, "no SSH CA engine mounted", http.StatusNotFound)
return
}
krl, err := ws.vault.GetSSHCAKRL(r.Context(), mountName)
if err != nil {
http.Error(w, "KRL not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment; filename=krl.bin")
_, _ = w.Write(krl) //nolint:gosec
}
func (ws *WebServer) handleSSHCASignUser(w http.ResponseWriter, r *http.Request) { func (ws *WebServer) handleSSHCASignUser(w http.ResponseWriter, r *http.Request) {
info := tokenInfoFromContext(r.Context()) info := tokenInfoFromContext(r.Context())
token := extractCookie(r) token := extractCookie(r)

View File

@@ -27,7 +27,7 @@
</tbody> </tbody>
</table> </table>
<p style="margin-top: 1rem; margin-bottom: 0;"> <p style="margin-top: 1rem; margin-bottom: 0;">
<a href="/v1/pki/{{.MountName}}/ca" download="root-ca.pem">Download Root CA (PEM)</a> <a href="/pki/ca" download="root-ca.pem">Download Root CA (PEM)</a>
</p> </p>
{{else}} {{else}}
<p>No root CA configured.</p> <p>No root CA configured.</p>

View File

@@ -14,7 +14,7 @@
{{if .CAPublicKey}} {{if .CAPublicKey}}
<textarea rows="3" class="pem-input" readonly>{{.CAPublicKey}}</textarea> <textarea rows="3" class="pem-input" readonly>{{.CAPublicKey}}</textarea>
<p style="margin-top: 0.5rem; margin-bottom: 0;"> <p style="margin-top: 0.5rem; margin-bottom: 0;">
<a href="/v1/sshca/{{.MountName}}/ca" download="ca.pub">Download CA Public Key</a> <a href="/sshca/ca" download="ca.pub">Download CA Public Key</a>
</p> </p>
{{else}} {{else}}
<p>CA public key not available.</p> <p>CA public key not available.</p>
@@ -210,7 +210,7 @@
{{if .IsAdmin}} {{if .IsAdmin}}
<div class="card"> <div class="card">
<div class="card-title">Key Revocation List</div> <div class="card-title">Key Revocation List</div>
<p><a href="/v1/sshca/{{.MountName}}/krl" download="krl.bin">Download KRL</a></p> <p><a href="/sshca/krl" download="krl.bin">Download KRL</a></p>
</div> </div>
{{end}} {{end}}
{{end}} {{end}}