Merge pull request #9 from mozilla/acls

Access Control List
This commit is contained in:
Julien Vehent 2014-04-14 20:18:47 -04:00
Родитель 8b9e360879 cd159a7220
Коммит 586342378e
13 изменённых файлов: 445 добавлений и 76 удалений

Просмотреть файл

@ -54,6 +54,8 @@ mig-action-verifier: gpgme
$(MKDIR) -p $(BINDIR)
$(GO) build $(GOOPTS) -o $(BINDIR)/mig-action-verifier $(GOLDFLAGS) mig/clients/verifier
go_get_deps_into_system:
make GOGETTER="go get -u" go_get_deps
go_get_deps:
$(GOGETTER) code.google.com/p/go.crypto/openpgp
@ -123,4 +125,4 @@ clean:
clean-all: clean
rm -rf pkg
.PHONY: clean clean-all gpgme
.PHONY: clean clean-all gpgme go_get_deps_into_system

Просмотреть файл

@ -58,6 +58,37 @@ var HEARTBEATFREQ time.Duration = 300 * time.Second
// timeout after which a module run is killed
var MODULETIMEOUT time.Duration = 300 * time.Second
// Control modules permissions by PGP keys
var AGENTACL = [...]string{
`{
"default": {
"minimumweight": 2,
"investigators": {
"Bob Kelso": {
"fingerprint": "E60892BB....",
"weight": 2
},
"Morpheus": {
"fingerprint": "AD595634....",
"weight": 3
}
}
}
}`,
`{
"pidkill": {
"minimumweight": 1,
"investigators": {
"MIG Scheduler": {
"fingerprint": "E60892BB...",
"weight": 1
}
}
}
}`,
}
// PGP public keys that are authorized to sign actions
// this is an array of strings, put each public key block
// into its own array entry, as shown below

Просмотреть файл

@ -112,20 +112,19 @@ The parameters are:
Upon generation, additional fields are appended to the action:
* PGPSignature: all of the parameters above are concatenated into a string and
* PGPSignatures: all of the parameters above are concatenated into a string and
signed with the investigator's private GPG key. The signature is part of the
action, and used by agents to verify that an action comes from a trusted
investigator.
* PGPSignatureDate: is the date of the PGP signature, used as a timestamp of
the action creation.
investigator. `PGPSignatures` is an array that contains one or more signature
from authorized investigators.
* ValidFrom and ExpireAt: two dates that constrains the validity of the action
to a time window.
to a UTC time window.
Actions files are submitted to the API or the Scheduler directly. Eventually,
the PGP signature will be verified by intermediary components, and in any case
by each agent before execution.
Actions files are submitted to the API or the Scheduler directly. The PGP
Signatures are always verified by the agents, and can optionally be verified by
other components along the way.
Additional attributes are added to the action by the scheduler. Those are
defined as "MetaAction" and are used to track the action status.
defined as `ExtendedAction` and are used to track the action status.
Commands
~~~~~~~~
@ -186,6 +185,114 @@ While the result is negative, the command itself has succeeded. Had a failure
happened on the agent, the scheduler would have been notified and the status
would be one of "failed", "timeout" or "cancelled".
Access Control Lists
--------------------
Not all keys can perform all actions. The scheduler, for example, sometimes need
to issue specific actions to agents (such as during the upgrade protocol) but
shouldn't be able to perform more dangerous actions. This is enforced by
an Access Control List, or ACL, stored on the agents. An ACL describes who can
access what function of which module. It can be used to require multiple
signatures on specific actions, and limit the list of investigators allowed to
perform an action.
An ACL is composed of permissions, which are JSON documents hardwired into
the agent configuration. In the future, MIG will dynamically ship permissions
to agents.
Below is an example of a permission for the `filechecker` module:
.. code:: json
{
"filechecker": {
"minimumweight": 2,
"investigators": {
"Bob Kelso": {
"fingerprint": "E60892BB9BD...",
"weight": 2
},
"John Smith": {
"fingerprint": "9F759A1A0A3...",
"weight": 1
}
}
}
}
`investigators` contains a list of users with their PGP fingerprints, and their
weight, an integer that represents their access level.
When an agent receives an action that calls the filechecker module, it will
first verify the signatures of the action, and then validates that the signers
are authorized to perform the action. This is done by summing up the weights of
the signatures, and verifying that they equal or exceed the minimum required
weight.
Thus, in the example above, investigator John Smith cannot issue a filechecker
action alone. His weight of 1 doesn't satisfy the minimum weight of 2 required
by the filechecker permission. Therefore, John will need to ask investigator Bob
Kelso to sign his action as well. The weight of both investigators are then
added, giving a total of 3, which satisfies the minimum weight of 2.
This method gives ample flexibility to require multiple signatures on modules,
and ensure that one investigator cannot perform sensitive actions on remote
endpoints without the permissions of others.
The default permission `default` can be used as a default for all modules. It
has the following syntax:
.. code:: json
{
"default": {
"minimumweight": 2,
"investigators": { ... }
]
}
}
The `default` permission is overridden by module specific permissions.
The ACL is currently applied to modules. In the future, ACL will have finer
control to authorize access to specific functions of modules. For example, an
investigator could be authorized to call the `regex` function of filechecker
module, but only in `/etc`. This functionality is not implemented yet.
Extracting PGP fingerprints from public keys
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
On Linux, the `gpg` command can easily display the fingerprint of a key using
`gpg --fingerprint <key id>`. For example:
.. code:: bash
$ gpg --fingerprint jvehent@mozilla.com
pub 2048R/3B763E8F 2013-04-30
Key fingerprint = E608 92BB 9BD8 9A69 F759 A1A0 A3D6 5217 3B76 3E8F
uid Julien Vehent (personal) <julien@linuxwall.info>
uid Julien Vehent (ulfr) <jvehent@mozilla.com>
sub 2048R/8026F39F 2013-04-30
You should always verify the trustworthiness of a key before using it:
.. code:: bash
$ gpg --list-sigs jvehent@mozilla.com
pub 2048R/3B763E8F 2013-04-30
uid Julien Vehent (personal) <julien@linuxwall.info>
sig 3 3B763E8F 2013-06-23 Julien Vehent (personal) <julien@linuxwall.info>
sig 3 28A860CE 2013-10-04 Curtis Koenig <ckoenig@mozilla.com>
.....
We want to extract the fingerprint, and obtain a 40 characters hexadecimal
string that can used in permissions.
.. code:: bash
$gpg --fingerprint --with-colons jvehent@mozilla.com |grep '^fpr'|cut -f 10 -d ':'
E60892BB9BD89A69F759A1A0A3D652173B763E8F
Agent registration process
--------------------------

