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
19 changes: 10 additions & 9 deletions cmd/server/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,22 +455,23 @@ var flags = append([]cli.Flag{
Hidden: true,
},
//
// secrets encryption in DB
// encryption
//
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_ENCRYPTION_KEY"},
Name: "encryption-raw-key",
Usage: "Raw encryption key",
FilePath: os.Getenv("WOODPECKER_ENCRYPTION_KEY_FILE"),
EnvVars: []string{"WOODPECKER_ENCRYPTION_AES_KEY"},
Name: "encryption-aes-key",
Usage: "AES encryption key",
FilePath: os.Getenv("WOODPECKER_ENCRYPTION_AES_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",
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_SECRETS_ENCRYPTION_MODE"},
Name: "secrets-encryption-mode",
Usage: "secrets encryption mode",
Value: "Disabled",
},
}, common.GlobalLoggerFlags...)
25 changes: 9 additions & 16 deletions cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,34 +260,27 @@ 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.Encryption = setupEncryptionService(c)
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
21 changes: 19 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,24 @@ 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 setupEncryptionService(ctx *cli.Context) encryption.EncryptionService {
if aesKey := ctx.String("encryption-aes-key"); aesKey != "" {
encSvc, err := encryption.NewAes(aesKey)
if err != nil {
log.Fatal().Err(err).Msg("failed to set up AES encryption service")
}
return encSvc
}

return nil
}

func setupSecretService(ctx *cli.Context, store model.SecretStore) model.SecretService {
secretSvc, err := secrets.NewService(ctx, store)
if err != nil {
log.Fatal().Err(err).Msg("failed to set up secrets service")
}
return secretSvc
}

func setupRegistryService(c *cli.Context, s store.Store) model.RegistryService {
Expand Down
77 changes: 34 additions & 43 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.
By default, Woodpecker does not encrypt secrets in its database. You can enable encryption using a simple AES key.

:::caution
Secrets encryption is experimental.
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_SECRETS_ENCRYPTION_MODE` environment variable to the one of:
- `Disabled` (default) - use plain text secrets;
- `Enabled` - use encryption without migration (encryption) of already existing secrets;
- `EnabledAndEncrypt` - use encryption and encrypt already existing secrets;
- `DisabledAndDecrypt` - use plain text secrets and run decryption of existing secrets.

:::caution
After migration, don't forget to switch the mode: `EnabledAndEncrypt` -> `Enabled`, `DisabledAndDecrypt` -> `Disabled`.
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.
- `WOODPECKER_ENCRYPTION_AES_KEY` - encryption key
- `WOODPECKER_ENCRYPTION_AES_KEY_FILE` - file to read encryption key from

### 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 also be used. Recommended key length is at least 32 bytes:
```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 the server with `WOODPECKER_ENCRYPTION_AES_KEY='GjVHT007c4x3N+YPbsZld+hifba1enXkOzIb/0h6oW8='`, and try to create a secret like `some_secret:super-secret-value`
then we'll get messages in the log similar to:
```log
{"level":"debug","id":1,"name":"s-name","time":"2023-09-24T10:49:21Z","caller":"/woodpecker/server/plugins/secrets/encrypted.go:48","message":"encryption"}
```
and a row in the database similar to:
```psql
woodpecker=# select secret_id, secret_name, secret_value from secrets;
secret_id | secret_name | secret_value
-----------+-------------+----------------------------------------------------------------
1 | some_secret | _aes_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 a random nonce is used.
:::
2 changes: 2 additions & 0 deletions server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/woodpecker-ci/woodpecker/server/logging"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/plugins/config"
"github.com/woodpecker-ci/woodpecker/server/plugins/encryption"
"github.com/woodpecker-ci/woodpecker/server/pubsub"
"github.com/woodpecker-ci/woodpecker/server/queue"
)
Expand All @@ -35,6 +36,7 @@ var Config = struct {
Pubsub pubsub.Publisher
Queue queue.Queue
Logs logging.Log
Encryption encryption.EncryptionService
Secrets model.SecretService
Registries model.RegistryService
Environ model.EnvironService
Expand Down
41 changes: 0 additions & 41 deletions server/model/encryption.go

This file was deleted.

Loading