Skip to content

Commit

Permalink
Added a Certs authenticator (#2678)
Browse files Browse the repository at this point in the history
This authenticator allows using client certificates for authentication
  • Loading branch information
scudette committed May 21, 2023
1 parent 33af410 commit 5a41a26
Show file tree
Hide file tree
Showing 22 changed files with 1,296 additions and 850 deletions.
23 changes: 23 additions & 0 deletions api/authenticators/auth.go
@@ -1,6 +1,7 @@
package authenticators

import (
"crypto/x509"
"errors"
"fmt"
"net/http"
Expand All @@ -26,6 +27,7 @@ type Authenticator interface {
AuthenticateUserHandler(parent http.Handler) http.Handler

IsPasswordLess() bool
RequireClientCerts() bool
AuthRedirectTemplate() string
}

Expand Down Expand Up @@ -127,6 +129,27 @@ func init() {
}, nil
})

RegisterAuthenticator("certs", func(config_obj *config_proto.Config,
auth_config *config_proto.Authenticator) (Authenticator, error) {
if config_obj.GUI == nil || config_obj.GUI.UsePlainHttp {
return nil, errors.New("'Certs' authenticator must use TLS!")
}

result := &CertAuthenticator{
config_obj: config_obj,
base: utils.GetBasePath(config_obj),
public_url: utils.GetPublicURL(config_obj),
x509_roots: x509.NewCertPool(),
default_roles: auth_config.DefaultRolesForUnknownUser,
}
if config_obj.Client != nil {
result.x509_roots.AppendCertsFromPEM([]byte(
config_obj.Client.CaCertificate))
}

return result, nil
})

RegisterAuthenticator("oidc", func(config_obj *config_proto.Config,
auth_config *config_proto.Authenticator) (Authenticator, error) {
err := configRequirePublicUrl(config_obj)
Expand Down
4 changes: 4 additions & 0 deletions api/authenticators/azure.go
Expand Up @@ -55,6 +55,10 @@ func (self *AzureAuthenticator) IsPasswordLess() bool {
return true
}

func (self *AzureAuthenticator) RequireClientCerts() bool {
return false
}

func (self *AzureAuthenticator) AuthRedirectTemplate() string {
return self.authenticator.AuthRedirectTemplate
}
Expand Down
4 changes: 4 additions & 0 deletions api/authenticators/basic.go
Expand Up @@ -57,6 +57,10 @@ func (self *BasicAuthenticator) IsPasswordLess() bool {
return false
}

func (self *BasicAuthenticator) RequireClientCerts() bool {
return false
}

func (self *BasicAuthenticator) AuthRedirectTemplate() string {
return ""
}
Expand Down
233 changes: 233 additions & 0 deletions api/authenticators/certs.go
@@ -0,0 +1,233 @@
/*
An authenticator that uses client side certificates.
WARNING: This authenticator is considered very experimental!!! There
are serious security considerations when using this so ensure you
understand all the ramifications before using it!
This authenticator makes it possible to use distributed
authentication - if the user has the client certificate they will be
automatically authenticated! This is more risky than centralized
authentication because the security depends on the certificates
themselves.
## How to issue client certificates
This authenticator uses the same certificates that are used in the
Velociraptor api. You can use the `config api_client` command to
generate new client certificates.
velociraptor --config server.config.yaml config api_client --name Mike --pkcs12 mike.pkcs12 Mike.pem -v --password
For convenience you can use the --pkcs12 flag to also save the
certificates in .pkcs12 format which can be imported into the
Windows trust store. It is recommended you use --password to armour
the certificates.
## Configuring the server for client certificates
The server's authenticator can be configured by replacing the Basic
authenticator with the `Certs` authenticator under the GUI section.
```
authenticator:
type: Certs
default_roles_for_unknown_user:
- reader
- administrator
```
You can specify roles under `default_roles_for_unknown_user` which
allows the server to automatically create user accounts with these
roles when a client certificate is presented for an unknown
users. Be careful with this setting as it might allow anyone with a
valid signed certificate (even an API certificate) to elevate to
administrator. It is recommended the API *not* be used when using
this feature.
## How can I revoke a certificate?
Currently certificates can not be revoked. Instead all the ACLs can
be removed from the user account which mean that user has no
access. You can not safely reuse the same user name once these
permissions are removed.
## Caveats
It is not possible for clients to present an TLS client certificate
because they dont have one. Therefore the Frontend (the service
connecting to clients) can not require client certificates. Since
TLS requires client certificates *before* the HTTP headers it is
currently impossible to require client certificates **only** for the
GUI and not the frontend if they share the same port!!!
This means that client certifacts do not work with using autocert
(in that case both frontend and GUI share the same port due to
limitations in the Let's Encrypt protocol).
The server will refuse to start when the frontend is forced to use
client certificates.
*/

package authenticators

import (
"context"
"crypto/x509"
"errors"
"fmt"
"net/http"
"time"

"github.com/Velocidex/ordereddict"
"github.com/gorilla/csrf"
acl_proto "www.velocidex.com/golang/velociraptor/acls/proto"
api_proto "www.velocidex.com/golang/velociraptor/api/proto"
utils "www.velocidex.com/golang/velociraptor/api/utils"
config_proto "www.velocidex.com/golang/velociraptor/config/proto"
"www.velocidex.com/golang/velociraptor/constants"
"www.velocidex.com/golang/velociraptor/json"
"www.velocidex.com/golang/velociraptor/services"
"www.velocidex.com/golang/velociraptor/users"
)

var (
invalidCertError = errors.New("Invalid Client Certificate")
)

// Certificate based authenticator.
type CertAuthenticator struct {
config_obj *config_proto.Config
base, public_url string

x509_roots *x509.CertPool
default_roles []string
}

// Cert auth does not need any special handlers.
func (self *CertAuthenticator) AddHandlers(mux *http.ServeMux) error {
return nil
}

// It is not really possible to log off when using client certs
func (self *CertAuthenticator) AddLogoff(mux *http.ServeMux) error {
mux.Handle(utils.Join(self.base, "/app/logoff.html"),
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
http.Error(w, "authorization failed", http.StatusUnauthorized)
return
}))

return nil
}

