Skip to content

Commit a35988f

Browse files
marainodopey
authored andcommitted
Add initial support for ssh config.
Related to smallstep/cli#170
1 parent b000b59 commit a35988f

File tree

9 files changed

+372
-14
lines changed

9 files changed

+372
-14
lines changed

api/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,8 @@ func (h *caHandler) Route(r Router) {
253253
// SSH CA
254254
r.MethodFunc("POST", "/ssh/sign", h.SignSSH)
255255
r.MethodFunc("GET", "/ssh/keys", h.SSHKeys)
256+
r.MethodFunc("POST", "/ssh/config", h.SSHConfig)
257+
r.MethodFunc("POST", "/ssh/config/{type}", h.SSHConfig)
256258

257259
// For compatibility with old code:
258260
r.MethodFunc("POST", "/re-sign", h.Renew)

api/api_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/smallstep/certificates/authority"
3030
"github.com/smallstep/certificates/authority/provisioner"
3131
"github.com/smallstep/certificates/logging"
32+
"github.com/smallstep/certificates/templates"
3233
"github.com/smallstep/cli/crypto/tlsutil"
3334
"github.com/smallstep/cli/jose"
3435
"golang.org/x/crypto/ssh"
@@ -513,6 +514,7 @@ type mockAuthority struct {
513514
getRoots func() ([]*x509.Certificate, error)
514515
getFederation func() ([]*x509.Certificate, error)
515516
getSSHKeys func() (*authority.SSHKeys, error)
517+
getSSHConfig func(typ string) ([]templates.Output, error)
516518
}
517519

518520
// TODO: remove once Authorize is deprecated.
@@ -625,6 +627,13 @@ func (m *mockAuthority) GetSSHKeys() (*authority.SSHKeys, error) {
625627
return m.ret1.(*authority.SSHKeys), m.err
626628
}
627629

630+
func (m *mockAuthority) GetSSHConfig(typ string) ([]templates.Output, error) {
631+
if m.getSSHConfig != nil {
632+
return m.getSSHConfig(typ)
633+
}
634+
return m.ret1.([]templates.Output), m.err
635+
}
636+
628637
func Test_caHandler_Route(t *testing.T) {
629638
type fields struct {
630639
Authority Authority

api/ssh.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/pkg/errors"
1010
"github.com/smallstep/certificates/authority"
1111
"github.com/smallstep/certificates/authority/provisioner"
12+
"github.com/smallstep/certificates/templates"
1213
"golang.org/x/crypto/ssh"
1314
)
1415

@@ -17,6 +18,7 @@ type SSHAuthority interface {
1718
SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
1819
SignSSHAddUser(key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)
1920
GetSSHKeys() (*authority.SSHKeys, error)
21+
GetSSHConfig(typ string) ([]templates.Output, error)
2022
}
2123

2224
// SignSSHRequest is the request body of an SSH certificate request.
@@ -138,6 +140,34 @@ func (p *SSHPublicKey) UnmarshalJSON(data []byte) error {
138140
return nil
139141
}
140142

143+
// Template represents the output of a template.
144+
type Template = templates.Output
145+
146+
// SSHConfigRequest is the request body used to get the SSH configuration
147+
// templates.
148+
type SSHConfigRequest struct {
149+
Type string `json:"type"`
150+
}
151+
152+
// Validate checks the values of the SSHConfigurationRequest.
153+
func (r *SSHConfigRequest) Validate() error {
154+
switch r.Type {
155+
case "":
156+
r.Type = provisioner.SSHUserCert
157+
return nil
158+
case provisioner.SSHUserCert, provisioner.SSHHostCert:
159+
return nil
160+
default:
161+
return errors.Errorf("unsupported type %s", r.Type)
162+
}
163+
}
164+
165+
// SSHConfigResponse is the response that returns the rendered templates.
166+
type SSHConfigResponse struct {
167+
UserTemplates []Template `json:"userTemplates,omitempty"`
168+
HostTemplates []Template `json:"hostTemplates,omitempty"`
169+
}
170+
141171
// SignSSH is an HTTP handler that reads an SignSSHRequest with a one-time-token
142172
// (ott) from the body and creates a new SSH certificate with the information in
143173
// the request.
@@ -228,3 +258,36 @@ func (h *caHandler) SSHKeys(w http.ResponseWriter, r *http.Request) {
228258
UserKey: user,
229259
})
230260
}
261+
262+
// SSHConfig is an HTTP handler that returns rendered templates for ssh clients
263+
// and servers.
264+
func (h *caHandler) SSHConfig(w http.ResponseWriter, r *http.Request) {
265+
var body SSHConfigRequest
266+
if err := ReadJSON(r.Body, &body); err != nil {
267+
WriteError(w, BadRequest(errors.Wrap(err, "error reading request body")))
268+
return
269+
}
270+
if err := body.Validate(); err != nil {
271+
WriteError(w, BadRequest(err))
272+
return
273+
}
274+
275+
ts, err := h.Authority.GetSSHConfig(body.Type)
276+
if err != nil {
277+
WriteError(w, InternalServerError(err))
278+
return
279+
}
280+
281+
var config SSHConfigResponse
282+
switch body.Type {
283+
case provisioner.SSHUserCert:
284+
config.UserTemplates = ts
285+
case provisioner.SSHHostCert:
286+
config.UserTemplates = ts
287+
default:
288+
WriteError(w, InternalServerError(errors.New("it should hot get here")))
289+
return
290+
}
291+
292+
JSON(w, config)
293+
}

