AUTH-2018: Adds support for authorized keys and short lived certs
This commit is contained in:
parent
df25ed9bde
commit
baec3e289e
|
@ -373,9 +373,12 @@
|
|||
version = "v2.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:04457f9f6f3ffc5fea48e71d62f2ca256637dee0a04d710288e27e05c8b41976"
|
||||
digest = "1:31538f8d774e8bf2fb9380d2d2a82d69e206a7d0603946586926f1819c546c26"
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
packages = [
|
||||
".",
|
||||
"hooks/test",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "839c75faf7f98a33d445d181f3018b5c3409a45e"
|
||||
version = "v1.4.2"
|
||||
|
@ -607,6 +610,7 @@
|
|||
"github.com/prometheus/client_golang/prometheus/promhttp",
|
||||
"github.com/rifflock/lfshook",
|
||||
"github.com/sirupsen/logrus",
|
||||
"github.com/sirupsen/logrus/hooks/test",
|
||||
"github.com/stretchr/testify/assert",
|
||||
"github.com/stretchr/testify/require",
|
||||
"golang.org/x/crypto/nacl/box",
|
||||
|
|
|
@ -338,7 +338,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
|||
logger.Infof("ssh-server set")
|
||||
|
||||
sshServerAddress := "127.0.0.1:" + c.String("local-ssh-port")
|
||||
server, err := sshserver.New(logger, sshServerAddress, shutdownC)
|
||||
server, err := sshserver.New(logger, sshServerAddress, shutdownC, c.Bool("short-lived-certs"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Cannot create new SSH Server")
|
||||
return errors.Wrap(err, "Cannot create new SSH Server")
|
||||
|
@ -915,5 +915,11 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
|||
EnvVars: []string{"LOCAL_SSH_PORT"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "short-lived-certs",
|
||||
Usage: "Enable short lived cert authentication for SSH server",
|
||||
EnvVars: []string{"SHORT_LIVED_CERTS"},
|
||||
Hidden: true,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
//+build !windows
|
||||
|
||||
package sshserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/pkg/errors"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var (
|
||||
systemConfigPath = "/etc/cloudflared/"
|
||||
authorizeKeysPath = ".cloudflared/authorized_keys"
|
||||
)
|
||||
|
||||
func (s *SSHServer) authorizedKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
sshUser, err := s.getUserFunc(ctx.User())
|
||||
if err != nil {
|
||||
s.logger.Debugf("Invalid user: %s", ctx.User())
|
||||
return false
|
||||
}
|
||||
|
||||
authorizedKeysPath := path.Join(sshUser.HomeDir, authorizeKeysPath)
|
||||
if _, err := os.Stat(authorizedKeysPath); os.IsNotExist(err) {
|
||||
s.logger.Debugf("authorized_keys file %s not found", authorizeKeysPath)
|
||||
return false
|
||||
}
|
||||
|
||||
authorizedKeysBytes, err := ioutil.ReadFile(authorizedKeysPath)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Errorf("Failed to load authorized_keys %s", authorizedKeysPath)
|
||||
return false
|
||||
}
|
||||
|
||||
for len(authorizedKeysBytes) > 0 {
|
||||
// Skips invalid keys. Returns error if no valid keys remain.
|
||||
pubKey, _, _, rest, err := ssh.ParseAuthorizedKey(authorizedKeysBytes)
|
||||
authorizedKeysBytes = rest
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Errorf("No valid keys found in %s", authorizeKeysPath)
|
||||
return false
|
||||
}
|
||||
|
||||
if ssh.KeysEqual(pubKey, key) {
|
||||
ctx.SetValue("sshUser", sshUser)
|
||||
return true
|
||||
}
|
||||
}
|
||||
s.logger.Debugf("Matching public key not found in %s", authorizeKeysPath)
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *SSHServer) shortLivedCertHandler(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
userCert, ok := key.(*gossh.Certificate)
|
||||
if !ok {
|
||||
s.logger.Debug("Received key is not an SSH certificate")
|
||||
return false
|
||||
}
|
||||
|
||||
if !ssh.KeysEqual(s.caCert, userCert.SignatureKey) {
|
||||
s.logger.Debug("CA certificate does not match user certificate signer")
|
||||
return false
|
||||
}
|
||||
|
||||
checker := gossh.CertChecker{}
|
||||
if err := checker.CheckCert(ctx.User(), userCert); err != nil {
|
||||
s.logger.Debug(err)
|
||||
return false
|
||||
} else {
|
||||
sshUser, err := s.getUserFunc(ctx.User())
|
||||
if err != nil {
|
||||
s.logger.Debugf("Invalid user: %s", ctx.User())
|
||||
return false
|
||||
}
|
||||
ctx.SetValue("sshUser", sshUser)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getCACert() (ssh.PublicKey, error) {
|
||||
caCertPath := path.Join(systemConfigPath, "ca.pub")
|
||||
caCertBytes, err := ioutil.ReadFile(caCertPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("Failed to load CA certertificate %s", caCertPath))
|
||||
}
|
||||
caCert, _, _, _, err := ssh.ParseAuthorizedKey(caCertBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to parse CA Certificate")
|
||||
}
|
||||
|
||||
return caCert, nil
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
//+build !windows
|
||||
|
||||
package sshserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
validPrincipal = "testUser"
|
||||
testDir = "testdata"
|
||||
testUserKeyFilename = "id_rsa.pub"
|
||||
testCAFilename = "ca.pub"
|
||||
testOtherCAFilename = "other_ca.pub"
|
||||
testUserCertFilename = "id_rsa-cert.pub"
|
||||
)
|
||||
|
||||
var logger, hook = test.NewNullLogger()
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
authorizeKeysPath = testUserKeyFilename
|
||||
logger.SetLevel(logrus.DebugLevel)
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestPublicKeyAuth_Success(t *testing.T) {
|
||||
context, cancel := newMockContext(validPrincipal)
|
||||
defer cancel()
|
||||
|
||||
sshServer := SSHServer{getUserFunc: getMockUser}
|
||||
|
||||
pubKey := getKey(t, testUserKeyFilename)
|
||||
assert.True(t, sshServer.authorizedKeyHandler(context, pubKey))
|
||||
}
|
||||
|
||||
func TestPublicKeyAuth_MissingKey(t *testing.T) {
|
||||
context, cancel := newMockContext(validPrincipal)
|
||||
defer cancel()
|
||||
|
||||
sshServer := SSHServer{logger: logger, getUserFunc: getMockUser}
|
||||
|
||||
pubKey := getKey(t, testOtherCAFilename)
|
||||
assert.False(t, sshServer.authorizedKeyHandler(context, pubKey))
|
||||
assert.Contains(t, hook.LastEntry().Message, "Matching public key not found in")
|
||||
}
|
||||
|
||||
func TestPublicKeyAuth_InvalidUser(t *testing.T) {
|
||||
context, cancel := newMockContext("notAUser")
|
||||
defer cancel()
|
||||
|
||||
sshServer := SSHServer{logger: logger, getUserFunc: lookupUser}
|
||||
|
||||
pubKey := getKey(t, testUserKeyFilename)
|
||||
assert.False(t, sshServer.authorizedKeyHandler(context, pubKey))
|
||||
assert.Contains(t, hook.LastEntry().Message, "Invalid user")
|
||||
}
|
||||
|
||||
func TestPublicKeyAuth_MissingFile(t *testing.T) {
|
||||
currentUser, err := user.Current()
|
||||
require.Nil(t, err)
|
||||
context, cancel := newMockContext(currentUser.Username)
|
||||
defer cancel()
|
||||
|
||||
sshServer := SSHServer{Server: ssh.Server{}, logger: logger, getUserFunc: lookupUser}
|
||||
|
||||
pubKey := getKey(t, testUserKeyFilename)
|
||||
assert.False(t, sshServer.authorizedKeyHandler(context, pubKey))
|
||||
assert.Contains(t, hook.LastEntry().Message, "not found")
|
||||
}
|
||||
|
||||
func TestShortLivedCerts_Success(t *testing.T) {
|
||||
context, cancel := newMockContext(validPrincipal)
|
||||
defer cancel()
|
||||
|
||||
caCert := getKey(t, testCAFilename)
|
||||
sshServer := SSHServer{logger: log.CreateLogger(), caCert: caCert, getUserFunc: getMockUser}
|
||||
|
||||
userCert := getKey(t, testUserCertFilename)
|
||||
assert.True(t, sshServer.shortLivedCertHandler(context, userCert))
|
||||
}
|
||||
|
||||
func TestShortLivedCerts_CAsDontMatch(t *testing.T) {
|
||||
context, cancel := newMockContext(validPrincipal)
|
||||
defer cancel()
|
||||
|
||||
caCert := getKey(t, testOtherCAFilename)
|
||||
sshServer := SSHServer{logger: logger, caCert: caCert, getUserFunc: getMockUser}
|
||||
|
||||
userCert := getKey(t, testUserCertFilename)
|
||||
assert.False(t, sshServer.shortLivedCertHandler(context, userCert))
|
||||
assert.Equal(t, "CA certificate does not match user certificate signer", hook.LastEntry().Message)
|
||||
}
|
||||
|
||||
func TestShortLivedCerts_UserDoesNotExist(t *testing.T) {
|
||||
context, cancel := newMockContext(validPrincipal)
|
||||
defer cancel()
|
||||
|
||||
caCert := getKey(t, testCAFilename)
|
||||
sshServer := SSHServer{logger: logger, caCert: caCert, getUserFunc: lookupUser}
|
||||
|
||||
userCert := getKey(t, testUserCertFilename)
|
||||
assert.False(t, sshServer.shortLivedCertHandler(context, userCert))
|
||||
assert.Contains(t, hook.LastEntry().Message, "Invalid user")
|
||||
}
|
||||
|
||||
func TestShortLivedCerts_InvalidPrincipal(t *testing.T) {
|
||||
context, cancel := newMockContext("notAUser")
|
||||
defer cancel()
|
||||
|
||||
caCert := getKey(t, testCAFilename)
|
||||
sshServer := SSHServer{logger: logger, caCert: caCert, getUserFunc: lookupUser}
|
||||
|
||||
userCert := getKey(t, testUserCertFilename)
|
||||
assert.False(t, sshServer.shortLivedCertHandler(context, userCert))
|
||||
assert.Contains(t, hook.LastEntry().Message, "not in the set of valid principals for given certificate")
|
||||
}
|
||||
|
||||
func getMockUser(_ string) (*User, error) {
|
||||
return &User{
|
||||
Username: validPrincipal,
|
||||
HomeDir: testDir,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func getKey(t *testing.T, filename string) ssh.PublicKey {
|
||||
path := path.Join(testDir, filename)
|
||||
bytes, err := ioutil.ReadFile(path)
|
||||
require.Nil(t, err)
|
||||
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(bytes)
|
||||
require.Nil(t, err)
|
||||
return pubKey
|
||||
}
|
||||
|
||||
type mockSSHContext struct {
|
||||
context.Context
|
||||
*sync.Mutex
|
||||
}
|
||||
|
||||
func newMockContext(user string) (*mockSSHContext, context.CancelFunc) {
|
||||
innerCtx, cancel := context.WithCancel(context.Background())
|
||||
mockCtx := &mockSSHContext{innerCtx, &sync.Mutex{}}
|
||||
mockCtx.SetValue("user", user)
|
||||
return mockCtx, cancel
|
||||
}
|
||||
|
||||
func (ctx *mockSSHContext) SetValue(key, value interface{}) {
|
||||
ctx.Context = context.WithValue(ctx.Context, key, value)
|
||||
}
|
||||
|
||||
func (ctx *mockSSHContext) User() string {
|
||||
return ctx.Value("user").(string)
|
||||
}
|
||||
|
||||
func (ctx *mockSSHContext) SessionID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ctx *mockSSHContext) ClientVersion() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ctx *mockSSHContext) ServerVersion() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ctx *mockSSHContext) RemoteAddr() net.Addr {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ctx *mockSSHContext) LocalAddr() net.Addr {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ctx *mockSSHContext) Permissions() *ssh.Permissions {
|
||||
return nil
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
// Taken from https://github.com/golang/go/blob/ad644d2e86bab85787879d41c2d2aebbd7c57db8/src/os/user/user.go
|
||||
// and modified to return login shell in User struct. cloudflared requires cgo for compilation because of this addition.
|
||||
|
||||
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
@ -145,7 +144,7 @@ func buildUser(pwd *C.struct_passwd) *User {
|
|||
Name: C.GoString(pwd.pw_gecos),
|
||||
HomeDir: C.GoString(pwd.pw_dir),
|
||||
/****************** Begin added code ******************/
|
||||
Shell: C.GoString(pwd.pw_shell),
|
||||
Shell: C.GoString(pwd.pw_shell),
|
||||
/****************** End added code ******************/
|
||||
}
|
||||
// The pw_gecos field isn't quite standardized. Some docs
|
||||
|
|
|
@ -19,14 +19,14 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
rsaFilename = "ssh_host_rsa_key"
|
||||
rsaFilename = "ssh_host_rsa_key"
|
||||
ecdsaFilename = "ssh_host_ecdsa_key"
|
||||
)
|
||||
|
||||
func (s *SSHServer) configureHostKeys() error {
|
||||
if _, err := os.Stat(configDir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("Error creating %s directory", configDir))
|
||||
if _, err := os.Stat(systemConfigPath); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(systemConfigPath, 0755); err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("Error creating %s directory", systemConfigPath))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ func (s *SSHServer) configureHostKey(keyFunc func() (string, error)) error {
|
|||
}
|
||||
|
||||
func (s *SSHServer) ensureRSAKeyExists() (string, error) {
|
||||
keyPath := filepath.Join(configDir, rsaFilename)
|
||||
keyPath := filepath.Join(systemConfigPath, rsaFilename)
|
||||
if _, err := os.Stat(keyPath); os.IsNotExist(err) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
|
@ -76,7 +76,7 @@ func (s *SSHServer) ensureRSAKeyExists() (string, error) {
|
|||
}
|
||||
|
||||
func (s *SSHServer) ensureECDSAKeyExists() (string, error) {
|
||||
keyPath := filepath.Join(configDir, ecdsaFilename)
|
||||
keyPath := filepath.Join(systemConfigPath, ecdsaFilename)
|
||||
if _, err := os.Stat(keyPath); os.IsNotExist(err) {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
|
|
|
@ -4,8 +4,8 @@ package sshserver
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
@ -19,18 +19,15 @@ import (
|
|||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultShellPrompt = `\e[0;31m[\u@\h \W]\$ \e[m `
|
||||
configDir = "/etc/cloudflared/"
|
||||
)
|
||||
|
||||
type SSHServer struct {
|
||||
ssh.Server
|
||||
logger *logrus.Logger
|
||||
shutdownC chan struct{}
|
||||
logger *logrus.Logger
|
||||
shutdownC chan struct{}
|
||||
caCert ssh.PublicKey
|
||||
getUserFunc func(string) (*User, error)
|
||||
}
|
||||
|
||||
func New(logger *logrus.Logger, address string, shutdownC chan struct{}) (*SSHServer, error) {
|
||||
func New(logger *logrus.Logger, address string, shutdownC chan struct{}, shortLivedCertAuth bool) (*SSHServer, error) {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -39,11 +36,28 @@ func New(logger *logrus.Logger, address string, shutdownC chan struct{}) (*SSHSe
|
|||
return nil, errors.New("cloudflared SSH server needs to run as root")
|
||||
}
|
||||
|
||||
sshServer := SSHServer{ssh.Server{Addr: address}, logger, shutdownC}
|
||||
sshServer := SSHServer{
|
||||
Server: ssh.Server{Addr: address},
|
||||
logger: logger,
|
||||
shutdownC: shutdownC,
|
||||
getUserFunc: lookupUser,
|
||||
}
|
||||
|
||||
if err := sshServer.configureHostKeys(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if shortLivedCertAuth {
|
||||
caCert, err := getCACert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sshServer.caCert = caCert
|
||||
sshServer.PublicKeyHandler = sshServer.shortLivedCertHandler
|
||||
} else {
|
||||
sshServer.PublicKeyHandler = sshServer.authorizedKeyHandler
|
||||
}
|
||||
|
||||
return &sshServer, nil
|
||||
}
|
||||
|
||||
|
@ -64,11 +78,9 @@ func (s *SSHServer) Start() error {
|
|||
func (s *SSHServer) connectionHandler(session ssh.Session) {
|
||||
|
||||
// Get uid and gid of user attempting to login
|
||||
sshUser, err := lookupUser(session.User())
|
||||
if err != nil {
|
||||
if _, err := io.WriteString(session, "Invalid credentials\n"); err != nil {
|
||||
s.logger.WithError(err).Error("Invalid credentials: Failed to write to SSH session")
|
||||
}
|
||||
sshUser, ok := session.Context().Value("sshUser").(*User)
|
||||
if !ok || sshUser == nil {
|
||||
s.logger.Error("Error retrieving credentials from session")
|
||||
s.CloseSession(session)
|
||||
return
|
||||
}
|
||||
|
@ -86,17 +98,22 @@ func (s *SSHServer) connectionHandler(session ssh.Session) {
|
|||
return
|
||||
}
|
||||
|
||||
uidInt, uidErr := stringToUint32(sshUser.Uid)
|
||||
gidInt, gidErr := stringToUint32(sshUser.Gid)
|
||||
if uidErr != nil || gidErr != nil {
|
||||
uidInt, err := stringToUint32(sshUser.Uid)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Invalid user")
|
||||
s.CloseSession(session)
|
||||
return
|
||||
}
|
||||
gidInt, err := stringToUint32(sshUser.Gid)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Invalid user group")
|
||||
s.CloseSession(session)
|
||||
return
|
||||
}
|
||||
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: uidInt, Gid: gidInt}}
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term))
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("USER=%s", sshUser.Name))
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("USER=%s", sshUser.Username))
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("HOME=%s", sshUser.HomeDir))
|
||||
cmd.Dir = sshUser.HomeDir
|
||||
psuedoTTY, err := pty.Start(cmd)
|
||||
|
|
|
@ -3,15 +3,17 @@
|
|||
package sshserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type SSHServer struct{}
|
||||
|
||||
func New(_ *logrus.Logger, _ string, _ chan struct{}) (*SSHServer, error) {
|
||||
return nil, nil
|
||||
func New(_ *logrus.Logger, _ string, _ chan struct{}, _ bool) (*SSHServer, error) {
|
||||
return nil, errors.New("cloudflared ssh server is not supported on windows")
|
||||
}
|
||||
|
||||
func (s *SSHServer) Start() error {
|
||||
return nil
|
||||
return errors.New("cloudflared ssh server is not supported on windows")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
|
||||
NhAAAAAwEAAQAAAQEA0c6EklYvC9B041qEGWDNuot6G4tTVm9LCQC0vA+v2n25ru9CINV6
|
||||
8IljmXBORXBwfG6PdLhg0SEabZUbsNX5WrIVbGovcghKS6GRsqI5+Quhm+o8eG042JE/hB
|
||||
oYdZ19TcMEyPOGzHsx0U/BSN9ZJWVCxqN51iI6qyhz9f6jlX2LQBFEvXlhxgF3owBEf8UC
|
||||
Zt/UvbZdmeeyKNQElPmiVLIJEAPCueECp7a2mjCiP3zqjDvSeeGk4CelB/1qZZ4V2n7fvb
|
||||
HZjAB5JJs4KXs5o8KgvQnqgQMxiLFZ4PATt4+mxEzh4JymppbqJOo2rYwOA3TAIEWWtYRV
|
||||
/ZKJ0AyhhQAAA8gciO8XHIjvFwAAAAdzc2gtcnNhAAABAQDRzoSSVi8L0HTjWoQZYM26i3
|
||||
obi1NWb0sJALS8D6/afbmu70Ig1XrwiWOZcE5FcHB8bo90uGDRIRptlRuw1flashVsai9y
|
||||
CEpLoZGyojn5C6Gb6jx4bTjYkT+EGhh1nX1NwwTI84bMezHRT8FI31klZULGo3nWIjqrKH
|
||||
P1/qOVfYtAEUS9eWHGAXejAER/xQJm39S9tl2Z57Io1ASU+aJUsgkQA8K54QKntraaMKI/
|
||||
fOqMO9J54aTgJ6UH/WplnhXaft+9sdmMAHkkmzgpezmjwqC9CeqBAzGIsVng8BO3j6bETO
|
||||
HgnKamluok6jatjA4DdMAgRZa1hFX9konQDKGFAAAAAwEAAQAAAQEApVzGdKhk8ETevAst
|
||||
rurze6JPHcKUbr3NQE1EJi2fBvCtF0oQrtxTx54h2GAB8Q0MO6bQfsiL1ojm0ZQCfUBJBs
|
||||
jxxb9zoccS98Vilo7ybm5SdBcMjkZX1am1jCMdQCZfCpk4/kGi7yvyOe1IhG01UBodpX5X
|
||||
mwTjhN+fdjW7LSiW6cKPClN49CZKgmtvI27FCt+/TtMzdCXOiJxJ4yZCzCRhSgssV0gWI1
|
||||
0VJr/MHirKUvv/qCLAuOBxIr9UgdduRZUpNX+KS2rfhFEbjnUqc/57aAakpQmuPB5I+s9G
|
||||
DnrF0HSHpq7u1XC1SvYlnFBN/0A7Hw/MX2SaBFH7mc9AAQAAAIAFuTHr6O8tCvWEawfxC0
|
||||
qiAPQ+Yy1vthq5uewmuQujMutUnc9JAUl32PdU1DbS7APC1Dg9XL7SyAB6A+ZpRJRAKgCY
|
||||
SneAKE6hOytH+yM206aekrz6VuZiSpBqpfEqDibVAaZIO8sv/9dtZd6kWemxNErPQoKJey
|
||||
Z7/cuWUWQovAAAAIEA6ugIlVj1irPmElyCCt5YfPv2x8Dl54ELoP/WsffsrPHNQog64hFd
|
||||
ahD7Wq63TA566bN85fkx8OVU5TbbEQmkHgOEV6nDRY2YsBSqIOblA/KehtfdUIqZB0iNBh
|
||||
Gn6TV/z6HwnSR3gKv4b66Gveek6LfRAG3mbsLCgyRAbYgn6YUAAACBAOSlf+n1eh6yjtvF
|
||||
Zecq3Zslj7O8cUs17PQx4vQ7sXNCFrIZdevWPIn9sVrt7/hsTrXunDz6eXCeclB35KZe3H
|
||||
WPVjRoD+xnr5+sXx2qXOnKCR0LdFybso6IR5bXAI6DNSNfP7D9LPEQ+R73Jk0jPuLYzocS
|
||||
iM89KZiuGpzr01gBAAAAEW1pa2VAQzAyWTUwVEdKR0g4AQ==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
|
@ -0,0 +1 @@
|
|||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDRzoSSVi8L0HTjWoQZYM26i3obi1NWb0sJALS8D6/afbmu70Ig1XrwiWOZcE5FcHB8bo90uGDRIRptlRuw1flashVsai9yCEpLoZGyojn5C6Gb6jx4bTjYkT+EGhh1nX1NwwTI84bMezHRT8FI31klZULGo3nWIjqrKHP1/qOVfYtAEUS9eWHGAXejAER/xQJm39S9tl2Z57Io1ASU+aJUsgkQA8K54QKntraaMKI/fOqMO9J54aTgJ6UH/WplnhXaft+9sdmMAHkkmzgpezmjwqC9CeqBAzGIsVng8BO3j6bETOHgnKamluok6jatjA4DdMAgRZa1hFX9konQDKGF mike@C02Y50TGJGH8
|
|
@ -0,0 +1,49 @@
|
|||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
-----END OPENSSH PRIVATE KEY-----
|
|
@ -0,0 +1 @@
|
|||
ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgOsuFqKdzp/nC3wQfKVJBdHa8axtGryKplPkDjdSXT4kAAAADAQABAAACAQDrQqd6jzuo+yH7MStYWXu/H3dzt/JPFtn2VQ2pp+BfL8Es4Jz+dBIxyixVIuxCtseWFjN+ZkSFOK5ua5hSsdyGayfWaXUcio/xPloOC01supidLgfmg7hKPX5ceVC1TufvMdeBxr1Kcxa3UWGTHWa+R6I4qzR6ruFXZO4hnF+Zth8hW8kbCsvS5bNtIQTWC/GBNdkfEQKEhDKs2+KWOCLi7YwP9wiLoox5hSdpMvUXE70bgYgWLLu54dHA5k8gUeBib5Wu1ftdp9IXT///MKsLrfPNa3/oDTqEV6k7PZVe+btJ1yRpHuoxcBvuSKmxzT50qT2CBTsWqnFljlxV5mj1GVFWFFcYPLuYAED8UzC+/6pgKAneeVlt0ptqI8tC7xJxOUn2wwjTr9mIRjThuZ5nQBIznaBoMaOsVtue4c1/ldX+it4RSG0KuvIWegjGYqAfOcaNnnmrvsW1MeesAoWYoU/XWzcwshf2jhGC2uI62Xgp6eQh9S6aC2vBj9QaM5o5BQ6tUfVqbnjCRSJUQXRB3cAPwoKX+X/FPn63pw6yln0YiHBrY0GZGM3/BUan5itvTxF7eTE8Jf1uzmIt76Qc3S+4WvtEOj55SiJI+FaXoVSrHZb01mp6SYSAqfegqt9sZowfVUuRopaXNDjEnM/SJp1BshcavIsRgBLJ16Zm2wAAAAAAAAAAAAAAAQAAAA10ZXN0VXNlckB0ZXN0AAAADAAAAAh0ZXN0VXNlcgAAAAAAAAAA//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAABFwAAAAdzc2gtcnNhAAAAAwEAAQAAAQEA0c6EklYvC9B041qEGWDNuot6G4tTVm9LCQC0vA+v2n25ru9CINV68IljmXBORXBwfG6PdLhg0SEabZUbsNX5WrIVbGovcghKS6GRsqI5+Quhm+o8eG042JE/hBoYdZ19TcMEyPOGzHsx0U/BSN9ZJWVCxqN51iI6qyhz9f6jlX2LQBFEvXlhxgF3owBEf8UCZt/UvbZdmeeyKNQElPmiVLIJEAPCueECp7a2mjCiP3zqjDvSeeGk4CelB/1qZZ4V2n7fvbHZjAB5JJs4KXs5o8KgvQnqgQMxiLFZ4PATt4+mxEzh4JymppbqJOo2rYwOA3TAIEWWtYRV/ZKJ0AyhhQAAAQ8AAAAHc3NoLXJzYQAAAQC2lL+6JYTGOdz1zNnck6onrFcVpO2onCVAKP8HdLoCeH0/upIugaCocPKuzoURYEfiHQotviNeprE/2CyAroJ5VBdqWftEeHn3FFvBCQ1gwRQ7oci4C5n72t0vjWWE6WBylS0RqpJjr6EQ8a1vuwIqAQrEJPp2yNLjRH2WD7eicBh5f43VKOMr73DtyTh4xoF0C2sNBROudt58npTaYqRHQgoI25V/aCmuYBgM3wdAGcoEZGoSerMfhID7GcWkvemq2hF8mQsspG3zgnyQXk+ahagmefzxutDnr3KdrZ637La0/XwABvBZ9L4l5RiEilVI1Shl96F2qbBW2YZ64pUQ test@cloudflare.com
|
|
@ -0,0 +1 @@
|
|||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDrQqd6jzuo+yH7MStYWXu/H3dzt/JPFtn2VQ2pp+BfL8Es4Jz+dBIxyixVIuxCtseWFjN+ZkSFOK5ua5hSsdyGayfWaXUcio/xPloOC01supidLgfmg7hKPX5ceVC1TufvMdeBxr1Kcxa3UWGTHWa+R6I4qzR6ruFXZO4hnF+Zth8hW8kbCsvS5bNtIQTWC/GBNdkfEQKEhDKs2+KWOCLi7YwP9wiLoox5hSdpMvUXE70bgYgWLLu54dHA5k8gUeBib5Wu1ftdp9IXT///MKsLrfPNa3/oDTqEV6k7PZVe+btJ1yRpHuoxcBvuSKmxzT50qT2CBTsWqnFljlxV5mj1GVFWFFcYPLuYAED8UzC+/6pgKAneeVlt0ptqI8tC7xJxOUn2wwjTr9mIRjThuZ5nQBIznaBoMaOsVtue4c1/ldX+it4RSG0KuvIWegjGYqAfOcaNnnmrvsW1MeesAoWYoU/XWzcwshf2jhGC2uI62Xgp6eQh9S6aC2vBj9QaM5o5BQ6tUfVqbnjCRSJUQXRB3cAPwoKX+X/FPn63pw6yln0YiHBrY0GZGM3/BUan5itvTxF7eTE8Jf1uzmIt76Qc3S+4WvtEOj55SiJI+FaXoVSrHZb01mp6SYSAqfegqt9sZowfVUuRopaXNDjEnM/SJp1BshcavIsRgBLJ16Zm2w== test@cloudflare.com
|
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
|
||||
NhAAAAAwEAAQAAAQEAzBO7TXxbpk7sGQm/Wa29N/NFe5uuoEQGC5hxfihmcvVgeKeNKiSS
|
||||
snxzCE1Y6SmNMoE4aQs92wtcn48GmxRwZSXbCqLq2CJrHfe9B2k3aPkJZpQkFMshcJGo7p
|
||||
G0Vlo7dWAbYf99/YKddf290uLK7vxw9ty0pM1hXSXHNShv1b+bTQm/COMZ5jNsncjc1yBH
|
||||
KGkFVHee9Dh4Z0xLlHipIyyNXXzI0RFYuHSNJz9GD310XQLIIroptr7+/7g6+sPPGsNlI+
|
||||
95OScba1/PQ2b/qy+KyIwNIMSd9ziJy5xnO7Vo3LrqQrza1Pkn2i29PljUcbc/F0hhXNIq
|
||||
ITdNWwVqsQAAA8iKllTIipZUyAAAAAdzc2gtcnNhAAABAQDME7tNfFumTuwZCb9Zrb0380
|
||||
V7m66gRAYLmHF+KGZy9WB4p40qJJKyfHMITVjpKY0ygThpCz3bC1yfjwabFHBlJdsKourY
|
||||
Imsd970HaTdo+QlmlCQUyyFwkajukbRWWjt1YBth/339gp11/b3S4sru/HD23LSkzWFdJc
|
||||
c1KG/Vv5tNCb8I4xnmM2ydyNzXIEcoaQVUd570OHhnTEuUeKkjLI1dfMjREVi4dI0nP0YP
|
||||
fXRdAsgiuim2vv7/uDr6w88aw2Uj73k5JxtrX89DZv+rL4rIjA0gxJ33OInLnGc7tWjcuu
|
||||
pCvNrU+SfaLb0+WNRxtz8XSGFc0iohN01bBWqxAAAAAwEAAQAAAQAKEtNFEOVpQS4QUlXa
|
||||
tGPJtj1wy4+EI7d0rRK1GoNsG0amzgZ+1Q1UuCXpe//uinmIy64gKUjlXhs1WRcHYqvlok
|
||||
e8r6wN/Szybr8q9Xuht+FJ6fgZ+qjs6JPBKvoO5SdYNOVFIhpzABaLs3nCRiWkRFvDI8Pa
|
||||
+rRap7m8mwFiOJtmdiIZYFxzw6xXwTsGCrWPKgTv3FKGZzXnCB9i7jC2vwT1MDYbcnzEH4
|
||||
Ba4dxI8bp6WWEX0biRIXj3jCtLb5gisNTSxdZs254Syh75HEXunSh2YO+yVSWQtZj19ewW
|
||||
6Rb1Z3x5rVfXcgSkg7gZd9EpbckIIg6+MFSH3wdGW6atAAAAgQDFXiMuNd4ZYwdyhjlM5n
|
||||
nFqQDXGgnwyNdiIqAapoqTdF5aZwNnbTU0fCFaDMLCQAHgntcgCEsW9A4HzDzYhOABKElv
|
||||
j973vXWF165wFiZwuKSfroq/6JH6CiIcjiqpszbnqSOzy1hq913RWILS6e9yMjxRv8PUjm
|
||||
E+IkcnfcFUwAAAAIEA+jwI3ICe8PGEIezV2tvQFeQy2Z2wGslu1yvqfTYEztSmtygns3wn
|
||||
ZBb+cBXCnpqUCtznG7hZhq7I4m1I47BYznULwwFiBTVtBASG5wNP7zeVKTVZ4SKprze+Fe
|
||||
I/nUZDJ5Q26um7eDbhvZ/n95GY+fucMVHoSBfX1wE16XBfp88AAACBANDHcgC4qP2oyOw/
|
||||
+p9HineMQd/ppG3fePe07jyZXLHLf0rByFveFgRAQ1m77O7FtP3fFKy3Y9nNy18LGq35ZK
|
||||
Blsz2B23bO8NuffgAhchDG7KzKFXCo+AraIj5znp/znK5zIkaiiSOQaYywJ36EooYVpRtj
|
||||
ep5ap6bBFDZ2e+V/AAAAEW1pa2VAQzAyWTUwVEdKR0g4AQ==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
|
@ -0,0 +1 @@
|
|||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDME7tNfFumTuwZCb9Zrb0380V7m66gRAYLmHF+KGZy9WB4p40qJJKyfHMITVjpKY0ygThpCz3bC1yfjwabFHBlJdsKourYImsd970HaTdo+QlmlCQUyyFwkajukbRWWjt1YBth/339gp11/b3S4sru/HD23LSkzWFdJcc1KG/Vv5tNCb8I4xnmM2ydyNzXIEcoaQVUd570OHhnTEuUeKkjLI1dfMjREVi4dI0nP0YPfXRdAsgiuim2vv7/uDr6w88aw2Uj73k5JxtrX89DZv+rL4rIjA0gxJ33OInLnGc7tWjcuupCvNrU+SfaLb0+WNRxtz8XSGFc0iohN01bBWqx mike@C02Y50TGJGH8
|
|
@ -0,0 +1,92 @@
|
|||
// The Test package is used for testing logrus. It is here for backwards
|
||||
// compatibility from when logrus' organization was upper-case. Please use
|
||||
// lower-case logrus and the `null` package instead of this one.
|
||||
package test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Hook is a hook designed for dealing with logs in test scenarios.
|
||||
type Hook struct {
|
||||
// Entries is an array of all entries that have been received by this hook.
|
||||
// For safe access, use the AllEntries() method, rather than reading this
|
||||
// value directly.
|
||||
Entries []logrus.Entry
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewGlobal installs a test hook for the global logger.
|
||||
func NewGlobal() *Hook {
|
||||
|
||||
hook := new(Hook)
|
||||
logrus.AddHook(hook)
|
||||
|
||||
return hook
|
||||
|
||||
}
|
||||
|
||||
// NewLocal installs a test hook for a given local logger.
|
||||
func NewLocal(logger *logrus.Logger) *Hook {
|
||||
|
||||
hook := new(Hook)
|
||||
logger.Hooks.Add(hook)
|
||||
|
||||
return hook
|
||||
|
||||
}
|
||||
|
||||
// NewNullLogger creates a discarding logger and installs the test hook.
|
||||
func NewNullLogger() (*logrus.Logger, *Hook) {
|
||||
|
||||
logger := logrus.New()
|
||||
logger.Out = ioutil.Discard
|
||||
|
||||
return logger, NewLocal(logger)
|
||||
|
||||
}
|
||||
|
||||
func (t *Hook) Fire(e *logrus.Entry) error {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.Entries = append(t.Entries, *e)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Hook) Levels() []logrus.Level {
|
||||
return logrus.AllLevels
|
||||
}
|
||||
|
||||
// LastEntry returns the last entry that was logged or nil.
|
||||
func (t *Hook) LastEntry() *logrus.Entry {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
i := len(t.Entries) - 1
|
||||
if i < 0 {
|
||||
return nil
|
||||
}
|
||||
return &t.Entries[i]
|
||||
}
|
||||
|
||||
// AllEntries returns all entries that were logged.
|
||||
func (t *Hook) AllEntries() []*logrus.Entry {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
// Make a copy so the returned value won't race with future log requests
|
||||
entries := make([]*logrus.Entry, len(t.Entries))
|
||||
for i := 0; i < len(t.Entries); i++ {
|
||||
// Make a copy, for safety
|
||||
entries[i] = &t.Entries[i]
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
// Reset removes all Entries from this test hook.
|
||||
func (t *Hook) Reset() {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.Entries = make([]logrus.Entry, 0)
|
||||
}
|
Loading…
Reference in New Issue