73
src/mig/acl.go Normal file
Просмотреть файл

@ -0,0 +1,73 @@
/* Mozilla InvestiGator
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (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.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
The Initial Developer of the Original Code is
Mozilla Corporation
Portions created by the Initial Developer are Copyright (C) 2014
the Initial Developer. All Rights Reserved.
Contributor(s):
Julien Vehent jvehent@mozilla.com [:ulfr]
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
*/
package mig
import (
"fmt"
"strings"
)
type ACL []Permission
type Permission map[string]struct {
MinimumWeight int
Investigators map[string]struct{
Fingerprint string
Weight int
}
}
// verifyPermission controls that the PGP keys, identified by their fingerprints, that
// signed an operation are sufficient to allow this operation to run
func verifyPermission(operation operation, permName string, perm Permission, fingerprints []string) (err error) {
if perm[permName].MinimumWeight < 1 {
return fmt.Errorf("Invalid permission '%s'. Must require at least 1 signature, has %d",
permName, perm[permName].MinimumWeight)
}
signaturesWeight := 0
for _, fp := range fingerprints {
for _, signer := range perm[permName].Investigators {
if strings.ToUpper(fp) == strings.ToUpper(signer.Fingerprint) {
signaturesWeight += signer.Weight
}
}
}
if signaturesWeight < perm[permName].MinimumWeight {
return fmt.Errorf("Permission denied for operation '%s'. Insufficient signatures weight. Need %d, got %d",
operation.Module, perm[permName].MinimumWeight, signaturesWeight)
}
return
}

Просмотреть файл

