Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AES secrets encryption #2300

Closed
wants to merge 14 commits into from
9 changes: 2 additions & 7 deletions cmd/server/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,18 +459,13 @@ var flags = append([]cli.Flag{
//
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_ENCRYPTION_KEY"},
Name: "encryption-raw-key",
Usage: "Raw encryption key",
Name: "encryption-key",
Usage: "AES encryption key",
FilePath: os.Getenv("WOODPECKER_ENCRYPTION_KEY_FILE"),
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_ENCRYPTION_TINK_KEYSET_FILE"},
Name: "encryption-tink-keyset",
Usage: "Google tink AEAD-compatible keyset file to encrypt secrets in DB",
},
&cli.BoolFlag{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there should be an option to revert encryption back to unencrypted ... we could add a warning if that's really the intended case ... but we should have it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sure migration of non encrypted and back

Done

mix in-between

This can be achieved via disabling encryption. For example, Plain -> AES -> Plain -> Tink. I would leave it like that, for this PR at least.

EnvVars: []string{"WOODPECKER_ENCRYPTION_DISABLE"},
Name: "encryption-disable-flag",
Usage: "Flag to decrypt all encrypted data and disable encryption on server",
},
}, common.GlobalLoggerFlags...)
24 changes: 8 additions & 16 deletions cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,34 +260,26 @@ func run(c *cli.Context) error {
return g.Wait()
}

