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

Use asym key to sign webhooks #916

Merged
merged 26 commits into from
Jun 1, 2022
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ef13725
use async key pair for webhooks
anbraten May 14, 2022
ce7f026
fix tests
anbraten May 14, 2022
d0c8986
fix linter
anbraten May 14, 2022
a63433f
improve code
anbraten May 14, 2022
960bfac
add key pair to database
anbraten May 15, 2022
021bdda
Merge branch 'master' into webhook-pubkey
6543 May 17, 2022
17e21b2
undo some changes
anbraten May 17, 2022
a117a20
Merge branch 'webhook-pubkey' of github.com:anbraten/woodpecker into …
anbraten May 17, 2022
ee916ad
more undo
anbraten May 17, 2022
9a8a893
improve docs
anbraten May 17, 2022
590d02b
add api-endpoint
anbraten May 17, 2022
1e2175c
add signaturne api endpoint
anbraten May 17, 2022
cc6a3c5
Merge remote-tracking branch 'upstream/master' into webhook-pubkey
anbraten May 17, 2022
61dc259
Merge remote-tracking branch 'upstream/master' into webhook-pubkey
anbraten May 17, 2022
417c5d0
fix error
anbraten May 17, 2022
34c2426
fix linting and test
anbraten May 17, 2022
965dba8
fix lint
anbraten May 17, 2022
d2deaa7
Merge branch 'master' into webhook-pubkey
anbraten May 24, 2022
2a24cbf
Merge remote-tracking branch 'upstream/master' into webhook-pubkey
anbraten May 30, 2022
327b6cb
add test
anbraten May 30, 2022
1a249e1
migration 006
6543 Jun 1, 2022
dbca470
no need for migration
6543 Jun 1, 2022
932edb1
Merge branch 'master' into webhook-pubkey
6543 Jun 1, 2022
b5e6452
replace httsign lib
anbraten Jun 1, 2022
8fc107b
Merge branch 'webhook-pubkey' of github.com:anbraten/woodpecker into …
anbraten Jun 1, 2022
1c4a74e
fix lint
anbraten Jun 1, 2022
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
6 changes: 0 additions & 6 deletions cmd/server/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,6 @@ var flags = []cli.Flag{
Name: "config-service-endpoint",
Usage: "url used for calling configuration service endpoint",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_CONFIG_SERVICE_SECRET"},
Name: "config-service-secret",
Usage: "secret to sign requests send to configuration service",
FilePath: os.Getenv("WOODPECKER_CONFIG_SERVICE_SECRET_FILE"),
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_DATABASE_DRIVER"},
Name: "driver",
Expand Down
11 changes: 4 additions & 7 deletions cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import (
woodpeckerGrpcServer "github.com/woodpecker-ci/woodpecker/server/grpc"
"github.com/woodpecker-ci/woodpecker/server/logging"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/plugins/configuration"
"github.com/woodpecker-ci/woodpecker/server/plugins/config"
"github.com/woodpecker-ci/woodpecker/server/pubsub"
"github.com/woodpecker-ci/woodpecker/server/remote"
"github.com/woodpecker-ci/woodpecker/server/router"
Expand Down Expand Up @@ -267,13 +267,10 @@ func setupEvilGlobals(c *cli.Context, v store.Store, r remote.Remote) {
server.Config.Services.Secrets = setupSecretService(c, v)
server.Config.Services.Environ = setupEnvironService(c, v)

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

if endpoint := c.String("config-service-endpoint"); endpoint != "" {
secret := c.String("config-service-secret")
if secret == "" {
log.Error().Msg("could not configure configuration service, missing secret")
} else {
server.Config.Services.ConfigService = configuration.NewAPI(endpoint, secret)
}
server.Config.Services.ConfigService = config.NewHTTP(endpoint, server.Config.Services.SignaturePrivateKey)
}

// authentication
Expand Down
36 changes: 36 additions & 0 deletions cmd/server/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ package main

import (
"context"
"crypto"
"crypto/ed25519"
"crypto/rand"
"encoding/hex"
"fmt"
"net/url"
"os"
Expand Down Expand Up @@ -352,3 +356,35 @@ func setupMetrics(g *errgroup.Group, _store store.Store) {
}
})
}

// generate or load key pair to sign webhooks requests (i.e. used for extensions)
func setupSignatureKeys(_store store.Store) (crypto.PrivateKey, crypto.PublicKey) {
privKeyID := "signature-private-key"

privKey, err := _store.ServerConfigGet(privKeyID)
if err != nil && err == datastore.RecordNotExist {
_, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
log.Fatal().Err(err).Msgf("Failed to generate private key")
return nil, nil
}
err = _store.ServerConfigSet(privKeyID, hex.EncodeToString(privKey))
if err != nil {
log.Fatal().Err(err).Msgf("Failed to generate private key")
return nil, nil
}
log.Info().Msg("Created private key")
return privKey, privKey.Public()
} else if err != nil {
log.Fatal().Err(err).Msgf("Failed to load private key")
return nil, nil
} else {
privKeyStr, err := hex.DecodeString(privKey)
if err != nil {
log.Fatal().Err(err).Msgf("Failed to decode private key")
return nil, nil
}
privKey := ed25519.PrivateKey(privKeyStr)
return privKey, privKey.Public()
}
}
10 changes: 0 additions & 10 deletions docs/docs/30-administration/10-server-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,16 +356,6 @@ Example: `WOODPECKER_LIMIT_CPU_SET=1,2`

