Initial working prototype

This commit is contained in:
Alexander Klizhentas
2015-03-14 14:35:43 -07:00
parent 46369efae3
commit 0c425d32d5
53 changed files with 4533 additions and 2 deletions
+1
View File
@@ -22,3 +22,4 @@ _testmain.go
*.exe
*.test
*.prof
flymake*
+61
View File
@@ -0,0 +1,61 @@
ETCD_NODE1 := http://127.0.0.1:4001
ETCD_NODE2 := http://127.0.0.1:4002
ETCD_NODE3 := http://127.0.0.1:4003
ETCD_NODES := ${ETCD_NODE1},${ETCD_NODE2},${ETCD_NODE3}
ETCD_FLAGS := TELEPORT_TEST_ETCD_NODES=${ETCD_NODES}
test: clean
go test -v ./... -cover
test-with-etcd: clean
${ETCD_FLAGS} go test -v ./... -cover
clean:
find . -name flymake_* -delete
test-package: clean
go test -v ./$(p)
test-package-with-etcd: clean
${ETCD_FLAGS} go test -v ./$(p)
update:
rm -rf Godeps/
find . -iregex .*go | xargs sed -i 's:".*Godeps/_workspace/src/:":g'
godep save -r ./...
test-grep-package: clean
go test -v ./$(p) -check.f=$(e)
cover-package: clean
go test -v ./$(p) -coverprofile=/tmp/coverage.out
go tool cover -html=/tmp/coverage.out
cover-package-with-etcd: clean
${ETCD_FLAGS} go test -v ./$(p) -coverprofile=/tmp/coverage.out
go tool cover -html=/tmp/coverage.out
install: clean
go install github.com/gravitational/teleport/teleport
run: install
teleport -addr=localhost:2022\
-log=console\
-logSeverity=INFO\
-shell=/bin/bash\
-hostCert=./fixtures/keys/hosts/node.gravitational.io-cert.pub\
-hostPrivateKey=./fixtures/keys/hosts/node.gravitational.io\
-authSrv\
-backend=etcd\
-backendConfig='{"nodes": ["${ETCD_NODE1}","${ETCD_NODE2}","${ETCD_NODE3}"], "key": "/teleport"}'\
profile:
go tool pprof http://localhost:6060/debug/pprof/profile
sloccount:
find . -path ./Godeps -prune -o -name "*.go" -print0 | xargs -0 wc -l
# sets development etcd keys
set-etcd:
bash ./scripts/etcd.sh
+107 -2
View File
@@ -1,4 +1,4 @@
# Medusa
# Teleport
Proxying SSH server with dynamic backend and ACL support
@@ -36,7 +36,7 @@ The goal of this project is to define a simple SSH infrastrucure that satisfies
## Proposed design
![medusa overview](/doc/img/MedusaOverview.png?raw=true "Medusa Overview")
![teleport overview](/doc/img/TeleportOverview.png?raw=true "Teleport Overview")
* Use Etcd for configuration, discovery, presence and key store
* Each server has ssh server heartbeating into the cluster
@@ -46,12 +46,117 @@ The goal of this project is to define a simple SSH infrastrucure that satisfies
* Use structured logging in syslog and json format and plug it into ES cluster
## Instalation and run
## Setting up certificates
Running teleport requires some set up of the certificates at the moment.
1. Create a server CA
This CA will be used to sign host certificates
```shell
ssh-keygen -f server_ca
```
2. Use server CA to sign the host key on your machine
```shell
# signs server cert with authority key
ssh-keygen -s server_ca -I host_auth_server -h -n auth.example.com -V +52w /etc/ssh/ssh_host_rsa_key.pub
```
3. Create a user CA
This authority will be used to sign user's public keys:
```shell
# generate key pair for authority signing user keys
ssh-keygen -f users_ca
```
4. Generate a user public/private key pair
This keypair will be used to access teleport server
```shell
ssh-keygen -f <put-user-name-here>
```
5. Use user CA to sign the user key
```shell
ssh-keygen -s users_ca -I user_username -n <put-user-name-here> -V +52w id_rsa.pub
```
Read on the guide here for more information:
https://www.digitalocean.com/community/tutorials/how-to-create-an-ssh-ca-to-validate-hosts-and-clients-with-ubuntu
## Running the teleport server
```shell
teleport -host=localhost -port=2022 -hostPrivateKey=/etc/ssh/ssh_host_rsa_key -caPublicKey=./path-to/users_ca.pub
```
## Connecting to teleport server
1. Start SSH agent
```shell
eval `ssh-agent`
```
2. Add the user key created to the agent (see the previous section)
```shell
ssh-add /path-to-user-private-and-public-key-directory
```
Make sure the identity has been loaded by running
```shell
ssh-add -l
```
3. Instruct ssh client to turn on agent forwarding
edit file `~./ssh/config` and add the following lines:
```
Host *
ForwardAgent yes
```
These lines allow ssh-client to use running ssh-agent for authorization with ssh server
4. If everything is correct, the following command will succeed:
```
ssh -p 2022 localhost
```
## Using teleport features
## TUN subsystem
Tun subsystem uses one teleport server to access another teleport server using client public key's without extra hassle of key management.
```
ssh -p 2022 localhost -s tun:127.0.0.1:2022
```
## MUX subsystem
MUX subsystem instructs teleport to connect to remote servers, execute the desired commands, collect return back the responses to the client:
```shell
# instructs teleport to run command 'ls -l' on 127.0.0.1 port 2022 and localhost port 2022
ssh -p 2022 localhost -s mux:127.0.0.1:2022,localhost:2022/ls -l
```
+124
View File
@@ -0,0 +1,124 @@
// package auth implements certificate signing authority and access control server
// Authority server is composed of several parts:
//
// * Authority server itself that implements signing and acl logic
// * HTTP server wrapper for authority server
// * HTTP client wrapper
//
package auth
import (
"time"
"github.com/gravitational/teleport/backend"
)
// Authority implements minimal key-management facility for generating OpenSSH
//compatible public/private key pairs and OpenSSH certificates
type Authority interface {
GenerateKeyPair(passphrase string) ([]byte, []byte, error)
// GenerateHostCert generates host certificate, it takes pkey as a signing private key (host certificate authority)
GenerateHostCert(pkey, key []byte, id, hostname string, ttl time.Duration) ([]byte, error)
// GenerateHostCert generates user certificate, it takes pkey as a signing private key (user certificate authority)
GenerateUserCert(pkey, key []byte, id, username string, ttl time.Duration) ([]byte, error)
}
func NewAuthServer(b backend.Backend, a Authority) *AuthServer {
return &AuthServer{
b: b,
a: a,
}
}
// AuthServer implements key signing, generation and ACL functionality used by teleport
type AuthServer struct {
b backend.Backend
a Authority
}
// UpsertUserKey takes user's public key, generates certificate for it
// and adds it to the authorized keys database. It returns certificate signed by user CA in case of success,
// error otherwise. The certificate will be valid for the duration of the ttl passed in.
func (s *AuthServer) UpsertUserKey(user string, key backend.AuthorizedKey, ttl time.Duration) ([]byte, error) {
cert, err := s.GenerateUserCert(key.Value, key.ID, user, ttl)
if err != nil {
return nil, err
}
key.Value = cert
if err := s.b.UpsertUserKey(user, key, ttl); err != nil {
return nil, err
}
return cert, nil
}
func (s *AuthServer) GetUsers() ([]string, error) {
return s.b.GetUsers()
}
func (s *AuthServer) DeleteUser(user string) error {
return s.b.DeleteUser(user)
}
func (s *AuthServer) GetUserKeys(user string) ([]backend.AuthorizedKey, error) {
return s.b.GetUserKeys(user)
}
// DeleteUserKey deletes user key by given ID
func (s *AuthServer) DeleteUserKey(user, key string) error {
return s.b.DeleteUserKey(user, key)
}
// GenerateKeyPair generates private and public key pair of OpenSSH style certificates
func (s *AuthServer) GenerateKeyPair(pass string) ([]byte, []byte, error) {
return s.a.GenerateKeyPair(pass)
}
// ResetHostCA generates host certificate authority and updates the backend
func (s *AuthServer) ResetHostCA(pass string) error {
priv, pub, err := s.a.GenerateKeyPair(pass)
if err != nil {
return err
}
return s.b.UpsertHostCA(backend.CA{Pub: pub, Priv: priv})
}
// ResetHostCA generates user certificate authority and updates the backend
func (s *AuthServer) ResetUserCA(pass string) error {
priv, pub, err := s.a.GenerateKeyPair(pass)
if err != nil {
return err
}
return s.b.UpsertUserCA(backend.CA{Pub: pub, Priv: priv})
}
// GetHostCAPub returns a public key for host key signing authority
func (s *AuthServer) GetHostCAPub() ([]byte, error) {
return s.b.GetHostCAPub()
}
// GetHostCAPub returns a public key for user key signing authority
func (s *AuthServer) GetUserCAPub() ([]byte, error) {
return s.b.GetUserCAPub()
}
// GenerateHostCert generates host certificate, it takes pkey as a signing private key (host certificate authority)
func (s *AuthServer) GenerateHostCert(key []byte, id, hostname string, ttl time.Duration) ([]byte, error) {
hk, err := s.b.GetHostCA()
if err != nil {
return nil, err
}
return s.a.GenerateHostCert(hk.Priv, key, id, hostname, ttl)
}
// GenerateHostCert generates user certificate, it takes pkey as a signing private key (user certificate authority)
func (s *AuthServer) GenerateUserCert(key []byte, id, username string, ttl time.Duration) ([]byte, error) {
hk, err := s.b.GetUserCA()
if err != nil {
return nil, err
}
return s.a.GenerateUserCert(hk.Priv, key, id, username, ttl)
}
const Week = time.Hour * 24 * 7
+228
View File
@@ -0,0 +1,228 @@
package auth
import (
"encoding/json"
"fmt"
"time"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/gravitational/teleport/backend"
)
const CurrentVersion = "v1"
type Client struct {
addr string
client *http.Client
}
func NewClient(addr string) *Client {
return &Client{
addr: addr,
client: http.DefaultClient,
}
}
func (c *Client) GetUsers() ([]string, error) {
out, err := c.Get(c.endpoint("users"), url.Values{})
if err != nil {
return nil, err
}
var users *usersResponse
if err := json.Unmarshal(out, &users); err != nil {
return nil, err
}
return users.Users, nil
}
func (c *Client) DeleteUser(user string) error {
return c.Delete(c.endpoint("users", user))
}
func (c *Client) UpsertUserKey(username string, key backend.AuthorizedKey, ttl time.Duration) ([]byte, error) {
out, err := c.PostForm(c.endpoint("users", username, "keys"), url.Values{
"key": []string{string(key.Value)},
"id": []string{key.ID},
"ttl": []string{ttl.String()},
})
if err != nil {
return nil, err
}
var cert *certResponse
if err := json.Unmarshal(out, &cert); err != nil {
return nil, err
}
return []byte(cert.Cert), err
}
func (c *Client) GetUserKeys(user string) ([]backend.AuthorizedKey, error) {
out, err := c.Get(c.endpoint("users", user, "keys"), url.Values{})
if err != nil {
return nil, err
}
var keys *pubKeysResponse
if err := json.Unmarshal(out, &keys); err != nil {
return nil, err
}
return keys.PubKeys, nil
}
func (c *Client) DeleteUserKey(username string, id string) error {
return c.Delete(c.endpoint("users", username, "keys", id))
}
func (c *Client) GetHostCAPub() ([]byte, error) {
out, err := c.Get(c.endpoint("ca", "host", "keys", "pub"), url.Values{})
if err != nil {
return nil, err
}
var pubkey *pubKeyResponse
if err := json.Unmarshal(out, &pubkey); err != nil {
return nil, err
}
return []byte(pubkey.PubKey), err
}
func (c *Client) GetUserCAPub() ([]byte, error) {
out, err := c.Get(c.endpoint("ca", "user", "keys", "pub"), url.Values{})
if err != nil {
return nil, err
}
var pubkey *pubKeyResponse
if err := json.Unmarshal(out, &pubkey); err != nil {
return nil, err
}
return []byte(pubkey.PubKey), err
}
func (c *Client) GenerateKeyPair(pass string) ([]byte, []byte, error) {
out, err := c.PostForm(c.endpoint("keypair"), url.Values{})
if err != nil {
return nil, nil, err
}
var kp *keyPairResponse
if err := json.Unmarshal(out, &kp); err != nil {
return nil, nil, err
}
return kp.PrivKey, []byte(kp.PubKey), err
}
func (c *Client) GenerateHostCert(key []byte, id, hostname string, ttl time.Duration) ([]byte, error) {
out, err := c.PostForm(c.endpoint("ca", "host", "certs"), url.Values{
"key": []string{string(key)},
"id": []string{id},
"hostname": []string{hostname},
"ttl": []string{ttl.String()},
})
if err != nil {
return nil, err
}
var cert *certResponse
if err := json.Unmarshal(out, &cert); err != nil {
return nil, err
}
return []byte(cert.Cert), err
}
func (c *Client) GenerateUserCert(key []byte, id, user string, ttl time.Duration) ([]byte, error) {
out, err := c.PostForm(c.endpoint("ca", "user", "certs"), url.Values{
"key": []string{string(key)},
"id": []string{id},
"user": []string{user},
"ttl": []string{ttl.String()},
})
if err != nil {
return nil, err
}
var cert *certResponse
if err := json.Unmarshal(out, &cert); err != nil {
return nil, err
}
return []byte(cert.Cert), err
}
func (c *Client) ResetHostCA() error {
_, err := c.PostForm(c.endpoint("ca", "host", "keys"), url.Values{})
return err
}
func (c *Client) ResetUserCA() error {
_, err := c.PostForm(c.endpoint("ca", "user", "keys"), url.Values{})
return err
}
func (c *Client) PostForm(endpoint string, vals url.Values) ([]byte, error) {
return c.RoundTrip(func() (*http.Response, error) {
return c.client.Post(
endpoint, "application/x-www-form-urlencoded",
strings.NewReader(vals.Encode()))
})
}
func (c *Client) Delete(endpoint string) error {
data, err := c.RoundTrip(func() (*http.Response, error) {
req, err := http.NewRequest("DELETE", endpoint, nil)
if err != nil {
return nil, err
}
return c.client.Do(req)
})
if err != nil {
return err
}
var re *StatusResponse
err = json.Unmarshal(data, &re)
return err
}
func (c *Client) Get(u string, params url.Values) ([]byte, error) {
baseUrl, err := url.Parse(u)
if err != nil {
return nil, err
}
baseUrl.RawQuery = params.Encode()
return c.RoundTrip(func() (*http.Response, error) {
return c.client.Get(baseUrl.String())
})
}
type RoundTripFn func() (*http.Response, error)
func (c *Client) RoundTrip(fn RoundTripFn) ([]byte, error) {
re, err := fn()
if err != nil {
return nil, err
}
defer re.Body.Close()
body, err := ioutil.ReadAll(re.Body)
if err != nil {
return nil, err
}
if re.StatusCode != http.StatusOK && re.StatusCode != http.StatusCreated {
var s *StatusResponse
if err := json.Unmarshal(body, &s); err != nil {
return nil, fmt.Errorf(
"failed to decode response '%s', error: %v", string(body), err)
}
s.StatusCode = re.StatusCode
return nil, s
}
return body, nil
}
func (c *Client) endpoint(params ...string) string {
return fmt.Sprintf("%s/%s/%s", c.addr, CurrentVersion, strings.Join(params, "/"))
}
type StatusResponse struct {
StatusCode int
Message string `json:"message"`
}
func (e *StatusResponse) Error() string {
return e.Message
}
+120
View File
@@ -0,0 +1,120 @@
// Form is a prototype for the form parser library based on functional arguments
// it's an early stage, so I'm keeping it here
package form
import (
"fmt"
"net/http"
"time"
"strconv"
)
// Param is a functional argument parameter passed to the Parse function
type Param func(r *http.Request) error
// Parse parses the form using Params passed in.
// E.g.
//
// var days int
// err := Parse(r, Int("days", &days))
//
func Parse(r *http.Request, params ...Param) error {
if err := r.ParseForm(); err != nil {
return err
}
for _, p := range params {
if err := p(r); err != nil {
return err
}
}
return nil
}
func Duration(name string, out *time.Duration, predicates ...Predicate) Param {
return func(r *http.Request) error {
for _, p := range predicates {
if err := p.Pass(name, r); err != nil {
return err
}
}
v := r.PostForm.Get(name)
if v == "" {
return nil
}
d, err := time.ParseDuration(v)
if err != nil {
return &BadParameterError{Param: name, Message: err.Error()}
}
*out = d
return nil
}
}
func String(name string, out *string, predicates ...Predicate) Param {
return func(r *http.Request) error {
for _, p := range predicates {
if err := p.Pass(name, r); err != nil {
return err
}
}
*out = r.PostForm.Get(name)
return nil
}
}
func Int(name string, out *int, predicates ...Predicate) Param {
return func(r *http.Request) error {
v := r.PostForm.Get(name)
for _, p := range predicates {
if err := p.Pass(name, r); err != nil {
return err
}
}
if v == "" {
return nil
}
p, err := strconv.Atoi(v)
if err != nil {
return err
}
*out = p
return nil
}
}
type Predicate interface {
Pass(param string, r *http.Request) error
}
type PredicateFunc func(param string, r *http.Request) error
func (p PredicateFunc) Pass(param string, r *http.Request) error {
return p(param, r)
}
func Required() Predicate {
return PredicateFunc(func(param string, r *http.Request) error {
if r.PostForm.Get(param) == "" {
return &MissingParameterError{Param: param}
}
return nil
})
}
type MissingParameterError struct {
Param string
}
func (p *MissingParameterError) Error() string {
return fmt.Sprintf("missing required parameter: '%v'", p.Param)
}
type BadParameterError struct {
Param string
Message string
}
func (p *BadParameterError) Error() string {
return fmt.Sprintf("bad parameter '%v', error: %v", p.Param, p.Message)
}
+118
View File
@@ -0,0 +1,118 @@
// openssh auth is a wrapper around openssh ssh-keygen program implementing Authority interface
package openssh
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"time"
"github.com/mailgun/log"
)
func New() *oauth {
return &oauth{}
}
// oAuth is an authority implementation based on open ssh toolkit
type oauth struct {
}
func (a *oauth) GenerateKeyPair(passphrase string) ([]byte, []byte, error) {
dir, err := ioutil.TempDir("", "teleport-auth")
if err != nil {
return nil, nil, err
}
defer os.RemoveAll(dir)
cmd := exec.Command("ssh-keygen", "-f", path.Join(dir, "key"), "-N", passphrase)
if out, e := cmd.CombinedOutput(); e != nil {
return nil, nil, fmt.Errorf("%v, out: %v", err, string(out))
}
priv, err := ioutil.ReadFile(path.Join(dir, "key"))
if err != nil {
return nil, nil, err
}
pub, err := ioutil.ReadFile(path.Join(dir, "key.pub"))
if err != nil {
return nil, nil, err
}
return priv, pub, nil
}
// GenerateHostCert implements call to the following command:
// ssh-keygen -s server_ca -I host_auth_server -h -n auth.example.com -V +52w /etc/ssh/ssh_host_rsa_key.pub
func (a *oauth) GenerateHostCert(pkey, key []byte, id, hostname string, ttl time.Duration) ([]byte, error) {
dir, err := writeKeys(pkey, key)
defer os.RemoveAll(dir)
if err != nil {
return nil, err
}
args := []string{"-s", path.Join(dir, "pkey"), "-I", id, "-h", "-n", hostname}
args = addTTL(args, ttl)
args = append(args, path.Join(dir, "key"))
cmd := exec.Command("ssh-keygen", args...)
if out, e := cmd.CombinedOutput(); e != nil {
return nil, fmt.Errorf("%v, out: %v", err, string(out))
}
cert, err := ioutil.ReadFile(path.Join(dir, "key-cert.pub"))
if err != nil {
return nil, err
}
return cert, nil
}
// GenerateUserCert implements call to the following command:
// ssh-keygen -s users_ca -I user_username -n username -V +52w id_rsa.pub
func (a *oauth) GenerateUserCert(pkey, key []byte, id, username string, ttl time.Duration) ([]byte, error) {
dir, err := writeKeys(pkey, key)
defer os.RemoveAll(dir)
if err != nil {
return nil, err
}
args := []string{"-s", path.Join(dir, "pkey"), "-I", id, "-n", username}
args = addTTL(args, ttl)
args = append(args, path.Join(dir, "key"))
log.Infof("generate certificate: %v", args)
cmd := exec.Command("ssh-keygen", args...)
if out, e := cmd.CombinedOutput(); e != nil {
return nil, fmt.Errorf("%v, out: %v", err, string(out))
}
cert, err := ioutil.ReadFile(path.Join(dir, "key-cert.pub"))
if err != nil {
return nil, err
}
return cert, nil
}
func writeKeys(pkey, key []byte) (string, error) {
dir, err := ioutil.TempDir("", "teleport-auth")
if err != nil {
return "", err
}
if err := ioutil.WriteFile(path.Join(dir, "pkey"), pkey, 0600); err != nil {
return "", err
}
if err := ioutil.WriteFile(path.Join(dir, "key"), key, 0600); err != nil {
return "", err
}
return dir, nil
}
func addTTL(args []string, ttl time.Duration) []string {
if ttl == 0 {
return args
}
return append(args, "-V", fmt.Sprintf("+%v", time.Now().Format("20060102150405")))
}
+37
View File
@@ -0,0 +1,37 @@
package openssh
import (
"testing"
"github.com/gravitational/teleport/auth/test"
. "gopkg.in/check.v1"
)
func TestO(t *testing.T) { TestingT(t) }
type OSuite struct {
suite *test.AuthSuite
}
var _ = Suite(&OSuite{})
func (s *OSuite) SetUpSuite(c *C) {
s.suite = &test.AuthSuite{A: New()}
}
func (s *OSuite) TestGenerateKeypairEmptyPass(c *C) {
s.suite.GenerateKeypairEmptyPass(c)
}
func (s *OSuite) TestGenerateKeypairPass(c *C) {
s.suite.GenerateKeypairPass(c)
}
func (s *OSuite) TestGenerateHostCert(c *C) {
s.suite.GenerateHostCert(c)
}
func (s *OSuite) TestGenerateUserCert(c *C) {
s.suite.GenerateUserCert(c)
}
+265
View File
@@ -0,0 +1,265 @@
package auth
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/julienschmidt/httprouter"
"github.com/gravitational/teleport/auth/form"
"github.com/gravitational/teleport/backend"
)
type Config struct {
Backend backend.Backend
Addr string
}
// APISrv implements http API server for authority
type APIServer struct {
httprouter.Router
s *AuthServer
}
func NewAPIServer(s *AuthServer) *APIServer {
srv := &APIServer{
s: s,
}
srv.Router = *httprouter.New()
// Auth is for operations involving CA
srv.POST("/v1/ca/host/keys", srv.ResetHostCA)
srv.POST("/v1/ca/user/keys", srv.ResetUserCA)
// Retrieving authority public keys
srv.GET("/v1/ca/host/keys/pub", srv.GetHostCAPub)
srv.GET("/v1/ca/user/keys/pub", srv.GetUserCAPub)
// Generating certificates for user and host authorities
srv.POST("/v1/ca/host/certs", srv.GenerateHostCert)
srv.POST("/v1/ca/user/certs", srv.GenerateUserCert)
// Operations on users
srv.GET("/v1/users", srv.GetUsers)
srv.DELETE("/v1/users/:user", srv.DeleteUser)
// Operations on user keys
srv.POST("/v1/users/:user/keys", srv.UpsertUserKey)
srv.DELETE("/v1/users/:user/keys/:key", srv.DeleteUserKey)
srv.GET("/v1/users/:user/keys", srv.GetUserKeys)
// Generating keypairs
srv.POST("/v1/keypair", srv.GenerateKeyPair)
return srv
}
func (s *APIServer) UpsertUserKey(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
var id, key string
var ttl time.Duration
err := form.Parse(r,
form.String("key", &key, form.Required()),
form.String("id", &id, form.Required()),
form.Duration("ttl", &ttl))
if err != nil {
replyErr(w, http.StatusBadRequest, err)
return
}
cert, err := s.s.UpsertUserKey(p[0].Value, backend.AuthorizedKey{ID: id, Value: []byte(key)}, ttl)
if err != nil {
replyErr(w, http.StatusBadRequest, err)
return
}
reply(w, http.StatusOK, certResponse{Cert: string(cert)})
}
func (s *APIServer) GetUsers(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
users, err := s.s.GetUsers()
if err != nil {
replyErr(w, http.StatusBadRequest, err)
return
}
reply(w, http.StatusOK, &usersResponse{Users: users})
}
func (s *APIServer) DeleteUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
user := p[0].Value
if err := s.s.DeleteUser(user); err != nil {
replyErr(w, http.StatusBadRequest, err)
return
}
reply(w, http.StatusOK, message(fmt.Sprintf("user '%v' deleted", user)))
}
func (s *APIServer) GetUserKeys(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
keys, err := s.s.GetUserKeys(p[0].Value)
if err != nil {
replyErr(w, http.StatusBadRequest, err)
return
}
reply(w, http.StatusOK, &pubKeysResponse{PubKeys: keys})
}
func (s *APIServer) DeleteUserKey(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
user, keyID := p[0].Value, p[1].Value
if err := s.s.DeleteUserKey(user, keyID); err != nil {
replyErr(w, http.StatusBadRequest, err)
return
}
reply(w, http.StatusOK, message(fmt.Sprintf("key '%v' deleted for user '%v'", keyID, user)))
}
func (s *APIServer) GenerateKeyPair(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
var pass string
if err := form.Parse(r, form.String("pass", &pass)); err != nil {
replyErr(w, http.StatusBadRequest, err)
return
}
priv, pub, err := s.s.GenerateKeyPair(pass)
if err != nil {
replyErr(w, http.StatusInternalServerError, err)
return
}
reply(w, http.StatusOK, &keyPairResponse{PrivKey: priv, PubKey: string(pub)})
}
func (s *APIServer) ResetHostCA(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
var pass string
if err := form.Parse(r, form.String("pass", &pass)); err != nil {
replyErr(w, http.StatusBadRequest, err)
return
}
if err := s.s.ResetHostCA(pass); err != nil {
replyErr(w, http.StatusInternalServerError, err)
return
}
reply(w, http.StatusOK, message("host CA regenerated"))
}
func (s *APIServer) ResetUserCA(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
var pass string
if err := form.Parse(r, form.String("pass", &pass)); err != nil {
replyErr(w, http.StatusBadRequest, err)
return
}
if err := s.s.ResetUserCA(pass); err != nil {
replyErr(w, http.StatusInternalServerError, err)
return
}
reply(w, http.StatusOK, message("user CA regenerated"))
}
func (s *APIServer) GetHostCAPub(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
bytes, err := s.s.GetHostCAPub()
if err != nil {
replyErr(w, http.StatusInternalServerError, err)
return
}
reply(w, http.StatusOK, pubKeyResponse{PubKey: string(bytes)})
}
func (s *APIServer) GetUserCAPub(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
bytes, err := s.s.GetUserCAPub()
if err != nil {
replyErr(w, http.StatusInternalServerError, err)
return
}
reply(w, http.StatusOK, pubKeyResponse{PubKey: string(bytes)})
}
func (s *APIServer) GenerateHostCert(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
var id, hostname, key string
var ttl time.Duration
err := form.Parse(r,
form.String("key", &key, form.Required()),
form.String("id", &id, form.Required()),
form.String("hostname", &hostname, form.Required()),
form.Duration("ttl", &ttl))
if err != nil {
reply(w, http.StatusBadRequest, err.Error())
}
cert, err := s.s.GenerateHostCert([]byte(key), id, hostname, ttl)
if err != nil {
reply(w, http.StatusInternalServerError, err.Error())
return
}
reply(w, http.StatusOK, certResponse{Cert: string(cert)})
}
func (s *APIServer) GenerateUserCert(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
var id, user, key string
var ttl time.Duration
err := form.Parse(r,
form.String("key", &key, form.Required()),
form.String("id", &id, form.Required()),
form.String("user", &user, form.Required()),
form.Duration("ttl", &ttl))
if err != nil {
reply(w, http.StatusBadRequest, err.Error())
}
cert, err := s.s.GenerateUserCert([]byte(key), id, user, ttl)
if err != nil {
reply(w, http.StatusInternalServerError, err.Error())
return
}
reply(w, http.StatusOK, certResponse{Cert: string(cert)})
}
func replyErr(w http.ResponseWriter, code int, err error) {
reply(w, code, message(err.Error()))
}
func reply(w http.ResponseWriter, code int, message interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
out, err := json.Marshal(message)
if err != nil {
out = []byte(`{"msg": "internal marshal error"}`)
}
w.Write(out)
}
type pubKeyResponse struct {
PubKey string `json:"pubkey"`
}
type pubKeysResponse struct {
PubKeys []backend.AuthorizedKey `json:"pubkeys"`
}
type certResponse struct {
Cert string `json:"cert"`
}
type usersResponse struct {
Users []string `json:"users"`
}
type keyPairResponse struct {
PrivKey []byte `json:"privkey"`
PubKey string `json:"pubkey"`
}
func message(msg string) map[string]interface{} {
return map[string]interface{}{"message": msg}
}
+139
View File
@@ -0,0 +1,139 @@
package auth
import (
"net/http/httptest"
"testing"
"time"
"golang.org/x/crypto/ssh"
"github.com/gravitational/teleport/auth/openssh"
"github.com/gravitational/teleport/backend"
"github.com/gravitational/teleport/backend/membk"
. "gopkg.in/check.v1"
)
func TestAPI(t *testing.T) { TestingT(t) }
type APISuite struct {
srv *httptest.Server
clt *Client
bk *membk.MemBackend
}
var _ = Suite(&APISuite{})
func (s *APISuite) SetUpSuite(c *C) {
}
func (s *APISuite) SetUpTest(c *C) {
s.bk = membk.New()
s.srv = httptest.NewServer(
NewAPIServer(
NewAuthServer(s.bk, openssh.New())))
s.clt = NewClient(s.srv.URL)
}
func (s *APISuite) TearDownTest(c *C) {
s.srv.Close()
}
func (s *APISuite) TestHostCACRUD(c *C) {
c.Assert(s.clt.ResetHostCA(), IsNil)
ca := s.bk.HostCA
c.Assert(s.clt.ResetHostCA(), IsNil)
c.Assert(ca, Not(DeepEquals), s.bk.HostCA)
key, err := s.clt.GetHostCAPub()
c.Assert(err, IsNil)
c.Assert(key, DeepEquals, s.bk.HostCA.Pub)
}
func (s *APISuite) TestUserCACRUD(c *C) {
c.Assert(s.clt.ResetUserCA(), IsNil)
ca := s.bk.UserCA
c.Assert(s.clt.ResetUserCA(), IsNil)
c.Assert(ca, Not(DeepEquals), s.bk.UserCA)
key, err := s.clt.GetUserCAPub()
c.Assert(err, IsNil)
c.Assert(key, DeepEquals, s.bk.UserCA.Pub)
}
func (s *APISuite) TestGenerateKeyPair(c *C) {
priv, pub, err := s.clt.GenerateKeyPair("")
c.Assert(err, IsNil)
// make sure we can parse the private and public key
_, err = ssh.ParsePrivateKey(priv)
c.Assert(err, IsNil)
_, _, _, _, err = ssh.ParseAuthorizedKey(pub)
c.Assert(err, IsNil)
}
func (s *APISuite) TestGenerateHostCert(c *C) {
c.Assert(s.clt.ResetHostCA(), IsNil)
_, pub, err := s.clt.GenerateKeyPair("")
c.Assert(err, IsNil)
// make sure we can parse the private and public key
cert, err := s.clt.GenerateHostCert(pub, "id1", "a.example.com", time.Hour)
c.Assert(err, IsNil)
_, _, _, _, err = ssh.ParseAuthorizedKey(cert)
c.Assert(err, IsNil)
}
func (s *APISuite) TestGenerateUserCert(c *C) {
c.Assert(s.clt.ResetUserCA(), IsNil)
_, pub, err := s.clt.GenerateKeyPair("")
c.Assert(err, IsNil)
// make sure we can parse the private and public key
cert, err := s.clt.GenerateUserCert(pub, "id1", "user1", time.Hour)
c.Assert(err, IsNil)
_, _, _, _, err = ssh.ParseAuthorizedKey(cert)
c.Assert(err, IsNil)
}
func (s *APISuite) TestKeysCRUD(c *C) {
c.Assert(s.clt.ResetUserCA(), IsNil)
_, pub, err := s.clt.GenerateKeyPair("")
c.Assert(err, IsNil)
// make sure we can parse the private and public key
cert, err := s.clt.GenerateUserCert(pub, "id1", "user1", time.Hour)
c.Assert(err, IsNil)
_, _, _, _, err = ssh.ParseAuthorizedKey(cert)
c.Assert(err, IsNil)
}
func (s *APISuite) TestUserKeyCRUD(c *C) {
c.Assert(s.clt.ResetUserCA(), IsNil)
_, pub, err := s.clt.GenerateKeyPair("")
c.Assert(err, IsNil)
key := backend.AuthorizedKey{ID: "id", Value: pub}
cert, err := s.clt.UpsertUserKey("user1", key, 0)
c.Assert(err, IsNil)
c.Assert(string(s.bk.Keys["user1"]["id"].Value), DeepEquals, string(cert))
_, _, _, _, err = ssh.ParseAuthorizedKey(cert)
c.Assert(err, IsNil)
keys, err := s.clt.GetUserKeys("user1")
c.Assert(err, IsNil)
c.Assert(len(keys), Equals, 1)
c.Assert(string(keys[0].Value), DeepEquals, string(cert))
c.Assert(s.clt.DeleteUserKey("user1", "id"), IsNil)
_, ok := s.bk.Keys["user1"]["id"]
c.Assert(ok, Equals, false)
}
+63
View File
@@ -0,0 +1,63 @@
// package test contains CA authority acceptance test suite
package test
import (
"testing"
"time"
"golang.org/x/crypto/ssh"
"github.com/gravitational/teleport/auth"
. "gopkg.in/check.v1"
)
func TestAuth(t *testing.T) { TestingT(t) }
type AuthSuite struct {
A auth.Authority
}
func (s *AuthSuite) GenerateKeypairEmptyPass(c *C) {
priv, pub, err := s.A.GenerateKeyPair("")
c.Assert(err, IsNil)
// make sure we can parse the private and public key
_, err = ssh.ParsePrivateKey(priv)
c.Assert(err, IsNil)
_, _, _, _, err = ssh.ParseAuthorizedKey(pub)
c.Assert(err, IsNil)
}
func (s *AuthSuite) GenerateKeypairPass(c *C) {
_, pub, err := s.A.GenerateKeyPair("pass1")
c.Assert(err, IsNil)
// make sure we can parse the private and public key
// TODO(klizhentas) test the private key actually
_, _, _, _, err = ssh.ParseAuthorizedKey(pub)
c.Assert(err, IsNil)
}
func (s *AuthSuite) GenerateHostCert(c *C) {
priv, pub, err := s.A.GenerateKeyPair("")
c.Assert(err, IsNil)
cert, err := s.A.GenerateHostCert(priv, pub, "auth", "auth.example.com", time.Hour)
c.Assert(err, IsNil)
_, _, _, _, err = ssh.ParseAuthorizedKey(cert)
c.Assert(err, IsNil)
}
func (s *AuthSuite) GenerateUserCert(c *C) {
priv, pub, err := s.A.GenerateKeyPair("")
c.Assert(err, IsNil)
cert, err := s.A.GenerateUserCert(priv, pub, "user", "user", time.Hour)
c.Assert(err, IsNil)
_, _, _, _, err = ssh.ParseAuthorizedKey(cert)
c.Assert(err, IsNil)
}
+92
View File
@@ -0,0 +1,92 @@
// backend represents interface for accessing configuration backend for storing ACL lists and other settings
package backend
import (
"fmt"
"time"
)
// Backend represents configuration backend implementation for Teleport
type Backend interface {
// UpsertUserCA upserts the user certificate authority keys in OpenSSH authorized_keys format
UpsertUserCA(CA) error
// GetUserCAPub returns the user certificate authority public key
GetUserCAPub() ([]byte, error)
// GetCA returns private, public key and certificate for user CA
GetUserCA() (*CA, error)
// UpsertHostCA upserts host certificate authority keys in OpenSSH authorized_keys format
UpsertHostCA(CA) error
// GetHostCA returns private, public key and certificate for host CA
GetHostCA() (*CA, error)
// GetHostCACert returns the host certificate authority certificate
GetHostCAPub() ([]byte, error)
// GetUserKeys returns a list of authorized keys for a given user
// in a OpenSSH key authorized_keys format
GetUserKeys(user string) ([]AuthorizedKey, error)
// GetUsers returns a list of users registered in the backend
GetUsers() ([]string, error)
// DeleteUser deletes a user with all the keys from the backend
DeleteUser(user string) error
// Upsert Public key in OpenSSH authorized Key format
// user is a user name, keyID is a unique IDentifier for the key
// in case if ttl is 0, the key will be upserted permanently, otherwise
// it will expire in ttl seconds
UpsertUserKey(user string, key AuthorizedKey, ttl time.Duration) error
// DeleteUserKey deletes user key by given ID
DeleteUserKey(user, key string) error
// GetServers returns a list of registered servers
GetServers() ([]Server, error)
// UpsertServer registers server presence, permanently if ttl is 0 or
// for the specified duration with second resolution if it's >= 1 second
UpsertServer(s Server, ttl time.Duration) error
}
// CA is a set of private and public keys
type CA struct {
Pub []byte
Priv []byte
}
// Server represents a running Teleport server instance
type Server struct {
ID string
Addr string
}
// AuthorizedKey is a key in form of OpenSSH authorized keys
type AuthorizedKey struct {
ID string `json:"id"`
Value []byte `json:"value"`
}
type NotFoundError struct {
Message string
}
func (n *NotFoundError) Error() string {
if n.Message != "" {
return n.Message
} else {
return "Object not found"
}
}
type MissingParameterError struct {
Param string
}
func (m *MissingParameterError) Error() string {
return fmt.Sprintf("missing required parameter '%v'", m.Param)
}
+27
View File
@@ -0,0 +1,27 @@
package etcdbk
import (
"encoding/json"
"fmt"
"github.com/gravitational/teleport/backend"
)
// cfg represents JSON config for etcd backlend
type cfg struct {
Nodes []string `json:"nodes"`
Key string `json:"key"`
}
// FromString initialized the backend from backend-specific string
func FromString(v string) (backend.Backend, error) {
if len(v) == 0 {
return nil, fmt.Errorf(`please supply a valid dictionary, e.g. {"nodes": ["http://localhost:4001]}`)
}
var c *cfg
if err := json.Unmarshal([]byte(v), &c); err != nil {
return nil, fmt.Errorf("invalid backend configuration format, err: %v", err)
}
return New(c.Nodes, c.Key)
}
+236
View File
@@ -0,0 +1,236 @@
// etcdbk implements Etcd powered backend
package etcdbk
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/coreos/go-etcd/etcd"
"github.com/gravitational/teleport/backend"
)
type BackendOption func(b *bk) error
func Consistency(v string) BackendOption {
return func(b *bk) error {
b.etcdConsistency = v
return nil
}
}
type bk struct {
nodes []string
etcdConsistency string
etcdKey string
client *etcd.Client
cancelC chan bool
stopC chan bool
}
func New(nodes []string, etcdKey string, options ...BackendOption) (backend.Backend, error) {
if len(nodes) == 0 {
return nil, fmt.Errorf("empty list of etcd nodes, supply at least one node")
}
if len(etcdKey) == 0 {
return nil, fmt.Errorf("supply a valid etcd key")
}
b := &bk{
nodes: nodes,
etcdKey: etcdKey,
cancelC: make(chan bool, 1),
stopC: make(chan bool, 1),
}
b.etcdConsistency = etcd.WEAK_CONSISTENCY
for _, o := range options {
if err := o(b); err != nil {
return nil, err
}
}
if err := b.reconnect(); err != nil {
return nil, err
}
return b, nil
}
func (b *bk) Close() error {
return nil
}
func (b *bk) key(keys ...string) string {
return strings.Join(append([]string{b.etcdKey}, keys...), "/")
}
func (b *bk) reconnect() error {
b.client = etcd.NewClient(b.nodes)
return nil
}
func (b *bk) UpsertUserCA(a backend.CA) error {
return b.upsertCA(UserCA, a)
}
func (b *bk) GetUserCA() (*backend.CA, error) {
return b.getCA(UserCA)
}
func (b *bk) GetUserCAPub() ([]byte, error) {
ca, err := b.GetUserCA()
if err != nil {
return nil, err
}
return ca.Pub, nil
}
func (b *bk) UpsertHostCA(a backend.CA) error {
return b.upsertCA(HostCA, a)
}
func (b *bk) GetHostCA() (*backend.CA, error) {
return b.getCA(HostCA)
}
func (b *bk) GetHostCAPub() ([]byte, error) {
ca, err := b.GetHostCA()
if err != nil {
return nil, err
}
return ca.Pub, nil
}
// GetUsers returns a list of users registered in the backend
func (b *bk) GetUsers() ([]string, error) {
values := []string{}
re, err := b.client.Get(b.key("users"), true, false)
if err != nil {
if notFound(err) {
return values, nil
}
return nil, convertErr(err)
}
if !isDir(re.Node) {
return values, nil
}
for _, sn := range re.Node.Nodes {
values = append(values, suffix(sn.Key))
}
return values, nil
}
// DeleteUser deletes a user with all the keys from the backend
func (b *bk) DeleteUser(user string) error {
_, err := b.client.Delete(b.key("users", user), true)
return convertErr(err)
}
func (b *bk) GetUserKeys(user string) ([]backend.AuthorizedKey, error) {
values := []backend.AuthorizedKey{}
re, err := b.client.Get(b.key("users", user, "keys"), true, true)
if err != nil {
if notFound(err) {
return values, nil
}
return nil, convertErr(err)
}
if !isDir(re.Node) {
return values, nil
}
for _, sn := range re.Node.Nodes {
if !isDir(sn) {
values = append(values, backend.AuthorizedKey{ID: suffix(sn.Key), Value: []byte(sn.Value)})
}
}
return values, nil
}
func (b *bk) UpsertUserKey(user string, key backend.AuthorizedKey, ttl time.Duration) error {
_, err := b.client.Set(b.key("users", user, "keys", key.ID), string(key.Value), uint64(ttl/time.Second))
return convertErr(err)
}
func (b *bk) DeleteUserKey(user, keyID string) error {
_, err := b.client.Delete(b.key("users", user, "keys", keyID), true)
return convertErr(err)
}
func (b *bk) GetServers() ([]backend.Server, error) {
values := []backend.Server{}
re, err := b.client.Get(b.key("servers"), true, true)
if err != nil {
if notFound(err) {
return values, nil
}
return nil, convertErr(err)
}
if !isDir(re.Node) {
return values, nil
}
for _, sn := range re.Node.Nodes {
if !isDir(sn) {
values = append(values, backend.Server{ID: suffix(sn.Key), Addr: sn.Value})
}
}
return values, nil
}
func (b *bk) UpsertServer(s backend.Server, ttl time.Duration) error {
_, err := b.client.Set(b.key("servers", s.ID), string(s.Addr), uint64(ttl/time.Second))
return convertErr(err)
}
func (b *bk) upsertCA(id string, a backend.CA) error {
out, err := json.Marshal(a)
if err != nil {
return err
}
_, err = b.client.Set(b.key("auth", id, "key"), string(out), 0)
return convertErr(err)
}
func (b *bk) getCA(id string) (*backend.CA, error) {
re, err := b.client.Get(b.key("auth", id, "key"), false, false)
if err != nil {
return nil, convertErr(err)
}
var ca *backend.CA
if err := json.Unmarshal([]byte(re.Node.Value), &ca); err != nil {
return nil, err
}
return ca, nil
}
func notFound(e error) bool {
err, ok := e.(*etcd.EtcdError)
return ok && err.ErrorCode == 100
}
func convertErr(e error) error {
if e == nil {
return nil
}
switch err := e.(type) {
case *etcd.EtcdError:
if err.ErrorCode == 100 {
return &backend.NotFoundError{Message: err.Error()}
}
}
return e
}
func isDir(n *etcd.Node) bool {
return n != nil && n.Dir == true
}
func suffix(key string) string {
vals := strings.Split(key, "/")
return vals[len(vals)-1]
}
const (
HostCA = "host"
UserCA = "user"
)
+99
View File
@@ -0,0 +1,99 @@
package etcdbk
import (
"fmt"
"os"
"strings"
"testing"
"github.com/coreos/go-etcd/etcd"
"github.com/gravitational/teleport/backend/test"
. "gopkg.in/check.v1"
)
func TestEtcd(t *testing.T) { TestingT(t) }
type EtcdSuite struct {
bk *bk
suite test.BackendSuite
nodes []string
etcdPrefix string
client *etcd.Client
changesC chan interface{}
key string
stopC chan bool
}
var _ = Suite(&EtcdSuite{
etcdPrefix: "/teleport_test",
})
func (s *EtcdSuite) SetUpSuite(c *C) {
nodes_string := os.Getenv("TELEPORT_TEST_ETCD_NODES")
if nodes_string == "" {
// Skips the entire suite
c.Skip("This test requires etcd, provide comma separated nodes in VULCAND_TEST_ETCD_NODES environment variable")
return
}
s.nodes = strings.Split(nodes_string, ",")
}
func (s *EtcdSuite) SetUpTest(c *C) {
// Initiate a backend with a registry
b, err := New(s.nodes, s.etcdPrefix)
c.Assert(err, IsNil)
s.bk = b.(*bk)
s.client = s.bk.client
s.changesC = make(chan interface{})
s.stopC = make(chan bool)
// Delete all values under the given prefix
_, err = s.client.Get(s.etcdPrefix, false, false)
if err != nil {
if !notFound(err) {
c.Assert(err, IsNil)
}
} else {
_, err = s.bk.client.Delete(s.etcdPrefix, true)
if !notFound(err) {
c.Assert(err, IsNil)
}
}
// Set up suite
s.suite.ChangesC = s.changesC
s.suite.B = b
}
func (s *EtcdSuite) TearDownTest(c *C) {
close(s.stopC)
c.Assert(s.bk.Close(), IsNil)
}
func (s *EtcdSuite) TestFromString(c *C) {
b, err := FromString(fmt.Sprintf(`{"nodes": [%#v], "key": "%v"}`, s.nodes[0], s.etcdPrefix))
c.Assert(err, IsNil)
c.Assert(b, NotNil)
}
func (s *EtcdSuite) TestUserKeyCRUD(c *C) {
s.suite.UserKeyCRUD(c)
}
func (s *EtcdSuite) TestUserCACRUD(c *C) {
s.suite.UserCACRUD(c)
}
func (s *EtcdSuite) TestHostCACRUD(c *C) {
s.suite.HostCACRUD(c)
}
func (s *EtcdSuite) TestServerCRUD(c *C) {
s.suite.ServerCRUD(c)
}
func (s *EtcdSuite) TestUsersCRUD(c *C) {
s.suite.UsersCRUD(c)
}
+149
View File
@@ -0,0 +1,149 @@
// package membk implements in-memory backend used for tests purposes
package membk
import (
"time"
"github.com/gravitational/teleport/backend"
)
type MemBackend struct {
HostCA *backend.CA
UserCA *backend.CA
Keys map[string]map[string]backend.AuthorizedKey
Servers map[string]backend.Server
}
func New() *MemBackend {
return &MemBackend{
Keys: make(map[string]map[string]backend.AuthorizedKey),
Servers: make(map[string]backend.Server),
}
}
func (b *MemBackend) Close() error {
return nil
}
// GetUsers returns a list of users registered in the backend
func (b *MemBackend) GetUsers() ([]string, error) {
out := []string{}
for k := range b.Keys {
out = append(out, k)
}
return out, nil
}
// DeleteUser deletes a user with all the keys from the backend
func (b *MemBackend) DeleteUser(user string) error {
_, ok := b.Keys[user]
if !ok {
return &backend.NotFoundError{}
}
delete(b.Keys, user)
return nil
}
func (b *MemBackend) UpsertUserCA(a backend.CA) error {
b.UserCA = &a
return nil
}
func (b *MemBackend) GetUserCA() (*backend.CA, error) {
if b.UserCA == nil {
return nil, &backend.NotFoundError{}
}
return b.UserCA, nil
}
func (b *MemBackend) GetUserCAPub() ([]byte, error) {
ca, err := b.GetUserCA()
if err != nil {
return nil, err
}
return ca.Pub, nil
}
func (b *MemBackend) UpsertHostCA(a backend.CA) error {
b.HostCA = &a
return nil
}
func (b *MemBackend) GetHostCA() (*backend.CA, error) {
if b.HostCA == nil {
return nil, &backend.NotFoundError{}
}
return b.HostCA, nil
}
func (b *MemBackend) GetHostCAPub() ([]byte, error) {
ca, err := b.GetHostCA()
if err != nil {
return nil, err
}
return ca.Pub, nil
}
func (b *MemBackend) GetUserKeys(user string) ([]backend.AuthorizedKey, error) {
if user == "" {
return nil, &backend.MissingParameterError{Param: "user"}
}
values := []backend.AuthorizedKey{}
out, ok := b.Keys[user]
if !ok || len(out) == 0 {
return values, nil
}
for _, k := range out {
values = append(values, k)
}
return values, nil
}
func (b *MemBackend) UpsertUserKey(user string, key backend.AuthorizedKey, ttl time.Duration) error {
if user == "" {
return &backend.MissingParameterError{Param: "user"}
}
if key.ID == "" {
return &backend.MissingParameterError{Param: "key.id"}
}
if len(key.Value) == 0 {
return &backend.MissingParameterError{Param: "key.val"}
}
if _, ok := b.Keys[user]; !ok {
b.Keys[user] = map[string]backend.AuthorizedKey{}
}
b.Keys[user][key.ID] = key
return nil
}
func (b *MemBackend) DeleteUserKey(user, keyID string) error {
if user == "" {
return &backend.MissingParameterError{Param: "user"}
}
if keyID == "" {
return &backend.MissingParameterError{Param: "key.id"}
}
out, ok := b.Keys[user]
if !ok {
return &backend.NotFoundError{}
}
if _, ok := out[keyID]; !ok {
return &backend.NotFoundError{}
}
delete(out, keyID)
return nil
}
func (b *MemBackend) UpsertServer(s backend.Server, ttl time.Duration) error {
b.Servers[s.ID] = s
return nil
}
func (b *MemBackend) GetServers() ([]backend.Server, error) {
values := []backend.Server{}
for _, s := range b.Servers {
values = append(values, s)
}
return values, nil
}
+50
View File
@@ -0,0 +1,50 @@
package membk
import (
"testing"
"github.com/gravitational/teleport/backend/test"
. "gopkg.in/check.v1"
)
func TestMem(t *testing.T) { TestingT(t) }
type MemSuite struct {
bk *MemBackend
suite test.BackendSuite
}
var _ = Suite(&MemSuite{})
func (s *MemSuite) SetUpTest(c *C) {
// Initiate a backend with a registry
s.bk = New()
s.suite.ChangesC = make(chan interface{})
s.suite.B = s.bk
}
func (s *MemSuite) TearDownTest(c *C) {
c.Assert(s.bk.Close(), IsNil)
}
func (s *MemSuite) TestUserKeyCRUD(c *C) {
s.suite.UserKeyCRUD(c)
}
func (s *MemSuite) TestUserCACRUD(c *C) {
s.suite.UserCACRUD(c)
}
func (s *MemSuite) TestHostCACRUD(c *C) {
s.suite.HostCACRUD(c)
}
func (s *MemSuite) TestServerCRUD(c *C) {
s.suite.ServerCRUD(c)
}
func (s *MemSuite) TestUsersCRUD(c *C) {
s.suite.UsersCRUD(c)
}
+123
View File
@@ -0,0 +1,123 @@
// package test contains a backend acceptance test suite that is backend implementation independant
// each backend will use the suite to test itself
package test
import (
"testing"
"time"
"github.com/gravitational/teleport/backend"
. "gopkg.in/check.v1"
)
func TestBackend(t *testing.T) { TestingT(t) }
type BackendSuite struct {
B backend.Backend
ChangesC chan interface{}
}
func (s *BackendSuite) collectChanges(c *C, expected int) []interface{} {
changes := make([]interface{}, expected)
for i, _ := range changes {
select {
case changes[i] = <-s.ChangesC:
// successfully collected changes
case <-time.After(2 * time.Second):
c.Fatalf("Timeout occured waiting for events")
}
}
return changes
}
func (s *BackendSuite) expectChanges(c *C, expected ...interface{}) {
changes := s.collectChanges(c, len(expected))
for i, ch := range changes {
c.Assert(ch, DeepEquals, expected[i])
}
}
func (s *BackendSuite) UserKeyCRUD(c *C) {
k := backend.AuthorizedKey{ID: "id1", Value: []byte("val1")}
c.Assert(s.B.UpsertUserKey("user1", k, 0), IsNil)
keys, err := s.B.GetUserKeys("user1")
c.Assert(err, IsNil)
c.Assert(keys, DeepEquals, []backend.AuthorizedKey{k})
c.Assert(s.B.DeleteUserKey("user1", k.ID), IsNil)
keys, err = s.B.GetUserKeys("user1")
c.Assert(err, IsNil)
c.Assert(keys, DeepEquals, []backend.AuthorizedKey{})
}
func (s *BackendSuite) UsersCRUD(c *C) {
k := backend.AuthorizedKey{ID: "id1", Value: []byte("val1")}
c.Assert(s.B.UpsertUserKey("user1", k, 0), IsNil)
c.Assert(s.B.UpsertUserKey("user2", k, 0), IsNil)
u, err := s.B.GetUsers()
c.Assert(err, IsNil)
c.Assert(toSet(u), DeepEquals, map[string]struct{}{"user1": struct{}{}, "user2": struct{}{}})
c.Assert(s.B.DeleteUser("user1"), IsNil)
u, err = s.B.GetUsers()
c.Assert(err, IsNil)
c.Assert(toSet(u), DeepEquals, map[string]struct{}{"user2": struct{}{}})
c.Assert(s.B.DeleteUser("user1"), FitsTypeOf, &backend.NotFoundError{})
}
func (s *BackendSuite) UserCACRUD(c *C) {
ca := backend.CA{
Pub: []byte("capub"),
Priv: []byte("capriv"),
}
c.Assert(s.B.UpsertUserCA(ca), IsNil)
out, err := s.B.GetUserCA()
c.Assert(err, IsNil)
c.Assert(out, DeepEquals, &ca)
outp, err := s.B.GetUserCAPub()
c.Assert(err, IsNil)
c.Assert(outp, DeepEquals, ca.Pub)
}
func (s *BackendSuite) HostCACRUD(c *C) {
ca := backend.CA{
Pub: []byte("capub"),
Priv: []byte("capriv"),
}
c.Assert(s.B.UpsertHostCA(ca), IsNil)
out, err := s.B.GetHostCA()
c.Assert(err, IsNil)
c.Assert(out, DeepEquals, &ca)
outp, err := s.B.GetHostCAPub()
c.Assert(err, IsNil)
c.Assert(outp, DeepEquals, ca.Pub)
}
func (s *BackendSuite) ServerCRUD(c *C) {
srv := backend.Server{ID: "srv1", Addr: "localhost:2022"}
c.Assert(s.B.UpsertServer(srv, 0), IsNil)
out, err := s.B.GetServers()
c.Assert(err, IsNil)
c.Assert(out, DeepEquals, []backend.Server{srv})
}
func toSet(vals []string) map[string]struct{} {
out := make(map[string]struct{}, len(vals))
for _, v := range vals {
out[v] = struct{}{}
}
return out
}

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