authority/authority.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"sync"
99
"time"
1010

11+
"github.com/smallstep/certificates/templates"
12+
1113
"github.com/pkg/errors"
1214
"github.com/smallstep/certificates/authority/provisioner"
1315
"github.com/smallstep/certificates/db"
@@ -154,6 +156,23 @@ func (a *Authority) init() error {
154156
}
155157
}
156158

159+
// Configure protected template variables:
160+
if t := a.config.Templates; t != nil {
161+
if t.Variables == nil {
162+
t.Variables = make(map[string]interface{})
163+
}
164+
var vars templates.Step
165+
if a.config.SSH != nil {
166+
if a.sshCAHostCertSignKey != nil {
167+
vars.SSH.HostKey = a.sshCAHostCertSignKey.PublicKey()
168+
}
169+
if a.sshCAUserCertSignKey != nil {
170+
vars.SSH.UserKey = a.sshCAUserCertSignKey.PublicKey()
171+
}
172+
}
173+
t.Variables["Step"] = vars
174+
}
175+
157176
// JWT numeric dates are seconds.
158177
a.startTime = time.Now().Truncate(time.Second)
159178
// Set flag indicating that initialization has been completed, and should

authority/config.go

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"os"
88
"time"
99

10+
"github.com/smallstep/certificates/templates"
11+
1012
"github.com/pkg/errors"
1113
"github.com/smallstep/certificates/authority/provisioner"
1214
"github.com/smallstep/certificates/db"
@@ -46,19 +48,20 @@ var (
4648

4749
// Config represents the CA configuration and it's mapped to a JSON object.
4850
type Config struct {
49-
Root multiString `json:"root"`
50-
FederatedRoots []string `json:"federatedRoots"`
51-
IntermediateCert string `json:"crt"`
52-
IntermediateKey string `json:"key"`
53-
Address string `json:"address"`
54-
DNSNames []string `json:"dnsNames"`
55-
SSH *SSHConfig `json:"ssh,omitempty"`
56-
Logger json.RawMessage `json:"logger,omitempty"`
57-
DB *db.Config `json:"db,omitempty"`
58-
Monitoring json.RawMessage `json:"monitoring,omitempty"`
59-
AuthorityConfig *AuthConfig `json:"authority,omitempty"`
60-
TLS *tlsutil.TLSOptions `json:"tls,omitempty"`
61-
Password string `json:"password,omitempty"`
51+
Root multiString `json:"root"`
52+
FederatedRoots []string `json:"federatedRoots"`
53+
IntermediateCert string `json:"crt"`
54+
IntermediateKey string `json:"key"`
55+
Address string `json:"address"`
56+
DNSNames []string `json:"dnsNames"`
57+
SSH *SSHConfig `json:"ssh,omitempty"`
58+
Logger json.RawMessage `json:"logger,omitempty"`
59+
DB *db.Config `json:"db,omitempty"`
60+
Monitoring json.RawMessage `json:"monitoring,omitempty"`
61+
AuthorityConfig *AuthConfig `json:"authority,omitempty"`
62+
TLS *tlsutil.TLSOptions `json:"tls,omitempty"`
63+
Password string `json:"password,omitempty"`
64+
Templates *templates.Templates `json:"templates,omitempty"`
6265
}
6366

6467
// AuthConfig represents the configuration options for the authority.
@@ -181,6 +184,11 @@ func (c *Config) Validate() error {
181184
c.TLS.Renegotiation = c.TLS.Renegotiation || DefaultTLSOptions.Renegotiation
182185
}
183186

187+
// Validate templates: nil is ok
188+
if err := c.Templates.Validate(); err != nil {
189+
return err
190+
}
191+
184192
return c.AuthorityConfig.Validate(c.getAudiences())
185193
}
186194

