forked from TrueCloudLab/certificates
Add support for /ssh/bastion method.
This commit is contained in:
parent
8585b29711
commit
86a0558587
8 changed files with 197 additions and 2 deletions
|
@ -261,6 +261,7 @@ func (h *caHandler) Route(r Router) {
|
||||||
r.MethodFunc("POST", "/ssh/config/{type}", h.SSHConfig)
|
r.MethodFunc("POST", "/ssh/config/{type}", h.SSHConfig)
|
||||||
r.MethodFunc("POST", "/ssh/check-host", h.SSHCheckHost)
|
r.MethodFunc("POST", "/ssh/check-host", h.SSHCheckHost)
|
||||||
r.MethodFunc("GET", "/ssh/get-hosts", h.SSHGetHosts)
|
r.MethodFunc("GET", "/ssh/get-hosts", h.SSHGetHosts)
|
||||||
|
r.MethodFunc("POST", "/ssh/bastion", h.SSHBastion)
|
||||||
|
|
||||||
// For compatibility with old code:
|
// For compatibility with old code:
|
||||||
r.MethodFunc("POST", "/re-sign", h.Renew)
|
r.MethodFunc("POST", "/re-sign", h.Renew)
|
||||||
|
|
|
@ -557,6 +557,7 @@ type mockAuthority struct {
|
||||||
getSSHFederation func() (*authority.SSHKeys, error)
|
getSSHFederation func() (*authority.SSHKeys, error)
|
||||||
getSSHConfig func(typ string, data map[string]string) ([]templates.Output, error)
|
getSSHConfig func(typ string, data map[string]string) ([]templates.Output, error)
|
||||||
checkSSHHost func(principal string) (bool, error)
|
checkSSHHost func(principal string) (bool, error)
|
||||||
|
getSSHBastion func(user string, hostname string) (*authority.Bastion, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove once Authorize is deprecated.
|
// TODO: remove once Authorize is deprecated.
|
||||||
|
@ -711,6 +712,13 @@ func (m *mockAuthority) CheckSSHHost(principal string) (bool, error) {
|
||||||
return m.ret1.(bool), m.err
|
return m.ret1.(bool), m.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockAuthority) GetSSHBastion(user string, hostname string) (*authority.Bastion, error) {
|
||||||
|
if m.getSSHBastion != nil {
|
||||||
|
return m.getSSHBastion(user, hostname)
|
||||||
|
}
|
||||||
|
return m.ret1.(*authority.Bastion), m.err
|
||||||
|
}
|
||||||
|
|
||||||
func Test_caHandler_Route(t *testing.T) {
|
func Test_caHandler_Route(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
Authority Authority
|
Authority Authority
|
||||||
|
|
47
api/ssh.go
47
api/ssh.go
|
@ -24,6 +24,7 @@ type SSHAuthority interface {
|
||||||
GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error)
|
GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error)
|
||||||
CheckSSHHost(principal string) (bool, error)
|
CheckSSHHost(principal string) (bool, error)
|
||||||
GetSSHHosts() ([]string, error)
|
GetSSHHosts() ([]string, error)
|
||||||
|
GetSSHBastion(user string, hostname string) (*authority.Bastion, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSHSignRequest is the request body of an SSH certificate request.
|
// SSHSignRequest is the request body of an SSH certificate request.
|
||||||
|
@ -207,6 +208,28 @@ type SSHCheckPrincipalResponse struct {
|
||||||
Exists bool `json:"exists"`
|
Exists bool `json:"exists"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SSHBastionRequest is the request body used to get the bastion for a given
|
||||||
|
// host.
|
||||||
|
type SSHBastionRequest struct {
|
||||||
|
User string `json:"user"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks the values of the SSHBastionRequest.
|
||||||
|
func (r *SSHBastionRequest) Validate() error {
|
||||||
|
if r.Hostname == "" {
|
||||||
|
return errors.New("missing or empty hostname")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHBastionResponse is the response body used to return the bastion for a
|
||||||
|
// given host.
|
||||||
|
type SSHBastionResponse struct {
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
Bastion *authority.Bastion `json:"bastion,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// SSHSign is an HTTP handler that reads an SignSSHRequest with a one-time-token
|
// SSHSign is an HTTP handler that reads an SignSSHRequest with a one-time-token
|
||||||
// (ott) from the body and creates a new SSH certificate with the information in
|
// (ott) from the body and creates a new SSH certificate with the information in
|
||||||
// the request.
|
// the request.
|
||||||
|
@ -392,3 +415,27 @@ func (h *caHandler) SSHGetHosts(w http.ResponseWriter, r *http.Request) {
|
||||||
Hosts: hosts,
|
Hosts: hosts,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SSHBastion provides returns the bastion configured if any.
|
||||||
|
func (h *caHandler) SSHBastion(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var body SSHBastionRequest
|
||||||
|
if err := ReadJSON(r.Body, &body); err != nil {
|
||||||
|
WriteError(w, BadRequest(errors.Wrap(err, "error reading request body")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := body.Validate(); err != nil {
|
||||||
|
WriteError(w, BadRequest(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bastion, err := h.Authority.GetSSHBastion(body.User, body.Hostname)
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, InternalServerError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
JSON(w, &SSHBastionResponse{
|
||||||
|
Hostname: body.Hostname,
|
||||||
|
Bastion: bastion,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -537,6 +537,61 @@ func Test_caHandler_SSHCheckHost(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_caHandler_SSHBastion(t *testing.T) {
|
||||||
|
bastion := &authority.Bastion{
|
||||||
|
Hostname: "bastion.local",
|
||||||
|
}
|
||||||
|
bastionPort := &authority.Bastion{
|
||||||
|
Hostname: "bastion.local",
|
||||||
|
Port: "2222",
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
bastion *authority.Bastion
|
||||||
|
bastionErr error
|
||||||
|
req []byte
|
||||||
|
body []byte
|
||||||
|
statusCode int
|
||||||
|
}{
|
||||||
|
{"ok", bastion, nil, []byte(`{"hostname":"host.local"}`), []byte(`{"hostname":"host.local","bastion":{"hostname":"bastion.local"}}`), http.StatusOK},
|
||||||
|
{"ok", bastionPort, nil, []byte(`{"hostname":"host.local","user":"user"}`), []byte(`{"hostname":"host.local","bastion":{"hostname":"bastion.local","port":"2222"}}`), http.StatusOK},
|
||||||
|
{"empty", nil, nil, []byte(`{"hostname":"host.local"}`), []byte(`{"hostname":"host.local"}`), http.StatusOK},
|
||||||
|
{"bad json", bastion, nil, []byte(`bad json`), nil, http.StatusBadRequest},
|
||||||
|
{"bad request", bastion, nil, []byte(`{"hostname": ""}`), nil, http.StatusBadRequest},
|
||||||
|
{"error", nil, fmt.Errorf("an error"), []byte(`{"hostname":"host.local"}`), nil, http.StatusInternalServerError},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
h := New(&mockAuthority{
|
||||||
|
getSSHBastion: func(user, hostname string) (*authority.Bastion, error) {
|
||||||
|
return tt.bastion, tt.bastionErr
|
||||||
|
},
|
||||||
|
}).(*caHandler)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://example.com/ssh/bastion", bytes.NewReader(tt.req))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
h.SSHBastion(logging.NewResponseLogger(w), req)
|
||||||
|
res := w.Result()
|
||||||
|
|
||||||
|
if res.StatusCode != tt.statusCode {
|
||||||
|
t.Errorf("caHandler.SSHBastion StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(res.Body)
|
||||||
|
res.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("caHandler.SSHBastion unexpected error = %v", err)
|
||||||
|
}
|
||||||
|
if tt.statusCode < http.StatusBadRequest {
|
||||||
|
if !bytes.Equal(bytes.TrimSpace(body), tt.body) {
|
||||||
|
t.Errorf("caHandler.SSHBastion Body = %s, wants %s", body, tt.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSSHPublicKey_MarshalJSON(t *testing.T) {
|
func TestSSHPublicKey_MarshalJSON(t *testing.T) {
|
||||||
key, err := ssh.NewPublicKey(sshUserKey.Public())
|
key, err := ssh.NewPublicKey(sshUserKey.Public())
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
|
|
@ -39,6 +39,8 @@ type Authority struct {
|
||||||
db db.AuthDB
|
db db.AuthDB
|
||||||
// Do not re-initialize
|
// Do not re-initialize
|
||||||
initOnce bool
|
initOnce bool
|
||||||
|
// Custom functions
|
||||||
|
sshBastionFunc func(user, hostname string) (*Bastion, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates and initiates a new Authority type.
|
// New creates and initiates a new Authority type.
|
||||||
|
|
|
@ -14,3 +14,11 @@ func WithDatabase(db db.AuthDB) Option {
|
||||||
a.db = db
|
a.db = db
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithSSHBastionFunc defines sets a custom function to get the bastion for a
|
||||||
|
// given user-host pair.
|
||||||
|
func WithSSHBastionFunc(fn func(user, host string) (*Bastion, error)) Option {
|
||||||
|
return func(a *Authority) {
|
||||||
|
a.sshBastionFunc = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -34,8 +34,18 @@ type SSHConfig struct {
|
||||||
HostKey string `json:"hostKey"`
|
HostKey string `json:"hostKey"`
|
||||||
UserKey string `json:"userKey"`
|
UserKey string `json:"userKey"`
|
||||||
Keys []*SSHPublicKey `json:"keys,omitempty"`
|
Keys []*SSHPublicKey `json:"keys,omitempty"`
|
||||||
AddUserPrincipal string `json:"addUserPrincipal"`
|
AddUserPrincipal string `json:"addUserPrincipal,omitempty"`
|
||||||
AddUserCommand string `json:"addUserCommand"`
|
AddUserCommand string `json:"addUserCommand,omitempty"`
|
||||||
|
Bastion *Bastion `json:"bastion,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bastion contains the custom properties used on bastion.
|
||||||
|
type Bastion struct {
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
User string `json:"user,omitempty"`
|
||||||
|
Port string `json:"port,omitempty"`
|
||||||
|
Command string `json:"cmd,omitempty"`
|
||||||
|
Flags string `json:"flags,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate checks the fields in SSHConfig.
|
// Validate checks the fields in SSHConfig.
|
||||||
|
@ -157,6 +167,24 @@ func (a *Authority) GetSSHConfig(typ string, data map[string]string) ([]template
|
||||||
return output, nil
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSSHBastion returns the bastion configuration, for the given pair user,
|
||||||
|
// hostname.
|
||||||
|
func (a *Authority) GetSSHBastion(user string, hostname string) (*Bastion, error) {
|
||||||
|
if a.sshBastionFunc != nil {
|
||||||
|
return a.sshBastionFunc(user, hostname)
|
||||||
|
}
|
||||||
|
if a.config.SSH != nil {
|
||||||
|
if a.config.SSH.Bastion != nil && a.config.SSH.Bastion.Hostname != "" {
|
||||||
|
return a.config.SSH.Bastion, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, &apiError{
|
||||||
|
err: errors.New("getSSHBastion: ssh is not configured"),
|
||||||
|
code: http.StatusNotFound,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// authorizeSSHSign loads the provisioner from the token, checks that it has not
|
// authorizeSSHSign loads the provisioner from the token, checks that it has not
|
||||||
// been used again and calls the provisioner AuthorizeSSHSign method. Returns a
|
// been used again and calls the provisioner AuthorizeSSHSign method. Returns a
|
||||||
// list of methods to apply to the signing flow.
|
// list of methods to apply to the signing flow.
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
"github.com/smallstep/certificates/db"
|
"github.com/smallstep/certificates/db"
|
||||||
|
@ -598,3 +599,48 @@ func TestSSHPublicKey_PublicKey(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuthority_GetSSHBastion(t *testing.T) {
|
||||||
|
bastion := &Bastion{
|
||||||
|
Hostname: "bastion.local",
|
||||||
|
Port: "2222",
|
||||||
|
}
|
||||||
|
type fields struct {
|
||||||
|
config *Config
|
||||||
|
sshBastionFunc func(user, hostname string) (*Bastion, error)
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
user string
|
||||||
|
hostname string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want *Bastion
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"config", fields{&Config{SSH: &SSHConfig{Bastion: bastion}}, nil}, args{"user", "host.local"}, bastion, false},
|
||||||
|
{"nil", fields{&Config{SSH: &SSHConfig{Bastion: nil}}, nil}, args{"user", "host.local"}, nil, false},
|
||||||
|
{"empty", fields{&Config{SSH: &SSHConfig{Bastion: &Bastion{}}}, nil}, args{"user", "host.local"}, nil, false},
|
||||||
|
{"func", fields{&Config{}, func(_, _ string) (*Bastion, error) { return bastion, nil }}, args{"user", "host.local"}, bastion, false},
|
||||||
|
{"func err", fields{&Config{}, func(_, _ string) (*Bastion, error) { return nil, errors.New("foo") }}, args{"user", "host.local"}, nil, true},
|
||||||
|
{"error", fields{&Config{SSH: nil}, nil}, args{"user", "host.local"}, nil, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
a := &Authority{
|
||||||
|
config: tt.fields.config,
|
||||||
|
sshBastionFunc: tt.fields.sshBastionFunc,
|
||||||
|
}
|
||||||
|
got, err := a.GetSSHBastion(tt.args.user, tt.args.hostname)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Authority.GetSSHBastion() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("Authority.GetSSHBastion() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue