accord

package module
v0.0.0-...-44558b4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 23, 2019 License: Apache-2.0 Imports: 32 Imported by: 0

README

Accord: End to End secure SSH Certificate management in Public Cloud

This was originally inspired by the desire to use Netflix's BLESS SSH CA service and Facebook's Scalable and Secure Access with SSH implementations. While these are great efforts, we wanted something that handled both host ssh certificates, client certificates and the certificate management lifecycle. Additionally, Lyft's blessclient is great but we have our authentication standardized on Google Apps, and wanted to utilize that.

Accord is a distillation of all that I have learned into looking at implementating a secure SSH-CA infrastructure.

Accord:

  • Runs on a separate AWS account, and can be ported relatively easily to any other cloud. The terraform task is included to spin up the instances in your own AWS account and start using it
  • You can deploy self-signed certificate or use LetsEncrypt certificate for the server
  • Pure Go: This makes deployment and validation of code straightforward in the sense that you are not linking with version of openssl that the system provides. This is arguable, and I still believe in using ssh-keygen on a fresh machine to create the ssh-ca certificates.
  • There are two SSH certificates allowed on the server, with potentially different lifecycles
    • User CA
    • Host CA
  • Accord server uses AWS Parameter Store to read the passphrase to decrypt the ssh ca keys, this can only be done by assuming the role that allows reading the keys
    • This means any kind of tampering for the keys are logged in CloudTrail's audit log

This service already covers a lot of required work but there are still upcoming features that would make managing it easier, see issues and the TODOs for more details.

TODOs

  • Allow mechanism other than logs for auditing which user or server got the keys
    • a Postgres server with JSON to store server details from multiple sources or something that's pluggable would be great
    • enable a query endpoint for the backend
  • Support hosting on GCE and Azure. I don't know about them quite as well to do it justice. A client can run anywhere but the accord server needs to run in AWS.
  • Reporting on who accessed
  • Run multiple instances by sharing the Let's Encrypt cert, if you're using it.
  • Alerting and reporting
  • Key Rotation and Validity buffer times, ie when you have a replacement set of keys to replace soon, how long before do you start signing with the new keys.
  • Enforce key size, rotation policies for users and servers
  • Allow querying for servers that are about to expire their certificates

How to develop

Get the latest grpc

go get google.golang.org/grpc

go get -u github.com/golang/protobuf/protoc-gen-go

Then run go generate

If you need to implement or have added a new method in the interface, you can generate it with impl against the protocol

go get -u github.com/josharian/impl

impl 's *CertServer' github.com/mistsys/accord/protocol.CertServer will print out an implementation based on the interface

Generating CA cert

There are two ways to generate CA cert.

OpenSSH

The standard way is to use OpenSSH's ssh-keygen, you do want to make sure the following pre-requisites are met.

OpenSSH is at least newer than 7.3 and linked with LibreSSL or BoringSSL. You can check this with the following command:

ssh -V

In my OSX 10.12.15 machine I get

>OpenSSH_7.4p1, LibreSSL 2.5.0

