diff --git a/README.rst b/README.rst index 64e5fba..a96da42 100644 --- a/README.rst +++ b/README.rst @@ -273,6 +273,7 @@ Effectively the format is:: MaxCertLifetime SigningKeyFingerprint PrivateKeyFile + KmsRegion AuthorizedSigners { : } @@ -294,10 +295,18 @@ Effectively the format is:: sign complete requests. This should be the fingerprint of your CA. When using this option you must, somehow, load the private key into the agent such that the daemon can use it. -- ``PrivateKeyFile``: A path to a private key file. As of this writing the key - must be unencrypted. Do take explicit care if you're using unencrypted - private keys. The next release / commit will include support for private key - files that are encrypted using Amazon's KMS. +- ``PrivateKeyFile``: A path to a private key file. The key may be + unencrypted or have previously been encrypted using Amazon's KMS. If + the key was encrypted using KMS simply name it with a ".kms" extension + and ssh-cert-authority will attempt to decrypt the key on startup. See + the section on Encrypting a CA Key for help in using KMS to encrypt + the key. +- ``KmsRegion``: If sign_certd encounters a privatekey file with an + extension of ".kms" it will attempt to decrypt it using KMS in the + same region that the software is running in. It determines this using + the local instance's metadata server. If you're not running + ssh-cert-authority within AWS or if the key is in a different region + you'll need to specify the region here as a string, e.g. us-west-2. - ``AuthorizedSigners``: A hash keyed by key fingerprints and values of key ids. I recommend this be set to a username. It will appear in the resultant SSH certificate in the KeyId field as well in @@ -336,6 +345,73 @@ You can take that value and add in your keys like so:: Once the server is up and running it is bound to 0.0.0.0 on port 8080. +Encrypting a CA Key Using Amazon's KMS +====================================== + +Amazon's KMS (Key Management Service) provides an encryption key +management service that can be used to encrypt small chunks of arbitrary +data (including other keys). This project supports using KMS to keep the +CA key secure. + +The recommended deployment is to launch ssh-cert-authority onto an EC2 +instance that has an EC2 instance profile attached to it that allows it +to use KMS to decrypt the CA key. A sample cloudformation stack is +forthcoming to do all of this on your behalf. + +Create Instance Profile +``````````````````````` + +In the mean time you can set things up by hand. A sample EC2 instance +profile access policy:: + + { + "Statement": [ + { + "Resource": [ + "*" + ], + "Action": [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt", + "kms:GenerateDataKey", + "kms:DescribeKey" + ], + "Effect": "Allow" + } + ], + "Version": "2012-10-17" + } + +Create KMS Key +`````````````` + +Create a KMS key in the AWS IAM console. When specifying key usage allow the +instance profile you created earlier to use the key. The key you create +will have an id associated with it, it looks something like this: + + arn:aws:kms:us-west-2:123412341234:key/debae348-3666-4cc7-9d25-41e33edb2909 + +Save that for the next step. + +Launch Instance +``````````````` + +Now launch an instance and use the EC2 instance profile. A t2 class instance is +likely sufficient. Copy over the latest ssh-cert-authority binary (you +can also use the container) and generate a new key for the CA using +ssh-keygen and then use ssh-cert-authority to encrypt it:: + + environment_name=production + ssh-keygen -q -t rsa -b 4096 -C "ssh-cert-authority ${environment_name}" -f ca-key-${environment_name} + cat ca-key-${environment_name} | ./ssh-cert-authority-linux-amd64 encrypt-key --key-id \ + arn:aws:kms:us-west-2:881577346222:key/d1401480-8220-4bb7-a1de-d03dfda44a13 \ + --output ca-key-${environment_name}.kms && rm ca-key-${environment_name} + +At this point you're ready to fire up the authority. The rest of this +document applies, simply add a PrivateKeyFile option to signer certd's +config for the environment you're working on and reference the path to +the encrypted file we just created, `ca-key-${environment_name}.kms` Requesting Certificates ======================= diff --git a/encrypt.go b/encrypt.go new file mode 100644 index 0000000..48b80f2 --- /dev/null +++ b/encrypt.go @@ -0,0 +1,53 @@ +package main + +import ( + "bufio" + "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/ec2metadata" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/kms" + "github.com/codegangsta/cli" + "io/ioutil" + "os" +) + +func encryptFlags() []cli.Flag { + return []cli.Flag{ + cli.StringFlag{ + Name: "key-id", + Value: "", + Usage: "The ARN of the KMS key to use", + }, + cli.StringFlag{ + Name: "output", + Value: "ca-key.kms", + Usage: "The filename for key output", + }, + } +} + +func encryptKey(c *cli.Context) { + region, err := ec2metadata.New(session.New(), aws.NewConfig()).Region() + if err != nil { + fmt.Printf("Unable to determine our region: %s", err) + os.Exit(1) + } + keyContents, err := ioutil.ReadAll(bufio.NewReader(os.Stdin)) + if err != nil { + fmt.Printf("Unable to read private key: %s", err) + os.Exit(1) + } + svc := kms.New(session.New(), aws.NewConfig().WithRegion(region)) + params := &kms.EncryptInput{ + Plaintext: keyContents, + KeyId: aws.String(c.String("key-id")), + } + resp, err := svc.Encrypt(params) + if err != nil { + fmt.Printf("Unable to Encrypt CA key: %v\n", err) + os.Exit(1) + } + keyContents = resp.CiphertextBlob + ioutil.WriteFile(c.String("output"), resp.CiphertextBlob, 0444) +} diff --git a/main.go b/main.go index 8e0b563..c3ac6b7 100644 --- a/main.go +++ b/main.go @@ -47,6 +47,12 @@ func main() { Usage: "Run the cert-authority web service", Action: signCertd, }, + { + Name: "encrypt-key", + Flags: encryptFlags(), + Usage: "Encrypt an ssh private key from stdin", + Action: encryptKey, + }, } app.Run(os.Args) } diff --git a/sign_certd.go b/sign_certd.go index 27836b6..3bc7024 100644 --- a/sign_certd.go +++ b/sign_certd.go @@ -8,6 +8,10 @@ import ( "encoding/json" "errors" "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/ec2metadata" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/kms" "github.com/cloudtools/ssh-cert-authority/client" "github.com/cloudtools/ssh-cert-authority/util" "github.com/codegangsta/cli" @@ -107,6 +111,29 @@ func (h *certRequestHandler) setupPrivateKeys(config map[string]ssh_ca_util.Sign if err != nil { return fmt.Errorf("Failed reading private key file %s: %v", cfg.PrivateKeyFile, err) } + if strings.HasSuffix(cfg.PrivateKeyFile, ".kms") { + var region string + if cfg.KmsRegion != "" { + region = cfg.KmsRegion + } else { + region, err = ec2metadata.New(session.New(), aws.NewConfig()).Region() + if err != nil { + return fmt.Errorf("Unable to determine our region: %s", err) + } + } + svc := kms.New(session.New(), aws.NewConfig().WithRegion(region)) + params := &kms.DecryptInput{ + CiphertextBlob: keyContents, + } + resp, err := svc.Decrypt(params) + if err != nil { + // We try only one time to speak with KMS. If this pukes, and it + // will occasionally because "the cloud", the caller is responsible + // for trying again, possibly after a crash/restart. + return fmt.Errorf("Unable to decrypt CA key: %v\n", err) + } + keyContents = resp.Plaintext + } key, err := ssh.ParseRawPrivateKey(keyContents) if err != nil { return fmt.Errorf("Failed parsing private key %s: %v", cfg.PrivateKeyFile, err) @@ -585,7 +612,11 @@ func runSignCertd(config map[string]ssh_ca_util.SignerdConfig) { } requestHandler := makeCertRequestHandler(config) requestHandler.sshAgentConn = sshAgentConn - requestHandler.setupPrivateKeys(config) + err = requestHandler.setupPrivateKeys(config) + if err != nil { + log.Println("Failed CA key load: %v\n", err) + os.Exit(1) + } log.Println("Server started with config", config) diff --git a/util/config.go b/util/config.go index 29ba1c9..4c63f80 100644 --- a/util/config.go +++ b/util/config.go @@ -21,6 +21,7 @@ type SignerdConfig struct { SlackChannel string MaxCertLifetime int PrivateKeyFile string + KmsRegion string } type SignerConfig struct {