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

(#42) spike around OPA #43

Merged
merged 1 commit into from
Dec 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
109 changes: 101 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ This is under active development, see the Issues list for current outstanding it

* Authentication
* [Okta identity cloud](https://okta.com/)
* Static configured users
* Static configured users with support for basic agent+action ACLs as well as Open Policy Agent policies
* Capable of running centrally separate from signers
* Authorization
* JWT token claims based allow list for access to agents and actions
* JWT token claims based Open Policy Agent rego files
* Auditing
* Log file based auditing
* Messages published to NATS Stream
Expand Down Expand Up @@ -99,6 +100,11 @@ The signer uses a JSON file for configuration and lets you compose the system as
"acls": [
"puppet.*"
]
},
{
"username": "admin",
"password": ".....",
"opa_policy_file": "/etc/choria/signer/common.rego"
}
]
}
Expand Down Expand Up @@ -188,7 +194,7 @@ At this point you can use `mco` cli as always, requests will be sent to the sign

Authentication is the act of validating a person is who he claims to be, this is done using a username, token, 2FA or other similar means.

This supports a number of authentication schemes each would issue a JWT token to the user that will then be authorised and signed by a Signer.
This supports a number of authentication schemes each would issue a JWT token to the user that will then be authorized and signed by a Signer.

While authentication is provided by this tool, it's optional you might choose to create JWT tokens using another method of your choosing, the login feature will only be enabled if any authenticator is configured.

Expand Down Expand Up @@ -216,6 +222,11 @@ $2y$05$c4b/0WZ5WJ3nhSZPN9m8keCUPlCYtNOTkqU4fDNEPCUy1C9Pfqn2e
"acls": [
"puppet.*",
]
},
{
"username": "admin",
"password": ".....",
"opa_policy_file": "/etc/choria/signer/common.rego"
}
]
}
Expand Down Expand Up @@ -253,8 +264,17 @@ Here we configure `acls` based on Okta groups - all users can `rpcutil ping`, th

## Authorization

