Certificates for other hosts: jarl cert get
jarld obtains and renews a certificate for another host; the host asks
for it with one command, waits, downloads it when it is newer than its
copy and runs a hook with the paths in JARL_CERT_* variables.
- Certificates have an owner user; another user can't take the name.
An existing one is obtained again only when a name is added or
removed. A new certificates grant (names=) says which names a user
may have; it is never part of "*". Users may own
certificates.max_per_user certificates (10) or their own cert_limit.
- POST /certificates/{name}/request; owners may list, show, export and
delete theirs; keys only over HTTPS, the socket or loopback; every
download is recorded and shown as "Pulled by".
- CLI: jarl cert get (saved settings, all saved when no name), cert
saved, cert forget, cert schedule (daily systemd timer, Linux, root).
- Migration 14. Tests: server, auth, and e2e against Pebble.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b4bb99b482
commit
9db76a113c
29 files changed
+1568
-40
No files matched your search
@@ -0,0 +1,173 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"jarl/internal/auth"
|
||||
"jarl/internal/model"
|
||||
)
|
||||
|
||||
// certUser creates a service user with grants and returns a token.
|
||||
func certUser(t *testing.T, e *apiEnv, name string, grants ...string) (string, *model.User) {
|
||||
t.Helper()
|
||||
gs := []model.Grant{}
|
||||
for _, s := range grants {
|
||||
g, err := auth.ParseGrant(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gs = append(gs, g)
|
||||
}
|
||||
u := &model.User{Username: name, Grants: gs}
|
||||
if err := e.st.CreateUser(context.Background(), u); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tok, pid, sh := auth.NewToken()
|
||||
if err := e.st.CreateToken(context.Background(), &model.Token{UserID: u.ID, Name: name + "-host", PublicID: pid, SecretHash: sh}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return tok, u
|
||||
}
|
||||
|
||||
// obtained gives a certificate a current certificate, as if the CA had
|
||||
// signed it.
|
||||
func obtained(t *testing.T, e *apiEnv, name string, domains ...string) {
|
||||
t.Helper()
|
||||
certFile, keyFile := selfSigned(t, t.TempDir(), domains...)
|
||||
cb, _ := os.ReadFile(certFile)
|
||||
kb, _ := os.ReadFile(keyFile)
|
||||
ctx := context.Background()
|
||||
for i := 0; i < 50 && e.srv.cfg.Certs.Issuing(name) != (time.Time{}); i++ {
|
||||
time.Sleep(20 * time.Millisecond) // the failing attempt (no CA) ends
|
||||
}
|
||||
c, err := e.st.CertificateByName(ctx, name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
na, nb := time.Now().Add(30*24*time.Hour), time.Now()
|
||||
c.CertPEM, c.KeyPEM, c.NotAfter, c.NotBefore, c.Serial, c.Failures, c.LastError = string(cb), string(kb), &na, &nb, "s-"+name, 0, ""
|
||||
if err := e.st.UpdateCertificate(ctx, c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertGet(t *testing.T) {
|
||||
e := newAPIEnv(t)
|
||||
withCerts(e)
|
||||
mx, mxUser := certUser(t, e, "mx", "certificates:write names=*.adbraun.lol")
|
||||
other, _ := certUser(t, e, "other", "certificates:write names=*.adbraun.lol")
|
||||
wide, _ := certUser(t, e, "wide", "*:write")
|
||||
|
||||
req := func(tok, name string, domains ...string) (map[string]any, int) {
|
||||
b, status := e.req("POST", "/certificates/"+name+"/request", tok, map[string]any{"domains": domains})
|
||||
var out map[string]any
|
||||
_ = jsonUnmarshal(b, &out)
|
||||
return out, status
|
||||
}
|
||||
// "*" doesn't cover certificates.
|
||||
if _, st := req(wide, "mail", "mail.adbraun.lol"); st != 403 {
|
||||
t.Errorf("*:write: %d", st)
|
||||
}
|
||||
// Names outside the grant.
|
||||
if out, st := req(mx, "mail", "mail.example.org"); st != 403 || !strings.Contains(errMsg(out), "mail.example.org") {
|
||||
t.Errorf("other names: %d %v", st, out)
|
||||
}
|
||||
// A new certificate, owned by mx.
|
||||
if out, st := req(mx, "mail", "mail.adbraun.lol"); st != 200 && st != 202 || out["created"] != true {
|
||||
t.Fatalf("create: %d %v", st, out)
|
||||
}
|
||||
c, _ := e.st.CertificateByName(context.Background(), "mail")
|
||||
if c.OwnerID != mxUser.ID || len(c.Uses) != 0 {
|
||||
t.Errorf("owner %d uses %v", c.OwnerID, c.Uses)
|
||||
}
|
||||
// Another user can't have the name, or see it.
|
||||
if out, st := req(other, "mail", "mail.adbraun.lol"); st != 409 || !strings.Contains(errMsg(out), "taken") {
|
||||
t.Errorf("taken: %d %v", st, out)
|
||||
}
|
||||
e.do("GET", "/certificates/mail", other, nil, 404)
|
||||
if l := e.do("GET", "/certificates", other, nil, 200)["certificates"].([]any); len(l) != 0 {
|
||||
t.Errorf("other's list: %v", l)
|
||||
}
|
||||
// Nothing to download until it is obtained.
|
||||
e.do("GET", "/certificates/mail/pem", mx, nil, 400)
|
||||
obtained(t, e, "mail", "mail.adbraun.lol")
|
||||
|
||||
// The same names: the same certificate.
|
||||
if out, st := req(mx, "mail", "mail.adbraun.lol"); st != 200 || out["created"] != false || out["changed"] != false {
|
||||
t.Errorf("again: %d %v", st, out)
|
||||
}
|
||||
if c2, _ := e.st.CertificateByName(context.Background(), "mail"); c2.Serial != "s-mail" {
|
||||
t.Errorf("replaced: %v", c2.Serial)
|
||||
}
|
||||
// Download: recorded, with the token.
|
||||
if b, st := e.req("GET", "/certificates/mail/pem?part=key", mx, nil); st != 200 || !strings.Contains(string(b), "PRIVATE KEY") {
|
||||
t.Errorf("key: %d", st)
|
||||
}
|
||||
got := e.do("GET", "/certificates/mail", e.token, nil, 200)
|
||||
pulls, _ := got["pulled_by"].([]any)
|
||||
if got["owner"] != "mx" || len(pulls) != 1 || !strings.Contains(pulls[0].(map[string]any)["who"].(string), "mx-host") || pulls[0].(map[string]any)["serial"] != "s-mail" {
|
||||
t.Errorf("owner %v pulled_by %v", got["owner"], got["pulled_by"])
|
||||
}
|
||||
// A name added: obtained again.
|
||||
if out, st := req(mx, "mail", "mail.adbraun.lol", "smtp.adbraun.lol"); out["changed"] != true || st != 200 && st != 202 {
|
||||
t.Errorf("SAN added: %d %v", st, out)
|
||||
}
|
||||
// The owner's own list.
|
||||
if l := e.do("GET", "/certificates", mx, nil, 200)["certificates"].([]any); len(l) != 1 {
|
||||
t.Errorf("mx's list: %v", l)
|
||||
}
|
||||
// Jarl's own certificates are the administrators'.
|
||||
certFile, keyFile := selfSigned(t, t.TempDir(), "jarl.adbraun.lol")
|
||||
e.do("POST", "/certificates", e.token, map[string]any{"name": "web", "source": "file", "cert_file": certFile, "key_file": keyFile}, 201)
|
||||
if _, st := req(mx, "web", "jarl.adbraun.lol"); st != 409 {
|
||||
t.Errorf("Jarl's own: %d", st)
|
||||
}
|
||||
e.do("GET", "/certificates/web/pem", mx, nil, 404)
|
||||
// The limit: one more for mx, then no more.
|
||||
limit := 2
|
||||
mxUser.CertLimit = &limit
|
||||
if err := e.st.UpdateUser(context.Background(), mxUser); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out, st := req(mx, "www", "www.adbraun.lol"); st != 200 && st != 202 {
|
||||
t.Errorf("second: %d %v", st, out)
|
||||
}
|
||||
if out, st := req(mx, "third", "x.adbraun.lol"); st != 400 || !strings.Contains(errMsg(out), "limit") {
|
||||
t.Errorf("over the limit: %d %v", st, out)
|
||||
}
|
||||
// The owner may delete theirs.
|
||||
e.do("DELETE", "/certificates/www", mx, nil, 200)
|
||||
}
|
||||
|
||||
func jsonUnmarshal(b []byte, v any) error { return json.Unmarshal(b, v) }
|
||||
|
||||
// errMsg is an error answer's message.
|
||||
func errMsg(out map[string]any) string {
|
||||
e, _ := out["error"].(map[string]any)
|
||||
m, _ := e["message"].(string)
|
||||
return m
|
||||
}
|
||||
|
||||
// A private key goes back only over HTTPS or to this server itself.
|
||||
func TestKeyTransport(t *testing.T) {
|
||||
e := newAPIEnv(t)
|
||||
for addr, want := range map[string]bool{"127.0.0.1:5000": true, "[::1]:5000": true, "192.0.2.7:5000": false} {
|
||||
r, _ := http.NewRequest("GET", "/x", nil)
|
||||
r.RemoteAddr = addr
|
||||
if ok, why := e.srv.keyTransport(r); ok != want {
|
||||
t.Errorf("%s: %v (%s)", addr, ok, why)
|
||||
}
|
||||
}
|
||||
r, _ := http.NewRequest("GET", "/x", nil)
|
||||
r.RemoteAddr, r.TLS = "192.0.2.7:5000", &tls.ConnectionState{}
|
||||
if ok, _ := e.srv.keyTransport(r); !ok {
|
||||
t.Error("HTTPS refused")
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ func selfSigned(t *testing.T, dir string, names ...string) (certFile, keyFile st
|
||||
|
||||
func withCerts(env *apiEnv) *certs.Manager {
|
||||
quiet := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
m := certs.New(certs.Config{Dir: filepath.Join(env.t.TempDir(), "tls"), Directory: "https://acme.invalid/dir"}, env.st, env.srv.eng, quiet)
|
||||
m := certs.New(certs.Config{Dir: filepath.Join(env.t.TempDir(), "tls"), Directory: "https://acme.invalid/dir", MaxPerUser: 10}, env.st, env.srv.eng, quiet)
|
||||
env.srv.cfg.Certs = m
|
||||
return m
|
||||
}
|
||||
|
||||
@@ -444,6 +444,12 @@ func applyUserInput(u *model.User, in *api.UserInput) error {
|
||||
if in.Comment != nil {
|
||||
u.Comment = *in.Comment
|
||||
}
|
||||
if in.CertLimit != nil {
|
||||
u.CertLimit = in.CertLimit
|
||||
if *in.CertLimit < 0 {
|
||||
u.CertLimit = nil
|
||||
}
|
||||
}
|
||||
if in.Password != nil {
|
||||
if *in.Password == "" {
|
||||
u.PasswordHash = "" // service account: tokens only
|
||||
|
||||
+194
-6
@@ -4,7 +4,11 @@ import (
|
||||
"context"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"jarl/internal/api"
|
||||
@@ -28,6 +32,10 @@ type certView struct {
|
||||
NamedUse []string `json:"named_use,omitempty"`
|
||||
// IssuingSince is set while Jarl is obtaining it.
|
||||
IssuingSince *time.Time `json:"issuing_since,omitempty"`
|
||||
// Owner is the user a certificate for another host belongs to; PulledBy
|
||||
// who downloaded it (list and get, administrators only).
|
||||
Owner string `json:"owner,omitempty"`
|
||||
PulledBy []model.CertificatePull `json:"pulled_by,omitempty"`
|
||||
}
|
||||
|
||||
type certFiles struct {
|
||||
@@ -62,6 +70,78 @@ func (s *Server) certsReady(p *auth.Principal) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// certsAccess lets in administrators and users with a certificates grant
|
||||
// (certificates for other hosts: their own only).
|
||||
func (s *Server) certsAccess(p *auth.Principal) error {
|
||||
if s.cfg.Certs == nil {
|
||||
return &httpError{http.StatusNotFound, "disabled", "certificates aren't available in this jarld"}
|
||||
}
|
||||
if p.IsAdmin() || p.Can(auth.Action{Resource: auth.ResCerts}) {
|
||||
return nil
|
||||
}
|
||||
return forbidden("certificates need an administrator, or a certificates grant for their names (certificates:write names=*.example.com)")
|
||||
}
|
||||
|
||||
// certAllowed reports whether p may see (or, with write, change) c: an
|
||||
// administrator any; anyone else their own, while their grants cover all
|
||||
// its names.
|
||||
func certAllowed(p *auth.Principal, c *model.Certificate, write bool) bool {
|
||||
if p.IsAdmin() {
|
||||
return true
|
||||
}
|
||||
if p.User == nil || c.OwnerID == 0 || c.OwnerID != p.User.ID {
|
||||
return false
|
||||
}
|
||||
for _, d := range c.Domains {
|
||||
if !p.Can(auth.Action{Resource: auth.ResCerts, Write: write, Name: d}) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// certFor returns the certificate in the path, if p may see it (else not
|
||||
// found, so names of others' certificates don't leak).
|
||||
func (s *Server) certFor(r *http.Request, p *auth.Principal, write bool) (*model.Certificate, error) {
|
||||
c, err := s.cfg.Certs.Get(r.Context(), r.PathValue("name"))
|
||||
if err != nil {
|
||||
return nil, certErr(err)
|
||||
}
|
||||
if !certAllowed(p, c, write) {
|
||||
if write && certAllowed(p, c, false) {
|
||||
return nil, forbidden("you may only download certificate %s", c.Name)
|
||||
}
|
||||
return nil, notFound("no such certificate")
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// withOwners adds owners and downloads to views (for administrators).
|
||||
func (s *Server) withOwners(ctx context.Context, views []certView) error {
|
||||
pulls, err := s.store.CertificatePulls(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
users, err := s.store.ListUsers(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
names := map[int64]string{}
|
||||
for _, u := range users {
|
||||
names[u.ID] = u.Username
|
||||
}
|
||||
for i := range views {
|
||||
if id := views[i].OwnerID; id != 0 {
|
||||
views[i].Owner = names[id]
|
||||
if views[i].Owner == "" {
|
||||
views[i].Owner = fmt.Sprintf("deleted user %d", id)
|
||||
}
|
||||
}
|
||||
views[i].PulledBy = pulls[views[i].ID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func certErr(err error) error {
|
||||
var ie *certs.InputError
|
||||
switch {
|
||||
@@ -71,13 +151,15 @@ func certErr(err error) error {
|
||||
return notFound("no such certificate")
|
||||
case errors.Is(err, certs.ErrConflict):
|
||||
return conflict("a certificate with that name exists")
|
||||
case errors.Is(err, certs.ErrTaken):
|
||||
return conflict("the certificate name is taken by another user's certificate (or one Jarl uses): choose another name")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// certList: GET /certificates.
|
||||
func (s *Server) certList(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
||||
if err := s.certsReady(p); err != nil {
|
||||
if err := s.certsAccess(p); err != nil {
|
||||
return err
|
||||
}
|
||||
all, err := s.cfg.Certs.List(r.Context())
|
||||
@@ -90,22 +172,32 @@ func (s *Server) certList(w http.ResponseWriter, r *http.Request, p *auth.Princi
|
||||
}
|
||||
out := []certView{}
|
||||
for _, c := range all {
|
||||
if !certAllowed(p, c, false) {
|
||||
continue
|
||||
}
|
||||
v := s.presentCert(c)
|
||||
v.NamedUse = encdns.CertUses(st, s.cfg.Certs.FilesDir(c.Name))
|
||||
out = append(out, v)
|
||||
}
|
||||
if !p.IsAdmin() {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"certificates": out})
|
||||
return nil
|
||||
}
|
||||
if err := s.withOwners(r.Context(), out); err != nil {
|
||||
return err
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"certificates": out, "account": s.cfg.Certs.AccountInfo(r.Context())})
|
||||
return nil
|
||||
}
|
||||
|
||||
// certGet: GET /certificates/{name}.
|
||||
func (s *Server) certGet(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
||||
if err := s.certsReady(p); err != nil {
|
||||
if err := s.certsAccess(p); err != nil {
|
||||
return err
|
||||
}
|
||||
c, err := s.cfg.Certs.Get(r.Context(), r.PathValue("name"))
|
||||
c, err := s.certFor(r, p, false)
|
||||
if err != nil {
|
||||
return certErr(err)
|
||||
return err
|
||||
}
|
||||
st, err := s.store.LoadState(r.Context())
|
||||
if err != nil {
|
||||
@@ -113,22 +205,80 @@ func (s *Server) certGet(w http.ResponseWriter, r *http.Request, p *auth.Princip
|
||||
}
|
||||
v := s.presentCert(c)
|
||||
v.NamedUse = encdns.CertUses(st, s.cfg.Certs.FilesDir(c.Name))
|
||||
if p.IsAdmin() {
|
||||
vs := []certView{v}
|
||||
if err := s.withOwners(r.Context(), vs); err != nil {
|
||||
return err
|
||||
}
|
||||
v = vs[0]
|
||||
}
|
||||
writeJSON(w, http.StatusOK, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
// certRequest: POST /certificates/{name}/request {"domains": [...],
|
||||
// "key_type": "ec256"}, for jarl cert get: the certificate for another
|
||||
// host, owned by the caller's user. A new one is created and obtained
|
||||
// (202 while that runs; poll GET /certificates/{name} until
|
||||
// issuing_since is gone). An existing one is the caller's, obtained again
|
||||
// only when its names change; otherwise it is returned as it is (200).
|
||||
func (s *Server) certRequest(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
||||
if err := s.certsAccess(p); err != nil {
|
||||
return err
|
||||
}
|
||||
if p.User == nil || p.User.ID == 0 {
|
||||
return forbidden("certificates for other hosts belong to a user: use an API token, not the local socket as root")
|
||||
}
|
||||
var in struct {
|
||||
Domains []string `json:"domains"`
|
||||
KeyType string `json:"key_type"`
|
||||
}
|
||||
if err := decodeJSON(r, &in); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, d := range in.Domains {
|
||||
d = strings.ToLower(strings.TrimSuffix(strings.TrimSpace(d), "."))
|
||||
if !p.Can(auth.Action{Resource: auth.ResCerts, Write: true, Name: d}) {
|
||||
return forbidden("you may not get a certificate for %s: that needs certificates:write with a names pattern that matches it", d)
|
||||
}
|
||||
}
|
||||
res, err := s.cfg.Certs.Ensure(r.Context(), r.PathValue("name"), in.Domains, in.KeyType, p)
|
||||
if err != nil {
|
||||
return certErr(err)
|
||||
}
|
||||
v := s.presentCert(res.Certificate)
|
||||
out := map[string]any{"certificate": v, "created": res.Created, "changed": res.Changed}
|
||||
if res.Ignored != "" {
|
||||
out["warnings"] = []string{res.Ignored}
|
||||
}
|
||||
status := http.StatusOK
|
||||
if v.IssuingSince != nil {
|
||||
status = http.StatusAccepted
|
||||
}
|
||||
writeJSON(w, status, out)
|
||||
return nil
|
||||
}
|
||||
|
||||
// certPEM: GET /certificates/{name}/pem?part=fullchain|cert|key|bundle,
|
||||
// the certificate as PEM for use elsewhere: the chain (default), the
|
||||
// certificate alone, the private key, or the key followed by the chain
|
||||
// (one file, as HAProxy wants). Anything with the key is audited.
|
||||
func (s *Server) certPEM(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
||||
if err := s.certsReady(p); err != nil {
|
||||
if err := s.certsAccess(p); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := s.certFor(r, p, false); err != nil {
|
||||
return err
|
||||
}
|
||||
part := r.URL.Query().Get("part")
|
||||
if part == "" {
|
||||
part = "fullchain"
|
||||
}
|
||||
if part == "key" || part == "bundle" {
|
||||
if ok, why := s.keyTransport(r); !ok {
|
||||
return &httpError{http.StatusForbidden, "https_required", "a private key is only handed out over HTTPS (or to this server itself), and this request came by " + why}
|
||||
}
|
||||
}
|
||||
c, chain, key, err := s.cfg.Certs.PEM(r.Context(), r.PathValue("name"))
|
||||
if err != nil {
|
||||
return certErr(err)
|
||||
@@ -155,6 +305,16 @@ func (s *Server) certPEM(w http.ResponseWriter, r *http.Request, p *auth.Princip
|
||||
if part == "key" || part == "bundle" {
|
||||
s.eng.Audit(r.Context(), p, "certificate.export_key", "certificate", c.ID, c.Name, "success", "exported the private key ("+part+")")
|
||||
}
|
||||
pull := &model.CertificatePull{CertID: c.ID, Who: pullWho(p), Serial: c.Serial, IP: s.clientIP(r), At: time.Now().UTC()}
|
||||
if p.User != nil {
|
||||
pull.UserID = p.User.ID
|
||||
}
|
||||
if p.Token != nil {
|
||||
pull.TokenID = p.Token.ID
|
||||
}
|
||||
if err := s.store.RecordCertificatePull(r.Context(), pull); err != nil {
|
||||
s.log.Warn("recording a certificate download failed", "certificate", c.Name, "err", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/x-pem-file")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="`+file+`"`)
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
@@ -208,7 +368,10 @@ func (s *Server) certUpdate(w http.ResponseWriter, r *http.Request, p *auth.Prin
|
||||
|
||||
// certDelete: DELETE /certificates/{name}.
|
||||
func (s *Server) certDelete(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
||||
if err := s.certsReady(p); err != nil {
|
||||
if err := s.certsAccess(p); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := s.certFor(r, p, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.cfg.Certs.Delete(r.Context(), r.PathValue("name"), p); err != nil {
|
||||
@@ -303,3 +466,28 @@ func (s *Server) certSummary(ctx context.Context) *api.CertSummary {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// keyTransport reports whether a private key may go back over r: HTTPS
|
||||
// (also through a trusted proxy), the local socket, or loopback.
|
||||
func (s *Server) keyTransport(r *http.Request) (bool, string) {
|
||||
https, why := s.transport(r)
|
||||
if https {
|
||||
return true, why
|
||||
}
|
||||
host, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
if a, err := netip.ParseAddr(host); err == nil && a.Unmap().IsLoopback() && !s.trusted(a.Unmap()) {
|
||||
return true, "plain HTTP from this server"
|
||||
}
|
||||
return false, why
|
||||
}
|
||||
|
||||
// pullWho names a downloader: its token (and user), or the user.
|
||||
func pullWho(p *auth.Principal) string {
|
||||
switch {
|
||||
case p.Token != nil:
|
||||
return "token " + p.Token.Name + " (" + p.Name() + ")"
|
||||
case p.User != nil && p.Method != "":
|
||||
return p.Method + " " + p.Name()
|
||||
}
|
||||
return p.Name()
|
||||
}
|
||||
@@ -175,6 +175,7 @@ func (s *Server) routes() {
|
||||
m.HandleFunc("POST "+p+"/certificates/check", s.authed(s.certCheck))
|
||||
m.HandleFunc("GET "+p+"/certificates/{name}", s.authed(s.certGet))
|
||||
m.HandleFunc("GET "+p+"/certificates/{name}/pem", s.authed(s.certPEM))
|
||||
m.HandleFunc("POST "+p+"/certificates/{name}/request", s.authed(s.certRequest))
|
||||
m.HandleFunc("PATCH "+p+"/certificates/{name}", s.authed(s.certUpdate))
|
||||
m.HandleFunc("DELETE "+p+"/certificates/{name}", s.authed(s.certDelete))
|
||||
m.HandleFunc("POST "+p+"/certificates/{name}/issue", s.authed(s.certIssue))
|
||||
|
||||
Reference in new issue
Block a user