func (self *CertAuthenticator) IsPasswordLess() bool {
return true
}

func (self *CertAuthenticator) RequireClientCerts() bool {
return true
}

func (self *CertAuthenticator) AuthRedirectTemplate() string {
return ""
}

func (self *CertAuthenticator) getUserNameFromTLSCerts(r *http.Request) (string, error) {
// We only trust certs issued by the Velociraptor CA.
x509_opts := x509.VerifyOptions{
CurrentTime: time.Now(),
Roots: self.x509_roots,
}

for _, cert := range r.TLS.PeerCertificates {
_, err := cert.Verify(x509_opts)
if err != nil {
continue
}
return cert.Subject.CommonName, nil
}
return "", invalidCertError
}

func (self *CertAuthenticator) AuthenticateUserHandler(
parent http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-CSRF-Token", csrf.Token(r))

username, err := self.getUserNameFromTLSCerts(r)
if err != nil {
http.Error(w,
fmt.Sprintf("authorization failed: Client Certificate is not valid: %v", err),
http.StatusUnauthorized)
return
}

users_manager := services.GetUserManager()
user_record, err := users_manager.GetUser(r.Context(), username)
if err != nil {
if err != services.UserNotFoundError || len(self.default_roles) == 0 {
http.Error(w,
fmt.Sprintf("authorization failed: %v", err),
http.StatusUnauthorized)
return
}

// Create a new user role on the fly.
policy := &acl_proto.ApiClientACL{
Roles: self.default_roles,
}
services.LogAudit(r.Context(),
self.config_obj, username, "Automatic User Creation",
ordereddict.NewDict().
Set("roles", self.default_roles).
Set("remote", r.RemoteAddr))

// Use the super user principal to actually add the
// username so we have enough permissions.
err = users.AddUserToOrg(r.Context(), users.AddNewUser,
constants.PinnedServerName, username,
[]string{"root"}, policy)
if err != nil {
http.Error(w,
fmt.Sprintf("authorization failed: automatic user creation: %v", err),
http.StatusUnauthorized)
return
}
}

// Does the user have access to the specified org?
err = CheckOrgAccess(r, user_record)
if err != nil {
services.LogAudit(r.Context(),
self.config_obj, username, "Unauthorized username",
ordereddict.NewDict().
Set("remote", r.RemoteAddr).
Set("status", http.StatusUnauthorized))

http.Error(w,
fmt.Sprintf("authorization failed: %v", err),
http.StatusUnauthorized)
return
}

// Checking is successful - user authorized. Here we
// build a token to pass to the underlying GRPC
// service with metadata about the user.
user_info := &api_proto.VelociraptorUser{
Name: username,
}

// Must use json encoding because grpc can not handle
// binary data in metadata.
serialized, _ := json.Marshal(user_info)
ctx := context.WithValue(
r.Context(), constants.GRPC_USER_CONTEXT, string(serialized))

// Need to call logging after auth so it can access
// the USER value in the context.
GetLoggingHandler(self.config_obj)(parent).ServeHTTP(
w, r.WithContext(ctx))
})
}
4 changes: 4 additions & 0 deletions api/authenticators/github.go
Expand Up @@ -54,6 +54,10 @@ func (self *GitHubAuthenticator) IsPasswordLess() bool {
return true
}

func (self *GitHubAuthenticator) RequireClientCerts() bool {
return false
}

func (self *GitHubAuthenticator) AuthRedirectTemplate() string {
return self.authenticator.AuthRedirectTemplate
}
Expand Down
4 changes: 4 additions & 0 deletions api/authenticators/google.go
Expand Up @@ -88,6 +88,10 @@ func (self *GoogleAuthenticator) IsPasswordLess() bool {
return true
}

func (self *GoogleAuthenticator) RequireClientCerts() bool {
return false
}

func (self *GoogleAuthenticator) AuthRedirectTemplate() string {
return self.authenticator.AuthRedirectTemplate
}
Expand Down
4 changes: 4 additions & 0 deletions api/authenticators/multiple.go
Expand Up @@ -63,6 +63,10 @@ func (self *MultiAuthenticator) IsPasswordLess() bool {
return true
}

func (self *MultiAuthenticator) RequireClientCerts() bool {
return false
}

func (self *MultiAuthenticator) AuthRedirectTemplate() string {
return ""
}
Expand Down
4 changes: 4 additions & 0 deletions api/authenticators/oidc.go
Expand Up @@ -23,6 +23,10 @@ func (self *OidcAuthenticator) IsPasswordLess() bool {
return true
}

func (self *OidcAuthenticator) RequireClientCerts() bool {
return false
}

func (self *OidcAuthenticator) AuthRedirectTemplate() string {
return self.authenticator.AuthRedirectTemplate
}
Expand Down
4 changes: 4 additions & 0 deletions api/authenticators/saml.go
Expand Up @@ -30,6 +30,10 @@ func (self *SamlAuthenticator) IsPasswordLess() bool {
return true
}

func (self *SamlAuthenticator) RequireClientCerts() bool {
return false
}

func (self *SamlAuthenticator) AuthRedirectTemplate() string {
return self.authenticator.AuthRedirectTemplate
}
Expand Down

0 comments on commit 5a41a26

Please sign in to comment.