authority/ssh.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"net/http"
77
"strings"
88

9+
"github.com/smallstep/certificates/templates"
10+
911
"github.com/pkg/errors"
1012
"github.com/smallstep/certificates/authority/provisioner"
1113
"github.com/smallstep/cli/crypto/randutil"
@@ -41,13 +43,51 @@ func (a *Authority) GetSSHKeys() (*SSHKeys, error) {
4143
}
4244
if keys.UserKey == nil && keys.HostKey == nil {
4345
return nil, &apiError{
44-
err: errors.New("sshConfig: ssh is not configured"),
46+
err: errors.New("getSSHKeys: ssh is not configured"),
4547
code: http.StatusNotFound,
4648
}
4749
}
4850
return &keys, nil
4951
}
5052

53+
// GetSSHConfig returns rendered templates for clients (user) or servers (host).
54+
func (a *Authority) GetSSHConfig(typ string) ([]templates.Output, error) {
55+
if a.sshCAUserCertSignKey == nil && a.sshCAHostCertSignKey == nil {
56+
return nil, &apiError{
57+
err: errors.New("getSSHConfig: ssh is not configured"),
58+
code: http.StatusNotFound,
59+
}
60+
}
61+
62+
var ts []templates.Template
63+
switch typ {
64+
case provisioner.SSHUserCert:
65+
if a.config.Templates != nil && a.config.Templates.SSH != nil {
66+
ts = a.config.Templates.SSH.User
67+
}
68+
case provisioner.SSHHostCert:
69+
if a.config.Templates != nil && a.config.Templates.SSH != nil {
70+
ts = a.config.Templates.SSH.Host
71+
}
72+
default:
73+
return nil, &apiError{
74+
err: errors.Errorf("getSSHConfig: type %s is not valid", typ),
75+
code: http.StatusBadRequest,
76+
}
77+
}
78+
79+
// Render templates.
80+
output := []templates.Output{}
81+
for _, t := range ts {
82+
o, err := t.Output(a.config.Templates.Variables)
83+
if err != nil {
84+
return nil, err
85+
}
86+
output = append(output, o)
87+
}
88+
return output, nil
89+
}
90+
5191
// SignSSH creates a signed SSH certificate with the given public key and options.
5292
func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
5393
var mods []provisioner.SSHCertificateModifier

ca/client.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,28 @@ func (c *Client) SSHKeys() (*api.SSHKeysResponse, error) {
545545
return &keys, nil
546546
}
547547

548+
// SSHConfig performs the POST request to the CA to get the ssh configuration
549+
// templates.
550+
func (c *Client) SSHConfig(req *api.SSHConfigRequest) (*api.SSHConfigResponse, error) {
551+
body, err := json.Marshal(req)
552+
if err != nil {
553+
return nil, errors.Wrap(err, "error marshaling request")
554+
}
555+
u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/config"})
556+
resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body))
557+
if err != nil {
558+
return nil, errors.Wrapf(err, "client POST %s failed", u)
559+
}
560+
if resp.StatusCode >= 400 {
561+
return nil, readError(resp.Body)
562+
}
563+
var config api.SSHConfigResponse
564+
if err := readJSON(resp.Body, &config); err != nil {
565+
return nil, errors.Wrapf(err, "error reading %s", u)
566+
}
567+
return &config, nil
568+
}
569+
548570
// RootFingerprint is a helper method that returns the current root fingerprint.
549571
// It does an health connection and gets the fingerprint from the TLS verified
550572
// chains.

0 commit comments

Comments
 (0)