func setupEvilGlobals(c *cli.Context, v store.Store, f forge.Forge) {
func setupEvilGlobals(c *cli.Context, store store.Store, f forge.Forge) {
anbraten marked this conversation as resolved.
Show resolved Hide resolved
// forge
server.Config.Services.Forge = f
server.Config.Services.Timeout = c.Duration("forge-timeout")

// services
server.Config.Services.Queue = setupQueue(c, v)
server.Config.Services.Queue = setupQueue(c, store)
server.Config.Services.Logs = logging.New()
server.Config.Services.Pubsub = pubsub.New()
if err := server.Config.Services.Pubsub.Create(context.Background(), "topic/events"); err != nil {
log.Error().Err(err).Msg("could not create pubsub service")
}
server.Config.Services.Registries = setupRegistryService(c, v)

// TODO(1544): fix encrypted store
// // encryption
// encryptedSecretStore := encryptedStore.NewSecretStore(v)
// err := encryption.Encryption(c, v).WithClient(encryptedSecretStore).Build()
// if err != nil {
// log.Fatal().Err(err).Msg("could not create encryption service")
// }
// server.Config.Services.Secrets = setupSecretService(c, encryptedSecretStore)
server.Config.Services.Secrets = setupSecretService(c, v)

server.Config.Services.Environ = setupEnvironService(c, v)
server.Config.Services.Registries = setupRegistryService(c, store)

server.Config.Services.Secrets = setupSecretService(c, store)

server.Config.Services.Environ = setupEnvironService(c, store)
server.Config.Services.Membership = setupMembershipService(c, f)

server.Config.Services.SignaturePrivateKey, server.Config.Services.SignaturePublicKey = setupSignatureKeys(v)
server.Config.Services.SignaturePrivateKey, server.Config.Services.SignaturePublicKey = setupSignatureKeys(store)

if endpoint := c.String("config-service-endpoint"); endpoint != "" {
server.Config.Services.ConfigService = config.NewHTTP(endpoint, server.Config.Services.SignaturePrivateKey)
Expand Down
23 changes: 21 additions & 2 deletions cmd/server/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
"github.com/woodpecker-ci/woodpecker/server/forge/github"
"github.com/woodpecker-ci/woodpecker/server/forge/gitlab"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/plugins/encryption"
"github.com/woodpecker-ci/woodpecker/server/plugins/environments"
"github.com/woodpecker-ci/woodpecker/server/plugins/registry"
"github.com/woodpecker-ci/woodpecker/server/plugins/secrets"
Expand Down Expand Up @@ -108,8 +109,26 @@ func setupQueue(c *cli.Context, s store.Store) queue.Queue {
return queue.WithTaskStore(queue.New(c.Context), s)
}

func setupSecretService(c *cli.Context, s model.SecretStore) model.SecretService {
return secrets.New(c.Context, s)
func setupSecretService(ctx *cli.Context, store model.SecretStore) model.SecretService {
secretSvc := secrets.New(ctx.Context, store)

if aesKey := ctx.String("encryption-key"); aesKey != "" {
aesSecretsSvc, err := setupAesSecretService(&secretSvc, aesKey)
if err != nil {
log.Fatal().Err(err).Msg("failed to set up encryption for secrets service")
}
return aesSecretsSvc
}

return secretSvc
}

func setupAesSecretService(secretSvc *model.SecretService, aesKey string) (model.SecretService, error) {
aesSvc, err := encryption.NewAes(aesKey)
if err != nil {
return nil, err
}
return secrets.NewEncrypted(secretSvc, &aesSvc), nil
}

func setupRegistryService(c *cli.Context, s store.Store) model.RegistryService {
Expand Down
71 changes: 31 additions & 40 deletions docs/docs/30-administration/40-encryption.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,55 @@
# Secrets encryption

By default, Woodpecker does not encrypt secrets in its database. You can enable encryption
using simple AES key or more advanced [Google TINK](https://developers.google.com/tink) encryption.
using simple AES key.
zc-devs marked this conversation as resolved.
Show resolved Hide resolved

:::caution
Secrets encryption is experimental.
Currently encryption is unrevertable (do backups)
and requires empty `secrets` table (can be evaluated in fresh installation or delete all secrets and create new after enabling encryption).

Check the [current state](https://github.com/woodpecker-ci/woodpecker/issues/1541)
:::

## Common

### Enabling secrets encryption

To enable secrets encryption and encrypt all existing secrets in database set
`WOODPECKER_ENCRYPTION_KEY`, `WOODPECKER_ENCRYPTION_KEY_FILE` or `WOODPECKER_ENCRYPTION_TINK_KEYSET_PATH` environment
variable depending on encryption method of your choice.
To enable secrets encryption set
`WOODPECKER_ENCRYPTION_KEY` or `WOODPECKER_ENCRYPTION_KEY_FILE` environment
variable.

After encryption is enabled you will be unable to start Woodpecker server without providing valid encryption key!

### Disabling encryption and decrypting all secrets

To disable secrets encryption and decrypt database you need to start server with valid
`WOODPECKER_ENCRYPTION_KEY` or `WOODPECKER_ENCRYPTION_TINK_KEYSET_FILE` environment variable set depending on
enabled encryption method, and `WOODPECKER_ENCRYPTION_DISABLE` set to true.

After secrets was decrypted server will proceed working in unencrypted mode. You will not need to use "disable encryption"
variable or encryption keys to start server anymore.


## AES
Simple AES encryption.

### Configuration
You can manage encryption on server using these environment variables:
- `WOODPECKER_ENCRYPTION_KEY` - encryption key
- `WOODPECKER_ENCRYPTION_KEY_FILE` - file to read encryption key from
- `WOODPECKER_ENCRYPTION_DISABLE` - disable encryption flag used to decrypt all data on server

## TINK
TINK uses AEAD encryption instead of simple AES and supports key rotation.

### Configuration
You can manage encryption on server using these two environment variables:
- `WOODPECKER_ENCRYPTION_TINK_KEYSET_FILE` - keyset filepath
- `WOODPECKER_ENCRYPTION_DISABLE` - disable encryption flag used to decrypt all data on server

### Encryption keys
You will need plaintext AEAD-compatible Google TINK keyset to encrypt your data.

To generate it and then rotate keys if needed, install `tinkey`([installation guide](https://developers.google.com/tink/install-tinkey))

Keyset contains one or more keys, used to encrypt or decrypt your data, and primary key ID, used to determine which key
to use while encrypting new data.

Keyset generation example:
One option to generate encryption key is to use OpenSSL, but any password generator can be used also. Recommended length is at least 32 bytes:
zc-devs marked this conversation as resolved.
Show resolved Hide resolved
```shell
tinkey create-keyset --key-template AES256_GCM --out-format json --out keyset.json
$ openssl rand -base64 32
GjVHT007c4x3N+YPbsZld+hifba1enXkOzIb/0h6oW8=
```

### Key rotation
Use `tinkey` to rotate encryption keys in your existing keyset:
```shell
tinkey rotate-keyset --in keyset_v1.json --out keyset_v2.json --key-template AES256_GCM
If we run server with `WOODPECKER_ENCRYPTION_KEY='GjVHT007c4x3N+YPbsZld+hifba1enXkOzIb/0h6oW8='`, and try to create secret `some_secret:super-secret-value`
zc-devs marked this conversation as resolved.
Show resolved Hide resolved
then we'll get messages in log similar to:
zc-devs marked this conversation as resolved.
Show resolved Hide resolved
```log
{"level":"debug","id":0,"name":"some_secret","time":"2023-08-20T19:37:42Z","caller":"/woodpecker/server/plugins/secrets/encrypted.go:219","message":"encryption"}
{"level":"debug","id":9,"name":"some_secret","time":"2023-08-20T19:37:42Z","caller":"/woodpecker/server/plugins/secrets/encrypted.go:230","message":"decryption"}
```
and row in database similar to:
zc-devs marked this conversation as resolved.
Show resolved Hide resolved
```psql
woodpecker=# select secret_id, secret_name, secret_value from secrets;
secret_id | secret_name | secret_value
-----------+-------------+----------------------------------------------------------------
9 | some_secret | PUattjAz6EOP28sbJOEaDSZyXRDrPxGQv9EyQHQPimrWLQELr59WYp83DUNQ6w
(1 row)
```

Then you just need to replace server keyset file with the new one. At the moment server detects new encryption
keyset it will re-encrypt all existing secrets with the new key, so you will be unable to start server with previous
keyset anymore.
:::note
You won't get exactly the same secret's encrypted value because random nonce is used.
zc-devs marked this conversation as resolved.
Show resolved Hide resolved
:::
41 changes: 0 additions & 41 deletions server/model/encryption.go

This file was deleted.

95 changes: 83 additions & 12 deletions server/plugins/encryption/aes.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,72 @@
package encryption

import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"

"github.com/google/tink/go/subtle/random"
"github.com/rs/zerolog/log"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/sha3"
)

"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/store"
const (
Sha256Size = 32
AESGCMSIVNonceSize = 12
)

type aesEncryptionService struct {
cipher cipher.AEAD
keyID string
store store.Store
clients []model.EncryptionClient
keyId string
cipher cipher.AEAD
}

func NewAes(password string) (EncryptionService, error) {
log.Debug().Msg("initializing AES encryption service")

key, err := hash([]byte(password))
if err != nil {
return nil, NewKeyGenerationError(err)
}

keyHash, err := bcrypt.GenerateFromPassword(key, bcrypt.DefaultCost)
if err != nil {
return nil, NewKeyGenerationIdError(err)
}

block, err := aes.NewCipher(key)
if err != nil {
return nil, NewCipherLoadingError(err)
}

aead, err := cipher.NewGCM(block)
if err != nil {
return nil, NewCipherLoadingError(err)
}

service := aesEncryptionService{
keyId: string(keyHash),
cipher: aead,
}

log.Debug().Msg("AES encryption service has been initialized")
return &service, nil
}

func hash(data []byte) ([]byte, error) {
result := make([]byte, Sha256Size)
sha := sha3.NewShake256()

_, err := sha.Write(data)
if err != nil {
return nil, NewHashCalculationError(err)
}
_, err = sha.Read(result)
if err != nil {
return nil, NewHashCalculationError(err)
}
return result, nil
}

func (svc *aesEncryptionService) Encrypt(plaintext, associatedData string) (string, error) {
Expand All @@ -43,25 +94,45 @@ func (svc *aesEncryptionService) Encrypt(plaintext, associatedData string) (stri
result = append(result, nonce...)
result = append(result, ciphertext...)

return base64.StdEncoding.EncodeToString(result), nil
return base64.RawStdEncoding.EncodeToString(result), nil
}

func (svc *aesEncryptionService) Decrypt(ciphertext, associatedData string) (string, error) {
bytes, err := base64.StdEncoding.DecodeString(ciphertext)
bytes, err := base64.RawStdEncoding.DecodeString(ciphertext)
if err != nil {
return "", fmt.Errorf(errTemplateBase64DecryptionFailed, err)
return "", NewBase64DecryptionError(err)
}

nonce := bytes[:AESGCMSIVNonceSize]
message := bytes[AESGCMSIVNonceSize:]

plaintext, err := svc.cipher.Open(nil, nonce, message, []byte(associatedData))
if err != nil {
return "", fmt.Errorf(errTemplateDecryptionFailed, err)
return "", NewDecryptionError(err)
}
return string(plaintext), nil
}

func (svc *aesEncryptionService) Disable() error {
return svc.disable()
func NewHashCalculationError(e error) error {
return fmt.Errorf("failed calculating hash: %w", e)
}

func NewKeyGenerationError(e error) error {
return fmt.Errorf("failed generating key from passphrase: %w", e)
}

func NewKeyGenerationIdError(e error) error {
return fmt.Errorf("failed generating key id: %w", e)
}

func NewCipherLoadingError(e error) error {
return fmt.Errorf("failed loading encryption cipher: %w", e)
}

func NewBase64DecryptionError(e error) error {
return fmt.Errorf("Base64 decryption failed: %w", e)
}

func NewDecryptionError(e error) error {
return fmt.Errorf("decryption error: %w", e)
}
Loading