Files
releases/plugins/bundle/plugin.go
T
Ashutosh Narkar 338583c18a Add support for OPA bundle signatures
These changes add support for digital signatures for policy bundles which
can be used to verify their authenticity.

Bundle signature verification involves the following steps:

* Verify the JWT signature
* Verify the files in the JWT payload exist in the bundle
* Verify the file content of the files in bundle match with those in the payload

This commit adds a new `sign` command to generate a digital signature for policy bundles.

For more details, run "opa sign --help"

The signatures generated by the 'sign' command can be verified by the
'build' command. The 'build' command can also sign the bundle it generates.

The 'run' command can verify a signed bundle or skip verification altogether.

OPA 'sign', 'build' and 'run' can be used to
sign/verify bundles in bundle mode (--bundle) mode only. Verification
can be also be performed when bundle downloading is enabled.

Fixes: #1757

Signed-off-by: Ashutosh Narkar <anarkar4387@gmail.com>
2020-07-14 09:49:59 -04:00

432 lines
12 KiB
Go

// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// Package bundle implements bundle downloading.
package bundle
import (
"context"
"errors"
"fmt"
"reflect"
"sync"
"time"
"github.com/sirupsen/logrus"
"github.com/open-policy-agent/opa/metrics"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/bundle"
"github.com/open-policy-agent/opa/download"
"github.com/open-policy-agent/opa/plugins"
"github.com/open-policy-agent/opa/storage"
)
// Plugin implements bundle activation.
type Plugin struct {
config Config
manager *plugins.Manager // plugin manager for storage and service clients
status map[string]*Status // current status for each bundle
etags map[string]string // etag on last successful activation
listeners map[interface{}]func(Status) // listeners to send status updates to
bulkListeners map[interface{}]func(map[string]*Status) // listeners to send aggregated status updates to
downloaders map[string]*download.Downloader
mtx sync.Mutex
cfgMtx sync.Mutex
legacyConfig bool
ready bool
}
// New returns a new Plugin with the given config.
func New(parsedConfig *Config, manager *plugins.Manager) *Plugin {
initialStatus := map[string]*Status{}
for name := range parsedConfig.Bundles {
initialStatus[name] = &Status{
Name: name,
}
}
p := &Plugin{
manager: manager,
config: *parsedConfig,
status: initialStatus,
downloaders: make(map[string]*download.Downloader),
etags: make(map[string]string),
ready: false,
}
manager.UpdatePluginStatus(Name, &plugins.Status{State: plugins.StateNotReady})
return p
}
// Name identifies the plugin on manager.
const Name = "bundle"
// Lookup returns the bundle plugin registered with the manager.
func Lookup(manager *plugins.Manager) *Plugin {
if p := manager.Plugin(Name); p != nil {
return p.(*Plugin)
}
return nil
}
// Start runs the plugin. The plugin will periodically try to download bundles
// from the configured service. When a new bundle is downloaded, the data and
// policies are extracted and inserted into storage.
func (p *Plugin) Start(ctx context.Context) error {
p.mtx.Lock()
defer p.mtx.Unlock()
p.initDownloaders()
for name, dl := range p.downloaders {
p.logInfo(name, "Starting bundle downloader.")
dl.Start(ctx)
}
return nil
}
// Stop stops the plugin.
func (p *Plugin) Stop(ctx context.Context) {
p.mtx.Lock()
defer p.mtx.Unlock()
for name, dl := range p.downloaders {
p.logInfo(name, "Stopping bundle downloader.")
dl.Stop(ctx)
}
}
// Reconfigure notifies the plugin that it's configuration has changed.
// Any bundle configs that have changed or been added/removed will take
// affect.
func (p *Plugin) Reconfigure(ctx context.Context, config interface{}) {
// Reconfiguring should not occur in parallel, lock to ensure
// nothing swaps underneath us with the current p.config and the updated one.
// Use p.cfgMtx instead of p.mtx so as to not block any bundle downloads/activations
// that are in progress. We upgrade to p.mtx locking after stopping downloaders.
p.cfgMtx.Lock()
defer p.cfgMtx.Unlock()
// Look for any bundles that have had their config changed, are new, or have been removed
newConfig := config.(*Config)
newBundles, updatedBundles, deletedBundles := p.configDelta(newConfig)
p.config = *newConfig
if len(updatedBundles) == 0 && len(newBundles) == 0 && len(deletedBundles) == 0 {
// no relevant config changes
return
}
// Stop the downloaders outside p.mtx to allow them to finish handling any in-progress requests.
for name, dl := range p.downloaders {
_, updated := updatedBundles[name]
_, deleted := deletedBundles[name]
if updated || deleted {
dl.Stop(ctx)
}
}
// Only lock p.mtx once we start changing the internal maps
// and downloader configs.
p.mtx.Lock()
defer p.mtx.Unlock()
// Cleanup existing downloaders that are deleted
for name := range p.downloaders {
if _, deleted := deletedBundles[name]; deleted {
p.logInfo(name, "Bundle downloader configuration removed. Stopping bundle downloader.")
delete(p.downloaders, name)
delete(p.status, name)
delete(p.etags, name)
}
}
// Deactivate the bundles that were removed
params := storage.WriteParams
params.Context = storage.NewContext()
err := storage.Txn(ctx, p.manager.Store, params, func(txn storage.Transaction) error {
opts := &bundle.DeactivateOpts{
Ctx: ctx,
Store: p.manager.Store,
Txn: txn,
BundleNames: deletedBundles,
}
err := bundle.Deactivate(opts)
if err != nil {
p.logError(fmt.Sprint(deletedBundles), "Failed to deactivate bundles: %s", err)
return err
}
return nil
})
if err != nil {
// TODO(patrick-east): This probably shouldn't panic.. But OPA shouldn't
// continue in a potentially inconsistent state.
panic(errors.New("Unable deactivate bundle: " + err.Error()))
}
readyNow := p.ready
for name, source := range p.config.Bundles {
_, updated := updatedBundles[name]
_, isNew := newBundles[name]
if isNew || updated {
if isNew {
p.status[name] = &Status{Name: name}
p.logInfo(name, "New bundle downloader configuration added. Starting bundle downloader.")
} else {
p.logInfo(name, "Bundle downloader configuration changed. Restarting bundle downloader.")
}
p.downloaders[name] = p.newDownloader(name, source)
p.downloaders[name].Start(ctx)
readyNow = false
}
}
if !readyNow {
p.ready = false
p.manager.UpdatePluginStatus(Name, &plugins.Status{State: plugins.StateNotReady})
}
}
// Register a listener to receive status updates. The name must be comparable.
// The listener will receive a status update for each bundle configured, they are
// not going to be aggregated. For all status updates use `RegisterBulkListener`.
func (p *Plugin) Register(name interface{}, listener func(Status)) {
p.mtx.Lock()
defer p.mtx.Unlock()
if p.listeners == nil {
p.listeners = map[interface{}]func(Status){}
}
p.listeners[name] = listener
}
// Unregister a listener to stop receiving status updates.
func (p *Plugin) Unregister(name interface{}) {
p.mtx.Lock()
defer p.mtx.Unlock()
delete(p.bulkListeners, name)
}
// RegisterBulkListener registers a listener to receive bulk (aggregated) status updates. The name must be comparable.
func (p *Plugin) RegisterBulkListener(name interface{}, listener func(map[string]*Status)) {
p.mtx.Lock()
defer p.mtx.Unlock()
if p.bulkListeners == nil {
p.bulkListeners = map[interface{}]func(map[string]*Status){}
}
p.bulkListeners[name] = listener
}
// UnregisterBulkListener unregisters a listener to stop receiving aggregated status updates.
func (p *Plugin) UnregisterBulkListener(name interface{}) {
p.mtx.Lock()
defer p.mtx.Unlock()
delete(p.bulkListeners, name)
}
// Config returns the plugins current configuration
func (p *Plugin) Config() *Config {
return &p.config
}
func (p *Plugin) initDownloaders() {
// Initialize a downloader for each bundle configured.
for name, source := range p.config.Bundles {
p.downloaders[name] = p.newDownloader(name, source)
}
}
func (p *Plugin) newDownloader(name string, source *Source) *download.Downloader {
conf := source.Config
client := p.manager.Client(source.Service)
path := source.Resource
return download.New(conf, client, path).WithCallback(func(ctx context.Context, u download.Update) {
// wrap the callback to include the name of the bundle that was updated
p.oneShot(ctx, name, u)
}).WithBundleVerificationConfig(source.Signing)
}
func (p *Plugin) oneShot(ctx context.Context, name string, u download.Update) {
p.mtx.Lock()
defer p.mtx.Unlock()
p.process(ctx, name, u)
for _, listener := range p.listeners {
listener(*p.status[name])
}
for _, listener := range p.bulkListeners {
// Send a copy of the full status map to the bulk listeners.
// They shouldn't have access to the original underlying
// map, primarily for thread safety issues with modifications
// made to it.
statusCpy := map[string]*Status{}
for k, v := range p.status {
statusCpy[k] = v
}
listener(statusCpy)
}
}
func (p *Plugin) process(ctx context.Context, name string, u download.Update) {
if u.Metrics != nil {
p.status[name].Metrics = u.Metrics
} else {
p.status[name].Metrics = metrics.New()
}
p.status[name].SetRequest()
if u.Error != nil {
p.logError(name, "Bundle download failed: %v", u.Error)
p.status[name].SetError(u.Error)
p.downloaders[name].ClearCache()
return
}
p.status[name].LastSuccessfulRequest = p.status[name].LastRequest
if u.Bundle != nil {
p.status[name].LastSuccessfulDownload = p.status[name].LastSuccessfulRequest
p.status[name].Metrics.Timer(metrics.RegoLoadBundles).Start()
defer p.status[name].Metrics.Timer(metrics.RegoLoadBundles).Stop()
if err := p.activate(ctx, name, u.Bundle); err != nil {
p.logError(name, "Bundle activation failed: %v", err)
p.status[name].SetError(err)
p.downloaders[name].ClearCache()
return
}
p.status[name].SetError(nil)
p.status[name].SetActivateSuccess(u.Bundle.Manifest.Revision)
if u.ETag != "" {
p.logInfo(name, "Bundle downloaded and activated successfully. Etag updated to %v.", u.ETag)
} else {
p.logInfo(name, "Bundle downloaded and activated successfully.")
}
p.etags[name] = u.ETag
// If the plugin wasn't ready yet then check if we are now after activating this bundle.
if !p.ready {
readyNow := true // optimistically
for _, status := range p.status {
if len(status.Errors) > 0 || (status.LastSuccessfulActivation == time.Time{}) {
readyNow = false // Not ready yet, check again on next bundle activation.
break
}
}
if readyNow {
p.ready = true
p.manager.UpdatePluginStatus(Name, &plugins.Status{State: plugins.StateOK})
}
}
return
}
if etag, ok := p.etags[name]; ok && u.ETag == etag {
p.logDebug(name, "Bundle download skipped, server replied with not modified.")
p.status[name].SetError(nil)
return
}
}
func (p *Plugin) activate(ctx context.Context, name string, b *bundle.Bundle) error {
p.logDebug(name, "Bundle activation in progress. Opening storage transaction.")
params := storage.WriteParams
params.Context = storage.NewContext()
err := storage.Txn(ctx, p.manager.Store, params, func(txn storage.Transaction) error {
p.logDebug(name, "Opened storage transaction (%v).", txn.ID())
defer p.logDebug(name, "Closing storage transaction (%v).", txn.ID())
// Compile the bundle modules with a new compiler and set it on the
// transaction params for use by onCommit hooks.
compiler := ast.NewCompiler().WithPathConflictsCheck(storage.NonEmpty(ctx, p.manager.Store, txn))
var activateErr error
opts := &bundle.ActivateOpts{
Ctx: ctx,
Store: p.manager.Store,
Txn: txn,
Compiler: compiler,
Metrics: p.status[name].Metrics,
Bundles: map[string]*bundle.Bundle{name: b},
}
if p.config.IsMultiBundle() {
activateErr = bundle.Activate(opts)
} else {
activateErr = bundle.ActivateLegacy(opts)
}
plugins.SetCompilerOnContext(params.Context, compiler)
return activateErr
})
return err
}
func (p *Plugin) logError(bundleName string, fmt string, a ...interface{}) {
logrus.WithFields(p.logrusFields(bundleName)).Errorf(fmt, a...)
}
func (p *Plugin) logInfo(bundleName string, fmt string, a ...interface{}) {
logrus.WithFields(p.logrusFields(bundleName)).Infof(fmt, a...)
}
func (p *Plugin) logDebug(bundleName string, fmt string, a ...interface{}) {
logrus.WithFields(p.logrusFields(bundleName)).Debugf(fmt, a...)
}
func (p *Plugin) logrusFields(bundleName string) logrus.Fields {
f := logrus.Fields{
"plugin": Name,
"name": bundleName,
}
return f
}
// configDelta will return a map of new bundle sources, updated bundle sources, and a set of deleted bundle names
func (p *Plugin) configDelta(newConfig *Config) (map[string]*Source, map[string]*Source, map[string]struct{}) {
deletedBundles := map[string]struct{}{}
for name := range p.config.Bundles {
deletedBundles[name] = struct{}{}
}
newBundles := map[string]*Source{}
updatedBundles := map[string]*Source{}
for name, source := range newConfig.Bundles {
oldSource, found := p.config.Bundles[name]
if !found {
newBundles[name] = source
} else {
delete(deletedBundles, name)
if !reflect.DeepEqual(oldSource, source) {
updatedBundles[name] = source
}
}
}
return newBundles, updatedBundles, deletedBundles
}