Authorization is how you declare what an authenticated user can do, in this system the JWT tokens can contain either a simple agent/action list or a full features [Open Policy Agent](https://www.openpolicyagent.org/) based policy.


Authorization is how you declare what an authenticated user can do, in this system the JWT tokens have an `agents` claim with the following:

The authorizers will then inspect this and determine if the user should be allowed to make a request he is requesting we sign.

### Action List

This authorizer reads the `agents` claim in the JWT token and allow/deny the user. Examples below.

```json
{
"agents": [
Expand All @@ -264,12 +284,6 @@ Authorization is how you declare what an authenticated user can do, in this syst
}
```

The authorizers will then inspect this and determine if the user should be allowed to make a request he is requesting we sign.

### Action List

This the only support authorizer at present, it reads the `agents` claim in the JWT token and allow/deny the user. Examples below.

* `*` - all actions are allowed
* `puppet.status` - one specific action is allowed
* `puppet.*` - all actions in the puppet agent are allowed
Expand All @@ -284,6 +298,85 @@ Multiple agent entries can be listed in the claim and any that match will allow

It has no specific configuration.

### Open Policy Agent

The Open Policy Agent based policies allow for very flexible policy to be embedded into the JWT tokens, it allow for policies we have never supported in the past:

* Ensuring filters are used to avoid huge blast radius requests by accident
* Ensuring specific fact, class or identity filters are used
* Ensuring a specific collective is used
* Contents of the JWT claim
* Checks based on the site the aaasvc is deployed in
* Checks on every input being sent to the action

Here's a complex policy:

```rego
# must be in this package
package choria.aaa.policy

# it only checks `allow`, its good to default false
default allow = false

# user can deploy only frontend of myco into production but only in malta
allow {
input.action == "deploy"
input.agent == "myco"
input.data.component == "frontend"
requires_fact_filter("country=mt")
input.collective == "production"
}

# can ask status anywhere in any environment
allow {
input.action == "status"
input.agent == "myco"
}

# user can do anything myco related in development
allow {
input.agent == "myco"
input.collective == "development"
}
```

Here we use the `requires_fact_filter()` to ensure a specific fact filter is used, we have these custom functions:

* `requires_filter()` - ensures that at least one of identity, class, compound of fact filters is not empty
* `requires_fact_filter("country=mt")` - ensures the specific fact filter is present in the request
* `requires_class_filter("apache")` - ensures the specific class filter is present in the request
* `requires_identity_filter("some.node")` - ensures the specific identity filter is present in the request

And you'll have these input items at your disposal:

* `agent` - the agent being invoked
* `action` - the action being invoked
* `data` - the contents of the request - all the inputs being sent to the action
* `sender` - the sender host
* `collective` - the targeted sub collective
* `ttl` - the ttl of the request
* `time` - the time the request was made
* `site` - the site hosting the aaasvcs (from its config)
* `claims` - all the JWT claims

You can store this in a file and specify the user in the userlist plugin like this:

```json
{
"username": "admin",
"password": ".....",
"opa_policy_file": "/etc/choria/signer/admin.rego"
}
```

To activate this authorizer configure it like this:

```json
{
"authorizer": "opa"
}
```

## Signing

The Signing service signs requests on behalf of CLI users, ths signing service has certificates that the Choria network trusts (known as privileged certificates). The signer validates the JWT token is valid before signing.
Expand Down
7 changes: 7 additions & 0 deletions authenticators/userlist/testdata/test.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package choria.aaa.policy

default allow = false

allow {
input.agent == "myco"
}
49 changes: 49 additions & 0 deletions authenticators/userlist/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package userlist

import (
"io/ioutil"
"sync"
)

// User is a choria user
type User struct {
// Username in plain text
Username string `json:"username"`

// Password is a bcrypted password
Password string `json:"password"`

// ACLs are for the action list authorizer
ACLs []string `json:"acls"`

// OPAPolicy is a string holding a Open Policy Agent rego policy
OPAPolicy string `json:"opa_policy"`

// OPAPolicyFile is the path to a rego file to embed as the policy for this user
OPAPolicyFile string `json:"opa_policy_file"`

sync.Mutex
}

// OpenPolicy retrieves the OPA Policy either from `OPAPolicy` or by reading the file in `OPAPolicyFile`
func (u *User) OpenPolicy() (policy string, err error) {
u.Lock()
defer u.Unlock()

if u.OPAPolicy != "" {
return u.OPAPolicy, nil
}

if u.OPAPolicyFile == "" {
return "", nil
}

out, err := ioutil.ReadFile(u.OPAPolicyFile)
if err != nil {
return "", err
}

u.OPAPolicy = string(out)

return u.OPAPolicy, nil
}
26 changes: 16 additions & 10 deletions authenticators/userlist/userlist.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Packager userlist provide a static configuration based authentication system
// Package userlist provide a static configuration based authentication system
//
// Each user has a set of ACLs that are applied to the generated token, ACL strings
// have to comply with the signer you choose, refer to signer documentation for
Expand All @@ -20,13 +20,6 @@ import (
"golang.org/x/crypto/bcrypt"
)

// User is a choria user
type User struct {
Username string `json:"username"`
Password string `json:"password"`
ACLs []string `json:"acls"`
}

// AuthenticatorConfig configures the user/pass authenticator
type AuthenticatorConfig struct {
Users []*User
Expand Down Expand Up @@ -90,15 +83,28 @@ func (a *Authenticator) processLogin(req *models.LoginRequest) (resp *models.Log
return
}

token := jwt.NewWithClaims(jwt.GetSigningMethod("RS512"), jwt.MapClaims{
claims := map[string]interface{}{
"exp": time.Now().UTC().Add(a.validity).Unix(),
"nbf": time.Now().UTC().Add(-1 * time.Minute).Unix(),
"iat": time.Now().UTC().Unix(),
"iss": "Choria Userlist Authenticator",
"callerid": fmt.Sprintf("up=%s", req.Username),
"sub": fmt.Sprintf("up=%s", req.Username),
"agents": user.ACLs,
})
}

policy, err := user.OpenPolicy()
if err != nil {
a.log.Warnf("Reading OPA policy for user %s failed: %s", req.Username, err)
resp.Error = "Login failed"
return
}

if policy != "" {
claims["opa_policy"] = policy
}

token := jwt.NewWithClaims(jwt.GetSigningMethod("RS512"), jwt.MapClaims(claims))

signKey, err := a.signKey()
if err != nil {
Expand Down
14 changes: 14 additions & 0 deletions authenticators/userlist/userlist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ var _ = Describe("Authenticators/Userlist", func() {
Username: "bob",
Password: "$2a$06$chB5d2pCKEzM6xlDoPvofuKW52piJ5f8fGvxHPTDaeSJOSNY76yai",
ACLs: []string{"*"},
OPAPolicyFile: "testdata/test.rego",
},
},
}
Expand Down Expand Up @@ -100,10 +101,23 @@ var _ = Describe("Authenticators/Userlist", func() {
Expect(ok).To(BeTrue())
Expect(agents).To(HaveLen(1))
Expect(agents[0].(string)).To(Equal("*"))

policy, ok := claims["opa_policy"].(string)
Expect(ok).To(BeTrue())
Expect(policy).To(Equal(readFixture("testdata/test.rego")))
})
})
})

func readFixture(f string) string {
c, err := ioutil.ReadFile(f)
if err != nil {
panic(err)
}

return string(c)
}

func signKey() (*rsa.PublicKey, error) {
certBytes, err := ioutil.ReadFile("testdata/cert.pem")
if err != nil {
Expand Down
Loading