Skip to content

Commit

Permalink
feat: add secrets management (#145)
Browse files Browse the repository at this point in the history
Co-authored-by: Marco Antonio Blanco <marcoantonio.blanco@nearform.com>
  • Loading branch information
LucaLanziani and mablanco authored Nov 16, 2023
1 parent d920d8a commit 9623e63
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 23 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,16 @@ The matrix below gives an overview of the integration status of our CLI with CI
| Gitlab CI | Coming Soon |
| Azure Devops | Coming Soon |
| More will be added...| |


Encrypt your secret with:

```
./initium secrets encrypt --publicKey age1zmh77nlvddsz55q5l67d4ufwewvyhentlku9z90t969szd2lnghslnlese --secret <your secret>
```

or if it's not a string

```
./initium secrets encrypt --publicKey age1zmh77nlvddsz55q5l67d4ufwewvyhentlku9z90t969szd2lnghslnlese --base64secret <your secret in base64>
```
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/nearform/initium-cli
go 1.20

require (
filippo.io/age v1.1.1
github.com/charmbracelet/log v0.2.2
github.com/docker/docker v23.0.3+incompatible
github.com/go-git/go-git/v5 v5.7.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d h1:LblfooH1lKOpp1hIhukktmSAxFkqMPFk9KR6iZ0MJNI=
contrib.go.opencensus.io/exporter/prometheus v0.4.0 h1:0QfIkj9z/iVZgK31D9H9ohjjIDApI2GOPScCKwxedbs=
filippo.io/age v1.1.1 h1:pIpO7l151hCnQ4BdyBujnGP2YlUo0uj6sAVNHGBvXHg=
filippo.io/age v1.1.1/go.mod h1:l03SrzDUrBkdBx8+IILdnn2KZysqQdbEBUQ4p3sqEQE=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
Expand Down
1 change: 1 addition & 0 deletions src/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ func (c icli) Run(args []string) error {
c.OnBranchCMD(),
c.TemplateCMD(),
c.InitCMD(),
c.SecretsCMD(),
{
Name: "version",
Usage: "Return the version of the cli",
Expand Down
79 changes: 56 additions & 23 deletions src/cli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,32 +26,39 @@ const (
InitGithub FlagsType = "init-github"
App FlagsType = "app"
Shared FlagsType = "shared"
Encrypt FlagsType = "encrypt"
Decrypt FlagsType = "decrypt"
)

const (
runtimeVersionFlag string = "runtime-version"
endpointFlag string = "cluster-endpoint"
tokenFlag string = "cluster-token"
caCRTFlag string = "cluster-ca-crt"
registryUserFlag string = "registry-user"
registryPasswordFlag string = "registry-password"
destinationFolderFlag string = "destination-folder"
defaultBranchFlag string = "default-branch"
appNameFlag string = "app-name"
appVersionFlag string = "app-version"
projectDirectoryFlag string = "project-directory"
projectTypeFlag string = "project-type"
repoNameFlag string = "container-registry"
dockerFileNameFlag string = "dockerfile-name"
configFileFlag string = "config-file"
namespaceFlag string = "namespace"
imagePullSecretsFlag string = "image-pull-secrets"
stopOnBuildFlag string = "stop-on-build"
stopOnPushFlag string = "stop-on-push"
envVarFileFlag string = "env-var-file"
secretRefEnvFileFlag string = "secret-ref-env-var-file"
isPrivateServiceFlag string = "private"
dryRunFlag string = "dry-run"
runtimeVersionFlag string = "runtime-version"
endpointFlag string = "cluster-endpoint"
tokenFlag string = "cluster-token"
caCRTFlag string = "cluster-ca-crt"
registryUserFlag string = "registry-user"
registryPasswordFlag string = "registry-password"
destinationFolderFlag string = "destination-folder"
defaultBranchFlag string = "default-branch"
appNameFlag string = "app-name"
appVersionFlag string = "app-version"
projectDirectoryFlag string = "project-directory"
projectTypeFlag string = "project-type"
repoNameFlag string = "container-registry"
dockerFileNameFlag string = "dockerfile-name"
configFileFlag string = "config-file"
namespaceFlag string = "namespace"
imagePullSecretsFlag string = "image-pull-secrets"
stopOnBuildFlag string = "stop-on-build"
stopOnPushFlag string = "stop-on-push"
envVarFileFlag string = "env-var-file"
secretRefEnvFileFlag string = "secret-ref-env-var-file"
isPrivateServiceFlag string = "private"
dryRunFlag string = "dry-run"
publicKeyFlag string = "public-key"
plainSecretFlag string = "plain-secret"
privateKeyFlag string = "private-key"
base64PlainSecretFlag string = "base64-plain-secret"
base64EncryptedSecretFlag string = "base64-encrypted-secret"
)

type flags struct {
Expand Down Expand Up @@ -196,6 +203,32 @@ func InitFlags() flags {
EnvVars: []string{"INITIUM_CONFIG_FILE"},
},
},
Encrypt: []cli.Flag{
&cli.StringFlag{
Name: publicKeyFlag,
Required: true,
EnvVars: []string{"INITIUM_SECRET_PUBLIC_KEY"},
},
&cli.StringFlag{
Name: plainSecretFlag,
Required: true,
},
&cli.StringFlag{
Name: base64PlainSecretFlag,
Required: true,
},
},
Decrypt: []cli.Flag{
&cli.StringFlag{
Name: privateKeyFlag,
Required: true,
EnvVars: []string{"INITIUM_SECRET_PRIVATE_KEY"},
},
&cli.StringFlag{
Name: base64EncryptedSecretFlag,
Required: true,
},
},
Shared: []cli.Flag{
&cli.StringFlag{
Name: appNameFlag,
Expand Down
5 changes: 5 additions & 0 deletions src/cli/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ func excludedFlagsFromConfig() []string {
caCRTFlag,
registryUserFlag,
endpointFlag,
publicKeyFlag,
plainSecretFlag,
privateKeyFlag,
base64PlainSecretFlag,
base64EncryptedSecretFlag,
}
}

Expand Down
93 changes: 93 additions & 0 deletions src/cli/secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package cli

import (
"encoding/base64"
"fmt"

"github.com/nearform/initium-cli/src/services/secrets"
"github.com/urfave/cli/v2"
)

func (c icli) generateKeys(ctx *cli.Context) error {
keys, err := secrets.GenerateKeys()
if err != nil {
return err
}

fmt.Fprintf(c.Writer, "Secret key: %q\n", keys.Private)
fmt.Fprintf(c.Writer, "Public key: %q\n", keys.Public)
return nil
}

func (c icli) encrypt(ctx *cli.Context) error {
publicKey := ctx.String(publicKeyFlag)
secret := ctx.String(plainSecretFlag)
base64Secret := ctx.String(base64PlainSecretFlag)

if base64Secret == "" {
base64Secret = base64.StdEncoding.EncodeToString([]byte(secret))
}

result, err := secrets.Encrypt(publicKey, base64Secret)
if err != nil {
return err
}
fmt.Fprintf(c.Writer, "%s\n", result)
return nil
}

func (c icli) decrypt(ctx *cli.Context) error {
privateKey := ctx.String(privateKeyFlag)
secret := ctx.String(base64EncryptedSecretFlag)
result, err := secrets.Decrypt(privateKey, secret)
if err != nil {
return err
}
fmt.Fprintf(c.Writer, "%s\n", result)
return nil
}

func (c icli) SecretsCMD() *cli.Command {

return &cli.Command{
Name: "secrets",
Usage: "A series of command to generate age keys, encrypt and decrypt secrets",
Subcommands: []*cli.Command{
{
Name: "generate-keys",
Usage: "Generate the public and private keys and output them on stdout",
Action: c.generateKeys,
Before: c.baseBeforeFunc,
},
{
Name: "encrypt",
Usage: "Encrypt a secret, if the secret flag is used the secret is first encoded in base64 and then encrypted",
Action: c.encrypt,
Flags: c.CommandFlags([]FlagsType{Encrypt}),
Before: func(ctx *cli.Context) error {
if err := c.loadFlagsFromConfig(ctx); err != nil {
return err
}

ignoredFlags := []string{}

if ctx.IsSet(plainSecretFlag) {
ignoredFlags = append(ignoredFlags, base64PlainSecretFlag)
}
if ctx.IsSet(base64PlainSecretFlag) {
ignoredFlags = append(ignoredFlags, plainSecretFlag)
}

return c.checkRequiredFlags(ctx, ignoredFlags)
},
},
{
Name: "decrypt",
Usage: "Decrypt a base64 encoded secret and output the base64 encoded value",
Action: c.decrypt,
Flags: c.CommandFlags([]FlagsType{Decrypt}),
Before: c.baseBeforeFunc,
},
},
}
}
78 changes: 78 additions & 0 deletions src/services/secrets/secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package secrets

import (
"bytes"
"encoding/base64"
"fmt"
"io"
"strings"

"filippo.io/age"
)

type Keys struct {
Private string
Public string
}

func GenerateKeys() (Keys, error) {
identity, err := age.GenerateX25519Identity()
if err != nil {
return Keys{}, err
}

keys := Keys{
Private: identity.String(),
Public: identity.Recipient().String(),
}
return keys, nil
}

func Decrypt(privateKey string, secret string) (string, error) {
identity, err := age.ParseX25519Identity(privateKey)
if err != nil {
return "", fmt.Errorf("failed to parse private key %q: %v", privateKey, err)
}

s, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
return "", fmt.Errorf("cannot decode base64 secret %v", err)
}
out := &bytes.Buffer{}
f := strings.NewReader(string(s))

r, err := age.Decrypt(f, identity)
if err != nil {
return "", fmt.Errorf("failed to open encrypted file: %v", err)
}
if _, err := io.Copy(out, r); err != nil {
return "", fmt.Errorf("failed to read encrypted file: %v", err)
}

return out.String(), nil
}

func Encrypt(publicKey string, secret string) (string, error) {
recipient, err := age.ParseX25519Recipient(publicKey)
if err != nil {
return "", fmt.Errorf("failed to parse public key %q: %v", publicKey, err)
}

buf := &bytes.Buffer{}
// armorWriter := armor.NewWriter(buf)

w, err := age.Encrypt(buf, recipient)
if err != nil {
return "", fmt.Errorf("failed to create encrypted file: %v", err)
}
defer w.Close()

if _, err := io.WriteString(w, secret); err != nil {
return "", fmt.Errorf("failed to write to encrypted file: %v", err)
}
if err := w.Close(); err != nil {
return "", fmt.Errorf("failed to close encrypted file: %v", err)
}

return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
}
47 changes: 47 additions & 0 deletions src/services/secrets/secrets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package secrets

import (
"strings"
"testing"

"gotest.tools/v3/assert"
)

func TestGenerateKeys(t *testing.T) {
keys, err := GenerateKeys()
secretKeyPrefix := "AGE-SECRET-KEY-"
publicKeyPrefix := "age1"

if err != nil {
t.Error(err)
}

if !strings.HasPrefix(keys.Private, secretKeyPrefix) {
t.Errorf("Secret key doesn't start with %s", secretKeyPrefix)
}

if !strings.HasPrefix(keys.Public, publicKeyPrefix) {
t.Errorf("Public key doesn't start with %s", publicKeyPrefix)
}
}

func TestDecrypt(t *testing.T) {
keys, err := GenerateKeys()
if err != nil {
t.Error(err)
}

expected := "simple"

secret, err := Encrypt(keys.Public, expected)
if err != nil {
t.Error(err)
}

plain, err := Decrypt(keys.Private, secret)
if err != nil {
t.Error(err)
}

assert.Assert(t, plain == expected, "Expected %q, got %q", expected, plain)
}

0 comments on commit 9623e63

Please sign in to comment.