Merge pull request #5991 from gyuho/manual

v2http: client certificate auth via common name
This commit is contained in:
Gyu-Ho Lee 2016-07-21 08:02:17 -07:00 committed by GitHub
commit de638a5e4d
10 changed files with 399 additions and 72 deletions

View File

@ -276,6 +276,42 @@ func TestCtlV2Backup(t *testing.T) { // For https://github.com/coreos/etcd/issue
}
}
func TestCtlV2AuthWithCommonName(t *testing.T) {
defer testutil.AfterTest(t)
copiedCfg := configClientTLS
copiedCfg.clientCertAuthEnabled = true
epc := setupEtcdctlTest(t, &copiedCfg, false)
defer func() {
if err := epc.Close(); err != nil {
t.Fatalf("error closing etcd processes (%v)", err)
}
}()
if err := etcdctlRoleAdd(epc, "testrole"); err != nil {
t.Fatalf("failed to add role (%v)", err)
}
if err := etcdctlRoleGrant(epc, "testrole", "--rw", "--path=/foo"); err != nil {
t.Fatalf("failed to grant role (%v)", err)
}
if err := etcdctlUserAdd(epc, "root", "123"); err != nil {
t.Fatalf("failed to add user (%v)", err)
}
if err := etcdctlUserAdd(epc, "Autogenerated CA", "123"); err != nil {
t.Fatalf("failed to add user (%v)", err)
}
if err := etcdctlUserGrant(epc, "Autogenerated CA", "testrole"); err != nil {
t.Fatalf("failed to grant role (%v)", err)
}
if err := etcdctlAuthEnable(epc); err != nil {
t.Fatalf("failed to enable auth (%v)", err)
}
if err := etcdctlSet(epc, "foo", "bar"); err != nil {
t.Fatalf("failed to write (%v)", err)
}
}
func etcdctlPrefixArgs(clus *etcdProcessCluster) []string {
endpoints := ""
if proxies := clus.proxies(); len(proxies) != 0 {
@ -348,6 +384,13 @@ func etcdctlRoleAdd(clus *etcdProcessCluster, role string) error {
return spawnWithExpect(cmdArgs, role)
}
func etcdctlRoleGrant(clus *etcdProcessCluster, role string, perms ...string) error {
cmdArgs := append(etcdctlPrefixArgs(clus), "role", "grant")
cmdArgs = append(cmdArgs, perms...)
cmdArgs = append(cmdArgs, role)
return spawnWithExpect(cmdArgs, role)
}
func etcdctlRoleList(clus *etcdProcessCluster, expectedRole string) error {
cmdArgs := append(etcdctlPrefixArgs(clus), "role", "list")
return spawnWithExpect(cmdArgs, expectedRole)

View File

@ -149,13 +149,14 @@ type etcdProcessClusterConfig struct {
snapCount int // default is 10000
clientTLS clientConnType
isPeerTLS bool
isPeerAutoTLS bool
isClientAutoTLS bool
forceNewCluster bool
initialToken string
quotaBackendBytes int64
clientTLS clientConnType
clientCertAuthEnabled bool
isPeerTLS bool
isPeerAutoTLS bool
isClientAutoTLS bool
forceNewCluster bool
initialToken string
quotaBackendBytes int64
}
// newEtcdProcessCluster launches a new cluster from etcd processes, returning
@ -325,6 +326,10 @@ func (cfg *etcdProcessClusterConfig) tlsArgs() (args []string) {
"--ca-file", caPath,
}
args = append(args, tlsClientArgs...)
if cfg.clientCertAuthEnabled {
args = append(args, "--client-cert-auth")
}
}
}

View File

@ -116,6 +116,7 @@ func StartEtcd(inCfg *Config) (e *Etcd, err error) {
QuotaBackendBytes: cfg.QuotaBackendBytes,
StrictReconfigCheck: cfg.StrictReconfigCheck,
EnablePprof: cfg.EnablePprof,
ClientCertAuthEnabled: cfg.ClientTLSInfo.ClientCertAuth,
}
if e.Server, err = etcdserver.NewServer(srvcfg); err != nil {

View File

@ -65,11 +65,12 @@ func NewClientHandler(server *etcdserver.EtcdServer, timeout time.Duration) http
sec := auth.NewStore(server, timeout)
kh := &keysHandler{
sec: sec,
server: server,
cluster: server.Cluster(),
timer: server,
timeout: timeout,
sec: sec,
server: server,
cluster: server.Cluster(),
timer: server,
timeout: timeout,
clientCertAuthEnabled: server.Cfg.ClientCertAuthEnabled,
}
sh := &statsHandler{
@ -82,6 +83,7 @@ func NewClientHandler(server *etcdserver.EtcdServer, timeout time.Duration) http
cluster: server.Cluster(),
timeout: timeout,
clock: clockwork.NewRealClock(),
clientCertAuthEnabled: server.Cfg.ClientCertAuthEnabled,
}
dmh := &deprecatedMachinesHandler{
@ -89,8 +91,9 @@ func NewClientHandler(server *etcdserver.EtcdServer, timeout time.Duration) http
}
sech := &authHandler{
sec: sec,
cluster: server.Cluster(),
sec: sec,
cluster: server.Cluster(),
clientCertAuthEnabled: server.Cfg.ClientCertAuthEnabled,
}
mux := http.NewServeMux()
@ -132,11 +135,12 @@ func NewClientHandler(server *etcdserver.EtcdServer, timeout time.Duration) http
}
type keysHandler struct {
sec auth.Store
server etcdserver.Server
cluster api.Cluster
timer etcdserver.RaftTimer
timeout time.Duration
sec auth.Store
server etcdserver.Server
cluster api.Cluster
timer etcdserver.RaftTimer
timeout time.Duration
clientCertAuthEnabled bool
}
func (h *keysHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@ -156,7 +160,7 @@ func (h *keysHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
// The path must be valid at this point (we've parsed the request successfully).
if !hasKeyPrefixAccess(h.sec, r, r.URL.Path[len(keysPrefix):], rr.Recursive) {
if !hasKeyPrefixAccess(h.sec, r, r.URL.Path[len(keysPrefix):], rr.Recursive, h.clientCertAuthEnabled) {
writeKeyNoAuth(w)
return
}
@ -199,18 +203,19 @@ func (h *deprecatedMachinesHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
}
type membersHandler struct {
sec auth.Store
server etcdserver.Server
cluster api.Cluster
timeout time.Duration
clock clockwork.Clock
sec auth.Store
server etcdserver.Server
cluster api.Cluster
timeout time.Duration
clock clockwork.Clock
clientCertAuthEnabled bool
}
func (h *membersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET", "POST", "DELETE", "PUT") {
return
}
if !hasWriteRootAccess(h.sec, r) {
if !hasWriteRootAccess(h.sec, r, h.clientCertAuthEnabled) {
writeNoAuth(w, r)
return
}

View File

@ -26,18 +26,56 @@ import (
)
type authHandler struct {
sec auth.Store
cluster api.Cluster
sec auth.Store
cluster api.Cluster
clientCertAuthEnabled bool
}
func hasWriteRootAccess(sec auth.Store, r *http.Request) bool {
func hasWriteRootAccess(sec auth.Store, r *http.Request, clientCertAuthEnabled bool) bool {
if r.Method == "GET" || r.Method == "HEAD" {
return true
}
return hasRootAccess(sec, r)
return hasRootAccess(sec, r, clientCertAuthEnabled)
}
func hasRootAccess(sec auth.Store, r *http.Request) bool {
func userFromBasicAuth(sec auth.Store, r *http.Request) *auth.User {
username, password, ok := r.BasicAuth()
if !ok {
plog.Warningf("auth: malformed basic auth encoding")
return nil
}
user, err := sec.GetUser(username)
if err != nil {
return nil
}
ok = sec.CheckPassword(user, password)
if !ok {
plog.Warningf("auth: incorrect password for user: %s", username)
return nil
}
return &user
}
func userFromClientCertificate(sec auth.Store, r *http.Request) *auth.User {
if r.TLS == nil {
return nil
}
for _, chains := range r.TLS.VerifiedChains {
for _, chain := range chains {
plog.Debugf("auth: found common name %s.\n", chain.Subject.CommonName)
user, err := sec.GetUser(chain.Subject.CommonName)
if err == nil {
plog.Debugf("auth: authenticated user %s by cert common name.", user.User)
return &user
}
}
}
return nil
}
func hasRootAccess(sec auth.Store, r *http.Request, clientCertAuthEnabled bool) bool {
if sec == nil {
// No store means no auth available, eg, tests.
return true
@ -45,30 +83,30 @@ func hasRootAccess(sec auth.Store, r *http.Request) bool {
if !sec.AuthEnabled() {
return true
}
username, password, ok := r.BasicAuth()
if !ok {
return false
}
rootUser, err := sec.GetUser(username)
if err != nil {
return false
var rootUser *auth.User
if r.Header.Get("Authorization") == "" && clientCertAuthEnabled {
rootUser = userFromClientCertificate(sec, r)
if rootUser == nil {
return false
}
} else {
rootUser = userFromBasicAuth(sec, r)
if rootUser == nil {
return false
}
}
ok = sec.CheckPassword(rootUser, password)
if !ok {
plog.Warningf("auth: wrong password for user %s", username)
return false
}
for _, role := range rootUser.Roles {
if role == auth.RootRoleName {
return true
}
}
plog.Warningf("auth: user %s does not have the %s role for resource %s.", username, auth.RootRoleName, r.URL.Path)
plog.Warningf("auth: user %s does not have the %s role for resource %s.", rootUser.User, auth.RootRoleName, r.URL.Path)
return false
}
func hasKeyPrefixAccess(sec auth.Store, r *http.Request, key string, recursive bool) bool {
func hasKeyPrefixAccess(sec auth.Store, r *http.Request, key string, recursive, clientCertAuthEnabled bool) bool {
if sec == nil {
// No store means no auth available, eg, tests.
return true
@ -76,25 +114,21 @@ func hasKeyPrefixAccess(sec auth.Store, r *http.Request, key string, recursive b
if !sec.AuthEnabled() {
return true
}
if r.Header.Get("Authorization") == "" {
plog.Warningf("auth: no authorization provided, checking guest access")
return hasGuestAccess(sec, r, key)
}
username, password, ok := r.BasicAuth()
if !ok {
plog.Warningf("auth: malformed basic auth encoding")
return false
}
user, err := sec.GetUser(username)
if err != nil {
plog.Warningf("auth: no such user: %s.", username)
return false
}
authAsUser := sec.CheckPassword(user, password)
if !authAsUser {
plog.Warningf("auth: incorrect password for user: %s.", username)
return false
var user *auth.User
if r.Header.Get("Authorization") == "" && clientCertAuthEnabled {
user = userFromClientCertificate(sec, r)
if user == nil {
plog.Warningf("auth: no authorization provided, checking guest access")
return hasGuestAccess(sec, r, key)
}
} else {
user = userFromBasicAuth(sec, r)
if user == nil {
return false
}
}
writeAccess := r.Method != "GET" && r.Method != "HEAD"
for _, roleName := range user.Roles {
role, err := sec.GetRole(roleName)
@ -109,7 +143,7 @@ func hasKeyPrefixAccess(sec auth.Store, r *http.Request, key string, recursive b
return true
}
}
plog.Warningf("auth: invalid access for user %s on key %s.", username, key)
plog.Warningf("auth: invalid access for user %s on key %s.", user.User, key)
return false
}
@ -145,7 +179,7 @@ func (sh *authHandler) baseRoles(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET") {
return
}
if !hasRootAccess(sh.sec, r) {
if !hasRootAccess(sh.sec, r, sh.clientCertAuthEnabled) {
writeNoAuth(w, r)
return
}
@ -209,7 +243,7 @@ func (sh *authHandler) forRole(w http.ResponseWriter, r *http.Request, role stri
if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") {
return
}
if !hasRootAccess(sh.sec, r) {
if !hasRootAccess(sh.sec, r, sh.clientCertAuthEnabled) {
writeNoAuth(w, r)
return
}
@ -293,7 +327,7 @@ func (sh *authHandler) baseUsers(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET") {
return
}
if !hasRootAccess(sh.sec, r) {
if !hasRootAccess(sh.sec, r, sh.clientCertAuthEnabled) {
writeNoAuth(w, r)
return
}
@ -365,7 +399,7 @@ func (sh *authHandler) forUser(w http.ResponseWriter, r *http.Request, user stri
if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") {
return
}
if !hasRootAccess(sh.sec, r) {
if !hasRootAccess(sh.sec, r, sh.clientCertAuthEnabled) {
writeNoAuth(w, r)
return
}
@ -478,7 +512,7 @@ func (sh *authHandler) enableDisable(w http.ResponseWriter, r *http.Request) {
if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") {
return
}
if !hasWriteRootAccess(sh.sec, r) {
if !hasWriteRootAccess(sh.sec, r, sh.clientCertAuthEnabled) {
writeNoAuth(w, r)
return
}

View File

@ -15,9 +15,13 @@
package v2http
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
@ -440,6 +444,32 @@ func mustAuthRequest(method, username, password string) *http.Request {
return req
}
func unauthedRequest(method string) *http.Request {
req, err := http.NewRequest(method, "path", strings.NewReader(""))
if err != nil {
panic("Cannot make request: " + err.Error())
}
return req
}
func tlsAuthedRequest(req *http.Request, certname string) *http.Request {
bytes, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.pem", certname))
if err != nil {
panic(err)
}
block, _ := pem.Decode(bytes)
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
panic(err)
}
req.TLS = &tls.ConnectionState{
VerifiedChains: [][]*x509.Certificate{{cert}},
}
return req
}
func TestPrefixAccess(t *testing.T) {
var table = []struct {
key string
@ -690,14 +720,161 @@ func TestPrefixAccess(t *testing.T) {
}
for i, tt := range table {
if tt.hasRoot != hasRootAccess(tt.store, tt.req) {
if tt.hasRoot != hasRootAccess(tt.store, tt.req, true) {
t.Errorf("#%d: hasRoot doesn't match (expected %v)", i, tt.hasRoot)
}
if tt.hasKeyPrefixAccess != hasKeyPrefixAccess(tt.store, tt.req, tt.key, false) {
if tt.hasKeyPrefixAccess != hasKeyPrefixAccess(tt.store, tt.req, tt.key, false, true) {
t.Errorf("#%d: hasKeyPrefixAccess doesn't match (expected %v)", i, tt.hasRoot)
}
if tt.hasRecursiveAccess != hasKeyPrefixAccess(tt.store, tt.req, tt.key, true) {
if tt.hasRecursiveAccess != hasKeyPrefixAccess(tt.store, tt.req, tt.key, true, true) {
t.Errorf("#%d: hasRecursiveAccess doesn't match (expected %v)", i, tt.hasRoot)
}
}
}
func TestUserFromClientCertificate(t *testing.T) {
witherror := &mockAuthStore{
users: map[string]*auth.User{
"user": {
User: "user",
Roles: []string{"root"},
Password: "password",
},
"basicauth": {
User: "basicauth",
Roles: []string{"root"},
Password: "password",
},
},
roles: map[string]*auth.Role{
"root": {
Role: "root",
},
},
err: errors.New(""),
}
noerror := &mockAuthStore{
users: map[string]*auth.User{
"user": {
User: "user",
Roles: []string{"root"},
Password: "password",
},
"basicauth": {
User: "basicauth",
Roles: []string{"root"},
Password: "password",
},
},
roles: map[string]*auth.Role{
"root": {
Role: "root",
},
},
}
var table = []struct {
req *http.Request
userExists bool
store auth.Store
username string
}{
{
// non tls request
req: unauthedRequest("GET"),
userExists: false,
store: witherror,
},
{
// cert with cn of existing user
req: tlsAuthedRequest(unauthedRequest("GET"), "user"),
userExists: true,
username: "user",
store: noerror,
},
{
// cert with cn of non-existing user
req: tlsAuthedRequest(unauthedRequest("GET"), "otheruser"),
userExists: false,
store: witherror,
},
}
for i, tt := range table {
user := userFromClientCertificate(tt.store, tt.req)
userExists := user != nil
if tt.userExists != userExists {
t.Errorf("#%d: userFromClientCertificate doesn't match (expected %v)", i, tt.userExists)
}
if user != nil && (tt.username != user.User) {
t.Errorf("#%d: userFromClientCertificate username doesn't match (expected %s, got %s)", i, tt.username, user.User)
}
}
}
func TestUserFromBasicAuth(t *testing.T) {
sec := &mockAuthStore{
users: map[string]*auth.User{
"user": {
User: "user",
Roles: []string{"root"},
Password: "password",
},
},
roles: map[string]*auth.Role{
"root": {
Role: "root",
},
},
}
var table = []struct {
username string
req *http.Request
userExists bool
}{
{
// valid user, valid pass
username: "user",
req: mustAuthRequest("GET", "user", "password"),
userExists: true,
},
{
// valid user, bad pass
username: "user",
req: mustAuthRequest("GET", "user", "badpass"),
userExists: false,
},
{
// valid user, no pass
username: "user",
req: mustAuthRequest("GET", "user", ""),
userExists: false,
},
{
// missing user
username: "missing",
req: mustAuthRequest("GET", "missing", "badpass"),
userExists: false,
},
{
// no basic auth
req: unauthedRequest("GET"),
userExists: false,
},
}
for i, tt := range table {
user := userFromBasicAuth(sec, tt.req)
userExists := user != nil
if tt.userExists != userExists {
t.Errorf("#%d: userFromBasicAuth doesn't match (expected %v)", i, tt.userExists)
}
if user != nil && (tt.username != user.User) {
t.Errorf("#%d: userFromBasicAuth username doesn't match (expected %s, got %s)", i, tt.username, user.User)
}
}
}

19
etcdserver/api/v2http/testdata/ca.pem vendored Normal file
View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDEjCCAfqgAwIBAgIIYpX+8HgWGfkwDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE
AxMKZXRjZCB0ZXN0czAeFw0xNTExMjQwMzA1MDBaFw0yMDExMjIwMzA1MDBaMBUx
EzARBgNVBAMTCmV0Y2QgdGVzdHMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQDa9PkwEwiBD8mB+VIKz5r5gRHnNF4Icj6T6R/RsdatecQe6vU0EU4FXtKZ
drWnCGlATyrQooqHpb+rDc7CUt3mXrIxrNkcGTMaesF7P0GWxVkyOGSjJMxGBv3e
bAZknBe4eLMi68L1aT/uYmxcp/B3L2mfdFtc1Gd6mYJpNm1PgilRyIrO0mY5ysIX
4WHCa3yudAv8HrFbQcw7l7OyKA6uSWg6h07lE3d5jw5YOly+hz0iaRtzhb4tJrYD
Lm1tehb0nnoLuW6yYblRSoyBVDT50MFVlyvW40Po5WkOXw/wnsnyxWRR4yqU23wq
quQU0HXJEBLFnT+KbLOQ0EAE35vXAgMBAAGjZjBkMA4GA1UdDwEB/wQEAwIBBjAS
BgNVHRMBAf8ECDAGAQH/AgECMB0GA1UdDgQWBBSbUCGB95ochDrbEZlzGGYuA7xu
xjAfBgNVHSMEGDAWgBSbUCGB95ochDrbEZlzGGYuA7xuxjANBgkqhkiG9w0BAQsF
AAOCAQEAardO/SGCu7Snz3YRBUinzpZEUFTFend+FJtBkxBXCao1RvTXg8PBMkza
LUsaR4mLsGoXLIbNCoIinvVG0QULYCZe11N3l1L0G2g5uhEM4MfJ2rwrMD0o17i+
nwNRRE3tfKAlWhYQg+4ye36kQVxASPniHjdQgjKYUFTNXdyG6DzuAclaVte9iVw6
cWl61fB2CZya3+uMtih8t/Kgl2KbMO2PvNByfnDjKmW+v58qHbXyoJZqnpvDn14+
p2Ox+AvvxYiEiUIvFdWy101QB7NJMCtdwq6oG6OvIOgXzLgitTFSq4kfWDfupQjW
iFoQ+vWmYhK5ld0nBaiz+JmHuemK7A==
-----END CERTIFICATE-----

View File

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDOTCCAiGgAwIBAgIINYpsso1f3SswDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE
AxMKZXRjZCB0ZXN0czAeFw0xNTExMjQwMzA4MDBaFw0xNjExMjMwMzA4MDBaMBQx
EjAQBgNVBAMTCW90aGVydXNlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBAPOAUa5GblwIjHTEnox2c/Am9jV1TMvzBuVXxnp2UnNHMNwstAooFrEs/Z+d
ft5AOsooP6zVuM3eBQa4i9huJbVNDfPU2H94yA89jYfJYUgo7C838V6NjGsCCptQ
WzkKPNlDbT9xA/7XpIUJ2WltuYDRrjWq8pXQONqTjcg5n4l0JO8xdHJHRUkFQ76F
1npXeLndgGaP11lqzpYlglEGi5URhzAT1xxQ0hLSe8WNmiCxxkq++C8Gx4sPg9mX
M94aoJDzZSnoaqDxckbP/7Q0ZKe/fVdCFkd5+jqT4Mt7hwmz9jTCHcVnAz4EKI+t
rbWgbCfMK6013GotXz7InStVe+MCAwEAAaOBjTCBijAOBgNVHQ8BAf8EBAMCBaAw
HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYD
VR0OBBYEFFwMmf+pnaejmri6y1T+lfU+MBq/MB8GA1UdIwQYMBaAFJtQIYH3mhyE
OtsRmXMYZi4DvG7GMAsGA1UdEQQEMAKCADANBgkqhkiG9w0BAQsFAAOCAQEACOn6
mec29MTMGPt/EPOmSyhvTKSwH+5YWjCbyUFeoB8puxrJlIphK4mvT+sXp2wzno89
FVCliO/rJurdErKvyOjlK1QrVGPYIt7Wz9ssAfvlwCyBM8PqgEG8dJN9aAkf2h4r
Ye+hBh1y6Nnataf7lxe9mqAOvD/7wVIgzjCnMD1q5QSY2Mln3HwVQXtbZFbY363Z
X9Fk3PUpjJSX9jbEz9kIlT8AJAdxl6GB8Z9B8PrA8qf4Bhk15ICRHxb67EhDrGWV
8q7ArU2XBqs/+GWpUIMoGKNZv+K+/SksZK1KnzaUvApUCJzt+ac+p8HOgMdvDRgr
GfVVJqcZgyEmeczy0A==
-----END CERTIFICATE-----

20
etcdserver/api/v2http/testdata/user.pem vendored Normal file
View File

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDNDCCAhygAwIBAgIIcQ0DAfgevocwDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE
AxMKZXRjZCB0ZXN0czAeFw0xNTExMjQwMzA4MDBaFw0xNjExMjMwMzA4MDBaMA8x
DTALBgNVBAMTBHVzZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD0
+3Lm1SmUJJLufaFTYz+e5qyQEshNRyeAhXIeZ1aw+yBjslXGZQ3/uGOwnOnGqUeA
Nidc9ty4NsK6RVppHlezUrBnpl4hws8vHWFKZpU2R6kKL8EYLmg+iVqEBj7XqfAp
8bJqqZI3KOqLXpRH55mA69KP7VEK9ngTVR/tERSrUPT8jcjwbvhSOqD8Qk07BUDR
6RpDr94Mnaf+fMGG36Sh7iUl+i4Oh6FFar+7+b0+5Bhs2/6uVsK4A1Z3jqqfSQH8
q8Wf5h9Ka4aqGSw4ia5G3Uw7Jsl2aDgpJ7uwJo1k8SclbMYnYdhZuo+U+esY/Fai
YdbjG+AroZ+y9TB8bMlHAgMBAAGjgY0wgYowDgYDVR0PAQH/BAQDAgWgMB0GA1Ud
JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQW
BBRuTt0lJIVKYaz76aSxl/MQOLRwfDAfBgNVHSMEGDAWgBSbUCGB95ochDrbEZlz
GGYuA7xuxjALBgNVHREEBDACggAwDQYJKoZIhvcNAQELBQADggEBABLRWZm+Lgjs
c5qDXbgOJW2pR630syY8ixR9c6HvzPVJim8mFioMX+xrlbOC6BmOUlOb9j83bTKn
aOg/0xlpxNbd8QYzgRxZmHZLULPdiNeeRvIzsrzrH88+inrmZhRXRVcHjdO6CG6t
hCdDdRiNU6GkF7dPna0xNcEOKe2wUfzd1ZtKOqzi1w+fKjSeMplZomeWgP4WRvkh
JJ/0ujlMMckgyTxRh8EEaJ35OnpXX7EdipoWhOMmiUnlPqye2icC8Y+CMdZsrod6
nkoEQnXDCLf/Iv0qj7B9iKbxn7t3QDVxY4UILUReDuD8yrGULlGOl//aY/T3pkZ6
R5trduZhI3o=
-----END CERTIFICATE-----

View File

@ -56,6 +56,9 @@ type ServerConfig struct {
StrictReconfigCheck bool
EnablePprof bool
// ClientCertAuthEnabled is true when cert has been signed by the client CA.
ClientCertAuthEnabled bool
}
// VerifyBootstrap sanity-checks the initial config for bootstrap case