Specify a configuration service endpoint, see [Configuration Extension](/docs/administration/external-configuration-api)

### `WOODPECKER_CONFIG_SERVICE_SECRET`
> Default: ``

Specify a signing secret for the configuration service endpoint, see [Configuration Extension](/docs/administration/external-configuration-api)

### `WOODPECKER_CONFIG_SERVICE_SECRET_FILE`
> Default: ``

Read the value for `WOODPECKER_CONFIG_SERVICE_SECRET` from the specified filepath

---

### `WOODPECKER_GITHUB_...`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
To provide additional management and preprocessing capabilities for pipeline configurations Woodpecker supports an HTTP api which can be enabled to call an external config service.
Before the run or restart of any pipeline Woodpecker will make a POST request to an external HTTP api sending the current repository, build information and all current config files retrieved from the repository. The external api can then send back new pipeline configurations that will be used immediately or respond with `HTTP 204` to tell the system to use the existing configuration.

Every request sent by Woodpecker is signed using a http-signature using the provided secret from `WOODPECKER_CONFIG_SERVICE_SECRET`. This way the external api can verify the authenticity request from the Woodpecker instance.
Every request sent by Woodpecker is signed using a [http-signature](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures) by a private key (ed25519) generated on the first start of the Woodpecker server. You can get the public key for the verification of the http-signature from `http(s)://your-woodpecker-server/api/signature/public-key`.

