automate certificate signing in storage node setup (#954)

This commit is contained in:
Bryan White 2019-01-04 18:23:23 +01:00 committed by GitHub
parent 5984bc9549
commit b6611e2800
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 272 additions and 32 deletions

View File

@ -73,6 +73,11 @@ matrix:
script: script:
- make test-captplanet - make test-captplanet
### certificate signing tests ###
- env: MODE=integration
script:
- make test-certificate-signing
### docker tests ### ### docker tests ###
- env: MODE=docker - env: MODE=docker
services: services:

View File

@ -77,6 +77,11 @@ test-captplanet: ## Test source with captain planet (travis)
@echo "Running ${@}" @echo "Running ${@}"
@./scripts/test-captplanet.sh @./scripts/test-captplanet.sh
.PHONY: test-certificate-signing
test-certificate-signing: ## Test certificate signing service and storagenode setup (travis)
@echo "Running ${@}"
@./scripts/test-certificate-signing.sh
.PHONY: test-docker .PHONY: test-docker
test-docker: ## Run tests in Docker test-docker: ## Run tests in Docker
docker-compose up -d --remove-orphans test docker-compose up -d --remove-orphans test

View File

@ -38,9 +38,10 @@ var (
} }
setupCmd = &cobra.Command{ setupCmd = &cobra.Command{
Use: "setup", Use: "setup",
Short: "Setup a certificate signing server", Short: "Setup a certificate signing server",
RunE: cmdSetup, RunE: cmdSetup,
Annotations: map[string]string{"type": "setup"},
} }
runCmd = &cobra.Command{ runCmd = &cobra.Command{
@ -77,8 +78,9 @@ var (
// NB: cert and key paths overridden in setup // NB: cert and key paths overridden in setup
CA identity.CASetupConfig CA identity.CASetupConfig
// NB: cert and key paths overridden in setup // NB: cert and key paths overridden in setup
Identity identity.SetupConfig Identity identity.SetupConfig
certificates.CertSignerConfig Signer certificates.CertSignerConfig
Overwrite bool `default:"false" help:"if true ca, identity, and authorization db will be overwritten/truncated"`
} }
runCfg struct { runCfg struct {
@ -105,11 +107,20 @@ var (
} }
defaultConfDir = fpath.ApplicationDir("storj", "cert-signing") defaultConfDir = fpath.ApplicationDir("storj", "cert-signing")
confDir *string
) )
func init() { func init() {
dirParam := cfgstruct.FindConfigDirParam()
if dirParam != "" {
defaultConfDir = dirParam
}
confDir = rootCmd.PersistentFlags().String("config-dir", defaultConfDir, "main directory for captplanet configuration")
rootCmd.AddCommand(setupCmd) rootCmd.AddCommand(setupCmd)
cfgstruct.Bind(setupCmd.Flags(), &setupCfg, cfgstruct.ConfDir(defaultConfDir)) cfgstruct.Bind(setupCmd.Flags(), &setupCfg, cfgstruct.ConfDir(defaultConfDir))
rootCmd.AddCommand(runCmd)
cfgstruct.Bind(runCmd.Flags(), &runCfg, cfgstruct.ConfDir(defaultConfDir))
rootCmd.AddCommand(authCmd) rootCmd.AddCommand(authCmd)
authCmd.AddCommand(authCreateCmd) authCmd.AddCommand(authCreateCmd)
cfgstruct.Bind(authCreateCmd.Flags(), &authCreateCfg, cfgstruct.ConfDir(defaultConfDir)) cfgstruct.Bind(authCreateCmd.Flags(), &authCreateCfg, cfgstruct.ConfDir(defaultConfDir))
@ -120,7 +131,12 @@ func init() {
} }
func cmdSetup(cmd *cobra.Command, args []string) error { func cmdSetup(cmd *cobra.Command, args []string) error {
setupDir, err := filepath.Abs(defaultConfDir) setupDir, err := filepath.Abs(*confDir)
if err != nil {
return err
}
err = os.MkdirAll(setupDir, 0700)
if err != nil { if err != nil {
return err return err
} }
@ -134,21 +150,21 @@ func cmdSetup(cmd *cobra.Command, args []string) error {
return nil return nil
} }
if _, err := setupCfg.NewAuthDB(); err != nil { if setupCfg.Overwrite {
return err setupCfg.CA.Overwrite = true
setupCfg.Identity.Overwrite = true
setupCfg.Signer.Overwrite = true
} }
err = os.MkdirAll(setupDir, 0700) if _, err := setupCfg.Signer.NewAuthDB(); err != nil {
if err != nil {
return err return err
} }
setupCfg.CA.CertPath = filepath.Join(setupDir, "ca.cert") setupCfg.CA.CertPath = filepath.Join(setupDir, "ca.cert")
setupCfg.CA.KeyPath = filepath.Join(setupDir, "ca.key") setupCfg.CA.KeyPath = filepath.Join(setupDir, "ca.key")
setupCfg.Identity.CertPath = filepath.Join(setupDir, "identity.cert") setupCfg.Identity.CertPath = filepath.Join(setupDir, "identity.cert")
setupCfg.Identity.KeyPath = filepath.Join(setupDir, "identity.key") setupCfg.Identity.KeyPath = filepath.Join(setupDir, "identity.key")
err = identity.SetupCA(process.Ctx(cmd), setupCfg.CA) err = identity.SetupIdentity(process.Ctx(cmd), setupCfg.CA, setupCfg.Identity)
if err != nil { if err != nil {
return err return err
} }
@ -158,6 +174,7 @@ func cmdSetup(cmd *cobra.Command, args []string) error {
"ca.key-path": setupCfg.CA.KeyPath, "ca.key-path": setupCfg.CA.KeyPath,
"identity.cert-path": setupCfg.Identity.CertPath, "identity.cert-path": setupCfg.Identity.CertPath,
"identity.key-path": setupCfg.Identity.KeyPath, "identity.key-path": setupCfg.Identity.KeyPath,
"log.level": "info",
} }
return process.SaveConfig(runCmd.Flags(), return process.SaveConfig(runCmd.Flags(),
filepath.Join(setupDir, "config.yaml"), o) filepath.Join(setupDir, "config.yaml"), o)
@ -306,7 +323,7 @@ func cmdExportAuth(cmd *cobra.Command, args []string) error {
return errs.New("Either use `--emails-path` or positional args, not both.") return errs.New("Either use `--emails-path` or positional args, not both.")
} }
emails = args emails = args
} else if authExportCfg.All { } else if len(args) == 0 || authExportCfg.All {
emails, err = authDB.UserIDs() emails, err = authDB.UserIDs()
if err != nil { if err != nil {
return err return err
@ -327,6 +344,9 @@ func cmdExportAuth(cmd *cobra.Command, args []string) error {
case "-": case "-":
output = os.Stdout output = os.Stdout
default: default:
if err := os.MkdirAll(filepath.Dir(authExportCfg.Out), 0600); err != nil {
return errs.Wrap(err)
}
output, err = os.OpenFile(authExportCfg.Out, os.O_CREATE, 0600) output, err = os.OpenFile(authExportCfg.Out, os.O_CREATE, 0600)
if err != nil { if err != nil {
return errs.Wrap(err) return errs.Wrap(err)

View File

@ -17,6 +17,7 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
"storj.io/storj/internal/fpath" "storj.io/storj/internal/fpath"
"storj.io/storj/pkg/certificates"
"storj.io/storj/pkg/cfgstruct" "storj.io/storj/pkg/cfgstruct"
"storj.io/storj/pkg/identity" "storj.io/storj/pkg/identity"
"storj.io/storj/pkg/kademlia" "storj.io/storj/pkg/kademlia"
@ -64,6 +65,7 @@ var (
setupCfg struct { setupCfg struct {
CA identity.CASetupConfig CA identity.CASetupConfig
Identity identity.SetupConfig Identity identity.SetupConfig
Signer certificates.CertSigningConfig
Overwrite bool `default:"false" help:"whether to overwrite pre-existing configuration files"` Overwrite bool `default:"false" help:"whether to overwrite pre-existing configuration files"`
} }
diagCfg struct { diagCfg struct {
@ -137,14 +139,28 @@ func cmdSetup(cmd *cobra.Command, args []string) (err error) {
return err return err
} }
// TODO: this is only applicable once we stop deleting the entire config dir on overwrite
// (see https://storjlabs.atlassian.net/browse/V3-1013)
// (see https://storjlabs.atlassian.net/browse/V3-949)
if setupCfg.Overwrite {
setupCfg.CA.Overwrite = true
setupCfg.Identity.Overwrite = true
}
setupCfg.CA.CertPath = filepath.Join(setupDir, "ca.cert") setupCfg.CA.CertPath = filepath.Join(setupDir, "ca.cert")
setupCfg.CA.KeyPath = filepath.Join(setupDir, "ca.key") setupCfg.CA.KeyPath = filepath.Join(setupDir, "ca.key")
setupCfg.Identity.CertPath = filepath.Join(setupDir, "identity.cert") setupCfg.Identity.CertPath = filepath.Join(setupDir, "identity.cert")
setupCfg.Identity.KeyPath = filepath.Join(setupDir, "identity.key") setupCfg.Identity.KeyPath = filepath.Join(setupDir, "identity.key")
err = identity.SetupIdentity(process.Ctx(cmd), setupCfg.CA, setupCfg.Identity) if setupCfg.Signer.AuthToken != "" && setupCfg.Signer.Address != "" {
if err != nil { err = setupCfg.Signer.SetupIdentity(process.Ctx(cmd), setupCfg.CA, setupCfg.Identity)
return err if err != nil {
zap.S().Warn(err)
}
} else {
err = identity.SetupIdentity(process.Ctx(cmd), setupCfg.CA, setupCfg.Identity)
if err != nil {
return err
}
} }
overrides := map[string]interface{}{ overrides := map[string]interface{}{

View File

@ -64,16 +64,16 @@ type CertSigningConfig struct {
// CertSignerConfig is a config struct for use with a certificate signing service server // CertSignerConfig is a config struct for use with a certificate signing service server
type CertSignerConfig struct { type CertSignerConfig struct {
Overwrite bool `help:"if true, overwrites config AND authorization db is truncated" default:"false"` Overwrite bool `default:"false" help:"if true, overwrites config AND authorization db is truncated"`
AuthorizationDBURL string `help:"url to the certificate signing authorization database" default:"bolt://$CONFDIR/authorizations.db"` AuthorizationDBURL string `default:"bolt://$CONFDIR/authorizations.db" help:"url to the certificate signing authorization database"`
MinDifficulty uint `help:"minimum difficulty of the requester's identity required to claim an authorization"` MinDifficulty uint `default:"16" help:"minimum difficulty of the requester's identity required to claim an authorization"`
CA provider.FullCAConfig CA identity.FullCAConfig
} }
// CertificateSigner implements pb.CertificatesServer // CertificateSigner implements pb.CertificatesServer
type CertificateSigner struct { type CertificateSigner struct {
Logger *zap.Logger Log *zap.Logger
Signer *provider.FullCertificateAuthority Signer *identity.FullCertificateAuthority
AuthDB *AuthorizationDB AuthDB *AuthorizationDB
MinDifficulty uint16 MinDifficulty uint16
} }
@ -115,7 +115,7 @@ type ClaimOpts struct {
type Claim struct { type Claim struct {
Addr string Addr string
Timestamp int64 Timestamp int64
Identity *provider.PeerIdentity Identity *identity.PeerIdentity
SignedChainBytes [][]byte SignedChainBytes [][]byte
} }
@ -130,7 +130,7 @@ func init() {
} }
// NewClient creates a new certificate signing grpc client // NewClient creates a new certificate signing grpc client
func NewClient(ctx context.Context, ident *provider.FullIdentity, address string) (*Client, error) { func NewClient(ctx context.Context, ident *identity.FullIdentity, address string) (*Client, error) {
tc := transport.NewClient(ident) tc := transport.NewClient(ident)
conn, err := tc.DialAddress(ctx, address) conn, err := tc.DialAddress(ctx, address)
if err != nil { if err != nil {
@ -191,6 +191,94 @@ func ParseToken(tokenString string) (*Token, error) {
return t, nil return t, nil
} }
// SetupIdentity loads or creates a CA and identity and submits a certificate
// signing request request for the CA; if successful, updated chains are saved.
func (c CertSigningConfig) SetupIdentity(
ctx context.Context,
caConfig identity.CASetupConfig,
identConfig identity.SetupConfig,
) error {
caStatus := caConfig.Status()
var (
ca *identity.FullCertificateAuthority
ident *identity.FullIdentity
err error
)
if caStatus == identity.CertKey && !caConfig.Overwrite {
ca, err = caConfig.FullConfig().Load()
if err != nil {
return err
}
} else if caStatus != identity.NoCertNoKey && !caConfig.Overwrite {
return identity.ErrSetup.New("certificate authority file(s) exist: %s", caStatus)
} else {
t, err := time.ParseDuration(caConfig.Timeout)
if err != nil {
return errs.Wrap(err)
}
ctx, cancel := context.WithTimeout(ctx, t)
defer cancel()
ca, err = caConfig.Create(ctx)
if err != nil {
return err
}
}
identStatus := identConfig.Status()
if identStatus == identity.CertKey && !identConfig.Overwrite {
ident, err = identConfig.FullConfig().Load()
if err != nil {
return err
}
} else if identStatus != identity.NoCertNoKey && !identConfig.Overwrite {
return identity.ErrSetup.New("identity file(s) exist: %s", identStatus)
} else {
ident, err = identConfig.Create(ca)
if err != nil {
return err
}
}
signedChainBytes, err := c.Sign(ctx, ident)
if err != nil {
return errs.New("error occured while signing certificate: %s\n(identity files were still generated and saved, if you try again existnig files will be loaded)", err)
}
signedChain, err := identity.ParseCertChain(signedChainBytes)
if err != nil {
return nil
}
ca.Cert = signedChain[0]
ca.RestChain = signedChain[1:]
err = identity.FullCAConfig{
CertPath: caConfig.FullConfig().CertPath,
}.Save(ca)
if err != nil {
return err
}
ident.RestChain = signedChain[1:]
err = identity.Config{
CertPath: identConfig.FullConfig().CertPath,
}.Save(ident)
if err != nil {
return err
}
return nil
}
// Sign submits a certificate signing request given the config
func (c CertSigningConfig) Sign(ctx context.Context, ident *identity.FullIdentity) ([][]byte, error) {
client, err := NewClient(ctx, ident, c.Address)
if err != nil {
return nil, err
}
return client.Sign(ctx, c.AuthToken)
}
// Sign claims an authorization using the token string and returns a signed // Sign claims an authorization using the token string and returns a signed
// copy of the client's CA certificate // copy of the client's CA certificate
func (c Client) Sign(ctx context.Context, tokenStr string) ([][]byte, error) { func (c Client) Sign(ctx context.Context, tokenStr string) ([][]byte, error) {
@ -216,7 +304,8 @@ func (c CertSignerConfig) NewAuthDB() (*AuthorizationDB, error) {
authDB := new(AuthorizationDB) authDB := new(AuthorizationDB)
switch driver { switch driver {
case "bolt": case "bolt":
if c.Overwrite { _, err := os.Stat(source)
if c.Overwrite && err == nil {
if err := os.Remove(source); err != nil { if err := os.Remove(source); err != nil {
return nil, err return nil, err
} }
@ -264,13 +353,26 @@ func (c CertSignerConfig) Run(ctx context.Context, server *provider.Provider) (e
} }
srv := &CertificateSigner{ srv := &CertificateSigner{
Logger: zap.L(), Log: zap.L(),
Signer: signer, Signer: signer,
AuthDB: authDB, AuthDB: authDB,
MinDifficulty: uint16(c.MinDifficulty), MinDifficulty: uint16(c.MinDifficulty),
} }
pb.RegisterCertificatesServer(server.GRPC(), srv) pb.RegisterCertificatesServer(server.GRPC(), srv)
srv.Log.Info(
"Certificate signing server running",
zap.String("address", server.Addr().String()),
)
go func() {
done := ctx.Done()
<-done
if err := server.Close(); err != nil {
srv.Log.Error("closing server", zap.Error(err))
}
}()
return server.Run(ctx) return server.Run(ctx)
} }

View File

@ -872,7 +872,7 @@ func TestCertificateSigner_Sign(t *testing.T) {
peerCtx := peer.NewContext(ctx, grpcPeer) peerCtx := peer.NewContext(ctx, grpcPeer)
certSigner := &CertificateSigner{ certSigner := &CertificateSigner{
Logger: zap.L(), Log: zap.L(),
Signer: signingCA, Signer: signingCA,
AuthDB: authDB, AuthDB: authDB,
} }

View File

@ -149,6 +149,14 @@ func (caS CASetupConfig) Create(ctx context.Context) (*FullCertificateAuthority,
return ca, caC.Save(ca) return ca, caC.Save(ca)
} }
// FullConfig converts a `CASetupConfig` to `FullCAConfig`
func (caS CASetupConfig) FullConfig() FullCAConfig {
return FullCAConfig{
CertPath: caS.CertPath,
KeyPath: caS.KeyPath,
}
}
// Load loads a CA from the given configuration // Load loads a CA from the given configuration
func (fc FullCAConfig) Load() (*FullCertificateAuthority, error) { func (fc FullCAConfig) Load() (*FullCertificateAuthority, error) {
p, err := fc.PeerConfig().Load() p, err := fc.PeerConfig().Load()

View File

@ -195,6 +195,14 @@ func (is SetupConfig) Create(ca *FullCertificateAuthority) (*FullIdentity, error
return fi, ic.Save(fi) return fi, ic.Save(fi)
} }
// FullConfig converts a `SetupConfig` to `Config`
func (is SetupConfig) FullConfig() Config {
return Config{
CertPath: is.CertPath,
KeyPath: is.KeyPath,
}
}
// Load loads a FullIdentity from the config // Load loads a FullIdentity from the config
func (ic Config) Load() (*FullIdentity, error) { func (ic Config) Load() (*FullIdentity, error) {
c, err := ioutil.ReadFile(ic.CertPath) c, err := ioutil.ReadFile(ic.CertPath)

View File

@ -14,7 +14,6 @@ import (
"time" "time"
"github.com/zeebo/errs" "github.com/zeebo/errs"
"go.uber.org/zap"
"storj.io/storj/pkg/utils" "storj.io/storj/pkg/utils"
"storj.io/storj/storage" "storj.io/storj/storage"
@ -421,13 +420,11 @@ func NewRevDB(revocationDBURL string) (*RevocationDB, error) {
if err != nil { if err != nil {
return nil, ErrRevocationDB.Wrap(err) return nil, ErrRevocationDB.Wrap(err)
} }
zap.S().Info("Starting overlay cache with BoltDB")
case "redis": case "redis":
db, err = NewRevocationDBRedis(revocationDBURL) db, err = NewRevocationDBRedis(revocationDBURL)
if err != nil { if err != nil {
return nil, ErrRevocationDB.Wrap(err) return nil, ErrRevocationDB.Wrap(err)
} }
zap.S().Info("Starting overlay cache with Redis")
default: default:
return nil, ErrRevocationDB.New("database scheme not supported: %s", driver) return nil, ErrRevocationDB.New("database scheme not supported: %s", driver)
} }

View File

@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -o errexit
trap "echo ERROR: exiting due to error; exit" ERR
trap "exit" INT TERM
trap "kill 0" EXIT
. $(dirname $0)/utils.sh
user_id="user@example.com"
signer_address="127.0.0.1:8888"
difficulty=16
cleanup() {
if [[ -z ${bg+x} ]]; then
kill ${bg}
fi
dirs="$tmp $tmp_build_dir"
for dir in ${dirs}; do
if [[ ! -z ${dir+x} ]]; then
rm -rf ${dir}
fi
done
}
temp_build storagenode certificates
tmp=$(mktemp -d)
trap "rm -rf ${tmp} ${tmp_build_dir}; cleanup" EXIT
certificates_dir=${tmp}/cert-signing
storagenode_dir=${tmp}/storagenode
# TODO: create separate signer CA and use `--signer.ca` options
# --signer.ca.cert-path ${signer_cert} \
# --signer.ca.key-path ${signer_key} \
echo "setting up certificate signing server"
$certificates setup --config-dir ${certificates_dir} \
--signer.min-difficulty ${difficulty}
echo "creating test authorization"
$certificates auth create --config-dir ${certificates_dir} \
1 ${user_id} >/dev/null 2>&1
export_tokens() {
$certificates auth export --config-dir ${certificates_dir} \
--out -
}
token=$(export_tokens 2>&1|cut -d , -f 2|grep -oE "$user_id:\w+")
echo "starting certificate signing server"
$certificates run --config-dir ${certificates_dir} \
--server.address ${signer_address} &
bg=$!
sleep 1
echo "setting up storage node"
$storagenode setup --config-dir ${storagenode_dir} \
--ca.difficulty ${difficulty} \
--signer.address ${signer_address} \
--signer.auth-token ${token}
ca_chain_len=$(cat ${storagenode_dir}/ca.cert|grep "BEGIN CERTIFICATE"|wc -l)
ident_chain_len=$(cat ${storagenode_dir}/identity.cert|grep "BEGIN CERTIFICATE"|wc -l)
failures=0
if [[ ! ${ca_chain_len} == 2 ]]; then
echo "FAIL: incorrect storage node CA chain length; expected: 2; actual: ${ca_chain_len}"
failures=$((failures+1))
fi
if [[ ! ${ident_chain_len} == 3 ]]; then
echo "FAIL: incorrect storage node identty chain length; expected: 2; actual: ${ident_chain_len}"
failures=$((failures+1))
fi
if [[ ${failures} == 0 ]]; then
echo "SUCCESS: all expectations met!"
fi
exit ${failures}

View File

@ -33,8 +33,8 @@ build() {
} }
temp_build() { temp_build() {
tmp_dir=$(mktemp -d) declare -g tmp_build_dir=$(mktemp -d)
build ${tmp_dir} $@ build ${tmp_build_dir} $@
} }
check_help() { check_help() {