@ -43,6 +43,7 @@ import (
"io"
"io/ioutil"
"math/rand"
"mig/pgp"
"mig/pgp/verify"
"strconv"
"time"
@ -74,22 +75,16 @@ type counters struct {
// an Action is the json object that is created by an investigator
// and provided to the MIG platform. It must be PGP signed.
type Action struct {
// meta
ID uint64 `json:"id"`
Name string `json:"name"`
Target string `json:"target"`
Description description `json:"description"`
Threat threat `json:"threat"`
// time window
ValidFrom time.Time `json:"validfrom"`
ExpireAfter time.Time `json:"expireafter"`
// operation to perform
Operations []operation `json:"operations"`
// action signature
PGPSignature string `json:"pgpsignature"`
PGPSignatureDate time.Time `json:"pgpsignaturedate"`
// action syntax version
SyntaxVersion int `json:"syntaxversion"`
ID uint64 `json:"id"`
Name string `json:"name"`
Target string `json:"target"`
Description description `json:"description"`
Threat threat `json:"threat"`
ValidFrom time.Time `json:"validfrom"`
ExpireAfter time.Time `json:"expireafter"`
Operations []operation `json:"operations"`
PGPSignatures []string `json:"pgpsignatures"`
SyntaxVersion int `json:"syntaxversion"`
}
// a description is a simple object that contains detail about the
@ -185,29 +180,29 @@ func (a Action) Validate() (err error) {
if a.Operations == nil {
return errors.New("Action.Operations is nil. Expecting string.")
}
if a.PGPSignature == "" {
return errors.New("Action.PGPSignature is empty. Expecting string.")
if len(a.PGPSignatures) < 1 {
return errors.New("Action.PGPSignatures is empty. Expecting array of strings.")
}
return
}
// Validate verifies that the Action received contained all the
// necessary fields, and returns an error when it doesn't.
func (a Action) VerifySignature(keyring io.Reader) (err error) {
// Verify the signature
// VerifySignatures verifies that the Action contains valid signatures from
// known investigators. It does not verify permissions.
func (a Action) VerifySignatures(keyring io.Reader) (err error) {
astr, err := a.String()
if err != nil {
return errors.New("Failed to stringify action")
}
valid, _, err := verify.Verify(astr, a.PGPSignature, keyring)
if err != nil {
return errors.New("Failed to verify PGP Signature")
for _, sig := range a.PGPSignatures {
valid, _, err := verify.Verify(astr, sig, keyring)
if err != nil {
return errors.New("Failed to verify PGP Signature")
}
if !valid {
return errors.New("Invalid PGP Signature")
}
}
if !valid {
return errors.New("Invalid PGP Signature")
}
return nil
return
}
// concatenates Action components into a string
@ -222,3 +217,51 @@ func (a Action) String() (str string, err error) {
return
}
// VerifyACL controls that an action has been issued by investigators
// that have the right permissions. This function looks at each operation
// listed in the action, and find the corresponding permission. If no
// permission is found, the default one `default` is used.
// The first permission that is found to apply to an operation, but
// doesn't allow the operation to run, will fail the verification globally
func (a Action) VerifyACL(acl ACL, keyring io.Reader) (err error) {
// first, verify all signatures and get a list of PGP
// fingerprints of the signers
var fingerprints []string
astr, err := a.String()
if err != nil {
return errors.New("Failed to stringify action")
}
for _, sig := range a.PGPSignatures {
fp, err := pgp.GetFingerprintFromSignature(astr, sig, keyring)
if err != nil {
return fmt.Errorf("Failed to retrieve fingerprint from signatures: %v", err)
}
fingerprints = append(fingerprints, fp)
}
// Then, for each operation contained in the action, look for
// a permission that apply to it, by comparing the operation name
// with permission name. If none is found, use the default permission.
for _, operation := range a.Operations {
for _, permission := range acl {
for permName, _ := range permission {
if permName == operation.Module {
return verifyPermission(operation, permName, permission, fingerprints)
}
}
}
// no specific permission found, apply the default permission
var defaultPerm Permission
for _, permission := range acl {
for permName, _ := range permission {
if permName == "default" {
defaultPerm = permission
break
}
}
}
return verifyPermission(operation, "default", defaultPerm, fingerprints)
}
return
}

86
src/mig/agent/acl.go Normal file
Просмотреть файл

@ -0,0 +1,86 @@
/* Mozilla InvestiGator Agent
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (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.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
The Initial Developer of the Original Code is
Mozilla Corporation
Portions created by the Initial Developer are Copyright (C) 2014
the Initial Developer. All Rights Reserved.
Contributor(s):
Julien Vehent jvehent@mozilla.com [:ulfr]
Guillaume Destuynder <kang@mozilla.com>
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
*/
package main
import (
"fmt"
"mig"
"mig/pgp"
"time"
)
// checkActionAuthorization verifies the PGP signatures of a given action
// against the Access Control List of the agent.
func checkActionAuthorization(a mig.Action, ctx Context) (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("checkActionAuthorization() -> %v", e)
}
ctx.Channels.Log <- mig.Log{ActionID: a.ID, Desc: "leaving checkActionAuthorization()"}.Debug()
}()
// get an io.Reader from the public pgp key
keyring, keycount, err := pgp.ArmoredPubKeysToKeyring(PUBLICPGPKEYS[0:])
if err != nil {
panic(err)
}
ctx.Channels.Log <- mig.Log{ActionID: a.ID, Desc: fmt.Sprintf("loaded %d keys", keycount)}.Debug()
// Check the action syntax and signature
err = a.Validate()
if err != nil {
desc := fmt.Sprintf("action validation failed: %v", err)
ctx.Channels.Log <- mig.Log{ActionID: a.ID, Desc: desc}.Err()
panic(desc)
}
// Validate() checks that the action hasn't expired, but we need to
// check the start time ourselves
if time.Now().Before(a.ValidFrom) {
ctx.Channels.Log <- mig.Log{ActionID: a.ID, Desc: "action is scheduled for later"}.Err()
panic("Action ValidFrom date is in the future")
}
// check ACLs, includes verifying signatures
err = a.VerifyACL(ctx.ACL, keyring)
if err != nil {
desc := fmt.Sprintf("action ACL verification failed: %v", err)
ctx.Channels.Log <- mig.Log{ActionID: a.ID, Desc: desc}.Err()
panic(desc)
}
ctx.Channels.Log <- mig.Log{ActionID: a.ID, Desc: "ACL verification succeeded."}.Debug()
return
}

Просмотреть файл

@ -47,7 +47,6 @@ import (
"mig"
"mig/modules/connected"
"mig/modules/filechecker"
"mig/pgp"
"os"
"os/exec"
"strings"
@ -245,33 +244,12 @@ func parseCommands(ctx Context, msg []byte) (err error) {
panic(err)
}
// get an io.Reader from the public pgp key
keyring, keycount, err := pgp.ArmoredPubKeysToKeyring(PUBLICPGPKEYS[0:])
// verify the PGP signature of the action, and verify that
// the signer is authorized to perform this action
err = checkActionAuthorization(cmd.Action, ctx)
if err != nil {
panic(err)
}
ctx.Channels.Log <- mig.Log{CommandID: cmd.ID, ActionID: cmd.Action.ID, Desc: fmt.Sprintf("loaded %d keys", keycount)}.Debug()
// Check the action syntax and signature
err = cmd.Action.Validate()
if err != nil {
desc := fmt.Sprintf("action validation failed: %v", err)
ctx.Channels.Log <- mig.Log{CommandID: cmd.ID, ActionID: cmd.Action.ID, Desc: desc}.Err()
panic(desc)
}
err = cmd.Action.VerifySignature(keyring)
if err != nil {
desc := fmt.Sprintf("action signature verification failed: %v", err)
ctx.Channels.Log <- mig.Log{CommandID: cmd.ID, ActionID: cmd.Action.ID, Desc: desc}.Err()
panic(desc)
}
// Expiration is verified by the Validate() call above, but we need
// to verify the ScheduledDate ourselves
if time.Now().Before(cmd.Action.ValidFrom) {
ctx.Channels.Log <- mig.Log{CommandID: cmd.ID, ActionID: cmd.Action.ID, Desc: "action is scheduled for later"}.Err()
panic("ScheduledDateInFuture")
}
// Each operation is ran separately by a module, a channel is created to receive the results from each module
// a goroutine is created to read from the result channel, and when all modules are done, build the response

Просмотреть файл

@ -39,6 +39,7 @@ import (
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"github.com/VividCortex/godaemon"
"github.com/streadway/amqp"
@ -55,7 +56,7 @@ import (
// logs and channels
// Context is intended as a single structure that can be passed around easily.
type Context struct {
OpID uint64 // ID of the current operation, used for tracking
ACL mig.ACL
Agent struct {
Hostname, OS, QueueLoc, UID, BinPath string
}
@ -77,6 +78,7 @@ type Context struct {
Chan *amqp.Channel
Bind mig.Binding
}
OpID uint64 // ID of the current operation, used for tracking
Sleeper time.Duration // timer used when the agent has to sleep for a while
Stats struct {
}
@ -146,6 +148,12 @@ func Init(foreground bool) (ctx Context, err error) {
panic(err)
}
// parse the ACLs
ctx, err = initACL(ctx)
if err != nil {
panic(err)
}
// connect to the message broker
ctx, err = initMQ(ctx)
if err != nil {
@ -277,6 +285,30 @@ func createIDFile(loc string) (id []byte, err error) {
return
}
// parse the permissions from the configuration into an ACL structure
func initACL(orig_ctx Context) (ctx Context, err error) {
ctx = orig_ctx
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("initACL() -> %v", e)
}
ctx.Channels.Log <- mig.Log{Desc: "leaving initACL()"}.Debug()
}()
for _, jsonPermission := range AGENTACL {
var parsedPermission mig.Permission
err = json.Unmarshal([]byte(jsonPermission), &parsedPermission)
if err != nil {
panic(err)
}
for permName, _ := range parsedPermission {
desc := fmt.Sprintf("Loading permission named '%s'", permName)
ctx.Channels.Log <- mig.Log{Desc: desc}.Debug()
}
ctx.ACL = append(ctx.ACL, parsedPermission)
}
return
}
func initMQ(orig_ctx Context) (ctx Context, err error) {
ctx = orig_ctx
defer func() {

Просмотреть файл

@ -336,7 +336,7 @@ func createAction(respWriter http.ResponseWriter, request *http.Request) {
if err != nil {
panic(err)
}
err = action.VerifySignature(keyring)
err = action.VerifySignatures(keyring)
if err != nil {
panic(err)
}

Просмотреть файл

@ -115,16 +115,15 @@ func main() {
if err != nil {
panic(err)
}
a.PGPSignature, err = sign.Sign(str, *key)
pgpsig, err := sign.Sign(str, *key)
if err != nil {
panic(err)
}
a.PGPSignatureDate = time.Now().UTC()
a.PGPSignatures = append(a.PGPSignatures, pgpsig)
var jsonAction []byte
if *pretty {
jsonAction, err = json.MarshalIndent(a, "", "\t")
fmt.Printf("%s\n", jsonAction)
} else {
jsonAction, err = json.Marshal(a)
}
@ -179,7 +178,7 @@ func main() {
}
// syntax checking
err = a.VerifySignature(keyring)
err = a.VerifySignatures(keyring)
if err != nil {
panic(err)
}

Просмотреть файл

@ -119,7 +119,7 @@ func main() {
defer keyring.Close()
// syntax checking
err = a.VerifySignature(keyring)
err = a.VerifySignatures(keyring)
if err != nil {
panic(err)
}

Просмотреть файл

@ -38,8 +38,10 @@ package pgp
import (
"bytes"
"code.google.com/p/go.crypto/openpgp"
"encoding/hex"
"fmt"
"io"
"mig/pgp/verify"
)
// TransformArmoredPubKeysToKeyring takes a list of public PGP key in armored form and transforms
@ -72,3 +74,19 @@ func ArmoredPubKeysToKeyring(pubkeys []string) (keyring io.Reader, keycount int,
keyring = bytes.NewReader(buf.Bytes())
return
}
// TransformArmoredPubKeysToKeyring takes a list of public PGP key in armored form and transforms
// it into a keyring that can be used in other openpgp's functions
func GetFingerprintFromSignature(data string, signature string, keyring io.Reader) (fingerprint string, err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("GetFingerprintFromSignature() -> %v", e)
}
}()
_, entity, err := verify.Verify(data, signature, keyring)
if err != nil {
panic(err)
}
fingerprint = hex.EncodeToString(entity.PrimaryKey.Fingerprint[:])
return
}

Просмотреть файл

@ -50,7 +50,7 @@ func Verify(data string, signature string, keyring io.Reader) (valid bool, entit
valid = false
// re-armor signature and transform into io.Reader
sigReader := strings.NewReader(reArmor(signature))
sigReader := strings.NewReader(reArmorSignature(signature))
// decode armor
sigBlock, err := armor.Decode(sigReader)
@ -82,9 +82,9 @@ func Verify(data string, signature string, keyring io.Reader) (valid bool, entit
return
}
// reArmor takes a single line armor and turns it back into an PGP-style
// reArmorSignature takes a single line armor and turns it back into an PGP-style
// multi-line armored string (thank you, camlistore folks)
func reArmor(line string) string {
func reArmorSignature(line string) string {
lastEq := strings.LastIndex(line, "=")
if lastEq == -1 {
return ""