A simplistic example configuration service can be found here: [https://github.com/woodpecker-ci/example-config-service](https://github.com/woodpecker-ci/example-config-service)

Expand All @@ -13,8 +13,6 @@ A simplistic example configuration service can be found here: [https://github.co
# Server
# ...
WOODPECKER_CONFIG_SERVICE_ENDPOINT=https://example.com/ciconfig
WOODPECKER_CONFIG_SERVICE_SECRET=mysecretsigningkey

```

### Example request made by Woodpecker
Expand Down
1 change: 1 addition & 0 deletions docs/docs/91-migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Some versions need some changes to the server configuration or the pipeline conf

## 1.0.0

- The signature used to verify extensions calls (like those used for the [config-extension](/docs/administration/external-configuration-api)) done by the Woodpecker server switched from using a shared-secret HMac to an ed25519 key-pair. Read more about it at the [config-extensions](/docs/administration/external-configuration-api) documentation.
- Refactored support of old agent filter labels and expression. Learn how to use the new [filter](/docs/usage/pipeline-syntax#labels).
- Renamed step environment variable `CI_SYSTEM_ARCH` to `CI_SYSTEM_PLATFORM`. Same applies for the cli exec variable.

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ go 1.16

require (
code.gitea.io/sdk/gitea v0.15.1-0.20220501190934-319a978c6c71
github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.0.2
github.com/containerd/containerd v1.5.9 // indirect
Expand All @@ -19,6 +18,7 @@ require (
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568
github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf
github.com/gin-gonic/gin v1.7.7
github.com/go-fed/httpsig v1.1.0
github.com/go-playground/validator/v10 v10.10.1 // indirect
github.com/go-sql-driver/mysql v1.6.0
github.com/goccy/go-json v0.9.7 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e h1:rl2Aq4ZODqTDkeSqQBy+fzpZPamacO1Srp8zq7jf2Sc=
github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e/go.mod h1:Xa6lInWHNQnuWoF0YPSsx+INFA9qk7/7pTjwb3PInkY=
github.com/Antonboom/errname v0.1.5 h1:IM+A/gz0pDhKmlt5KSNTVAvfLMb+65RxavBXpRtCUEg=
github.com/Antonboom/errname v0.1.5/go.mod h1:DugbBstvPFQbv/5uLcRRzfrNqKE9tVdVCqWCLp6Cifo=
github.com/Antonboom/nilnil v0.1.0 h1:DLDavmg0a6G/F4Lt9t7Enrbgb3Oph6LnDE6YVsmTt74=
Expand Down Expand Up @@ -450,6 +448,8 @@ github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-critic/go-critic v0.6.2 h1:L5SDut1N4ZfsWZY0sH4DCrsHLHnhuuWak2wa165t9gs=
github.com/go-critic/go-critic v0.6.2/go.mod h1:td1s27kfmLpe5G/DPjlnFI7o1UCzePptwU7Az0V5iCM=
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
Expand Down
2 changes: 1 addition & 1 deletion server/api/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ func PostBuild(c *gin.Context) {
currentFileMeta[i] = &remote.FileMeta{Name: cfg.Name, Data: cfg.Data}
}

newConfig, useOld, err := server.Config.Services.ConfigService.FetchExternalConfig(c, repo, build, currentFileMeta)
newConfig, useOld, err := server.Config.Services.ConfigService.FetchConfig(c, repo, build, currentFileMeta)
if err != nil {
msg := fmt.Sprintf("On fetching external build config: %s", err)
c.String(http.StatusBadRequest, msg)
Expand Down
41 changes: 41 additions & 0 deletions server/api/signature_public_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2021 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package api

import (
"crypto/x509"
"encoding/pem"
"net/http"

"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/woodpecker-ci/woodpecker/server"
)

func GetSignaturePublicKey(c *gin.Context) {
b, err := x509.MarshalPKIXPublicKey(server.Config.Services.SignaturePublicKey)
if err != nil {
log.Error().Err(err).Msg("can't marshal public key")
c.AbortWithStatus(http.StatusInternalServerError)
return
}

block := &pem.Block{
Type: "PUBLIC KEY",
Bytes: b,
}

c.String(200, "%s", pem.EncodeToMemory(block))
}
21 changes: 12 additions & 9 deletions server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,29 @@
package server

import (
"crypto"
"time"

"github.com/woodpecker-ci/woodpecker/server/logging"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/plugins/configuration"
"github.com/woodpecker-ci/woodpecker/server/plugins/config"
"github.com/woodpecker-ci/woodpecker/server/pubsub"
"github.com/woodpecker-ci/woodpecker/server/queue"
"github.com/woodpecker-ci/woodpecker/server/remote"
)

var Config = struct {
Services struct {
Pubsub pubsub.Publisher
Queue queue.Queue
Logs logging.Log
Secrets model.SecretService
Registries model.RegistryService
Environ model.EnvironService
Remote remote.Remote
ConfigService configuration.ConfigService
Pubsub pubsub.Publisher
Queue queue.Queue
Logs logging.Log
Secrets model.SecretService
Registries model.RegistryService
Environ model.EnvironService
Remote remote.Remote
ConfigService config.Extension
SignaturePrivateKey crypto.PrivateKey
SignaturePublicKey crypto.PublicKey
}
Storage struct {
// Users model.UserStore
Expand Down
27 changes: 27 additions & 0 deletions server/model/server_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2022 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package model

// ServerConfigStore persists key-value pairs for storing server configurations.
type ServerConfigStore interface {
ServerConfigGet(key string) (string, error)
ServerConfigSet(key int64, value string) error
}

// ServerConfig represents a key-value pair for storing server configurations.
type ServerConfig struct {
Key string `json:"key" xorm:"pk"`
Value string `json:"value" xorm:""`
}
13 changes: 13 additions & 0 deletions server/plugins/config/extension.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package config

import (
"context"

"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/remote"
)

type Extension interface {
IsConfigured() bool
FetchConfig(ctx context.Context, repo *model.Repo, build *model.Build, currentFileMeta []*remote.FileMeta) (configData []*remote.FileMeta, useOld bool, err error)
}
66 changes: 66 additions & 0 deletions server/plugins/config/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package config

import (
"context"
"crypto"
"fmt"

"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/plugins/utils"
"github.com/woodpecker-ci/woodpecker/server/remote"
)

type http struct {
endpoint string
privateKey crypto.PrivateKey
}

// Same as remote.FileMeta but with json tags and string data
type config struct {
Name string `json:"name"`
Data string `json:"data"`
}

type requestStructure struct {
Repo *model.Repo `json:"repo"`
Build *model.Build `json:"build"`
Configuration []*config `json:"configs"`
}

type responseStructure struct {
Configs []config `json:"configs"`
}

func NewHTTP(endpoint string, privateKey crypto.PrivateKey) Extension {
return &http{endpoint, privateKey}
}

func (cp *http) IsConfigured() bool {
return cp.endpoint != ""
}

func (cp *http) FetchConfig(ctx context.Context, repo *model.Repo, build *model.Build, currentFileMeta []*remote.FileMeta) (configData []*remote.FileMeta, useOld bool, err error) {
currentConfigs := make([]*config, len(currentFileMeta))
for i, pipe := range currentFileMeta {
currentConfigs[i] = &config{Name: pipe.Name, Data: string(pipe.Data)}
}

response := new(responseStructure)
body := requestStructure{Repo: repo, Build: build, Configuration: currentConfigs}
status, err := utils.Send(ctx, "POST", cp.endpoint, cp.privateKey, body, response)
if err != nil && status != 204 {
return nil, false, fmt.Errorf("Failed to fetch config via http (%d) %w", status, err)
}

var newFileMeta []*remote.FileMeta
if status != 200 {
newFileMeta = make([]*remote.FileMeta, 0)
} else {
newFileMeta = make([]*remote.FileMeta, len(response.Configs))
for i, pipe := range response.Configs {
newFileMeta[i] = &remote.FileMeta{Name: pipe.Name, Data: []byte(pipe.Data)}
}
}

return newFileMeta, status == 204, nil
}
Loading