If it is linked against OpenSSL, make sure it is after version 1.0.1. Make sure the system you are running this on has a reliable source of randomness with /dev/urandom (just make sure that this exists, it's very hard to determine with real accuracy your random number generator is truly random. For the adventurous, the NIST-800-22 document has details)

keygen -b 4096 -f ssh_ca

The default is RSA which, as far as we know is relatively safe for 4096 bits as long as its rotated frequently enough. Protecting against NSA in a public cloud isn't a reasonable threat model. There is a useful website for checking keylengths against various standard bodies recommendations.

Pick a reasonably random passphrase for it, save it in Parameter Store (TBD) so that it can be retrieved programmatically in code and used to decrypt the key.

Accord

I initially wrote this to understand what was going under the hood of a SSH CA cert and it can possibly be good enough for dynamically rotating a certificate.

This syntax will change but for the time being this is how you can get the same certs

go run accord.go -task=genrootcert test_ca

This will generate a root cert without a passphrase, so it's not secure yet. In a production deployment, you want to use a passphrase for protecting the key. You can do that using the -password argument. The generated certs will include the validity period that tools can use to validate the host cert from known_hosts, and if the cert is close to expiry, get a new one.

go run accord.go -task=genrootcert -password hello test_encrypted_ca

Rotation Procedure

  • Delete the old passphrase key and then the certificate files, they shouldn't be accessible past the time

Rotation Cycle

  • Certificates should have a life time of no more than a year, ideally they should be only 3 months. This is for practical reasons

Creating and signing certs

Creating cert

Two certs are generated - for signing for hosts and users separately

Root Cert with a passphrase

go run cmd/accord/accord.go -task=genrootcert -password "staple horse apple newton" root_ca_20170927

This will create two files root_ca_20170927 and root_ca_20170927.pub

User Cert with a passphrase

go run cmd/accord/accord.go -task=genrootcert -password "staple horse apple thatcher" user_ca_20170927

This will create two files user_ca_20170927 and user_ca_20170927.pub

Signing the User Key

go run cmd/accord/accord.go -certkey=user_ca_20170927 -password="staple horse apple thatcher" -task=genusercert -pubkey=$HOME/.ssh/id_rsa.pub

This will write the user key to stdout (this is generally intended to be signed and emailed to the user or replied in API)

Signing the Host Key

go run cmd/accord/accord.go -certkey=root_ca_20170927 -password="staple horse apple newton" -hostname ec2-<ip-addr>.amazonaws.com -task=genhostcert -pubkey=ssh_host_rsa_key.pub

This will write the host key to stdout too.

Testing end to end

The client and server talk over HTTP/2 gRPC protocol, the generated certificates can be used in -insecure mode (that works without TLS and intended only for development) to test end to end.

Running the server

Run this from cmd/accord_server

go run server.go -rootca ../root_ca_20170927 -rootcapassword="staple horse apple newton" -userca ../user_ca_20170927 -usercapassword "staple horse apple thatcher" -insecure

Running the client to sign host SSH keys

Run this from cmd/accord_client

go run client.go -task=hostcert -insecure -deploymentId=test -psk=JpUtbRukLuIFyjeKpA4fIpjgs6MTV8eH -hostkeys=test_assets/test_pubkeys/ -host=host.example.com

You can check the generated cert with ssh-keygen

-> % ssh-keygen -f test_assets/test_pubkeys/ssh_host_rsa_key-cert.pub -L
test_assets/test_pubkeys/ssh_host_rsa_key-cert.pub:
        Type: ssh-rsa-cert-v01@openssh.com host certificate
        Public key: RSA-CERT SHA256:uOYQUE2YSU0AJVfIgEafHcrldX++liMRc5hDDcitD2Y
        Signing CA: RSA SHA256:HCJ9E83f7KdVF+yolsAJx1B+a8WWZvOUoX8ZQtBZQrU
        Key ID: "f93b6b67-19f6-f5fc-2793-01e101dfb073"
        Serial: 1
        Valid: from 2017-10-08T00:26:51 to 2017-11-06T23:26:51
        Principals:
                host.example.com
        Critical Options: (none)
        Extensions: (none)

Users requesting their SSH certificates

This will print the cert files after getting them signed by the server.

Similar Projects

References

Documentation

Index

Constants

View Source
const (
	KeySize   = 32
	NonceSize = 12
)

Variables

View Source
var (
	ErrInvalidSerial      = errors.New("Serial should be something meaningful, not 0")
	ErrInvalidStartTime   = errors.New("Cannot sign for certs with time in the past")
	ErrEndBeforeStartTime = errors.New("End Time cannot be before start time")
	ErrEmptyID            = errors.New("Empty ID supplied")
	ErrValidityTooLong    = errors.New("The Validity for certs is too long")
)
View Source
var (
	ErrEncrypt     = errors.New("secret: encryption failed")
	ErrDecrypt     = errors.New("secret: decryption failed")
	ErrKeyNotFound = errors.New("keylookup: key not found")
)
View Source
var (
	DefaultServer = "localhost:50051"

	ClientID     = defaultClientID
	ClientSecret = defaultClientSecret
)

Functions

func EncodePublicKey

func EncodePublicKey(key interface{}, comment string) (string, error)

ssh one-line format (for lack of a better term) consists of three text fields: { key_type, data, comment } data is base64 encoded binary which consists of tuples of length (4 bytes) and data of the length described previously. For RSA keys, there should be three tuples which should be: { key_type, public_exponent, modulus }

func GenerateKey

func GenerateKey() []byte

GenerateKey generates a new AES-256 key.

func GenerateNonce

func GenerateNonce(size int) ([]byte, error)

GenerateNonce creates a new random nonce.

func JoinPublicKeys

func JoinPublicKeys(keys []ssh.PublicKey) []byte

JoinPublickeys encodes the public key and joins them in a single bytearray

func OAuth2Token

func OAuth2Token(tokenPb *protocol.OauthToken) (*oauth2.Token, error)

Convert from Protobuf Oauth2 Token to *oauth2.Token

func OAuthTokenPb

func OAuthTokenPb(token *oauth2.Token) (*protocol.OauthToken, error)

This package is used for oauth2 3-legged authentication and conversion work Convert Oauth token to protobuf version

func RandAsciiBytes

func RandAsciiBytes(n int) []byte

A helper function create and fill a slice of length n with characters from a-zA-Z0-9_-. It panics if there are any problems getting random bytes.

func ToHostCA

func ToHostCA(public CAPublic) *protocol.HostCA

func ToUserCA

func ToUserCA(public CAPublic) *protocol.UserCA

func Unquote

func Unquote(s string) string

func UpdateSSHD

func UpdateSSHD(filePath string) error

Types

type AESGCM

type AESGCM struct {
	// contains filtered or unexported fields
}

Initialize the AESGCM with a PSK store, this can be anything from a local instance or something that reads from a HSM or memory, the logic for getting the key securely will be in the PSKStore implmentation

func InitAESGCM

func InitAESGCM(store PSKStore) *AESGCM

func (*AESGCM) Decrypt

func (a *AESGCM) Decrypt(message []byte) ([]byte, []byte, uint32, error)

Decrypt returns plaintext, nonce, senderid, error

func (*AESGCM) Encrypt

func (a *AESGCM) Encrypt(message []byte, sender uint32) ([]byte, error)

func (*AESGCM) EncryptWithNonce

func (a *AESGCM) EncryptWithNonce(message []byte, nonce []byte, sender uint32) ([]byte, error)

type Authz

type Authz interface {
	Authorized(user string, principals []string) ([]string, error)
}

Authz is a bare minimum Authorization interface that just checks if any of the requested principals are actually valid for the user The service should only grant the access to the principals the user should have access to. If there is no overlap with the existing principals the result is empty and with an error for more details for the service to log for administrators or to send to the users Any other authorization backends can be added by implementing this interface

type CACertPair

type CACertPair struct {
	Type           CAType
	Metadata       CertMetadata
	PrivateKeyPath string
	PublicKey      ssh.PublicKey
	PublicKeyPath  string
}

type CAPublic

type CAPublic struct {
	Id         int       `json:"id"`
	ValidFrom  time.Time `json:"valid_from"`
	ValidUntil time.Time `json:"valid_until"`
	PublicKey  []byte
}

CAPublic is the public data that we want to return the user

type CAType

type CAType string
const (
	User CAType = "user"
	Host CAType = "host"
)

type CertManager

type CertManager struct {
	// contains filtered or unexported fields
}

I have doubts about this approach the passwords are stored in plaintext in memory but the actual certs are read on demand, and not kept in memory so any kind of overflow attack needs a filesystem access too while this may leak the CA Passwords, as long as the certs are read on demand, and forgotten immediately, I think this should be relatively safe. This can be an interface to read from different stores for secrets, but that needs a lot more testing right now TODO: refactor to use CACertPair structure

func NewCertManagerWithParameters

func NewCertManagerWithParameters(certsDir string, region string, roleArn string, paramsPrefix string) (*CertManager, error)

NewCertManagerwithParameters looks for files ending ca_(user|host)_(identifier) and ca_(user|host)_(identifier).pub in the certsDirectory, the comment field is read from the corresponding public key file the comment in the public key file contains how long the certificate is valid for the identifier is used to lookup the passphrase in the parameter store with _(identifier) appended to paramsPrefix TODO: this should be implementing an interface so that this file doesn't remain dirty with aws deps

func NewCertManagerWithPasswords

func NewCertManagerWithPasswords(rootCAPath string, rootCAPassword string,
	userCAPath string, userCAPassword string) (*CertManager, error)

NewCertmanagerwithPasswords just reads the files and decrypts them with corresponding passwords

func (*CertManager) HostCAs

func (m *CertManager) HostCAs() []CAPublic

func (*CertManager) RootCAPublicKeys

func (m *CertManager) RootCAPublicKeys() []ssh.PublicKey

these return array because we have to overlap multiple root and user keys when we need to rotate the keys in future it makes sense to make the API exposed to users be a little more flexible

func (*CertManager) SignHostCert

func (m *CertManager) SignHostCert(request *CertSignRequest) ([]byte, error)

func (*CertManager) SignUserCert

func (m *CertManager) SignUserCert(request *CertSignRequest) ([]byte, error)

func (*CertManager) UserCAPublicKeys

func (m *CertManager) UserCAPublicKeys() []ssh.PublicKey

func (*CertManager) UserCAs

func (m *CertManager) UserCAs() []CAPublic

type CertMetadata

type CertMetadata struct {
	Id         int       `json:"id"`
	ValidFrom  time.Time `json:"valid_from"`
	ValidUntil time.Time `json:"valid_until"`
}

CertMetadata is included in the comment for public key

type CertSignRequest

type CertSignRequest struct {
	PubKey     []byte
	ValidFrom  time.Time
	ValidUntil time.Time
	Id         string
	Serial     uint64
	Principals []string
	// include the criticalOptions and Extensions too
	ssh.Permissions
}

CertSignRequest is intended to be usable from other go libraries easily with the start and end times to be in time.Time. Since the upstream service is likely only seeing the bytes for a public key, the parsing is done here too

type GoogleAuth

type GoogleAuth struct {
	ClientId     string
	ClientSecret string
	// Whether to use a webserver
	UseWebServer bool
	// If we are listening to a webserver, what port
	WebServerPort int
	// Domain to restrict users to
	Domain string
	// Where to save the token
	TokenCachePath string

	Token *oauth2.Token
}

this only does Google Auth because that's what we need right now I don't think it's trivial to add other authentication protocols so until we need to, I'm sticking with Google Auth

func (*GoogleAuth) Authenticate

func (u *GoogleAuth) Authenticate() (*oauth2.Token, error)

func (*GoogleAuth) ValidateToken

func (u *GoogleAuth) ValidateToken(ctx context.Context) (bool, string, error)

Validate the token to avoid the "Confused Deputy Problem" https://www.ietf.org/mail-archive/web/unbearable/current/msg00135.html Classic paper:

type GrantAll

type GrantAll struct{}

GrantAll is the authz module that grants everyone everything

func (GrantAll) Authorized

func (g GrantAll) Authorized(user string, principals []string) ([]string, error)

type PSKStore

type PSKStore interface {
	GetPSK([]byte) ([]byte, error)
}

lets figure out the methods we need first and what we'll do with it the idea is that the clients will lookup the PSK with different mechanisms depending on the deployment requirements the same code is shared with the server but it looks them up from different sources

type SimpleAuth

type SimpleAuth struct {
	Principals []string `json:"principals" yaml:"principals"`
	// these users can get the root-everywhere if they request for it
	// allowing to sign in with full sudo
	AdminUsers []string `json:"admin_users" yaml:"admin_users"`

	// Users -> Principals map
	AccessMap map[string][]string `json:"access_map" yaml:"access_map"`
}

func NewSimpleAuthFromBuffer

func NewSimpleAuthFromBuffer(content []byte) (*SimpleAuth, error)

func NewSimpleAuthFromFile

func NewSimpleAuthFromFile(filePath string) (*SimpleAuth, error)

func (SimpleAuth) Authorized

func (s SimpleAuth) Authorized(user string, principals []string) ([]string, error)

func (SimpleAuth) IsAdmin

func (s SimpleAuth) IsAdmin(user string) bool

Directories

Path Synopsis
cmd
Package protocol is a generated protocol buffer package.
Package protocol is a generated protocol buffer package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL