pdns-auth-proxy/http.go

568 lines
15 KiB
Go

package main
import (
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"embed"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"io/fs"
"io/ioutil"
"log"
"math/rand"
"net/http"
"os"
"regexp"
"strings"
"sync"
"time"
"golang.org/x/crypto/openpgp/clearsign"
"gopkg.in/djherbis/times.v1"
)
type (
// HTTPServer is the webservice main object with its configuration parameters
HTTPServer struct {
Port string
key string
cert string
crl string
decodedCrl *pkix.CertificateList
decodedCA []*x509.Certificate
decodedCrlTime time.Time
authProfiles map[string]AuthProfile
pdnsAcls []*PdnsACL
jrpcAPIACls []*JSONRPCACL
certPool *x509.CertPool
debug bool
m sync.RWMutex
dns *PowerDNS
nonceGen string
certCache map[string]time.Time
zoneProfiles map[string]*zoneProfile
}
zoneProfile struct {
Default bool
NameServers []string
SOA string
DefaultEntries []*JSONInput
Regexp []*regexp.Regexp
AutoInc bool
}
)
// initalize local files
//
//go:embed web/*
var embeddedFS embed.FS
// JSONRPCNewError return a valid json rpc 2.0 error
func JSONRPCNewError(code, id int, message string) JSONRPCError {
e := JSONRPCError{ID: id, JSONRPC: "2.0"}
e.Error.Code = code
e.Error.Message = message
return e
}
func (e JSONRPCError) String() string {
return string(printJSON(e))
}
// NewHTTPServer initializes HTTPServer
func NewHTTPServer(port, key, cert, crl, ca, pdnsServer, pdnsKey string, timeout, ttl int) *HTTPServer {
rand.Seed(time.Now().UnixNano())
h := HTTPServer{
Port: port,
key: key,
cert: cert,
crl: crl,
nonceGen: NewSalt(25),
zoneProfiles: map[string]*zoneProfile{},
certCache: map[string]time.Time{},
authProfiles: map[string]AuthProfile{},
pdnsAcls: []*PdnsACL{},
certPool: x509.NewCertPool(),
decodedCA: []*x509.Certificate{},
}
rawCA, err := ioutil.ReadFile(ca)
if err != nil {
log.Fatal("ca:", err)
}
for len(rawCA) > 0 {
var block *pem.Block
block, rawCA = pem.Decode(rawCA)
if block == nil {
break
}
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
continue
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
continue
}
h.certPool.AddCert(cert)
h.decodedCA = append(h.decodedCA, cert)
}
if h.dns, err = NewClient(pdnsServer, pdnsKey, timeout, ttl); err != nil {
log.Fatal(err)
}
if _, err := h.RefreshCRL(); err != nil {
log.Fatal(err)
}
return &h
}
// NewZoneProfile add zone profiles to the structure
func (h *HTTPServer) NewZoneProfile(zoneType, soa string, isDefault, autoInc bool, nameServers, rules []string) error {
for t, profile := range h.zoneProfiles {
if t == zoneType {
return fmt.Errorf("zone type %s already defined", t)
}
if isDefault && profile.Default {
return fmt.Errorf("zone type %s is already the default one", t)
}
}
if len(nameServers) == 0 {
return errors.New("no nameservers in the configuration")
}
for i := range nameServers {
nameServers[i] = addPoint(nameServers[i])
}
z := &zoneProfile{
Default: isDefault,
NameServers: nameServers,
Regexp: []*regexp.Regexp{},
SOA: soa,
AutoInc: autoInc,
}
for _, r := range rules {
re, err := regexp.Compile(r)
if err != nil {
return err
}
z.Regexp = append(z.Regexp, re)
}
h.lock()
defer h.unlock()
h.zoneProfiles[zoneType] = z
return nil
}
func (z *zoneProfile) addDefaultEntry(name, action, value string) {
params := JSONInputParams{
Name: name,
Value: value,
TTL: 172800,
}
input := &JSONInput{Method: action, ignoreBadDomain: true, Params: params}
z.DefaultEntries = append(z.DefaultEntries, input)
}
// Lock the structure
func (h *HTTPServer) lock() {
h.m.Lock()
}
// ...or unlock it
func (h *HTTPServer) unlock() {
h.m.Unlock()
}
// Debug facilities
func (h *HTTPServer) Debug() {
h.debug = true
h.dns.Debug()
}
// verifyNonce check that the once is valid and less than 10s old
func (h *HTTPServer) verifyNonce(nonce string) bool {
// cannot appear in production, but useful for tests
if len(h.nonceGen) == 0 {
return true
}
if len(nonce) < 47 {
return false
}
now := time.Now().Unix()
salt := nonce[:4]
for i := 0; i < 10; i++ {
if salt+ComputeHmac256(fmt.Sprintf("%s%d", salt, now-int64(i)), h.nonceGen) == nonce {
return true
}
}
return false
}
// sendNonce return a valid nonce to the user
func (h *HTTPServer) sendNonce(w http.ResponseWriter, r *http.Request) {
now := time.Now().Unix()
salt := NewSalt(4)
fmt.Fprintf(w, salt+ComputeHmac256(fmt.Sprintf("%s%d", salt, now), h.nonceGen))
}
// AddAuthProfile adds a profile in the server config
func (h *HTTPServer) AddAuthProfile(name string, p AuthProfile) {
h.lock()
defer h.unlock()
h.authProfiles[name] = p
}
// AddPdnsACL adds an acl in the server config
func (h *HTTPServer) AddPdnsACL(a *PdnsACL) {
h.lock()
defer h.unlock()
h.pdnsAcls = append(h.pdnsAcls, a)
}
// AddjsonRPCACL adds a JRPC acl in the server config
func (h *HTTPServer) AddjsonRPCACL(a *JSONRPCACL) {
h.lock()
defer h.unlock()
h.jrpcAPIACls = append(h.jrpcAPIACls, a)
}
// getPgpProfiles returns the list of profiles validated by the signed payload,
// and the name of the cert
func (h *HTTPServer) getPgpProfiles(message, signature []byte) (string, []string) {
valid := []string{}
signer := ""
for name, p := range h.authProfiles {
if keyUser := PgpMessageVerify(message, signature, p.PgpKeys()); keyUser != "" {
valid = append(valid, name)
signer = keyUser
}
}
return signer, valid
}
// getProfiles returns the list of profiles validated for a certificate subject
func (h *HTTPServer) getProfiles(subject string) []string {
valid := []string{}
for name, p := range h.authProfiles {
ok, err := p.Match(subject)
if err == nil && ok {
valid = append(valid, name)
}
if err != nil {
log.Println(err)
}
}
return valid
}
// nativeValidAuth validates that a user can access a particular path with a specified method
func (h *HTTPServer) nativeValidAuth(path, user, method string) bool {
profiles := h.getProfiles(user)
// no profile validated, no need to continue
if len(profiles) == 0 {
return false
}
// check every acl
for _, acl := range h.pdnsAcls {
if acl.Match(path, method, profiles) {
return true
}
}
log.Println("Could not find profile/acl for ", user, method, path)
return false
}
// nativeValidAuth validates that a user can access a particular path with a specified method
func (h *HTTPServer) jsonrpcValidAuth(j JSONArray, message, signature []byte, certUser string) (bool, string) {
// if the DryRun flag is set on all commands, let's authorize it
// in debug mode, since we will do nothing
if isDryRun(j) && h.debug && len(signature) == 0 {
return true, certUser
}
var pgpProfiles, sslProfiles []string
if len(j) == 0 {
// nothing to check
return false, ""
}
// try to get some profile
switch {
case h.verifyNonce(j[0].Params.Nonce):
certUser, pgpProfiles = h.getPgpProfiles(message, signature)
case certUser != "":
sslProfiles = h.getProfiles(certUser)
}
// we need at least one profile
if len(sslProfiles)+len(pgpProfiles) == 0 {
log.Printf("[jsonRPC API] User %s was not authorized to execute anything", certUser)
return false, certUser
}
// get the available acl for the user if there is a search or list
listFilters := map[string][]*regexp.Regexp{}
for _, acl := range h.jrpcAPIACls {
for _, method := range []string{"list", "search"} {
listFilters[method] = append(listFilters[method], acl.GetListFilters(pgpProfiles, sslProfiles, method)...)
}
}
// check every acl on every action
for i, action := range j {
// for list and search, we do the checking after the fact
if action.Method == "list" || action.Method == "search" {
j[i].listFilters = listFilters[action.Method]
continue
}
// the domain method is always permitted
if action.Method == "domain" {
continue
}
ok := false
for _, acl := range h.jrpcAPIACls {
if acl.Match(action.Method, action.Params.Name, pgpProfiles, sslProfiles) {
ok = true
break
}
}
if !ok {
log.Printf("[jsonRPC API] User %s was not authorized to execute \"%s\"", certUser, action.String())
return false, certUser
}
}
return true, certUser
}
// Get Certificate user Name from the http.Request
func (h *HTTPServer) getCertificate(r *http.Request) (bool, string, error) {
// Check if the TLS user certificate is valid
var sslUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageAny}
opts := x509.VerifyOptions{Roots: h.certPool, KeyUsages: sslUsage}
if len(r.TLS.PeerCertificates) == 0 {
return false, "", nil
}
if _, err := r.TLS.PeerCertificates[0].Verify(opts); err != nil {
return false, "", err
}
if err := h.checkCRL(r.TLS.PeerCertificates); err != nil {
return false, "", err
}
return true, strings.Replace(r.TLS.PeerCertificates[0].Subject.CommonName, " ", "", -1), nil
}
// decode the http request and return the payload, username and signature
func (h *HTTPServer) jrpcDecodeQuery(body []byte) (JSONArray, []byte, []byte, bool, error) {
// the payload can be a PGP signed payload
// if it's not, message will contain the whole payload
b, message := clearsign.Decode(body)
// this is not a valid PGP signed payload, meaning message is all we got
if b == nil {
jsonRPC, wasArray, err := ParsejsonRPCRequest(message, h.dns)
return jsonRPC, message, nil, wasArray, err
}
// this is a valid PGP signed payload, we can extract the real payload and
// use the signature to authenticate
message = b.Plaintext
// there is a newline appended to the payload somehow
if message[len(message)-1] == 10 {
message = message[:len(message)-1]
}
// we need the signature to be a []byte and not an Reader, since we
// may have to use it several times
signature, err := ioutil.ReadAll(b.ArmoredSignature.Body)
if err != nil {
return JSONArray{}, message, signature, false, err
}
jsonRPC, wasArray, err := ParsejsonRPCRequest(message, h.dns)
return jsonRPC, message, signature, wasArray, err
}
// PowerDNS json RPC API support. Support Cert Auth or PGP signed messages
func (h *HTTPServer) jsonRPCServe(w http.ResponseWriter, r *http.Request) {
// This API is POST only
if r.Method != "POST" {
http.Error(w, JSONRPCNewError(-32603, 0, "Internal error").String(), 405)
return
}
w.Header().Set("content-type", "application/json")
username := ""
_, username, certError := h.getCertificate(r)
if certError != nil {
http.Error(w, JSONRPCNewError(-32008, 0, certError.Error()).String(), 403)
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Println(err)
http.Error(w, JSONRPCNewError(-32603, 0, "Internal error").String(), 500)
return
}
// decode the body
jsonRPC, message, signature, wasArray, err := h.jrpcDecodeQuery(body)
if err != nil {
log.Println(err)
http.Error(w, JSONRPCNewError(-32603, 0, "Internal error").String(), 500)
return
}
// try to validate the query
valid, username := h.jsonrpcValidAuth(jsonRPC, message, signature, username)
if !valid {
http.Error(w, JSONRPCNewError(-32004, 0, "You are not authorized").String(), 403)
return
}
fmt.Fprintf(w, jsonRPC.Run(h, username, wasArray, r.Header.Get("PDNS-Output") == "plaintext"))
}
// PowerDNS native API support. Add certificate support for auth
func (h *HTTPServer) nativeAPIServe(w http.ResponseWriter, r *http.Request) {
// Check if the user/server is allowed to perform the required action
ok, commonName, certError := h.getCertificate(r)
if !ok {
http.Error(w, certError.Error(), 403)
return
}
log.Println("[Native API] User", commonName, "requested", r.Method, "on", r.RequestURI)
if !h.nativeValidAuth(strings.TrimPrefix(r.RequestURI, h.dns.apiURL+"/"), commonName, r.Method) {
http.Error(w, "The user "+commonName+" is not authorized to perform this action", 403)
log.Println("[Native API] User ", commonName, " was not authorized to perform the action")
return
}
h.dns.Proxy(w, r)
}
// GetZoneConfig returns a configuration for a new zone
func (h *HTTPServer) GetZoneConfig(s string) (string, string, []string, JSONArray, bool, error) {
valid := ""
def := JSONArray{}
for zoneType, profile := range h.zoneProfiles {
for _, re := range profile.Regexp {
if !re.MatchString(s) {
continue
}
if valid != "" {
return "", "", nil, nil, false, errors.New("Multiple profiles matched, check the config")
}
valid = zoneType
}
}
if valid != "" {
profile := h.zoneProfiles[valid]
for _, e := range profile.DefaultEntries {
def = append(def, e)
}
return valid, profile.SOA, profile.NameServers, def, profile.AutoInc, nil
}
// check for the default
for zoneType, profile := range h.zoneProfiles {
if profile.Default {
for _, e := range profile.DefaultEntries {
def = append(def, e)
}
return zoneType, profile.SOA, profile.NameServers, def, profile.AutoInc, nil
}
}
return "", "", nil, nil, false, errors.New("no valid configuration found")
}
// RefreshCRL decodes the crl file if it's configured, and store it for later use
func (h *HTTPServer) RefreshCRL() (*pkix.CertificateList, error) {
// if there is no crl, no need to do anything
if h.crl == "" {
return nil, nil
}
t, err := times.Stat(h.crl)
if err != nil {
return nil, err
}
mtime := t.ModTime()
if h.decodedCrlTime.Equal(mtime) {
return h.decodedCrl, nil
}
rawCrl, err := ioutil.ReadFile(h.crl)
if err != nil {
return nil, err
}
decoded, err := x509.ParseCRL(rawCrl)
if err != nil {
return nil, err
}
ok := false
for _, ca := range h.decodedCA {
if err := ca.CheckCRLSignature(decoded); err == nil {
ok = true
}
}
if !ok {
return nil, fmt.Errorf("CRL issued with the wrong CA")
}
h.lock()
defer h.unlock()
h.decodedCrlTime = mtime
h.decodedCrl = decoded
return h.decodedCrl, nil
}
func (h *HTTPServer) checkCRL(allCerts []*x509.Certificate) error {
crl, err := h.RefreshCRL()
if err != nil || crl == nil {
return err
}
now := time.Now()
expire := now.Add(24 * time.Hour)
for _, cert := range allCerts {
sign := base64.StdEncoding.EncodeToString(cert.Signature)
if t, ok := h.certCache[sign]; ok && t.Before(expire) {
continue
}
for _, revoked := range h.decodedCrl.TBSCertList.RevokedCertificates {
if cert.SerialNumber.Cmp(revoked.SerialNumber) == 0 {
return fmt.Errorf("Certificate for %s is revoked", cert.Subject.CommonName)
}
}
h.certCache[sign] = now
}
return nil
}
// Run the server
func (h *HTTPServer) Run() {
mux := http.NewServeMux()
server := &http.Server{
Addr: h.Port,
Handler: mux,
TLSConfig: &tls.Config{
ClientAuth: tls.RequestClientCert,
ClientCAs: h.certPool,
},
}
serverRoot, err := fs.Sub(embeddedFS, "static")
if err != nil {
log.Fatal(err)
}
// give acces part of the native API
mux.HandleFunc("/api/v1/servers/localhost/", h.nativeAPIServe)
// but not all
mux.Handle("/api/v1/", http.NotFoundHandler())
// new json RPC api
mux.HandleFunc("/jsonrpc", h.jsonRPCServe)
// needed for security
mux.HandleFunc("/nonce", h.sendNonce)
// status page
mux.HandleFunc("/stats/", h.nativeAPIServe)
mux.HandleFunc("/style.css", h.nativeAPIServe)
if _, err := os.Stat("./web"); err == nil && h.debug {
h.dns.LogDebug("serving local files")
mux.Handle("/", http.FileServer(http.Dir("web")))
} else {
mux.Handle("/", http.FileServer(http.FS(serverRoot)))
}
log.Println("Ready to serve")
log.Fatal(server.ListenAndServeTLS(h.cert, h.key))
}