Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sshcert auth #245

Merged
merged 11 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ name: "CodeQL"

on:
push:
branches: [ master ]
branches: [ 'master', 'develop' ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
branches: [ 'master', 'develop' ]
schedule:
- cron: '32 14 * * 5'

Expand Down
81 changes: 62 additions & 19 deletions cmd/keymasterd/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import (
"github.com/Cloud-Foundations/tricorder/go/tricorder"
"github.com/Cloud-Foundations/tricorder/go/tricorder/units"
"github.com/cloudflare/cfssl/revoke"
"github.com/cviecco/webauth-sshcert/lib/server/sshcertauth"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😢

"github.com/duo-labs/webauthn/webauthn"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
Expand All @@ -76,6 +77,7 @@ const (
AuthTypeKeymasterX509
AuthTypeWebauthForCLI
AuthTypeFIDO2
AuthTypeKeymasterSSHCert
)

const (
Expand Down Expand Up @@ -220,6 +222,13 @@ type RuntimeState struct {
webAuthn *webauthn.WebAuthn
totpLocalRateLimit map[string]totpRateLimitInfo
totpLocalTateLimitMutex sync.Mutex
sshCertAuthenticator *sshcertauth.Authenticator
serviceMux *http.ServeMux
serviceServer *http.Server
adminServer *http.Server
serviceAccessLogger *serverlogger.Logger
adminAccessLogger *serverlogger.Logger
adminDashboard *adminDashboardType
logger log.DebugLogger
}

Expand Down Expand Up @@ -716,14 +725,19 @@ func (state *RuntimeState) sendFailureToClientIfLocked(w http.ResponseWriter, r

func (state *RuntimeState) setNewAuthCookie(w http.ResponseWriter,
username string, authlevel int) (string, error) {
expiration := time.Now().Add(time.Duration(maxAgeSecondsAuthCookie) *
time.Second)
return state.setNewAuthCookieWithExpiration(w, username, authlevel, expiration)
}

func (state *RuntimeState) setNewAuthCookieWithExpiration(w http.ResponseWriter,
username string, authlevel int, expiration time.Time) (string, error) {
cookieVal, err := state.genNewSerializedAuthJWT(username, authlevel,
maxAgeSecondsAuthCookie)
if err != nil {
logger.Println(err)
return "", err
}
expiration := time.Now().Add(time.Duration(maxAgeSecondsAuthCookie) *
time.Second)
authCookie := http.Cookie{
Name: authCookieName,
Value: cookieVal,
Expand Down Expand Up @@ -1818,28 +1832,38 @@ func main() {
os.Exit(1)
}
logger.Debugf(3, "After load verify")
startServerAfterLoad(runtimeState, realLogger)
logger.Debugf(3, "After server initbase")
err = startListenersAndWaitForUnsealing(runtimeState)
if err != nil {
panic(err)
}
}

func startServerAfterLoad(runtimeState *RuntimeState, realLogger *serverlogger.Logger) {
var err error

publicLogs := runtimeState.Config.Base.PublicLogs
adminDashboard := newAdminDashboard(realLogger, publicLogs)
runtimeState.adminDashboard = newAdminDashboard(realLogger, publicLogs)

logBufOptions := logbuf.GetStandardOptions()
accessLogDirectory := filepath.Join(logBufOptions.Directory, "access")
logger.Debugf(1, "accesslogdir=%s\n", accessLogDirectory)
serviceAccessLogger := serverlogger.NewWithOptions("access",
runtimeState.serviceAccessLogger = serverlogger.NewWithOptions("access",
logbuf.Options{MaxFileSize: 10 << 20,
Quota: 100 << 20, MaxBufferLines: 100,
Directory: accessLogDirectory},
stdlog.LstdFlags)

adminAccesLogDirectory := filepath.Join(logBufOptions.Directory, "access-admin")
adminAccessLogger := serverlogger.NewWithOptions("access-admin",
runtimeState.adminAccessLogger = serverlogger.NewWithOptions("access-admin",
logbuf.Options{MaxFileSize: 10 << 20,
Quota: 100 << 20, MaxBufferLines: 100,
Directory: adminAccesLogDirectory},
stdlog.LstdFlags)

// Expose the registered metrics via HTTP.
http.Handle("/", adminDashboard)
http.Handle("/", runtimeState.adminDashboard)
http.Handle("/prometheus_metrics", promhttp.Handler()) //lint:ignore SA1019 TODO: newer prometheus handler
http.HandleFunc(secretInjectorPath, runtimeState.secretInjectorHandler)
http.HandleFunc(readyzPath, runtimeState.readyzHandler)
Expand Down Expand Up @@ -1937,7 +1961,20 @@ func main() {
serviceMux.HandleFunc(paths.VerifyAuthToken,
runtimeState.VerifyAuthTokenHandler)
}
// TODO: only enable these handlers if sshcertauth is enabled
cviecco marked this conversation as resolved.
Show resolved Hide resolved
if runtimeState.isSelfSSHCertAuthenticatorEnabled() {
serviceMux.HandleFunc(sshcertauth.DefaultCreateChallengePath,
runtimeState.sshCertAuthCreateChallengeHandler)
serviceMux.HandleFunc(sshcertauth.DefaultLoginWithChallengePath,
runtimeState.sshCertAuthLoginWithChallengeHandler)
}
serviceMux.HandleFunc("/", runtimeState.defaultPathHandler)
runtimeState.serviceMux = serviceMux
}

func startListenersAndWaitForUnsealing(runtimeState *RuntimeState) error {
var err error
publicLogs := runtimeState.Config.Base.PublicLogs

cfg := &tls.Config{
ClientCAs: runtimeState.ClientCAPool,
Expand All @@ -1957,9 +1994,9 @@ func main() {
}
logFilterHandler := NewLogFilterHandler(http.DefaultServeMux, publicLogs,
runtimeState)
serviceHTTPLogger := httpLogger{AccessLogger: serviceAccessLogger}
adminHTTPLogger := httpLogger{AccessLogger: adminAccessLogger}
adminSrv := &http.Server{
serviceHTTPLogger := httpLogger{AccessLogger: runtimeState.serviceAccessLogger}
adminHTTPLogger := httpLogger{AccessLogger: runtimeState.adminAccessLogger}
runtimeState.adminServer = &http.Server{
Addr: runtimeState.Config.Base.AdminAddress,
TLSConfig: cfg,
Handler: instrumentedwriter.NewLoggingHandler(logFilterHandler, adminHTTPLogger),
Expand All @@ -1971,8 +2008,8 @@ func main() {
&tls.Config{ClientCAs: runtimeState.ClientCAPool, MinVersion: tls.VersionTLS12},
true)
go func() {
err := adminSrv.ListenAndServeTLS("", "")
if err != nil {
err := runtimeState.adminServer.ListenAndServeTLS("", "")
if err != nil && err != http.ErrServerClosed {
panic(err)
}

Expand All @@ -1985,13 +2022,18 @@ func main() {
}
isReady := <-runtimeState.SignerIsReady
if isReady != true {
panic("got bad signer ready data")
return fmt.Errorf("got bad signer ready data")
}

err = runtimeState.initialzeSelfSSHCertAuthenticator()
if err != nil {
return fmt.Errorf("cannot inialize ssh identities for certauth %s", err)
}

if len(runtimeState.Config.Ldap.LDAPTargetURLs) > 0 && !runtimeState.Config.Ldap.DisablePasswordCache {
err = runtimeState.passwordChecker.UpdateStorage(runtimeState)
if err != nil {
logger.Fatalf("Cannot update password checker")
return fmt.Errorf("Cannot update password checker %s", err)
}
}
if runtimeState.ClientCAPool == nil {
Expand All @@ -2000,7 +2042,7 @@ func main() {
for _, derCert := range runtimeState.caCertDer {
myCert, err := x509.ParseCertificate(derCert)
if err != nil {
panic(err)
return err
}
runtimeState.ClientCAPool.AddCert(myCert)
}
Expand All @@ -2024,9 +2066,9 @@ func main() {
tls.TLS_AES_256_GCM_SHA384,
},
}
serviceSrv := &http.Server{
runtimeState.serviceServer = &http.Server{
Addr: runtimeState.Config.Base.HttpAddress,
Handler: instrumentedwriter.NewLoggingHandler(serviceMux, serviceHTTPLogger),
Handler: instrumentedwriter.NewLoggingHandler(runtimeState.serviceMux, serviceHTTPLogger),
TLSConfig: serviceTLSConfig,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
Expand All @@ -2037,10 +2079,11 @@ func main() {
go func() {
time.Sleep(time.Millisecond * 10)
healthserver.SetReady()
adminDashboard.setReady()
runtimeState.adminDashboard.setReady()
}()
err = serviceSrv.ListenAndServeTLS("", "")
if err != nil {
err = runtimeState.serviceServer.ListenAndServeTLS("", "")
if err != nil && err != http.ErrServerClosed {
panic(err)
}
return err
}
13 changes: 9 additions & 4 deletions cmd/keymasterd/auth_oauth2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,16 @@ func init() {
//logger = stdlog.New(os.Stderr, "", stdlog.LstdFlags)
slogger := stdlog.New(os.Stderr, "", stdlog.LstdFlags)
logger = debuglogger.New(slogger)
http.HandleFunc("/userinfo", userinfoHandler)
http.HandleFunc("/token", tokenHandler)
http.HandleFunc("/", handler)
testMux := http.NewServeMux()
testMux.HandleFunc("/userinfo", userinfoHandler)
testMux.HandleFunc("/token", tokenHandler)
testMux.HandleFunc("/", handler)
testServer := http.Server{
Handler: testMux,
Addr: "127.0.0.1:12345",
}
logger.Printf("about to start server")
go http.ListenAndServe("127.0.0.1:12345", nil)
go testServer.ListenAndServe()
time.Sleep(20 * time.Millisecond)
_, err := http.Get("http://localhost:12345")
if err != nil {
Expand Down
76 changes: 76 additions & 0 deletions cmd/keymasterd/auth_sshcert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package main

import (
"encoding/json"
"net/http"

"github.com/Cloud-Foundations/keymaster/lib/webapi/v0/proto"
"golang.org/x/crypto/ssh"
)

// This function can only be called after all known keymaster public keys
// have been loaded, that is, after the server is ready
func (state *RuntimeState) initialzeSelfSSHCertAuthenticator() error {

// build ssh pubkey list
var sshTrustedKeys []string
for _, pubkey := range state.KeymasterPublicKeys {
sshPubkey, err := ssh.NewPublicKey(pubkey)
if err != nil {
return err
}
authorizedKey := ssh.MarshalAuthorizedKey(sshPubkey)
sshTrustedKeys = append(sshTrustedKeys, string(authorizedKey))
}
return state.sshCertAuthenticator.UnsafeUpdateCaKeys(sshTrustedKeys)
}

func (state *RuntimeState) isSelfSSHCertAuthenticatorEnabled() bool {
for _, certPref := range state.Config.Base.AllowedAuthBackendsForCerts {
if certPref == proto.AuthTypeSSHCert {
return true
}
}
return false
}

// CreateChallengeHandler is an example of how to write a handler for
// the path to create the challenge
func (s *RuntimeState) sshCertAuthCreateChallengeHandler(w http.ResponseWriter, r *http.Request) {
// TODO: add some rate limiting
err := s.sshCertAuthenticator.CreateChallengeHandler(w, r)
if err != nil {
// we are assuming bad request
s.logger.Debugf(1,
"CreateSSHCertAuthChallengeHandler: there was an err computing challenge: %s", err)
s.writeFailureResponse(w, r, http.StatusBadRequest, "Invalid Operation")
return
}
}

func (s *RuntimeState) sshCertAuthLoginWithChallengeHandler(w http.ResponseWriter, r *http.Request) {
username, expiration, userErrString, err := s.sshCertAuthenticator.LoginWithChallenge(r)
if err != nil {
s.logger.Printf("error=%s", err)
errorCode := http.StatusBadRequest
if userErrString == "" {
errorCode = http.StatusInternalServerError
}
s.writeFailureResponse(w, r, errorCode, userErrString)
return
}
// Make new auth cookie
_, err = s.setNewAuthCookieWithExpiration(w, username, AuthTypeKeymasterSSHCert, expiration)
if err != nil {
s.writeFailureResponse(w, r, http.StatusInternalServerError,
"error internal")
s.logger.Println(err)
return
}

// TODO: The cert backend should depend also on per user preferences.
loginResponse := proto.LoginResponse{Message: "success"}
// TODO needs eventnotifier?
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(loginResponse)
}
82 changes: 82 additions & 0 deletions cmd/keymasterd/auth_sshcert_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package main

import (
"net/http"
"os"
"testing"

"github.com/Cloud-Foundations/Dominator/lib/log/serverlogger"
"github.com/Cloud-Foundations/keymaster/lib/webapi/v0/proto"
"github.com/cviecco/webauth-sshcert/lib/server/sshcertauth"
)

func TestInitializeSSHAuthenticator(t *testing.T) {
state, passwdFile, err := setupValidRuntimeStateSigner(t)
if err != nil {
t.Fatal(err)
}
defer os.Remove(passwdFile.Name()) // clean up
state.sshCertAuthenticator = sshcertauth.NewAuthenticator(
[]string{state.HostIdentity}, []string{})
err = state.initialzeSelfSSHCertAuthenticator()
if err != nil {
t.Fatal(err)
}
}

func TestIsSelfSSHCertAuthenticatorEnabled(t *testing.T) {
state := RuntimeState{}
if state.isSelfSSHCertAuthenticatorEnabled() {
t.Fatal("it should not be enabled on empty state")
}
state.Config.Base.AllowedAuthBackendsForCerts = append(state.Config.Base.AllowedAuthBackendsForCerts, proto.AuthTypeSSHCert)
if !state.isSelfSSHCertAuthenticatorEnabled() {
t.Fatal("it should be enabled on empty state")
}
}

func TestSshCertAuthCreateChallengeHandlert(t *testing.T) {
state, passwdFile, err := setupValidRuntimeStateSigner(t)
if err != nil {
t.Fatal(err)
}
defer os.Remove(passwdFile.Name()) // clean up
state.sshCertAuthenticator = sshcertauth.NewAuthenticator(
[]string{state.HostIdentity}, []string{})
err = state.initialzeSelfSSHCertAuthenticator()
if err != nil {
t.Fatal(err)
}
// make call with bad data
//initially the request should fail for lack of preconditions
req, err := http.NewRequest("POST", redirectPath, nil)
if err != nil {
t.Fatal(err)
}
// oath2 config is invalid
_, err = checkRequestHandlerCode(req, state.sshCertAuthCreateChallengeHandler, http.StatusBadRequest)
if err != nil {
t.Fatal(err)
}
// simulaet good call, ignore result for now
goodURL := "foobar?nonce1=12345678901234567890123456789"
// TODO: replce this for a post
req2, err := http.NewRequest("GET", goodURL, nil)
_, err = checkRequestHandlerCode(req2, state.sshCertAuthCreateChallengeHandler, http.StatusOK)
if err != nil {
t.Fatal(err)
}
}

func TestSshCertAuthLoginWithChallengeHandler(t *testing.T) {
state, passwdFile, err := setupValidRuntimeStateSigner(t)
if err != nil {
t.Fatal(err)
}
defer os.Remove(passwdFile.Name()) // clean up
state.Config.Base.AllowedAuthBackendsForCerts = append(state.Config.Base.AllowedAuthBackendsForCerts, proto.AuthTypeSSHCert)
realLogger := serverlogger.New("") //TODO, we need to find a simulator for this
startServerAfterLoad(state, realLogger)

//TODO: write the actual test, at this point we only have the endpoints initalized
}
Loading
Loading