mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
Initial working prototype
This commit is contained in:
@@ -22,3 +22,4 @@ _testmain.go
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
flymake*
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||

|
||||
|
||||
* 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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")))
|
||||
}
|
||||
@@ -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
@@ -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}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 |
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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-----
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDAg/1ATNYxdjv0iv0IkVkYH8kHNbfDA0BhZSqahUb3VPYV5EnTVixpYtrpD5XPJljmOp+lg2DQzK+M0cC17xwGkFz7uhiMZTOUGgOoT0TxAmCOm/eClUtT2+6neCiVGGFKLrGqBOgYYZXHTAoI47HaoFgsnpQt2M8+Svi9spS2ezu2faYI9Ypd3GyzdJ7MNsgVWwwlcDc2txsvwd4FtWRNbbakFmlDEuUl5fn15BzGdohkxwrENyTMFqQJbv2HTuilr+4z2QpyNR9YGQmMEp7UeTDeE9aCgC7gw99vZWjwtPx36ya1UUzmVNbnbQP7eotnmVocumsxhYKweYTMhHhr alex@vulcand
|
||||
@@ -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-----
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8+tx1EIrcmKknxw76Mypa30bCHbaotopUCXPuJn72LiYtrro44uTQfyMv4egGXgyveuN8C0vthOeK1UH4C+6k94cF1qAetjjPHFOSRJHgOKS2zWHd5QBsMXC86fKVM9hNIBM5Vfnm4j8gDsXAx8oPfKfHgnODQNdvTGrisdXPrFuQTosiQXNq5oxMTK8dcSo9+qjrtKxSR9ukM11fubV01bPmQoVYaXebT2qOb/2AeQm115Ke2zydwMMYiXD9iLDF2vRFOPGFFKGwqkTqYOXZV2BfrQKC7oC1XgXvckRVwhk1B1kd3z2DpW6o4MP/k+TYNWEJpnPmgTVXucCZ4bnZ alex@vulcand
|
||||
@@ -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-----
|
||||
@@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3kxZBLziVsl91ZFwBAt1yzTp7oeq8MBU9ZmNcdc/7RgXkuONKBsnGpmondCQHvMPAjwzuJeUoPUZT/yxV5rhrjuRIchFcntNWaL8oioBb/C78/C918BlwkAbfBGGvpBeo5fB12rMK6nshDhJJj7WTj48z+qlf5B0/pByPVKN61SZDeVwKNOFPvL0JjrEN0NbmHI8mAZ5DfChPXD8AOhPcS5xspYHNSk6CJIdp7+gW0ZA0j8ZMyGMfQDB4TZ+m6UGhQHLU6fHTA7019tEVJHd9mGVIGgQ4TdjN6bXK56CFSEdoRnh9rEC2Ad8ApOJLBG4EfE1Ellx0hfa4SEyT8vc7 alex@vulcand
|
||||
@@ -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)"
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user