Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ CCF_DB_CONNECTION="host=db user=postgres password=postgres dbname=ccf port=5432
CCF_JWT_SECRET="some-secret"
CCF_JWT_PRIVATE_KEY=private.pem
CCF_JWT_PUBLIC_KEY=public.pem
# To use in-memory key generation, remove/comment out both CCF_JWT_PRIVATE_KEY and CCF_JWT_PUBLIC_KEY lines.
# Do not set these variables to empty strings.

CCF_API_ALLOWED_ORIGINS="http://localhost:3000,http://localhost:8000"
CCF_RISK_CONFIG="risk.yaml"
Expand Down
3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,7 @@ swag: ## swag setup and lint
.PHONY: generate-keys
generate-keys:
@$(INFO) "Generating keys for the service"
@openssl genrsa -out private_key.pem 2048
@openssl rsa -in private_key.pem -pubout -out public_key.pem
@go run main.go bootstrap --private-key private_key.pem --public-key public_key.pem --force
@$(OK) keys generated

tag: ## Build and tag a production-based image of the service
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Some examples include:
```shell
$ go run main.go run # Run the API itself

$ go run main.go bootstrap # Bootstrap JWT key files (defaults to CCF_JWT_* or private.pem/public.pem)

$ go run main.go users add # Create a new user in the CCF API which can be used to authenticate with

$ go run main.go migrate up # Create the database schema, or upgrade it to the current version
Expand Down Expand Up @@ -62,6 +64,10 @@ You can configure the API using environment variables or a `.env` file.

Available variables are shown in [`.env.example`](./.env.example)

JWT key behavior:
- If both `CCF_JWT_PRIVATE_KEY` and `CCF_JWT_PUBLIC_KEY` are set, `run` bootstraps key files at those paths (if needed) and then loads them.
- If either variable is unset, the API falls back to in-memory key generation.

Copy this file to .env to configure environment variables
```shell
cp .env.example .env
Expand Down
41 changes: 41 additions & 0 deletions cmd/bootstrap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package cmd

import "github.com/spf13/cobra"

func newBootstrapCMD() *cobra.Command {
var (
privateKeyPath string
publicKeyPath string
bitSize int
force bool
)

bootstrap := &cobra.Command{
Use: "bootstrap",
Short: "Initialize JWT signing key files for API startup",
RunE: func(cmd *cobra.Command, args []string) error {
privateKeyPath, publicKeyPath = resolveJWTKeyPathsForBootstrap(privateKeyPath, publicKeyPath)

action, err := runJWTBootstrap(privateKeyPath, publicKeyPath, bitSize, force)
if err != nil {
return err
}

cmd.Printf(
"JWT bootstrap complete (action=%s, private=%s, public=%s)\n",
action,
privateKeyPath,
publicKeyPath,
)

return nil
},
}

bootstrap.Flags().StringVar(&privateKeyPath, "private-key", "", "Path to the JWT private key file")
bootstrap.Flags().StringVar(&publicKeyPath, "public-key", "", "Path to the JWT public key file")
bootstrap.Flags().IntVar(&bitSize, "bit-size", defaultJWTKeyBitSize, "RSA key size in bits")
bootstrap.Flags().BoolVar(&force, "force", false, "Regenerate key files even when they already exist")

return bootstrap
}
76 changes: 76 additions & 0 deletions cmd/jwt_bootstrap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package cmd

import (
"errors"
"strings"

"github.com/compliance-framework/api/internal/config"
"github.com/spf13/viper"
)

const defaultJWTKeyBitSize = 2048

func bootstrapConfiguredJWTKeys(bitSize int, force bool) (config.JWTKeyBootstrapAction, string, string, bool, error) {
privateKeyConfigured := viper.IsSet("jwt_private_key")
publicKeyConfigured := viper.IsSet("jwt_public_key")

if !privateKeyConfigured || !publicKeyConfigured {
return "", "", "", false, nil
}

privateKeyPath := normalizePathValue(viper.GetString("jwt_private_key"))
publicKeyPath := normalizePathValue(viper.GetString("jwt_public_key"))

if privateKeyPath == "" || publicKeyPath == "" {
return "", privateKeyPath, publicKeyPath, true, configErrorForEmptyJWTKeyPath()
}

action, err := runJWTBootstrap(privateKeyPath, publicKeyPath, bitSize, force)
if err != nil {
return "", privateKeyPath, publicKeyPath, true, err
}

return action, privateKeyPath, publicKeyPath, true, nil
}

func resolveJWTKeyPathsForBootstrap(privateKeyPath, publicKeyPath string) (string, string) {
privateKeyPath = normalizePathValue(privateKeyPath)
publicKeyPath = normalizePathValue(publicKeyPath)

if privateKeyPath == "" {
privateKeyPath = normalizePathValue(viper.GetString("jwt_private_key"))
}
if publicKeyPath == "" {
publicKeyPath = normalizePathValue(viper.GetString("jwt_public_key"))
}

if privateKeyPath == "" {
privateKeyPath = "private.pem"
}
if publicKeyPath == "" {
publicKeyPath = "public.pem"
}

return privateKeyPath, publicKeyPath
}

func runJWTBootstrap(privateKeyPath, publicKeyPath string, bitSize int, force bool) (config.JWTKeyBootstrapAction, error) {
return config.BootstrapJWTKeyPair(privateKeyPath, publicKeyPath, bitSize, force)
}

func normalizePathValue(value string) string {
value = strings.TrimSpace(value)
if len(value) >= 2 {
if (value[0] == '"' && value[len(value)-1] == '"') || (value[0] == '\'' && value[len(value)-1] == '\'') {
value = value[1 : len(value)-1]
}
}
return strings.TrimSpace(value)
}

func configErrorForEmptyJWTKeyPath() error {
return errors.New(
"CCF_JWT_PRIVATE_KEY and CCF_JWT_PUBLIC_KEY are set but one or both are empty. " +
"Set both to non-empty paths, or remove/comment out both to use in-memory key generation",
)
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func init() {
rootCmd.AddCommand(users.RootCmd)
rootCmd.AddCommand(seed.RootCmd)
rootCmd.AddCommand(newMigrateCMD())
rootCmd.AddCommand(newBootstrapCMD())
rootCmd.AddCommand(dashboards.RootCmd)
rootCmd.AddCommand(DigestCmd)
}
Expand Down
24 changes: 24 additions & 0 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,30 @@ func RunServer(cmd *cobra.Command, args []string) {
}
}()

bootstrapAction, privateKeyPath, publicKeyPath, bootstrapConfigured, err := bootstrapConfiguredJWTKeys(defaultJWTKeyBitSize, false)
if err != nil {
sugar.Fatalw(
"Failed to bootstrap JWT key files",
"error",
err,
"private_key_path",
privateKeyPath,
"public_key_path",
publicKeyPath,
)
}
if bootstrapConfigured {
sugar.Infow(
"JWT key bootstrap completed",
"action",
bootstrapAction,
"private_key_path",
privateKeyPath,
"public_key_path",
publicKeyPath,
)
}

cfg := config.NewConfig(sugar)

db, err := service.ConnectSQLDb(ctx, cfg, sugar)
Expand Down
Loading
Loading