+114
View File
@@ -0,0 +1,114 @@
package events
import (
"io"
"io/ioutil"
"strings"
"golang.org/x/crypto/ssh"
)
// AuthSuccess indicates a successfull connection and authentication attempt
type AuthAttempt struct {
// Session is SSH session ID
SessionID string `json:"sid"`
// User is SSH user
User string `json:"user"`
// Success - true if auth was successfull, false otherwise
Success bool `json:"success"`
// Error contains rejection reason if present
Error string `json:"error"`
// LocalAddr local connecting address
LocalAddr string `json:"laddr"`
// RemoteAddr remote connecting address
RemoteAddr string `json:"raddr"`
// Key is a public key used for auth
Key string `json:"key"`
}
func NewAuthAttempt(conn ssh.ConnMetadata, key ssh.PublicKey, success bool, err error) *AuthAttempt {
return &AuthAttempt{
SessionID: string(conn.SessionID()),
User: conn.User(),
Key: strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key))),
Success: success,
LocalAddr: conn.LocalAddr().String(),
RemoteAddr: conn.RemoteAddr().String(),
Error: errMsg(err),
}
}
func (*AuthAttempt) Schema() string {
return "teleport.auth.attempt"
}
func NewExec(command string, out io.Reader, code int, err error) *Exec {
return &Exec{
Command: command,
Out: collectOutput(out),
Code: code,
Error: errMsg(err),
}
}
// Exec is a result of execution of a remote command on the target server
type Exec struct {
// Command is a command name with arguments
Command string `json:"command"`
// Code is a return code
Code int `json:"code"`
// Error is a error if command failed to execute
Error string `json:"error"`
// Out is a captured command output
Out string `json:"out"`
}
func (*Exec) Schema() string {
return "teleport.exec"
}
func NewShell(shell string, log io.Reader, code int, err error) *Shell {
return &Shell{
Shell: shell,
Log: collectOutput(log),
Code: code,
Error: errMsg(err),
}
}
// Shell is a result of execution of a in interactive shell
type Shell struct {
// Shell is a shell name
Shell string `json:"command"`
// Code is a return code of a shell
Code int `json:"code"`
// Error is a error if shell failed to execute
Error string `json:"error"`
// Log is a captured session log
Log string `json:"log"`
}
func (*Shell) Schema() string {
return "teleport.shell"
}
func errMsg(err error) string {
if err != nil {
return err.Error()
}
return ""
}
func collectOutput(r io.Reader) string {
b, err := ioutil.ReadAll(io.LimitReader(r, 10*1024))
if err != nil {
return err.Error()
}
return string(b)
}
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA4xasuaTt0VJWPTA8rSZgpbxTkdLZkGvVxj4TSMD7m2bdjL1y
Nx84AIob4GKD6Aw6uhMAUE3MaHoswb/cdhealpcxqpfSm1toayeqD7ZPCuhX87qf
+m4H4Fpfgd3Az9TdIvufxN1jsvm6Df/9Nsrp1w9RThgPjZCJgHhTa6hCXAWAG5Q6
MEcRiiFCTVtZOClfe+GmyGYxa/2h5GglS6dH9MCDxrBOXrA/vGU/195HlZvrAY6K
EU8cZsNhNnD9KH0pGgSWIDJA+Cg31tA/DbuGGK+LINHH5Mg7cjtQJ0BPlEGMvyHe
Xlhu9QyAWCEOHSwOtSvkBogb+NUHEbKAlQAoqQIDAQABAoIBAHL2lbbeEi94OYR8
mRsnZKJC9yPJ1K7SiMT1xBl+6WHA78RCXEIfoeBUCszL0u900X5GBFCjMipqP6JO
mTOHu5ElNdWhStOvsOseh2+w4c8p1Ozku/yyG3avCc7bDrOoldMSA85MLh77Yf+Y
av6EN2HStVyNvcHzPEk3oTPNWNrwUbPSYWuz8TuU+gnTHJ1VP1h3QpcfoMxF8iJn
NDGI9Q2f9O/neX26f59shsW8NspoMQ2h/QmTfE3LW7PN/amyYbFLPWuqRszd7udH
TJtz2J9bPeVwblKxrBNtJSucVBN0QkG8DRHK9KMFoNs3hiJEXBEdMHAbAvudOqkn
2pvxW+kCgYEA9+NYlxrt+E8JVeGNM2GCcAxTL+QhcAVNs2KGBl1prCjEGwNQoikZ
FnDVZ3aBT3s9xv/I9/ZQ/5B2PDiGmFnmMZbH4QJGJax52/sVgkbxiDnfJ3wMU6N2
wit96Fpd0Fq4i6Dib/kEyEPECneF12eIRM6h3551sfp46+hwi4DzQIMCgYEA6oUV
T11Xw6I5KrK4sa3V83ssDAXQ3McxUzqP5R3yfzZhl4nTL2roZmAR24q2jB8BUmWH
8igyxGr1pkGKZV1tWghQlAoQUl0BZlPF0YLeo5WbJ6eP3GV9nlXWmebkMmgHaM4j
aoYKzye4jw5EMrrOQTVuzHyZwkpIfXGs9sr3EmMCgYEAyDQDXrbPL1aWEZiC89Zz
BI/Pt8QmBVUjNhgCaq2TJQofqDl7LBzJOEiU8mTRdyHv+QKoiyuSq3U/KM/2VKF2
uuIuOFof0VmFaddVvcwSUpV612bmhb5lsdhybfBeCGLc3VhoyUpzRFMGx/VUUW46
rme8cOCH6eCDPPSTwPe6foUCgYB+HWEvU8f9iYBqiL+vtQboL6WpNXtzLp9E3apC
EM+2P11N660ow7oES8duo3K1eXrxf24S+DEM1WQSqw4soohKsUv7wEm0sRUunQA1
QcZOfAZ61a4UXHOoMQr9ql+vB6hsYOZEw56OdhLWw1pDWZfPE1fpGVxqUh4Xb4kP
Emi5dwKBgHTsFNGuLt9stO4KwAlmUgO6AET2Stn/4yB62ygoOkPpZr2hhWlVpBwB
jHhsbThoyZkzVdnnHWc3rjDh47zPBLuMTFOju8krgvd8zN9TVUheFwfkAoZrdTyc
AKwe/utprQRX1t6JR2KzKC7CuSffJxcqlg4BmCiPtKOkmcEb+UzI
-----END RSA PRIVATE KEY-----
@@ -0,0 +1 @@
[email protected] AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg/PB+xUDXKqxFIohxZjxTlBl9ER1ZwqtAlnJGWiSJ8pMAAAADAQABAAABAQDjFqy5pO3RUlY9MDytJmClvFOR0tmQa9XGPhNIwPubZt2MvXI3HzgAihvgYoPoDDq6EwBQTcxoeizBv9x2F5qWlzGql9KbW2hrJ6oPtk8K6Ffzup/6bgfgWl+B3cDP1N0i+5/E3WOy+boN//02yunXD1FOGA+NkImAeFNrqEJcBYAblDowRxGKIUJNW1k4KV974abIZjFr/aHkaCVLp0f0wIPGsE5esD+8ZT/X3keVm+sBjooRTxxmw2E2cP0ofSkaBJYgMkD4KDfW0D8Nu4YYr4sg0cfkyDtyO1AnQE+UQYy/Id5eWG71DIBYIQ4dLA61K+QGiBv41QcRsoCVACipAAAAAAAAAAAAAAACAAAAFW5vZGUuZ3Jhdml0YXRpb25hbC5pbwAAABkAAAAVbm9kZS5ncmF2aXRhdGlvbmFsLmlvAAAAAFT6WcAAAAAAVto8IQAAAAAAAAAAAAAAAAAAARcAAAAHc3NoLXJzYQAAAAMBAAEAAAEBAMCD/UBM1jF2O/SK/QiRWRgfyQc1t8MDQGFlKpqFRvdU9hXkSdNWLGli2ukPlc8mWOY6n6WDYNDMr4zRwLXvHAaQXPu6GIxlM5QaA6hPRPECYI6b94KVS1Pb7qd4KJUYYUousaoE6BhhlcdMCgjjsdqgWCyelC3Yzz5K+L2ylLZ7O7Z9pgj1il3cbLN0nsw2yBVbDCVwNza3Gy/B3gW1ZE1ttqQWaUMS5SXl+fXkHMZ2iGTHCsQ3JMwWpAlu/YdO6KWv7jPZCnI1H1gZCYwSntR5MN4T1oKALuDD329laPC0/HfrJrVRTOZU1udtA/t6i2eZWhy6azGFgrB5hMyEeGsAAAEPAAAAB3NzaC1yc2EAAAEATiREUOp8SDmUV/pi3Kt2OcssqD8Kp1D1OPHDwCVETrP0hbKy/PizjSszw98H95oks1HbmzAPIoKjaz57fOMrJAJYL31NauuX8dO5Fng7aWp6sj3xr2IFeBFOPpUezwKEjbyEeXEKog0dodVFLfAlVNRlzkRrMXNcQ3phqeRqDLI5PpvERslM3acrBH3MHrAKauBXjjN78XeMXQzD6EdAR/yiUevcV168GIvZCCzTXZ4fh3i8WmtUabFvETeK/unlWFzf5i+5ZFadmqTrpFW2ariecJR+7Uo7D7pQ0DsUt8A4fjTFhbGNm/VGy10Z+Bie7eNPOVR1WP1ZCgOEEdGGtg== alex@vulcand
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDjFqy5pO3RUlY9MDytJmClvFOR0tmQa9XGPhNIwPubZt2MvXI3HzgAihvgYoPoDDq6EwBQTcxoeizBv9x2F5qWlzGql9KbW2hrJ6oPtk8K6Ffzup/6bgfgWl+B3cDP1N0i+5/E3WOy+boN//02yunXD1FOGA+NkImAeFNrqEJcBYAblDowRxGKIUJNW1k4KV974abIZjFr/aHkaCVLp0f0wIPGsE5esD+8ZT/X3keVm+sBjooRTxxmw2E2cP0ofSkaBJYgMkD4KDfW0D8Nu4YYr4sg0cfkyDtyO1AnQE+UQYy/Id5eWG71DIBYIQ4dLA61K+QGiBv41QcRsoCVACip alex@vulcand
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAwIP9QEzWMXY79Ir9CJFZGB/JBzW3wwNAYWUqmoVG91T2FeRJ
01YsaWLa6Q+VzyZY5jqfpYNg0MyvjNHAte8cBpBc+7oYjGUzlBoDqE9E8QJgjpv3
gpVLU9vup3golRhhSi6xqgToGGGVx0wKCOOx2qBYLJ6ULdjPPkr4vbKUtns7tn2m
CPWKXdxss3SezDbIFVsMJXA3NrcbL8HeBbVkTW22pBZpQxLlJeX59eQcxnaIZMcK
xDckzBakCW79h07opa/uM9kKcjUfWBkJjBKe1Hkw3hPWgoAu4MPfb2Vo8LT8d+sm
tVFM5lTW520D+3qLZ5laHLprMYWCsHmEzIR4awIDAQABAoIBABegWzVH+MWLOCJz
6Efj5qZCSF6dJhdFz+HkE41xK9twzt6y7KiGG983JZySsBUDJMfki/E5JWmapTTI
DTmGxc68atzt4tNgHCth4e8HA3P8kbMOyxp4jg0Vob1lzr01h8Md5enyvjU3zLTv
zQ94OjyVjxyyvYa+4/hGJS7KutuGH2AYQn3TBNbcTXNTdz0wSPuucQRA/ytgfEIC
43g4dIAwoBiAGqzUDhUIojo7Khej9qW80OxfxYcZX2n9JVa8Ihy+hKuvD+O3PBr0
b0MhfmJOZLx1QxiCMPe9jsCog6dNrxK45IrNoq4L8eLswRci8K1IbBh1rpLZMfJh
iq0An1kCgYEA+JM3OsSlQ1fnThCAjeTKTmDUpdrksuSp/qzAK2JVOMEhgFx3Lw/R
3w98ahwMFb4zeQ+5KDg5MQMAGzeDDL8EcvCE7PaM5ZbgDAg8S3h6XaE3uNuLyUer
s3B7CMr8dHovwicLJfvNNtgMX1gZiFQaqmYhMhhV/BJPOaYJKXgB0FcCgYEAxkQa
MKKjZLk2pUC7ZH9sBjsdsSmaBHOsbT7zpVyvn7wh7yNcy3g0Oz0wY4ylcObGsfSp
WSKl4oGqxvhqtGge5A/81vbWMxdjuPGRXVy+0KCwzhR7pxD3QYtCO/Jh4QV0D12j
td9rJhNrYq8qnTyejlbEiJ8mz+25tqQoMuPbvA0CgYAcUGCFICsW7adaPAT4pKvq
m5r9PV65hdRbPwAwsOW2LkGLpnyo7qtgNhpc188ElwKz8neLvkO9cnMLppoV1JhE
I2NwvnC0IihpFnMPwaxmh9uaAd/9UF2ddd191elFtzlDa0EY1YK9kYRvQ9C8t5/F
8xYq55y7trYxgdkfUR0bxwKBgQCvZSXqKqbzfypJjPd6U4NEC94bkVsQXyiFyLAQ
dOtlGzDF9nmsAp3qXhpFpvHg7DeQzIlYMl+f/ugOd0smaAoT5dTecqVRs3pj7UiU
2Gxx8gYXeWVeiZLDKCgjEizmvUVMJqGiTpDisUFJnv7K9QqrjzT0TB8wyPSHD7gE
NTF5rQKBgFy7pUkat6wDrOFk4l2OW5Jfx51g3VrzDnfVg/ihiN6+wnS+cImk7XhL
8fHtSSL3kyzv33yPeg4RNatsvtUt61T0p3Xesns9ZtPswYoAerw3thSv9Ux9dV35
70oVMsPfFJ9I4Ykelk25sMcrPnAD5PvBlzYp7XkeRRhEFyzEsDz9
-----END RSA PRIVATE KEY-----
+1
View File
@@ -0,0 +1 @@
[email protected] AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgoxRrPnkEDSwVgPoAFo0z6kTyFDuQZpyRHkPrCvNLQU8AAAADAQABAAABAQDAg/1ATNYxdjv0iv0IkVkYH8kHNbfDA0BhZSqahUb3VPYV5EnTVixpYtrpD5XPJljmOp+lg2DQzK+M0cC17xwGkFz7uhiMZTOUGgOoT0TxAmCOm/eClUtT2+6neCiVGGFKLrGqBOgYYZXHTAoI47HaoFgsnpQt2M8+Svi9spS2ezu2faYI9Ypd3GyzdJ7MNsgVWwwlcDc2txsvwd4FtWRNbbakFmlDEuUl5fn15BzGdohkxwrENyTMFqQJbv2HTuilr+4z2QpyNR9YGQmMEp7UeTDeE9aCgC7gw99vZWjwtPx36ya1UUzmVNbnbQP7eotnmVocumsxhYKweYTMhHhrAAAAAAAAAAAAAAACAAAAFWF1dGguZ3Jhdml0YXRpb25hbC5pbwAAABkAAAAVYXV0aC5ncmF2aXRhdGlvbmFsLmlvAAAAAFT6WNAAAAAAVto7NwAAAAAAAAAAAAAAAAAAARcAAAAHc3NoLXJzYQAAAAMBAAEAAAEBAMCD/UBM1jF2O/SK/QiRWRgfyQc1t8MDQGFlKpqFRvdU9hXkSdNWLGli2ukPlc8mWOY6n6WDYNDMr4zRwLXvHAaQXPu6GIxlM5QaA6hPRPECYI6b94KVS1Pb7qd4KJUYYUousaoE6BhhlcdMCgjjsdqgWCyelC3Yzz5K+L2ylLZ7O7Z9pgj1il3cbLN0nsw2yBVbDCVwNza3Gy/B3gW1ZE1ttqQWaUMS5SXl+fXkHMZ2iGTHCsQ3JMwWpAlu/YdO6KWv7jPZCnI1H1gZCYwSntR5MN4T1oKALuDD329laPC0/HfrJrVRTOZU1udtA/t6i2eZWhy6azGFgrB5hMyEeGsAAAEPAAAAB3NzaC1yc2EAAAEAn3bdbRApEaQg1aggf6ErxwMSwgNVsKIiQ93hOIh3M/Uu+5z3qIsr0cqGOh2mfZilInD35sWP3lr3TtOr23soEVaaZ9cTbxNGQ8rrzRonvt/IJfGOLhy8HLz23JnUgXjHP633iMGeIWnHIWzftBU51vCiZk+em6iYCYPld7p7KjfSYI2X07H/kA6A4C8eF+2e/XeypXbFidCSO95WZv11Q/SEzxhn7GY7SmsmY7Zwwt+TWxYmlvOnJ40VElR/9APShb3e24V9jjkV2HQj3e+HU6EZA1fObDIDcgoYp81MWJXWVD7EaMb3ZQv7gzfI9uvo5FpLwkv/bIqjsDzMSvvfJw== alex@vulcand
+1
View File
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDAg/1ATNYxdjv0iv0IkVkYH8kHNbfDA0BhZSqahUb3VPYV5EnTVixpYtrpD5XPJljmOp+lg2DQzK+M0cC17xwGkFz7uhiMZTOUGgOoT0TxAmCOm/eClUtT2+6neCiVGGFKLrGqBOgYYZXHTAoI47HaoFgsnpQt2M8+Svi9spS2ezu2faYI9Ypd3GyzdJ7MNsgVWwwlcDc2txsvwd4FtWRNbbakFmlDEuUl5fn15BzGdohkxwrENyTMFqQJbv2HTuilr+4z2QpyNR9YGQmMEp7UeTDeE9aCgC7gw99vZWjwtPx36ya1UUzmVNbnbQP7eotnmVocumsxhYKweYTMhHhr alex@vulcand
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAvPrcdRCK3JipJ8cO+jMqWt9Gwh22qLaKVAlz7iZ+9i4mLa66
OOLk0H8jL+HoBl4Mr3rjfAtL7YTnitVB+AvupPeHBdagHrY4zxxTkkSR4Dikts1h
3eUAbDFwvOnylTPYTSATOVX55uI/IA7FwMfKD3ynx4Jzg0DXb0xq4rHVz6xbkE6L
IkFzauaMTEyvHXEqPfqo67SsUkfbpDNdX7m1dNWz5kKFWGl3m09qjm/9gHkJtdeS
nts8ncDDGIlw/Yiwxdr0RTjxhRShsKpE6mDl2VdgX60Cgu6AtV4F73JEVcIZNQdZ
Hd89g6VuqODD/5Pk2DVhCaZz5oE1V7nAmeG52QIDAQABAoIBAH2fUN8ssPI1NPTx
OP6O9o/TrmipcduzBD8azYq44lrpkI17geUAxtPmP9C7ng5lhI895Gx34fAxEe3T
F3g5DllxBbuW0Xe48AiVLuSjWkkvviaaXn0vlllVgDjEV3JhNV8HfF/3zBhzuzyn
NUOSS7qBxdI04G6Wst8UcFTjnFrgdKvdZKlM80qwXMSmvJTZx58v+MRLStjFo+5r
jViLoealNfKlsKtIKw9uoE8TxxpQsKAdYiS4wRUgMsdLYk7Dablv70MJ0JsY7MnZ
X3pMu2makbTmXD4nexySqjJXONsqyoVeiLksXo59ZTBrN9YxNhDQYZlJrNxoisCG
CFbgYyUCgYEA+CGy8z4m/68IwD2CWs0eU9DIZsBWaYggw7gNLtASmlhOoln7KJcK
WasHBGHPLlruYNxcNBVo5P1x6VwS8GwOU/7nPUJYDtEDG6QHV6kBoH0dTyNloJ/q
aBMf7sJS23ZjCq6Kz/i8TNriHPlSEIZvzhAYs3Hff4LFvqYTjWUQhA8CgYEAwvj5
4YdIsGQSK29/yBKt58YExT57v0o6q0gQIq3Bzhnn+IKOPvl8LdMUXb/emN9wWhFD
7lQqw7cTsdJVXMNkiPk4PWtjOTVldIWRuxpvSRV2/aEjjEl28k1v+0IOskVVKbVC
5RBB7WGPdnvuk6+eoaBPMyyiJpENuwdeI9ol25cCgYEAyKswNsmX7oWWtSnJZzel
hDFAMODxwW0G2mHZv3ck5E/wYBoXJY9pHnVTOncDf8CoMgtkQfbimvZhDlGZO3iX
6dQnHIKwBmOAePUhvwH+P9WPQ3Yj9U/+2WZGF+ht6/CFTpkzx9eoKRuHa9kyRg0D
FIot/8li75NJ6tIS0/Hc9n8CgYBaQ7REEkK3zwyOaWdqNCOpCwcIXj4iOHZi99Do
jTMNfPblGWRLiWtRN8jVAlBGzCMSv62IeBT4WRjYGX2Fnbw8oczEuqPMR4k54eCv
E7Tzy+kz0NdJSnW0Bd/THij5pZmg3pciv10WTjWB8Cxq2UKCp1ojYgWL/1mw/d86
j/M47wKBgHhhgpK/FNq8B4XxiHXF/bUXl0w4ihsfoA1NvbN3LH0H0dPkFGnBuVdl
3xzqOA0EFyVGeW8iTP2mmLnXBdI5hxwIulfPMhgaXIToRJCI8X+p2Yu8d8vGIKoi
9Vx4s5sU+d7gCSR/JLyzOCZQ4/SV0d57LL8WRbPqY/V4+DKZrefU
-----END RSA PRIVATE KEY-----
+1
View File
@@ -0,0 +1 @@
[email protected] AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgJoVSfvG7nznWevg20VB0k3fzPraM17uLHBJRqYsBjMMAAAADAQABAAABAQC8+tx1EIrcmKknxw76Mypa30bCHbaotopUCXPuJn72LiYtrro44uTQfyMv4egGXgyveuN8C0vthOeK1UH4C+6k94cF1qAetjjPHFOSRJHgOKS2zWHd5QBsMXC86fKVM9hNIBM5Vfnm4j8gDsXAx8oPfKfHgnODQNdvTGrisdXPrFuQTosiQXNq5oxMTK8dcSo9+qjrtKxSR9ukM11fubV01bPmQoVYaXebT2qOb/2AeQm115Ke2zydwMMYiXD9iLDF2vRFOPGFFKGwqkTqYOXZV2BfrQKC7oC1XgXvckRVwhk1B1kd3z2DpW6o4MP/k+TYNWEJpnPmgTVXucCZ4bnZAAAAAAAAAAAAAAABAAAABGtleTEAAAAIAAAABGFsZXgAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAARcAAAAHc3NoLXJzYQAAAAMBAAEAAAEBANeuX5E5lTYaFX5yWJhIWADg0xSeQsniCEoPWqbY3udVmw36Z7wWEvKce/gz+USMcvaP6Jm5eIu20suwrY1t/5fwUJufBXpSQt3jpfIcb7WGA6k6Yaqfu9JHOcN2ut8Qy0cNoBJuoMtAyjwDRXnneDtL8EUOd+Ek+8vcO3KiT+IrIgbLVyWDnHPIu8N/p17U7c/XhZgAPVCK4I2IE57RoGtAKCmsn17kD+BMMldxTb1vTAureUqX+aPBOUP3Gwq02pRkh7LISbvS1WYbaolg6ln8YYq1Qc3YZih5sG/PnjcrtSvQxAP5fMAtMVRxZedpwFHkBJNhsFDNRnGObDank2EAAAEPAAAAB3NzaC1yc2EAAAEAw1YQT0jYTM/dTag/vl5PjsGsMpJRYnMhqGg+zOBeOoY29vFZ0FQNSxOW5EWhbW1JDLV1cPKJ4I/cIOqDLWfbnBfz7xxcGpONs1jCKVOAIHScnoA9tP8yizWTlXD04Bk3OwP/dwkpgTlhzWgfYxbPTiWk9fL0/7BrQR6+jX3jKrKKDV2yw1oxpOKh7V5OS95cUUWY72V8LpKWdUG6LWdyQLVZ5Q5pet+2cAu2s+ayaW43rryBsV+/Z2HtwjCaK91/h2j5rBy4xLwoYldlwrzUOaIsssJzRh8ki2UigeSGIqYB6iT/4XMCMHa4OXbjBR3tPliHx7VXUEAjskDD5R42mA== alex@vulcand
+1
View File
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8+tx1EIrcmKknxw76Mypa30bCHbaotopUCXPuJn72LiYtrro44uTQfyMv4egGXgyveuN8C0vthOeK1UH4C+6k94cF1qAetjjPHFOSRJHgOKS2zWHd5QBsMXC86fKVM9hNIBM5Vfnm4j8gDsXAx8oPfKfHgnODQNdvTGrisdXPrFuQTosiQXNq5oxMTK8dcSo9+qjrtKxSR9ukM11fubV01bPmQoVYaXebT2qOb/2AeQm115Ke2zydwMMYiXD9iLDF2vRFOPGFFKGwqkTqYOXZV2BfrQKC7oC1XgXvckRVwhk1B1kd3z2DpW6o4MP/k+TYNWEJpnPmgTVXucCZ4bnZ alex@vulcand
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAt5MWQS84lbJfdWRcAQLdcs06e6HqvDAVPWZjXHXP+0YF5Ljj
SgbJxqZqJ3QkB7zDwI8M7iXlKD1GU/8sVea4a47kSHIRXJ7TVmi/KIqAW/wu/Pwv
dfAZcJAG3wRhr6QXqOXwddqzCup7IQ4SSY+1k4+PM/qpX+QdP6Qcj1SjetUmQ3lc
CjThT7y9CY6xDdDW5hyPJgGeQ3woT1w/ADoT3EucbKWBzUpOgiSHae/oFtGQNI/G
TMhjH0AweE2fpulBoUBy1Onx0wO9NfbRFSR3fZhlSBoEOE3Yzem1yueghUhHaEZ4
faxAtgHfAKTiSwRuBHxNRJZcdIX2uEhMk/L3OwIDAQABAoIBACF5En/ESn8RTVKs
gBI3IYYDYfyVI6Y45kaR2uBLIydgN0bjZ55h6nzsb0LObJt0kgUUkHkEM+rIcYBz
cPQg49PI9Fs5YW0QEN0asHCGAK3FKBQ9L7JDm3/o4zfbJqzOMPo9I0F6JVeCNf1r
AW6kY6mKbcQ+mo+/mo22MPb5qe2FjSuXv4OJ4ZEKPFg9SfnzhBguqfagv8tVEX1h
eWrUp/nIL6cZuGZxVbFMwE8mF4qsyUWADpe6xl3JgjLvSyR0y9JWCYx2qjUScQDB
L8r86jaoT7j58GOg9mH/kfK9Soh6DHCh2wDVHKLwakvAKYP66FwxinOVwHfWNfnk
nSSnFOECgYEA2cj5cQWcH1b5JCDWNxlFVcHeqTf011VUby8cwWHwLrxpQRJwR8Fr
JFqf9GV0OiFpYfax8NyjrKocS3frTneAegtXaN7yOZ8TECzHgKvEhx4fkuoc1kwo
TpXa+fayL7VpX6wPqQuoUKqVR5Ac3v0zRlbCj+UQVcQRD7kv/pldNyMCgYEA18lb
pbWwV3tRTK1jS6fn4LFbiz3l6E4RK0GEoDAW2WAmBYGCqKBWA6pxeUUKz31u84mh
l2cd2v4TfDFS+hYWz5LJHyiQMPI+nBYeuKZ080Cyp5R+zLdKISRZiuSif0xzutGh
X8aszUvMSABEYEZlTm4PSarBXZaQSM9PVqY3zQkCgYBo4hsPTU5U4YP23EGOI4N6
u3VKNYKauaOCMEQ+9bXALEjcZ87fH81Doslp3/pLjI+9+BrWDsxceUhLKCOQlIix
PgkInmY1TeDWZ1plI1PwZpd6H3ln0yGORylAPIiv5UQsKScZmF2lC6H85vCgi3xI
IbxtpDlrtJMq6Vqt2JpF1QKBgQCugQkbdlYYOQLjNmjRewSbAfLdJP5zIgbia0ac
4Kbpvcahtz15rshAiKrhfkMizvZV/pxuOxa6zbPmQGExG4Hw9WyuRPhujMizI3RO
sVZsjYqs2Pq+BQ/uw12iZAlBVwtULkkJkjqWf1DBfW3OSb+NcJXuZiefzq36I6Su
lxcTOQKBgQDBaiOIMQa9XADTpHIkZT/se0CUSh4mzwx/XL6k2FPx9aqJBFqj9FXi
DszQH66RD0auQw7kWvRsPNjw68CUrSMzppZirmhU1msPvB9Hw4KXQfSTOIKJXfIm
hb8/5tOZOSTogvi+z+vyyQRg123oMyk6MD1vOafPGOYXAQfC144BtQ==
-----END RSA PRIVATE KEY-----
+1
View File
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3kxZBLziVsl91ZFwBAt1yzTp7oeq8MBU9ZmNcdc/7RgXkuONKBsnGpmondCQHvMPAjwzuJeUoPUZT/yxV5rhrjuRIchFcntNWaL8oioBb/C78/C918BlwkAbfBGGvpBeo5fB12rMK6nshDhJJj7WTj48z+qlf5B0/pByPVKN61SZDeVwKNOFPvL0JjrEN0NbmHI8mAZ5DfChPXD8AOhPcS5xspYHNSk6CJIdp7+gW0ZA0j8ZMyGMfQDB4TZ+m6UGhQHLU6fHTA7019tEVJHd9mGVIGgQ4TdjN6bXK56CFSEdoRnh9rEC2Ad8ApOJLBG4EfE1Ellx0hfa4SEyT8vc7 alex@vulcand
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
# this scripts inits proper keys using local fixtures file
# set user certificate authority
etcdctl set /teleport/auth/user/key "{\"Pub\": \"$(base64 -w 0 fixtures/keys/usr_ca/user_ca.pub)\"}"
# add user alex
etcdctl set /teleport/users/alex/keys/k1 "$(cat fixtures/keys/users/alex-cert.pub)"
+17
View File
@@ -0,0 +1,17 @@
language: go
go:
- 1.4
build_image: shippableimages/ubuntu1404_go
before_install:
- source $HOME/.gvm/scripts/gvm
- gvm install go$SHIPPABLE_GO_VERSION
- gvm use go$SHIPPABLE_GO_VERSION
- export GOPATH=/root/workspace/
- go get golang.org/x/tools/cmd/cover
script:
- echo $GOPATH
- make test
+161
View File
@@ -0,0 +1,161 @@
package srv
import (
"fmt"
"io"
"sync"
"sync/atomic"
"github.com/codahale/lunk"
"github.com/mailgun/log"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent" // ctxID is a incremental context ID used for debugging and logging purposes
)
var ctxID int32
// closeMsg is a close request
type closeMsg struct {
reason string
}
// ctx holds session specific context, such as SSH auth agents
// PTYs, and other resources. ctx can be used to attach resources
// that should be closed once the session closes.
type ctx struct {
// srv is a pointer to the server holding the context
srv *Server
// this event id will be associated with all events emitted with this context
eid lunk.EventID
// server specific incremental session id
id int
// info about connection for debugging purposes
info ssh.ConnMetadata
sync.RWMutex
// term holds PTY if it was requested by the session
term *term
// agent is a client to remote SSH agent
agent agent.Agent
// agentCh is SSH channel using SSH agent protocol
agentCh ssh.Channel
// result channel will be used by remote executions
// that are processed in separate process, once the result is collected
// they would send the result to this channel
result chan execResult
// close used by channel operations asking to close the session
close chan closeMsg
// closers is a list of io.Closer that will be called when session closes
// this is handy as sometimes client closes session, in this case resources
// will be properly closed and deallocated, otherwise they could be kept hanging
closers []io.Closer
}
// emit emits event
func (c *ctx) emit(e lunk.Event) {
c.srv.elog.Log(c.eid, e)
}
// addCloser adds any closer in ctx that will be called
// whenever server closes session channel
func (c *ctx) addCloser(closer io.Closer) {
c.Lock()
defer c.Unlock()
c.closers = append(c.closers, closer)
}
func (c *ctx) getAgent() agent.Agent {
c.RLock()
defer c.RUnlock()
return c.agent
}
func (c *ctx) setAgent(a agent.Agent, ch ssh.Channel) {
c.Lock()
defer c.Unlock()
if c.agentCh != nil {
log.Infof("closing previous agent channel")
c.agentCh.Close()
}
c.agentCh = ch
c.agent = a
}
func (c *ctx) getTerm() *term {
c.RLock()
defer c.RUnlock()
return c.term
}
func (c *ctx) setTerm(t *term) {
c.Lock()
defer c.Unlock()
c.term = t
}
// takeClosers returns all resources that should be closed and sets the properties to null
// we do this to avoid calling Close() under lock to avoid potential deadlocks
func (c *ctx) takeClosers() []io.Closer {
// this is done to avoid any operation holding the lock for too long
c.Lock()
defer c.Unlock()
closers := []io.Closer{}
if c.term != nil {
closers = append(closers, c.term)
c.term = nil
}
if c.agentCh != nil {
closers = append(closers, c.agentCh)
c.agentCh = nil
}
closers = append(closers, c.closers...)
c.closers = nil
return closers
}
func (c *ctx) Close() error {
log.Infof("%v ctx.Close()", c)
return closeAll(c.takeClosers()...)
}
func (c *ctx) sendResult(r execResult) {
log.Infof("%v sendResult(%v)", c, r)
select {
case c.result <- r:
default:
log.Infof("blocked on sending exec result %v", r)
}
}
func (c *ctx) requestClose(msg string) {
select {
case c.close <- closeMsg{reason: msg}:
default:
log.Infof("blocked on sending close request %v", msg)
}
}
func (c *ctx) resolver() resolver {
return c.srv.rr
}
func (c *ctx) String() string {
return fmt.Sprintf("sess(%v->%v, user=%v, id=%v)", c.info.RemoteAddr(), c.info.LocalAddr(), c.info.User(), c.id)
}
func newCtx(srv *Server, info ssh.ConnMetadata) *ctx {
return &ctx{
eid: lunk.NewRootEventID(),
info: info,
id: int(atomic.AddInt32(&ctxID, int32(1))),
result: make(chan execResult, 10),
close: make(chan closeMsg, 10),
srv: srv,
}
}
+100
View File
@@ -0,0 +1,100 @@
package srv
import (
"bytes"
"fmt"
"io"
"os/exec"
"syscall"
"github.com/mailgun/log"
"golang.org/x/crypto/ssh"
"github.com/gravitational/teleport/events"
)
type execResult struct {
command string
code int
}
type execReq struct {
Command string
}
type execFn struct {
cmdName string
cmd *exec.Cmd
ctx *ctx
// TODO(klizhentas) implement capturing as a thread safe factored out feature
// what is important is that writes and reads to buffer should be protected
// out contains captured command output
out *bytes.Buffer
}
func parseExecRequest(req *ssh.Request, ctx *ctx) (*execFn, error) {
var e execReq
if err := ssh.Unmarshal(req.Payload, &e); err != nil {
return nil, fmt.Errorf("failed to parse exec request, error: %v", err)
}
return &execFn{
ctx: ctx,
cmdName: e.Command,
out: &bytes.Buffer{},
}, nil
}
func (e *execFn) String() string {
return fmt.Sprintf("Exec(cmd=%v)", e.cmdName)
}
// execute is a blocking execution of a command
func (e *execFn) start(shell string, ch ssh.Channel) (*execResult, error) {
e.cmd = exec.Command(shell, []string{"-c", e.cmdName}...)
// we capture output to the buffer
e.cmd.Stdout = io.MultiWriter(e.out, ch)
e.cmd.Stderr = io.MultiWriter(e.out, ch)
// TODO(klizhentas) figure out the way to see if stdin is ever needed.
// e.cmd.Stdin = ch leads to the following problem:
// e.cmd.Wait() never returns because stdin never gets closed or never reached
// see cmd.Stdin comments
e.cmd.Stdin = nil
if err := e.cmd.Start(); err != nil {
log.Infof("%v %v start failure err: %v", e.ctx, e, err)
return e.collectStatus(e.cmd, err)
}
log.Infof("%v %v started", e.ctx, e)
return nil, nil
}
func (e *execFn) collectStatus(cmd *exec.Cmd, err error) (*execResult, error) {
status, err := collectStatus(e.cmd, err)
if err != nil {
e.ctx.emit(events.NewExec(e.cmdName, e.out, -1, err))
} else {
e.ctx.emit(events.NewExec(e.cmdName, e.out, status.code, err))
}
return status, err
}
func (e *execFn) wait() (*execResult, error) {
if e.cmd.Process == nil {
log.Errorf("%v no process", e)
}
return e.collectStatus(e.cmd, e.cmd.Wait())
}
func collectStatus(cmd *exec.Cmd, err error) (*execResult, error) {
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
status := exitErr.Sys().(syscall.WaitStatus)
return &execResult{code: status.ExitStatus(), command: cmd.Path}, nil
}
return nil, err
}
status, ok := cmd.ProcessState.Sys().(syscall.WaitStatus)
if !ok {
return nil, fmt.Errorf("unspoorted exit status: %T(%v)", cmd.ProcessState.Sys(), cmd.ProcessState.Sys())
}
return &execResult{code: status.ExitStatus(), command: cmd.Path}, nil
}
+135
View File
@@ -0,0 +1,135 @@
package srv
import (
"fmt"
"io"
"regexp"
"github.com/mailgun/log"
"golang.org/x/crypto/ssh"
)
// muxSubsys implements a multiplexing subsystem, in essence it connects to SSH upstreams,
// calls the same exec command on each upstream and sends back all the output from the command
// the muxSubsys uses client's public keys for authentication
type muxSubsys struct {
query string
cmd string
srv *Server
}
func parseMuxSubsys(name string) (*muxSubsys, error) {
out := regexp.MustCompile("mux:([^/]+)/(.+)").FindStringSubmatch(name)
if len(out) != 3 {
return nil, fmt.Errorf("invalid format for mux request: '%v', expected 'mux:host:port,host:port/command'", name)
}
hs, command := out[1], out[2]
return &muxSubsys{
query: hs,
cmd: command,
}, nil
}
func (m *muxSubsys) String() string {
return fmt.Sprintf("muxSubsys(query=%v, cmd=%v)", m.query, m.cmd)
}
func (m *muxSubsys) execute(sconn *ssh.ServerConn, ch ssh.Channel, req *ssh.Request, ctx *ctx) error {
log.Infof("%v %v execute()", ctx, m)
a := ctx.getAgent()
if a == nil {
return fmt.Errorf("%v agent forwarding turned off, can not authorize %v", m, a)
}
signers, err := a.Signers()
if err != nil {
return fmt.Errorf("failed to get signers from agent: %v", err)
}
if len(signers) == 0 {
return fmt.Errorf("no signers in the agent")
}
hosts, err := ctx.resolver().resolve(m.query)
if err != nil {
log.Infof("%v host resolver failed to resolve query '%v', err: %v", m, m.query, err)
return err
}
mux, err := newMux(m, hosts, sconn.User(), signers)
if err != nil {
log.Infof("%v failed to create mux: %v", m, err)
return err
}
// we are registering the closer here, it will be called when server will close the channel
// because of the end of the operation, or if the client closes connection
ctx.addCloser(mux)
code, err := mux.pipe(ctx, ch, m.cmd)
log.Infof("%v %v collected pipe status: %v %v", ctx, m, code, err)
ctx.sendResult(execResult{command: m.cmd, code: code})
return nil
}
func newMux(s *muxSubsys, addrs []string, user string, signers []ssh.Signer) (*mux, error) {
if len(addrs) == 0 {
return nil, fmt.Errorf("no upstreams to connect to")
}
m := &mux{
s: s,
ups: make(map[string]*upstream),
}
for _, a := range addrs {
up, err := connectUpstream(user, a, signers)
if err != nil {
log.Infof("%v failed to connect to %v, err: %v", s, a, err)
continue
}
m.ups[a] = up
}
if len(m.ups) == 0 {
return nil, fmt.Errorf("failed connecting to all upstreams")
}
return m, nil
}
type mux struct {
s *muxSubsys
ups map[string]*upstream
}
func (m *mux) String() string {
return m.s.String()
}
func (m *mux) Close() error {
closers := make([]io.Closer, 0, len(m.ups))
for _, u := range m.ups {
closers = append(closers, u)
}
return closeAll(closers...)
}
func (m *mux) pipe(ctx *ctx, ch ssh.Channel, command string) (int, error) {
out := make(chan muxResult, len(m.ups))
for _, u := range m.ups {
go func(u *upstream) {
code, err := u.pipe(ctx, ch, command)
result := muxResult{err: err, code: code, u: u}
log.Infof("%v %v got %v", ctx, u, &result)
out <- result
}(u)
}
log.Infof("%v %v will wait for %d results", ctx, m, len(m.ups))
var lastResult muxResult
for i := 0; i < len(m.ups); i++ {
result := <-out
log.Infof("%v %v collected %v of %d:%d", ctx, m, &result, i, len(m.ups))
}
return lastResult.code, lastResult.err
}
type muxResult struct {
u *upstream
err error
code int
}
func (m *muxResult) String() string {
return fmt.Sprintf("muxResult(code=%v, err=%v)", m.code, m.err)
}
+38
View File
@@ -0,0 +1,38 @@
package srv
import (
"strings"
"github.com/gravitational/teleport/backend"
)
// resolver is an interface implementing a query resolver,
// it get's a query and resolves it into a list of address strings
type resolver interface {
resolve(query string) ([]string, error)
}
// backend resolver is a simple implementation of the resolver
// that uses servers presence information to find the servers
type backendResolver struct {
b backend.Backend
}
// resolve provides a simple demo resolve functionality,such as globbing, expanding to all hosts
// or accepting a list of hosts:port pairs
func (b *backendResolver) resolve(query string) ([]string, error) {
// simply expand the query to all known hosts
if query == "*" {
out := []string{}
srvs, err := b.b.GetServers()
if err != nil {
return nil, err
}
for _, s := range srvs {
out = append(out, s.Addr)
}
return out, nil
}
// else treat it as a list of hosts
return strings.Split(query, ","), nil
}
+527
View File
@@ -0,0 +1,527 @@
// srv implements SSH server that supports multiplexing, tunneling and key-based auth
package srv
import (
"bytes"
"crypto/subtle"
"fmt"
"io"
"net"
"os"
"os/exec"
"strings"
"time"
"github.com/gravitational/teleport/backend"
"github.com/gravitational/teleport/events"
"github.com/gravitational/teleport/utils"
"github.com/codahale/lunk"
"github.com/mailgun/log"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent" // Server implements SSH server that uses configuration backend and certificate-based authentication.
)
type Server struct {
cfg Config
certChecker ssh.CertChecker
b backend.Backend
rr resolver
l net.Listener
elog lunk.EventLogger
}
type Config struct {
// ServerConfig is a go standard library server configuration
ssh.ServerConfig
// Addr is a string with interface:port for server to bind it's listener
Addr string
// Shell holds the path to the shell to execute, e.g. /bin/sh
Shell string
// HostKey is a private key, used for establishing an SSH connection
// and is used by SSH server to prove it's identity
HostKey []byte
// HostCert is a host certificate signed by host certificate authority
// it is used by clients to identify that the host has a valid identity.
HostCert []byte
// Backend is initialized configuration backend storage, e.g. Etcd
Backend backend.Backend
// EventLogger is an event logger for structured events
EventLogger lunk.EventLogger
}
// New returns an unstarted server
func New(cfg Config) (*Server, error) {
signer, err := newHostSigner(cfg)
if err != nil {
return nil, err
}
cfg.AddHostKey(signer)
if cfg.EventLogger == nil {
cfg.EventLogger = utils.NullEventLogger
}
srv := &Server{
b: cfg.Backend,
rr: &backendResolver{b: cfg.Backend},
elog: cfg.EventLogger,
}
if _, err := srv.getUserCAKey(); err != nil {
return nil, err
}
go srv.heartbeatPresence()
cfg.PublicKeyCallback = srv.keyAuth
srv.cfg = cfg
// TODO(klihentas) add revocation checking interface and authority updates
srv.certChecker = ssh.CertChecker{IsAuthority: srv.isAuthority}
return srv, nil
}
func (s *Server) heartbeatPresence() {
for {
srv := backend.Server{
ID: strings.Replace(s.cfg.Addr, ":", "_", -1),
Addr: s.cfg.Addr,
}
if err := s.b.UpsertServer(srv, 6*time.Second); err != nil {
log.Warningf("failed to announce presence: %v", err)
}
time.Sleep(3 * time.Second)
}
}
func (s *Server) getUserCAKey() (ssh.PublicKey, error) {
key, err := s.b.GetUserCAPub()
if err != nil {
return nil, err
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(key)
if err != nil {
return nil, fmt.Errorf("failed to parse CA public key '%v', err: %v", string(key), err)
}
return pubKey, nil
}
// isAuthority is called during checking the client key, to see if the signing key is the real CA authority key.
func (s *Server) isAuthority(auth ssh.PublicKey) bool {
key, err := s.getUserCAKey()
if err != nil {
log.Errorf("failed to retrieve user authority key, err: %v", err)
return false
}
if !keysEqual(key, auth) {
log.Warningf("authority signature check failed, signing keys mismatch")
return false
}
return true
}
// userKeys returns keys registered for a given user in a configuration backend
func (s *Server) userKeys(user string) ([]ssh.PublicKey, error) {
authKeys, err := s.b.GetUserKeys(user)
if err != nil {
log.Errorf("failed to retrieve user keys for %v, err: %v", user, err)
return nil, err
}
out := []ssh.PublicKey{}
for _, ak := range authKeys {
key, _, _, _, err := ssh.ParseAuthorizedKey(ak.Value)
// we choose to skip malformed keys here instead of aborting
if err != nil {
return nil, fmt.Errorf(
"skipping, failed to parse user public key for user=%v, id=%v, key='%v', err: %v", user, ak.ID, string(ak.Value), err)
}
out = append(out, key)
}
return out, nil
}
// keyAuth implements SSH client authentication using public keys and is called by the server every time the client connects
func (s *Server) keyAuth(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
cid := fmt.Sprintf("conn(%v->%v, user=%v)", conn.RemoteAddr(), conn.LocalAddr(), conn.User())
eventID := lunk.NewRootEventID()
// log.Infof("%v auth attempt with key %v %v", cid, key.Type(), string(ssh.MarshalAuthorizedKey(key)))
log.Infof("%v auth attempt with key %v", cid, key.Type())
p, err := s.certChecker.Authenticate(conn, key)
if err != nil {
s.elog.Log(eventID, events.NewAuthAttempt(conn, key, false, err))
log.Warningf("conn(%v->%v, user=%v) ERROR: Failed to authorize user %v, err: %v",
conn.RemoteAddr(), conn.LocalAddr(), conn.User(), conn.User(), err)
return nil, err
}
keys, err := s.userKeys(conn.User())
if err != nil {
log.Errorf("failed to retrieve user keys: %v", err)
return nil, err
}
for _, k := range keys {
if keysEqual(k, key) {
log.Infof("%v SUCCESS auth", cid)
s.elog.Log(eventID, events.NewAuthAttempt(conn, key, true, nil))
return p, nil
}
}
log.Infof("%v FAIL auth, no matching keys found", cid)
return nil, fmt.Errorf("authentication failed")
}
// Close closes listening socket and stops accepting connections
func (s *Server) Close() error {
return s.l.Close()
}
func (s *Server) acceptConnections() {
for {
conn, err := s.l.Accept()
if err != nil {
// our best shot to avoid excessive logging
if op, ok := err.(*net.OpError); ok && !op.Timeout() {
log.Infof("socket closed: %v", op)
return
}
log.Infof("accept error: %T %v", err, err)
return
}
// initiate an SSH connection, note that we don't need to close the conn here
// in case of error as ssh server takes care of this
sconn, chans, reqs, err := ssh.NewServerConn(conn, &s.cfg.ServerConfig)
if err != nil {
log.Infof("failed to initiate connection, err: %v", err)
continue
}
// Connection successfully initiated
log.Infof("new ssh connection %v -> %v vesion: %v",
sconn.RemoteAddr(), sconn.LocalAddr(), string(sconn.ClientVersion()))
// Print incoming out-of-band Requests
go s.handleRequests(reqs)
// Accept all channels
go s.handleChannels(sconn, chans)
// TODO(klizhentas) figure out the right way to close connection
}
}
func (s *Server) Start() error {
socket, err := net.Listen("tcp", s.cfg.Addr)
if err != nil {
return err
}
s.l = socket
go s.acceptConnections()
return nil
}
func (s *Server) handleRequests(reqs <-chan *ssh.Request) {
for req := range reqs {
log.Infof("recieved out-of-band request: %+v", req)
}
}
func (s *Server) handleChannels(sconn *ssh.ServerConn, chans <-chan ssh.NewChannel) {
for newCh := range chans {
if t := newCh.ChannelType(); t != "session" {
newCh.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %v", t))
continue
}
sshCh, requests, err := newCh.Accept()
if err != nil {
log.Infof("could not accept channel (%s)", err)
continue
}
// handle session requests
go s.handleSessionRequests(sconn, sshCh, requests)
}
}
// handleSessionRequests handles out of band session requests once the session channel has been created
// this function's loop handles all the "exec", "subsystem" and "shell" requests.
func (s *Server) handleSessionRequests(sconn *ssh.ServerConn, ch ssh.Channel, in <-chan *ssh.Request) {
// ctx holds the session context and all associated resources
ctx := newCtx(s, sconn)
// closeCh will close the connection and the context once the session closes
var closeCh = func() {
log.Infof("%v closing session channel", ctx)
if err := ctx.Close(); err != nil {
log.Infof("failed to close channel context: %v", err)
}
ch.Close()
}
for {
select {
case creq := <-ctx.close:
// this means that the session process stopped and desies to close the session and the channel
// e.g. shell has stopped and won't send any data back to client any more
log.Infof("close session request: %v", creq.reason)
closeCh()
return
case req := <-in:
if req == nil {
// this will happen when the client closes/drops the connection
log.Infof("%v client disconnected", ctx)
closeCh()
return
}
if err := s.dispatch(sconn, ch, req, ctx); err != nil {
log.Infof("error dispatching request: %v, closing channel", err)
replyError(req, err)
closeCh()
return
}
if req.WantReply {
req.Reply(true, nil)
}
case result := <-ctx.result:
// this means that exec process has finished and delivered the execution result, we send it back and close the session
log.Infof("%v got execution result: %v", ctx, result)
_, err := ch.SendRequest("exit-status", false, ssh.Marshal(struct{ C uint32 }{C: uint32(result.code)}))
if err != nil {
log.Infof("%v %v failed to send exit status: %v", ctx, result.command, err)
}
closeCh()
return
}
}
}
func (s *Server) dispatch(sconn *ssh.ServerConn, ch ssh.Channel, req *ssh.Request, ctx *ctx) error {
log.Infof("%v dispatch(req=%v, wantReply=%v)", ctx, req.Type, req.WantReply)
switch req.Type {
case "exec":
// exec is a remote execution of a program, does not use PTY
return s.handleExec(ch, req, ctx)
case "pty-req":
// SSH client asked to allocate PTY
return s.handlePTYReq(ch, req, ctx)
case "shell":
// SSH client asked to launch shell, we allocate PTY and start shell session
return s.handleShell(ch, req, ctx)
case "env":
// we currently ignore setting any environment variables via SSH for security purposes
return s.handleEnv(ch, req, ctx)
case "subsystem":
// subsystems are SSH subsystems defined in http://tools.ietf.org/html/rfc4254 6.6
// they are in essence SSH session extensions, allowing to implement new SSH commands
return s.handleSubsystem(sconn, ch, req, ctx)
case "window-change":
return s.handleWinChange(ch, req, ctx)
case "[email protected]":
// This happens when SSH client has agent forwarding enabled, in this case
// client sends a special request, in return SSH server opens new channel
// that uses SSH protocol for agent drafted here:
// https://tools.ietf.org/html/draft-ietf-secsh-agent-02
// the open ssh proto spec that we implement is here:
// http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.agent
return s.handleAgentForward(sconn, ch, req, ctx)
default:
return fmt.Errorf("unsupported request type: %v", req.Type)
}
}
func (s *Server) handleAgentForward(sconn *ssh.ServerConn, ch ssh.Channel, req *ssh.Request, ctx *ctx) error {
authChan, _, err := sconn.OpenChannel("[email protected]", nil)
if err != nil {
return err
}
log.Infof("%v opened agent channel", ctx)
ctx.setAgent(agent.NewClient(authChan), authChan)
return nil
}
func (s *Server) handleWinChange(ch ssh.Channel, req *ssh.Request, ctx *ctx) error {
log.Infof("%v handleWinChange()", ctx)
t := ctx.getTerm()
if t == nil {
return fmt.Errorf("no PTY allocated for winChange")
}
return t.reqWinChange(req)
}
func (s *Server) handleSubsystem(sconn *ssh.ServerConn, ch ssh.Channel, req *ssh.Request, ctx *ctx) error {
log.Infof("%v handleSubsystem()", ctx)
sb, err := parseSubsystemRequest(req)
if err != nil {
log.Infof("%v failed to parse subsystem request: %v", ctx, err)
return err
}
go func() {
if err := sb.execute(sconn, ch, req, ctx); err != nil {
ctx.requestClose(fmt.Sprintf("%v failed to execute request, err: %v", ctx, err))
log.Infof("%v failed to execute request, err: %v", err, ctx)
return
}
ctx.requestClose(fmt.Sprintf("%v subsystem executed successfully", ctx))
}()
return nil
}
func (s *Server) handleShell(ch ssh.Channel, req *ssh.Request, ctx *ctx) error {
log.Infof("%v handleShell()", ctx)
if ctx.getTerm() == nil {
t, err := newTerm()
if err != nil {
log.Infof("handleShell failed to create term: %v", err)
return err
}
ctx.setTerm(t)
}
t := ctx.getTerm()
cmd := exec.Command(s.cfg.Shell)
// TODO(klizhentas) figure out linux user policy for launching shells, what user and environment
// should we use to execute the shell? the simplest answer is to use current user and env, however
// what if we are root?
cmd.Env = []string{"TERM=xterm", fmt.Sprintf("HOME=%v", os.Getenv("HOME"))}
if err := t.run(cmd); err != nil {
log.Infof("%v failed to start shell: %v", ctx, err)
return err
}
log.Infof("%v starting shell input/output streaming", ctx)
// Pipe session to shell and visa-versa capturing input and output
out := &bytes.Buffer{}
// TODO(klizhentas) implement capturing as a thread safe factored out feature
// what is important is that writes and reads to buffer should be protected
// out contains captured command output
go func() {
written, err := io.Copy(io.MultiWriter(ch, out), t.pty)
log.Infof("%v shell to session copy closed, bytes written: %v, err: %v",
ctx, written, err)
}()
go func() {
written, err := io.Copy(io.MultiWriter(t.pty, out), ch)
log.Infof("%v session to shell copy closed, bytes written: %v, err: %v",
ctx, written, err)
}()
go func() {
result, err := collectStatus(cmd, cmd.Wait())
if err != nil {
log.Errorf("%v wait failed: %v", ctx, err)
ctx.emit(events.NewShell(s.cfg.Shell, out, -1, err))
}
if result != nil {
log.Infof("%v result collected: %v", ctx, result)
ctx.emit(events.NewShell(s.cfg.Shell, out, result.code, nil))
ctx.sendResult(*result)
}
}()
return nil
}
func (s *Server) handleEnv(ch ssh.Channel, req *ssh.Request, ctx *ctx) error {
log.Infof("%v handleEnv()", ctx)
return nil
}
func (s *Server) handlePTYReq(ch ssh.Channel, req *ssh.Request, ctx *ctx) error {
log.Infof("%v handlePTYReq()", ctx)
if term := ctx.getTerm(); term != nil {
r, err := parsePTYReq(req)
if err != nil {
log.Infof("%v failed to parse PTY request: %v", ctx, err)
replyError(req, err)
return err
}
term.setWinsize(r.W, r.H)
return nil
}
term, err := reqPTY(req)
if err != nil {
log.Infof("%v failed to allocate PTY: %v", ctx, err)
replyError(req, err)
return err
}
log.Infof("%v PTY allocated successfully", ctx)
ctx.setTerm(term)
return nil
}
func (s *Server) handleExec(ch ssh.Channel, req *ssh.Request, ctx *ctx) error {
log.Infof("%v handleExec()", ctx)
e, err := parseExecRequest(req, ctx)
if err != nil {
log.Infof("%v failed to parse exec request: %v", ctx, err)
replyError(req, err)
return err
}
result, err := e.start(s.cfg.Shell, ch)
if err != nil {
log.Infof("%v error starting command, %v", ctx, err)
replyError(req, err)
}
if result != nil {
log.Infof("%v %v result collected: %v", ctx, e, result)
ctx.sendResult(*result)
}
// in case if result is nil and no error, this means that program is
// running in the background
go func() {
log.Infof("%v %v waiting for result", ctx, e)
result, err := e.wait()
if err != nil {
log.Infof("%v %v wait failed: %v", ctx, e, err)
}
if result != nil {
log.Infof("%v %v result collected: %v", ctx, e, result)
ctx.sendResult(*result)
}
}()
return nil
}
func replyError(req *ssh.Request, err error) {
if req.WantReply {
req.Reply(false, []byte(err.Error()))
}
}
func closeAll(closers ...io.Closer) error {
var err error
for _, cl := range closers {
if cl == nil {
continue
}
if e := cl.Close(); e != nil {
log.Infof("%T close failure: %v", cl, e)
err = e
}
}
return err
}
// constant time compare of the keys to avoid timing attacks
func keysEqual(ak, bk ssh.PublicKey) bool {
a := ak.Marshal()
b := bk.Marshal()
return (len(a) == len(b) && subtle.ConstantTimeCompare(a, b) == 1)
}
func newHostSigner(cfg Config) (ssh.Signer, error) {
hostSigner, err := ssh.ParsePrivateKey(cfg.HostKey)
if err != nil {
return nil, fmt.Errorf("failed to parse host private key, err: %v", err)
}
hostCAKey, _, _, _, err := ssh.ParseAuthorizedKey(cfg.HostCert)
if err != nil {
return nil, fmt.Errorf("failed to parse server CA certificate '%v', err: %v", string(cfg.HostCert), err)
}
hostCert, ok := hostCAKey.(*ssh.Certificate)
if !ok {
return nil, fmt.Errorf("expected host CA certificate, got %T ", hostCAKey)
}
signer, err := ssh.NewCertSigner(hostCert, hostSigner)
if err != nil {
return nil, fmt.Errorf("failed to create certificate signer, err: %v", err)
}
return signer, nil
}
+304
View File
@@ -0,0 +1,304 @@
package srv
import (
"bytes"
"fmt"
"io"
"net"
"strings"
"testing"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"github.com/gravitational/teleport/auth"
"github.com/gravitational/teleport/auth/openssh"
"github.com/gravitational/teleport/backend"
"github.com/gravitational/teleport/backend/membk"
. "gopkg.in/check.v1"
)
func TestSrv(t *testing.T) { TestingT(t) }
type SrvSuite struct {
srv *Server
clt *ssh.Client
bk *membk.MemBackend
a *auth.AuthServer
up *upack
}
var _ = Suite(&SrvSuite{})
func (s *SrvSuite) SetUpSuite(c *C) {
}
func (s *SrvSuite) SetUpTest(c *C) {
s.bk = membk.New()
s.a = auth.NewAuthServer(s.bk, openssh.New())
// set up host private key and certificate
c.Assert(s.a.ResetHostCA(""), IsNil)
hpriv, hpub, err := s.a.GenerateKeyPair("")
c.Assert(err, IsNil)
hcert, err := s.a.GenerateHostCert(hpub, "localhost", "localhost", 0)
c.Assert(err, IsNil)
// set up user CA and set up a user that has access to the server
c.Assert(s.a.ResetUserCA(""), IsNil)
cfg := Config{
Addr: "localhost:0",
HostCert: hcert,
HostKey: hpriv,
Backend: s.bk,
Shell: "/bin/sh",
}
srv, err := New(cfg)
c.Assert(err, IsNil)
s.srv = srv
c.Assert(s.srv.Start(), IsNil)
// set up SSH client using the user private key for signing
up, err := newUpack("test", s.a)
c.Assert(err, IsNil)
// set up an agent server and a client that uses agent for forwarding
keyring := agent.NewKeyring()
c.Assert(keyring.Add(up.pkey, up.pcert, ""), IsNil)
s.up = up
sshConfig := &ssh.ClientConfig{
User: "test",
Auth: []ssh.AuthMethod{ssh.PublicKeys(up.certSigner)},
}
client, err := ssh.Dial("tcp", s.srv.l.Addr().String(), sshConfig)
c.Assert(err, IsNil)
c.Assert(agent.ForwardToAgent(client, keyring), IsNil)
s.clt = client
}
func (s *SrvSuite) TearDownTest(c *C) {
c.Assert(s.clt.Close(), IsNil)
c.Assert(s.srv.Close(), IsNil)
}
// TestExec executes a command on a remote server
func (s *SrvSuite) TestExec(c *C) {
se, err := s.clt.NewSession()
c.Assert(err, IsNil)
defer se.Close()
out, err := se.Output("expr 2 + 3")
c.Assert(err, IsNil)
c.Assert(strings.Trim(string(out), " \n"), Equals, "5")
}
// TestShell launches interactive shell session and executes a command
func (s *SrvSuite) TestShell(c *C) {
se, err := s.clt.NewSession()
c.Assert(err, IsNil)
w, err := se.StdinPipe()
c.Assert(err, IsNil)
stdout := &bytes.Buffer{}
se.Stdout = stdout
c.Assert(se.Shell(), IsNil)
_, err = io.WriteString(w, "expr 7 + 70;exit\r\n")
c.Assert(err, IsNil)
c.Assert(se.Wait(), IsNil)
c.Assert(removeNL(stdout.String()), Matches, ".*77.*")
}
// TestMux tests multiplexing command with agent forwarding
func (s *SrvSuite) TestMux(c *C) {
se, err := s.clt.NewSession()
c.Assert(err, IsNil)
defer se.Close()
c.Assert(agent.RequestAgentForwarding(se), IsNil)
stdout := &bytes.Buffer{}
reader, err := se.StdoutPipe()
done := make(chan struct{})
go func() {
io.Copy(stdout, reader)
close(done)
}()
c.Assert(se.RequestSubsystem(fmt.Sprintf("mux:%v/expr 22 + 55", s.srv.l.Addr().String())), IsNil)
<-done
c.Assert(removeNL(stdout.String()), Matches, ".*77.*")
}
// TestTun tests tunneling command with agent forwarding
func (s *SrvSuite) TestTun(c *C) {
se, err := s.clt.NewSession()
c.Assert(err, IsNil)
defer se.Close()
c.Assert(agent.RequestAgentForwarding(se), IsNil)
writer, err := se.StdinPipe()
c.Assert(err, IsNil)
stdout := &bytes.Buffer{}
reader, err := se.StdoutPipe()
done := make(chan struct{})
go func() {
io.Copy(stdout, reader)
close(done)
}()
c.Assert(se.RequestSubsystem(fmt.Sprintf("tun:%v", s.srv.l.Addr().String())), IsNil)
_, err = io.WriteString(writer, "expr 7 + 70;exit\r\n")
c.Assert(err, IsNil)
<-done
c.Assert(removeNL(stdout.String()), Matches, ".*77.*")
}
// TestPTY requests PTY for an interactive session
func (s *SrvSuite) TestPTY(c *C) {
se, err := s.clt.NewSession()
c.Assert(err, IsNil)
defer se.Close()
// request PTY
c.Assert(se.RequestPty("xterm", 30, 30, ssh.TerminalModes{}), IsNil)
}
// TestEnv requests setting environment variables. (We are currently ignoring these requests)
func (s *SrvSuite) TestEnv(c *C) {
se, err := s.clt.NewSession()
c.Assert(err, IsNil)
defer se.Close()
c.Assert(se.Setenv("HOME", "/"), IsNil)
}
// TestNoAuth tries to log in with no auth methods and should be rejected
func (s *SrvSuite) TestNoAuth(c *C) {
_, err := ssh.Dial("tcp", s.srv.l.Addr().String(), &ssh.ClientConfig{})
c.Assert(err, NotNil)
}
// TestPasswordAuth tries to log in with empty pass and should be rejected
func (s *SrvSuite) TestPasswordAuth(c *C) {
config := &ssh.ClientConfig{Auth: []ssh.AuthMethod{ssh.Password("")}}
_, err := ssh.Dial("tcp", s.srv.l.Addr().String(), config)
c.Assert(err, NotNil)
}
// TODO(klizhentas): figure out the way to check that resources are properly deallocated
// on client disconnects
func (s *SrvSuite) TestClientDisconnect(c *C) {
config := &ssh.ClientConfig{
User: "test",
Auth: []ssh.AuthMethod{ssh.PublicKeys(s.up.certSigner)},
}
clt, err := ssh.Dial("tcp", s.srv.l.Addr().String(), config)
c.Assert(clt, NotNil)
c.Assert(err, IsNil)
se, err := s.clt.NewSession()
c.Assert(err, IsNil)
c.Assert(se.Shell(), IsNil)
c.Assert(clt.Close(), IsNil)
}
// upack holds all ssh signing artefacts needed for signing and checking user keys
type upack struct {
// key is a raw private user key
key []byte
// pkey is parsed private SSH key
pkey interface{}
// pub is a public user key
pub []byte
//cert is a certificate signed by user CA
cert []byte
// pcert is a parsed ssh Certificae
pcert *ssh.Certificate
// signer is a signer that answers signing challenges using private key
signer ssh.Signer
// certSigner is a signer that answers signing challenges using private
// key and a certificate issued by user certificate authority
certSigner ssh.Signer
}
func newUpack(user string, a *auth.AuthServer) (*upack, error) {
upriv, upub, err := a.GenerateKeyPair("")
if err != nil {
return nil, err
}
ucert, err := a.UpsertUserKey(user, backend.AuthorizedKey{ID: user, Value: upub}, 0)
if err != nil {
return nil, err
}
upkey, err := ssh.ParseRawPrivateKey(upriv)
if err != nil {
return nil, err
}
usigner, err := ssh.NewSignerFromKey(upkey)
if err != nil {
return nil, err
}
pcert, _, _, _, err := ssh.ParseAuthorizedKey(ucert)
if err != nil {
return nil, err
}
ucertSigner, err := ssh.NewCertSigner(pcert.(*ssh.Certificate), usigner)
if err != nil {
return nil, err
}
return &upack{
key: upriv,
pkey: upkey,
pub: upub,
cert: ucert,
pcert: pcert.(*ssh.Certificate),
signer: usigner,
certSigner: ucertSigner,
}, nil
}
func removeNL(v string) string {
v = strings.Replace(v, "\r", "", -1)
v = strings.Replace(v, "\n", "", -1)
return v
}
// netPipe is analogous to net.Pipe, but it uses a real net.Conn, and
// therefore is buffered (net.Pipe deadlocks if both sides start with
// a write.). This code is courtesy of crypto/ssh/agent/client_test
func netPipe() (net.Conn, net.Conn, error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, nil, err
}
defer listener.Close()
c1, err := net.Dial("tcp", listener.Addr().String())
if err != nil {
return nil, nil, err
}
c2, err := listener.Accept()
if err != nil {
c1.Close()
return nil, nil, err
}
return c1, c2, nil
}
+30
View File
@@ -0,0 +1,30 @@
package srv
import (
"fmt"
"strings"
"golang.org/x/crypto/ssh"
)
type subsys struct {
Name string
}
type subsystem interface {
execute(*ssh.ServerConn, ssh.Channel, *ssh.Request, *ctx) error
}
func parseSubsystemRequest(req *ssh.Request) (subsystem, error) {
var s subsys
if err := ssh.Unmarshal(req.Payload, &s); err != nil {
return nil, fmt.Errorf("failed to parse subsystem request, error: %v", err)
}
if strings.HasPrefix(s.Name, "tun:") {
return parseTunSubsys(s.Name)
}
if strings.HasPrefix(s.Name, "mux:") {
return parseMuxSubsys(s.Name)
}
return nil, fmt.Errorf("unrecognized subsystem: %v", s.Name)
}
+130
View File
@@ -0,0 +1,130 @@
package srv
import (
"os"
"os/exec"
"syscall"
"unsafe"
"github.com/kr/pty"
"github.com/mailgun/log"
"golang.org/x/crypto/ssh"
)
// term provides handy functions for managing PTY, usch as resizing windows
// execing processes with PTY and cleaning up
type term struct {
pty *os.File
tty *os.File
err error
done bool
}
type ptyReq struct {
Env string
W uint32
H uint32
Wpx uint32
Hpx uint32
Modes string
}
type winChangeReq struct {
W uint32
H uint32
Wpx uint32
Hpx uint32
Modes string
}
func parsePTYReq(req *ssh.Request) (*ptyReq, error) {
var r ptyReq
if err := ssh.Unmarshal(req.Payload, &r); err != nil {
log.Infof("failed to parse PTY request: %v", err)
return nil, err
}
return &r, nil
}
func newTerm() (*term, error) {
// Create new PTY
pty, tty, err := pty.Open()
if err != nil {
log.Infof("could not start pty (%s)", err)
return nil, err
}
return &term{pty: pty, tty: tty, err: err}, nil
}
func reqPTY(req *ssh.Request) (*term, error) {
var r ptyReq
if err := ssh.Unmarshal(req.Payload, &r); err != nil {
log.Infof("failed to parse PTY request: %v", err)
return nil, err
}
log.Infof("Parsed pty request pty(enn=%v, w=%v, h=%v)", r.Env, r.W, r.H)
t, err := newTerm()
if err != nil {
log.Infof("failed to create term: %v", err)
return nil, err
}
t.setWinsize(r.W, r.H)
return t, nil
}
func (t *term) reqWinChange(req *ssh.Request) error {
var r winChangeReq
if err := ssh.Unmarshal(req.Payload, &r); err != nil {
log.Infof("failed to parse window change request: %v", err)
return err
}
log.Infof("Parsed window change request: %#v", r)
t.setWinsize(r.W, r.H)
return nil
}
func (t *term) setWinsize(w, h uint32) {
log.Infof("window resize %dx%d", w, h)
ws := &winsize{Width: uint16(w), Height: uint16(h)}
syscall.Syscall(syscall.SYS_IOCTL, t.pty.Fd(), uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(ws)))
}
func (t *term) closeTTY() {
if err := t.tty.Close(); err != nil {
log.Infof("failed to close TTY: %v", err)
}
t.tty = nil
}
func (t *term) run(c *exec.Cmd) error {
defer t.closeTTY()
c.Stdout = t.tty
c.Stdin = t.tty
c.Stderr = t.tty
c.SysProcAttr = &syscall.SysProcAttr{
Setctty: true,
Setsid: true,
}
return c.Start()
}
func (t *term) Close() error {
var err error
if e := t.pty.Close(); e != nil {
err = e
}
if t.tty != nil {
if e := t.tty.Close(); e != nil {
err = e
}
}
return err
}
// winsize stores the Height and Width of a terminal.
type winsize struct {
Height uint16
Width uint16
x uint16
y uint16
}
+54
View File
@@ -0,0 +1,54 @@
package srv
import (
"fmt"
"strings"
"github.com/mailgun/log"
"golang.org/x/crypto/ssh"
)
// tunSubsys is an SSH subsystem for easy tunneling through proxy server
// This subsystem creates a new SSH connection using auth with agent forwarding
// and launches a remote shell on the target server
type tunSubsys struct {
host string
port string
}
func parseTunSubsys(name string) (*tunSubsys, error) {
out := strings.Split(name, ":")
if len(out) != 3 {
return nil, fmt.Errorf("invalid format for tun request: '%v', expected 'tun:host:port'", name)
}
return &tunSubsys{
host: out[1],
port: out[2],
}, nil
}
func (t *tunSubsys) String() string {
return fmt.Sprintf("tunSubsys(host=%v, port=%v)", t.host, t.port)
}
func (t *tunSubsys) execute(sconn *ssh.ServerConn, ch ssh.Channel, req *ssh.Request, ctx *ctx) error {
log.Infof("%v execute()", t)
a := ctx.getAgent()
if a == nil {
return fmt.Errorf("%v agent forwarding turned off, can not authorize", ctx)
}
signers, err := a.Signers()
if err != nil {
return fmt.Errorf("%v failed to get signers from agent: %v", err, ctx)
}
if len(signers) == 0 {
return fmt.Errorf("%v no signers in the agent", ctx)
}
up, err := connectUpstream(sconn.User(), fmt.Sprintf("%v:%v", t.host, t.port), signers)
if err != nil {
return fmt.Errorf("%v failed to connect to upstream, error: %v", ctx, err)
}
ctx.addCloser(up)
return up.pipeShell(ctx, ch)
}
+106
View File
@@ -0,0 +1,106 @@
package srv
import (
"fmt"
"github.com/mailgun/log"
"io"
"golang.org/x/crypto/ssh"
)
func connectUpstream(username, addr string, signers []ssh.Signer) (*upstream, error) {
sshConfig := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{ssh.PublicKeys(signers...)},
}
client, err := ssh.Dial("tcp", addr, sshConfig)
if err != nil {
return nil, err
}
session, err := client.NewSession()
if err != nil {
client.Close()
return nil, err
}
return &upstream{
addr: addr,
client: client,
session: session,
}, nil
}
type upstream struct {
addr string
client *ssh.Client
session *ssh.Session
}
func (u *upstream) Close() error {
return closeAll(u.session, u.client)
}
func (m *upstream) String() string {
return fmt.Sprintf("upstream(addr=%v)", m.addr)
}
func (u *upstream) pipe(ctx *ctx, ch ssh.Channel, command string) (int, error) {
log.Infof("%v %v pipe start", ctx, u)
stderr, err := u.session.StderrPipe()
if err != nil {
return -1, fmt.Errorf("%v fail to pipe stderr: %v", ctx, err)
}
stdout, err := u.session.StdoutPipe()
if err != nil {
return -1, fmt.Errorf("%v fail to pipe stdout: %v", ctx, err)
}
stdin, err := u.session.StdinPipe()
if err != nil {
return -1, fmt.Errorf("%v fail to pipe stdin: %v", ctx, err)
}
// TODO(klizhentas) close stdin/stdout ?
go io.Copy(stdin, ch)
go io.Copy(ch.Stderr(), stderr)
go io.Copy(ch, stdout)
err = u.session.Start(command)
if err != nil {
log.Infof("%v pipe failed to start command %v, err: %v", ctx, command, err)
return -1, fmt.Errorf("%v pipe failed to start command %v, err: %v", ctx, command, err)
}
log.Infof("%v %v pipe: session.Wait()", ctx, u)
err = u.session.Wait()
log.Infof("%v %v pipe: session.Wait() completed", ctx, u)
if err != nil {
log.Infof("%v %v got error: %v %T", ctx, u, err, err)
if err, ok := err.(*ssh.ExitError); ok {
log.Infof("%v %v got exit error: %v", ctx, u, err)
return err.ExitStatus(), nil
} else {
return -1, fmt.Errorf("%v %v failed to wait for ssh command: %v", ctx, u, err)
}
}
return 0, nil
}
func (u *upstream) pipeShell(ctx *ctx, ch ssh.Channel) error {
targetStderr, err := u.session.StderrPipe()
if err != nil {
return fmt.Errorf("fail to pipe stderr: %v", err)
}
targetStdout, err := u.session.StdoutPipe()
if err != nil {
return fmt.Errorf("fail to pipe stdout: %v", err)
}
targetStdin, err := u.session.StdinPipe()
if err != nil {
return fmt.Errorf("fail to pipe stdin: %v", err)
}
go io.Copy(targetStdin, ch)
go io.Copy(ch.Stderr(), targetStderr)
go io.Copy(ch, targetStdout)
if err := u.session.Shell(); err != nil {
log.Infof("Failed to start shell: %v", err)
return err
}
log.Infof("session started successfully")
return u.session.Wait()
}
+18
View File
@@ -0,0 +1,18 @@
test: clean
go test -v ./command -cover
test-grep: clean
go test -v ./command -check.f=$(e)
cover: clean
go test -v ./command -coverprofile=/tmp/coverage.out
go tool cover -html=/tmp/coverage.out
clean:
find . -name flymake_* -delete
sloccount:
find . -name "*.go" -print0 | xargs -0 wc -l
install: clean
go install github.com/gravitational/teleport/tctl
+95
View File
@@ -0,0 +1,95 @@
package command
import (
"fmt"
"github.com/codegangsta/cli"
)
func newHostCACommand(c *Command) cli.Command {
return cli.Command{
Name: "hostca",
Usage: "Operations with host certificate authority",
Subcommands: []cli.Command{
{
Name: "reset",
Usage: "Reset host certificate authority keys",
Flags: []cli.Flag{
cli.BoolFlag{Name: "confirm", Usage: "Automatically apply the operation without confirmation"},
},
Action: c.resetHostCA,
},
{
Name: "pubkey",
Usage: "print host certificate authority public key",
Action: c.getHostCAPub,
},
},
}
}
func newUserCACommand(c *Command) cli.Command {
return cli.Command{
Name: "userca",
Usage: "Operations with user certificate authority",
Subcommands: []cli.Command{
{
Name: "reset",
Usage: "Reset user certificate authority keys",
Flags: []cli.Flag{
cli.BoolFlag{Name: "confirm", Usage: "Automatically apply the operation without confirmation"},
},
Action: c.resetUserCA,
},
{
Name: "pubkey",
Usage: "print user certificate authority public key",
Action: c.getUserCAPub,
},
},
}
}
func (cmd *Command) resetHostCA(c *cli.Context) {
if !c.Bool("confirm") && !cmd.confirm("Reseting private and public keys for Host CA. This will invalidate all signed host certs. Continue?") {
cmd.printError(fmt.Errorf("aborted by user"))
return
}
if err := cmd.client.ResetHostCA(); err != nil {
cmd.printError(err)
return
}
cmd.printOK("CA keys have been regenerated")
}
func (cmd *Command) getHostCAPub(c *cli.Context) {
key, err := cmd.client.GetHostCAPub()
if err != nil {
cmd.printError(err)
return
}
cmd.printOK("Host CA Key")
fmt.Fprintf(cmd.out, string(key))
}
func (cmd *Command) resetUserCA(c *cli.Context) {
if !c.Bool("confirm") && !cmd.confirm("Reseting private and public keys for User CA. This will invalidate all signed user certs. Continue?") {
cmd.printError(fmt.Errorf("aborted by user"))
return
}
if err := cmd.client.ResetUserCA(); err != nil {
cmd.printError(err)
return
}
cmd.printOK("CA keys have been regenerated")
}
func (cmd *Command) getUserCAPub(c *cli.Context) {
key, err := cmd.client.GetUserCAPub()
if err != nil {
cmd.printError(err)
return
}
cmd.printOK("User CA Key")
fmt.Fprintf(cmd.out, string(key))
}
+127
View File
@@ -0,0 +1,127 @@
package command
import (
"bufio"
"fmt"
"io"
"os"
"strings"
"github.com/buger/goterm"
"github.com/codegangsta/cli"
"github.com/gravitational/teleport/auth"
"github.com/gravitational/teleport/utils"
)
type Command struct {
url string
client *auth.Client
out io.Writer
in io.Reader
}
func NewCommand() *Command {
return &Command{
out: os.Stdout,
in: os.Stdin,
}
}
func (cmd *Command) Run(args []string) error {
url, args, err := findURL(args)
if err != nil {
return err
}
cmd.url = url
cmd.client = auth.NewClient(cmd.url)
app := cli.NewApp()
app.Name = "tctl"
app.Usage = "CLI for key management of teleport SSH cluster"
app.Flags = flags()
app.Commands = []cli.Command{
newHostCACommand(cmd),
newUserCACommand(cmd),
newUserCommand(cmd),
}
return app.Run(args)
}
func (cmd *Command) readInput(path string) ([]byte, error) {
if path != "" {
return utils.ReadPath(path)
}
reader := bufio.NewReader(cmd.in)
return reader.ReadSlice('\n')
}
func (cmd *Command) confirm(message string) bool {
reader := bufio.NewReader(cmd.in)
fmt.Fprintf(cmd.out, fmt.Sprintf("%v (Y/N): ", message))
text, _ := reader.ReadString('\n')
text = strings.Trim(text, "\n\r\t")
return text == "Y" || text == "yes" || text == "y"
}
func (cmd *Command) printResult(format string, in interface{}, err error) {
if err != nil {
cmd.printError(err)
} else {
cmd.printOK(format, fmt.Sprintf("%v", in))
}
}
func (cmd *Command) printStatus(in interface{}, err error) {
if err != nil {
cmd.printError(err)
} else {
cmd.printOK("%s", in)
}
}
func (cmd *Command) printError(err error) {
fmt.Fprint(cmd.out, goterm.Color(fmt.Sprintf("ERROR: %s", err), goterm.RED)+"\n")
}
func (cmd *Command) printOK(message string, params ...interface{}) {
fmt.Fprintf(cmd.out,
goterm.Color(
fmt.Sprintf("OK: %s\n", fmt.Sprintf(message, params...)), goterm.GREEN)+"\n")
}
func (cmd *Command) printInfo(message string, params ...interface{}) {
fmt.Fprintf(cmd.out, "INFO: %s\n", fmt.Sprintf(message, params...))
}
// This function extracts url from the command line regardless of it's position
// this is a workaround, as cli libary does not support "superglobal" urls yet.
func findURL(args []string) (string, []string, error) {
for i, arg := range args {
if strings.HasPrefix(arg, "--teleport=") || strings.HasPrefix(arg, "-teleport=") {
out := strings.Split(arg, "=")
return out[1], cut(i, i+1, args), nil
} else if strings.HasPrefix(arg, "-teleport") || strings.HasPrefix(arg, "--teleport") {
// This argument should not be the last one
if i > len(args)-2 {
return "", nil, fmt.Errorf("provide a valid URL")
}
return args[i+1], cut(i, i+2, args), nil
}
}
return "http://localhost:2023", args, nil
}
func cut(i, j int, args []string) []string {
s := []string{}
s = append(s, args[:i]...)
return append(s, args[j:]...)
}
func flags() []cli.Flag {
return []cli.Flag{
cli.StringFlag{Name: "auth", Value: DefaultTeleportURL, Usage: "Teleport URL"},
}
}
const DefaultTeleportURL = "localhost:2023"
+118
View File
@@ -0,0 +1,118 @@
package command
import (
"bytes"
"fmt"
"io/ioutil"
"net/http/httptest"
"strings"
"testing"
"github.com/gravitational/teleport/auth"
"github.com/gravitational/teleport/auth/openssh"
"github.com/gravitational/teleport/backend/membk"
. "gopkg.in/check.v1"
)
const OK = ".*OK.*"
func TestTeleportCLI(t *testing.T) { TestingT(t) }
type CmdSuite struct {
srv *httptest.Server
asrv *auth.AuthServer
clt *auth.Client
cmd *Command
out *bytes.Buffer
bk *membk.MemBackend
}
var _ = Suite(&CmdSuite{})
func (s *CmdSuite) SetUpSuite(c *C) {
}
func (s *CmdSuite) SetUpTest(c *C) {
s.bk = membk.New()
s.asrv = auth.NewAuthServer(s.bk, openssh.New())
s.srv = httptest.NewServer(auth.NewAPIServer(s.asrv))
s.clt = auth.NewClient(s.srv.URL)
s.out = &bytes.Buffer{}
s.cmd = &Command{out: s.out, url: s.srv.URL}
}
func (s *CmdSuite) TearDownTest(c *C) {
s.srv.Close()
}
func (s *CmdSuite) runString(in string) string {
return s.run(strings.Split(in, " ")...)
}
func (s *CmdSuite) run(params ...string) string {
args := []string{"tctl"}
args = append(args, params...)
args = append(args, fmt.Sprintf("--teleport=%s", s.srv.URL))
s.out = &bytes.Buffer{}
s.cmd = &Command{out: s.out, url: s.srv.URL}
s.cmd.Run(args)
return strings.Replace(s.out.String(), "\n", " ", -1)
}
func (s *CmdSuite) TestHostCACRUD(c *C) {
c.Assert(
s.run("hostca", "reset", "-confirm"),
Matches, fmt.Sprintf(".*%v.*", "regenerated"))
c.Assert(s.bk.HostCA, NotNil)
c.Assert(
s.run("hostca", "pubkey"),
Matches, fmt.Sprintf(".*%v.*", s.bk.HostCA.Pub))
}
func (s *CmdSuite) TestUserCACRUD(c *C) {
c.Assert(
s.run("userca", "reset", "-confirm"),
Matches, fmt.Sprintf(".*%v.*", "regenerated"))
c.Assert(s.bk.UserCA, NotNil)
c.Assert(
s.run("userca", "pubkey"),
Matches, fmt.Sprintf(".*%v.*", s.bk.UserCA.Pub))
}
func (s *CmdSuite) TestUserCRUD(c *C) {
c.Assert(s.asrv.ResetUserCA(""), IsNil)
_, pub, err := s.asrv.GenerateKeyPair("")
c.Assert(err, IsNil)
fkey, err := ioutil.TempFile("", "teleport")
c.Assert(err, IsNil)
defer fkey.Close()
fkey.Write(pub)
out := s.run("user", "upsert_key", "-user", "alex", "-keyid", "key1", "-key", fkey.Name())
c.Assert(out, Matches, fmt.Sprintf(".*%v.*", "certificate:"))
parts := strings.Split(out, "certificate:")
c.Assert(len(parts), Equals, 2)
c.Assert(trim(string(s.bk.Keys["alex"]["key1"].Value)), Equals, trim(parts[1]))
c.Assert(
s.run("user", "ls"),
Matches, fmt.Sprintf(".*%v.*", "alex"))
c.Assert(s.run("user", "ls_keys", "-user", "alex"), Matches, fmt.Sprintf(".*%v.*", "key1"))
c.Assert(
s.run("user", "delete", "-user", "alex"),
Matches, fmt.Sprintf(".*%v.*", "alex"))
}
func trim(val string) string {
return strings.Trim(val, " \t\n")
}
+117
View File
@@ -0,0 +1,117 @@
package command
import (
"fmt"
"github.com/buger/goterm"
"github.com/codegangsta/cli"
"github.com/gravitational/teleport/backend"
)
func newUserCommand(c *Command) cli.Command {
return cli.Command{
Name: "user",
Usage: "Operations with registered users",
Subcommands: []cli.Command{
{
Name: "ls",
Usage: "List users registered in teleport",
Action: c.getUsers,
},
{
Name: "delete",
Usage: "Delete user",
Action: c.deleteUser,
Flags: []cli.Flag{
cli.StringFlag{Name: "user", Usage: "User to delete"},
},
},
{
Name: "upsert_key",
Usage: "Grant access to the user key, returns signed certificate",
Flags: []cli.Flag{
cli.StringFlag{Name: "user", Usage: "User holding the key"},
cli.StringFlag{Name: "keyid", Usage: "SSH key ID"},
cli.StringFlag{Name: "key", Usage: "Path to public key"},
cli.DurationFlag{Name: "ttl", Usage: "Access time to live, certificate and access entry will expire when set"},
},
Action: c.upsertKey,
},
{
Name: "ls_keys",
Usage: "List user's keys registered in teleport",
Action: c.getUserKeys,
Flags: []cli.Flag{
cli.StringFlag{Name: "user", Usage: "User to list keys form"},
},
},
},
}
}
func (cmd *Command) upsertKey(c *cli.Context) {
bytes, err := cmd.readInput(c.String("key"))
if err != nil {
cmd.printError(err)
return
}
signed, err := cmd.client.UpsertUserKey(
c.String("user"), backend.AuthorizedKey{ID: c.String("keyid"), Value: bytes}, c.Duration("ttl"))
if err != nil {
cmd.printError(err)
return
}
fmt.Fprintf(cmd.out, "certificate:\n%v", string(signed))
}
func (cmd *Command) deleteUser(c *cli.Context) {
if err := cmd.client.DeleteUser(c.String("user")); err != nil {
cmd.printError(err)
return
}
cmd.printOK("User %v deleted", c.String("user"))
}
func (cmd *Command) getUsers(c *cli.Context) {
users, err := cmd.client.GetUsers()
if err != nil {
cmd.printError(err)
return
}
cmd.printOK("Users")
fmt.Fprintf(cmd.out, usersView(users))
}
func (cmd *Command) getUserKeys(c *cli.Context) {
keys, err := cmd.client.GetUserKeys(c.String("user"))
if err != nil {
cmd.printError(err)
return
}
cmd.printOK("Users")
fmt.Fprintf(cmd.out, keysView(keys))
}
func usersView(users []string) string {
t := goterm.NewTable(0, 10, 5, ' ', 0)
fmt.Fprint(t, "User\n")
if len(users) == 0 {
return t.String()
}
for _, u := range users {
fmt.Fprintf(t, "%v\n", u)
}
return t.String()
}
func keysView(keys []backend.AuthorizedKey) string {
t := goterm.NewTable(0, 10, 5, ' ', 0)
fmt.Fprint(t, "KeyID\tKey\n")
if len(keys) == 0 {
return t.String()
}
for _, k := range keys {
fmt.Fprintf(t, "%v\t%v\n", k.ID, string(k.Value))
}
return t.String()
}
+19
View File
@@ -0,0 +1,19 @@
package main
import (
"os"
"github.com/mailgun/log" // TODO(klizhentas) fix the interface for logging
"github.com/gravitational/teleport/tctl/command"
)
func main() {
log.Init([]*log.LogConfig{&log.LogConfig{Name: "console"}})
cmd := command.NewCommand()
err := cmd.Run(os.Args)
if err != nil {
log.Infof("error: %s\n", err)
}
}
+122
View File
@@ -0,0 +1,122 @@
package main
import (
"fmt"
"net/http"
"os"
"github.com/codahale/lunk"
"github.com/codegangsta/cli"
"github.com/mailgun/log"
"github.com/gravitational/teleport/auth"
"github.com/gravitational/teleport/auth/openssh"
"github.com/gravitational/teleport/backend"
"github.com/gravitational/teleport/backend/etcdbk"
"github.com/gravitational/teleport/srv"
"github.com/gravitational/teleport/utils"
"sync"
)
func main() {
app := cli.NewApp()
app.Name = "teleport"
app.Usage = "Clustering SSH and key management server"
app.Flags = []cli.Flag{
cli.StringFlag{Name: "addr", Value: "localhost:2022", Usage: "SSH host:port pair to listen on"},
cli.StringFlag{Name: "shell", Value: "/bin/sh", Usage: "path to shell to launch for interactive sessions"},
cli.StringFlag{Name: "hostPrivateKey", Usage: "path to host SSH private key"},
cli.StringFlag{Name: "hostCert", Usage: "path to host SSH signed certificate"},
cli.StringFlag{Name: "backend", Value: "etcd", Usage: "backend type, currently only 'etcd'"},
cli.StringFlag{Name: "backendConfig", Value: "", Usage: "backend-specific configuration string"},
cli.BoolFlag{Name: "authSrv", Usage: "whether to start CA authority HTTP controller server"},
cli.StringFlag{Name: "authAddr", Value: "localhost:2023", Usage: "CA Auth controller server host:port pair to listen on"},
cli.StringFlag{Name: "log", Value: "console", Usage: "Log output, currently 'console' or 'syslog'"},
cli.StringFlag{Name: "logSeverity", Value: "WARN", Usage: "Log severity, logs warning by default"},
}
app.Action = run
app.Run(os.Args)
}
func run(c *cli.Context) {
if err := parseArgs(c); err != nil {
log.Errorf("failed to parse arguments, err %v", err)
}
}
func setupLogging(c *cli.Context) error {
s, err := log.SeverityFromString(c.String("logSeverity"))
if err != nil {
return err
}
log.Init([]*log.LogConfig{&log.LogConfig{Name: c.String("log")}})
log.SetSeverity(s)
return nil
}
func parseArgs(c *cli.Context) error {
if err := setupLogging(c); err != nil {
return err
}
hostKey, err := utils.ReadPath(c.String("hostPrivateKey"))
if err != nil {
return err
}
hostCert, err := utils.ReadPath(c.String("hostCert"))
if err != nil {
return err
}
b, err := initBackend(c.String("backend"), c.String("backendConfig"))
if err != nil {
return err
}
cfg := srv.Config{
Addr: c.String("addr"),
HostCert: hostCert,
HostKey: hostKey,
Backend: b,
Shell: c.String("shell"),
EventLogger: lunk.NewTextEventLogger(log.GetLogger().Writer(log.SeverityInfo)),
}
s, err := srv.New(cfg)
if err != nil {
return err
}
log.Infof("teleport ssh starting on %v", cfg.Addr)
if err := s.Start(); err != nil {
log.Fatalf("failed to start: %v", err)
}
// TODO(klizhentas): cleanup this code and remove this logic to supervisor
wg := &sync.WaitGroup{}
if c.Bool("authSrv") {
wg.Add(1)
go func() {
defer wg.Done()
authAddr := c.String("authAddr")
log.Infof("teleport http authority starting on %v", authAddr)
srv := auth.NewAPIServer(auth.NewAuthServer(b, openssh.New()))
hsrv := &http.Server{
Addr: authAddr,
Handler: srv,
}
if err := hsrv.ListenAndServe(); err != nil {
log.Fatalf("failed to start: %v", err)
}
log.Infof("teleport auth server exited gracefully")
}()
}
wg.Wait()
return nil
}
func initBackend(btype, bcfg string) (backend.Backend, error) {
switch btype {
case "etcd":
return etcdbk.FromString(bcfg)
}
return nil, fmt.Errorf("unsupported backend type: %v", btype)
}
+13
View File
@@ -0,0 +1,13 @@
package utils
import (
"github.com/codahale/lunk"
)
var NullEventLogger lunk.EventLogger = &NOPEventLogger{}
type NOPEventLogger struct {
}
func (*NOPEventLogger) Log(lunk.EventID, lunk.Event) {
}
+23
View File
@@ -0,0 +1,23 @@
package utils
import (
"fmt"
"io/ioutil"
"path/filepath"
)
func ReadPath(path string) ([]byte, error) {
s, err := filepath.Abs(path)
if err != nil {
return nil, fmt.Errorf("failed to convert path %v, err %v", s, err)
}
abs, err := filepath.EvalSymlinks(s)
if err != nil {
return nil, fmt.Errorf("failed to eval symlinks in path %v, err %v", path, err)
}
bytes, err := ioutil.ReadFile(abs)
if err != nil {
return nil, fmt.Errorf("failed to read private host key by path '%v', error: %v", abs, err)
}
return bytes, nil
}