Skip to content

Commit

Permalink
Hostkey support (closes #4)
Browse files Browse the repository at this point in the history
A bit of duplication but the functionality is there. Implements the "other side"
of SSH certs - signed host keys. This is designed to be executed as part of an
instance's userdata startup script.
  • Loading branch information
aidansteele committed Jul 3, 2017
1 parent faf7727 commit fc2b995
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 17 deletions.
2 changes: 2 additions & 0 deletions ci/cfn.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ Resources:
StringEqualsIfExists:
aws:username: "${kms:EncryptionContext:fromName}"
kms:EncryptionContext:fromName: "${aws:username}"
ec2:SourceInstanceARN: "${kms:EncryptionContext:hostInstanceArn}"
kms:EncryptionContext:hostInstanceArn: "${ec2:SourceInstanceARN}"
# Bool:
# aws:MultiFactorAuthPresent: true
- Sid: KmsLambdaAuth
Expand Down
4 changes: 3 additions & 1 deletion ci/expected-output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
Serial: (uint64) 0,
CertType: (uint32) 1,
KeyId: (string) (len=62) "lkp-travis-user-TravisUser-1ILBFVUOPN36N-AIDAJZF7RF5JNJR5CBDIE",
ValidPrincipals: ([]string) <nil>,
ValidPrincipals: ([]string) (len=1 cap=1) {
(string) (len=8) "ec2-user"
},
Permissions: (ssh.Permissions) {
CriticalOptions: (map[string]string) {
},
Expand Down
182 changes: 182 additions & 0 deletions cmd/host.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package cmd

import (
"fmt"

"github.com/spf13/cobra"
"io/ioutil"
"github.com/pkg/errors"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"log"
"github.com/glassechidna/lastkeypair/common"
"github.com/aws/aws-sdk-go/aws"
"encoding/json"
"github.com/aws/aws-sdk-go/service/lambda"
"os"
)

var hostCmd = &cobra.Command{
Use: "host",
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
hostKeyPath, _ := cmd.PersistentFlags().GetString("host-key-path")
signedHostKeyPath, _ := cmd.PersistentFlags().GetString("signed-host-key-path")
caPubkeyPath, _ := cmd.PersistentFlags().GetString("cert-authority-path")
sshdConfigPath, _ := cmd.PersistentFlags().GetString("sshd-config-path")
functionName, _ := cmd.PersistentFlags().GetString("lambda-name")
kmsKeyId, _ := cmd.PersistentFlags().GetString("kms-key")
funcIdentity, _ := cmd.PersistentFlags().GetString("func-identity")

err := doit(hostKeyPath, signedHostKeyPath, caPubkeyPath, sshdConfigPath, functionName, kmsKeyId, funcIdentity)
if err != nil {
log.Panicf("err: %s\n", err.Error())
}
},
}

func doit(hostKeyPath, signedHostKeyPath, caPubkeyPath, sshdConfigPath, functionName, kmsKeyId, funcIdentity string) error {
hostKeyBytes, err := ioutil.ReadFile(hostKeyPath)
if err != nil {
return errors.Wrap(err, "reading ssh host key")
}
hostKey := string(hostKeyBytes)

sessOpts := session.Options{
SharedConfigState: session.SharedConfigEnable,
AssumeRoleTokenProvider: stscreds.StdinTokenProvider,
}

sess, err := session.NewSessionWithOptions(sessOpts)
if err != nil {
return errors.Wrap(err, "creating aws session")
}

ident, err := common.CallerIdentityUser(sess)
instanceArn, err := getInstanceArn(sess)
token, err := hostCertToken(sess, *ident, kmsKeyId, funcIdentity, *instanceArn)

client := ec2metadata.New(sess)
caPubkey, err := client.GetMetadata("public-keys/0/openssh-key")
if err != nil {
return errors.Wrap(err, "fetching ssh CA key")
}

response, err := requestSignedHostKey(sess, functionName, common.HostCertReqJson{
EventType: "HostCertReq",
Token: *token,
PublicKey: hostKey,
})

err = ioutil.WriteFile(signedHostKeyPath, []byte(response.SignedHostPublicKey), 0600)
if err != nil {
return errors.Wrap(err, "writing signed host key to filesystem")
}

err = ioutil.WriteFile(caPubkeyPath, []byte(caPubkey), 0600)
if err != nil {
return errors.Wrap(err, "writing ca pubkey to filesystem")
}

err = appendToFile(sshdConfigPath, fmt.Sprintf(`
HostCertificate %s
TrustedUserCAKeys %s
`, signedHostKeyPath, caPubkeyPath))
if err != nil {
return errors.Wrap(err, "appending to sshd config")
}

return nil
}

func getInstanceArn(sess *session.Session) (*string, error) {
client := ec2metadata.New(sess)

region, err := client.Region()
if err != nil {
return nil, errors.Wrap(err, "getting region")
}

ident, err := client.GetInstanceIdentityDocument()
if err != nil {
return nil, errors.Wrap(err, "getting identity doc for account id and instance id")
}

ret := fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", region, ident.AccountID, ident.InstanceID)
return &ret, nil

}

func requestSignedHostKey(sess *session.Session, functionName string, request common.HostCertReqJson) (*common.HostCertRespJson, error) {
payload, err := json.Marshal(&request)
if err != nil {
return nil, errors.Wrap(err, "couldn't serialise host cert req json")
}

client := lambda.New(sess)

input := lambda.InvokeInput{
FunctionName: aws.String(functionName),
Payload: payload,
}

resp, err := client.Invoke(&input)
if err != nil {
return nil, errors.Wrap(err, "invoking CA lambda")
}

response := common.HostCertRespJson{}
err = json.Unmarshal(resp.Payload, &response)
if err != nil {
return nil, errors.Wrap(err, "unmarshalling lambda resp payload")
}

return &response, nil
}

func hostCertToken(sess *session.Session, ident common.StsIdentity, kmsKeyId, funcIdentity, instanceArn string) (*common.Token, error) {
params := common.TokenParams{
KeyId: kmsKeyId,
FromId: ident.UserId,
FromAccount: ident.AccountId,
To: funcIdentity,
Type: "AssumedRole",
HostInstanceArn: instanceArn,
}

ret := common.CreateToken(sess, params)
return &ret, nil
}

func appendToFile(path, text string) error {
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
if err != nil {
return err
}
defer f.Close()

_, err = f.WriteString(text)
if err != nil {
return err
}
return nil
}

func init() {
RootCmd.AddCommand(hostCmd)

hostCmd.PersistentFlags().String("host-key-path", "/etc/ssh/ssh_host_rsa_key.pub", "")
hostCmd.PersistentFlags().String("signed-host-key-path", "/etc/ssh/ssh_host_rsa_key-cert.pub", "")
hostCmd.PersistentFlags().String("cert-authority-path", "/etc/ssh/cert_authority.pub", "")
hostCmd.PersistentFlags().String("sshd-config-path", "/etc/ssh/sshd_config", "")
hostCmd.PersistentFlags().String("lambda-name", "LastKeypair", "")
hostCmd.PersistentFlags().String("func-identity", "LastKeypair", "")
hostCmd.PersistentFlags().String("kms-key", "alias/LastKeypair", "ID, ARN or alias of KMS key for auth to CA")
}
4 changes: 3 additions & 1 deletion cmd/sshExec.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ to quickly create a Cobra application.`,
lambdaFunc, _ := cmd.PersistentFlags().GetString("lambda-func")
kmsKeyId, _ := cmd.PersistentFlags().GetString("kms-key")
funcIdentity, _ := cmd.PersistentFlags().GetString("func-identity")
username, _ := cmd.PersistentFlags().GetString("ssh-username")

common.SshExec(sess, lambdaFunc, funcIdentity, kmsKeyId, args)
common.SshExec(sess, lambdaFunc, funcIdentity, kmsKeyId, username, args)
},
}

Expand All @@ -33,4 +34,5 @@ func init() {
sshExecCmd.PersistentFlags().String("lambda-func", "LastKeypair", "Function name or ARN")
sshExecCmd.PersistentFlags().String("kms-key", "alias/LastKeypair", "ID, ARN or alias of KMS key for auth to CA")
sshExecCmd.PersistentFlags().String("func-identity", "LastKeypair", "")
sshExecCmd.PersistentFlags().String("ssh-username", "ec2-user", "Username that you wish to SSH in with")
}
54 changes: 51 additions & 3 deletions common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,54 @@ import (
var ApplicationVersion string
var ApplicationBuildDate string

func SignSsh(caKeyBytes, userPubkeyBytes []byte, durationSecs int64, keyId string, principals []string) (*string, error) {
func SignHostSsh(caKeyBytes, pubkeyBytes []byte, expiry uint64, keyId string) (*string, error) {
signer, err := ssh.ParsePrivateKey(caKeyBytes)
if err != nil {
return nil, errors.Wrap(err, "err parsing ca priv key")
}

userPubkey, _, _, _, err := ssh.ParseAuthorizedKey(userPubkeyBytes)
pubkey, _, _, _, err := ssh.ParseAuthorizedKey(pubkeyBytes)
if err != nil {
return nil, errors.Wrap(err, "err parsing user pub key")
}

now := time.Now()
after := now.Add(-300 * time.Second)

cert := &ssh.Certificate{
//Nonce: is generated by cert.SignCert
Key: pubkey,
Serial: 0,
CertType: ssh.HostCert,
ValidAfter: uint64(after.Unix()),
ValidBefore: expiry,
Permissions: ssh.Permissions{
CriticalOptions: map[string]string{},
Extensions: map[string]string{},
},
Reserved: []byte{},
}

randSource := rand.Reader
err = cert.SignCert(randSource, signer)
if err != nil {
return nil, errors.Wrap(err, "err signing cert")
}

signed := cert.Marshal()

b64 := base64.StdEncoding.EncodeToString(signed)
formatted := fmt.Sprintf("%s %s", cert.Type(), b64)
return &formatted, nil
}

func SignSsh(caKeyBytes, pubkeyBytes []byte, durationSecs int64, keyId string, principals []string) (*string, error) {
signer, err := ssh.ParsePrivateKey(caKeyBytes)
if err != nil {
return nil, errors.Wrap(err, "err parsing ca priv key")
}

userPubkey, _, _, _, err := ssh.ParseAuthorizedKey(pubkeyBytes)
if err != nil {
return nil, errors.Wrap(err, "err parsing user pub key")
}
Expand Down Expand Up @@ -107,9 +148,12 @@ type TokenParams struct {
KeyId string
FromId string
FromAccount string
FromName string
To string
Type string

// optional fields
FromName string
HostInstanceArn string
}

func (params *TokenParams) ToKmsContext() map[string]*string {
Expand All @@ -123,6 +167,10 @@ func (params *TokenParams) ToKmsContext() map[string]*string {
context["fromName"] = &params.FromName
}

if len(params.HostInstanceArn) > 0 {
context["hostInstanceArn"] = &params.HostInstanceArn
}

return context
}

Expand Down
Loading

0 comments on commit fc2b995

Please sign in to comment.