mirror of
https://github.com/hashicorp/vault.git
synced 2026-06-04 22:32:09 -04:00
credential/app-id
This commit is contained in:
parent
2b12d51d70
commit
61b7b71dec
6 changed files with 260 additions and 3 deletions
97
builtin/credential/app-id/backend.go
Normal file
97
builtin/credential/app-id/backend.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
package appId
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
func Factory(map[string]string) (logical.Backend, error) {
|
||||
return Backend(), nil
|
||||
}
|
||||
|
||||
func Backend() *framework.Backend {
|
||||
var b backend
|
||||
b.MapAppId = &framework.PolicyMap{
|
||||
PathMap: &framework.PathMap{"app-id"},
|
||||
DefaultKey: "default",
|
||||
}
|
||||
b.MapUserId = &framework.PathMap{
|
||||
Name: "user-id",
|
||||
}
|
||||
|
||||
b.Backend = &framework.Backend{
|
||||
Help: backendHelp,
|
||||
|
||||
PathsSpecial: &logical.Paths{
|
||||
Unauthenticated: []string{
|
||||
"login",
|
||||
},
|
||||
},
|
||||
|
||||
Paths: framework.PathAppend([]*framework.Path{
|
||||
pathLogin(&b),
|
||||
},
|
||||
b.MapAppId.Paths(),
|
||||
b.MapUserId.Paths(),
|
||||
),
|
||||
}
|
||||
|
||||
return b.Backend
|
||||
}
|
||||
|
||||
type backend struct {
|
||||
*framework.Backend
|
||||
|
||||
MapAppId *framework.PolicyMap
|
||||
MapUserId *framework.PathMap
|
||||
}
|
||||
|
||||
const backendHelp = `
|
||||
The App ID credential provider is used to perform authentication from
|
||||
within applications or machine by pairing together two hard-to-guess
|
||||
unique pieces of information: a unique app ID, and a unique user ID.
|
||||
|
||||
The goal of this credential provider is to allow elastic users
|
||||
(dynamic machines, containers, etc.) to authenticate with Vault without
|
||||
having to store passwords outside of Vault. It is a single method of
|
||||
solving the chicken-and-egg problem of setting up Vault access on a machine.
|
||||
With this provider, nobody except the machine itself has access to both
|
||||
pieces of information necessary to authenticate. For example:
|
||||
configuration management will have the app IDs, but the machine itself
|
||||
will detect its user ID based on some unique machine property such as a
|
||||
MAC address (or a hash of it with some salt).
|
||||
|
||||
An example, real world process for using this provider:
|
||||
|
||||
1. Create unique app IDs (UUIDs work well) and map them to policies.
|
||||
(Path: map/app-id/<app-id>)
|
||||
|
||||
2. Store the app IDs within configuration management systems.
|
||||
|
||||
3. An out-of-band process run by security operators map unique user IDs
|
||||
to these app IDs. Example: when an instance is launched, a cloud-init
|
||||
system tells security operators a unique ID for this machine. This
|
||||
process can be scripted, but the key is that it is out-of-band and
|
||||
out of reach of configuration management.
|
||||
(Path: map/user-id/<user-id>)
|
||||
|
||||
4. A new server is provisioned. Configuration management configures the
|
||||
app ID, the server itself detects its user ID. With both of these
|
||||
pieces of information, Vault can be accessed according to the policy
|
||||
set by the app ID.
|
||||
|
||||
More details on this process follow:
|
||||
|
||||
The app ID is a unique ID that maps to a set of policies. This ID is
|
||||
generated by an operator and configured into the backend. The ID itself
|
||||
is usually a UUID, but any hard-to-guess unique value can be used.
|
||||
|
||||
After creating app IDs, an operator authorizes a fixed set of user IDs
|
||||
with each app ID. When the valid {app ID, user ID} set is tuple is given
|
||||
to the "login" path, then the user is authenticated with the configured
|
||||
app ID policies.
|
||||
|
||||
The user ID can be any value (just like the app ID), however it is
|
||||
generally a value unique to a machine, such as a MAC address or instance ID,
|
||||
or a value hashed from these unique values.
|
||||
`
|
||||
69
builtin/credential/app-id/backend_test.go
Normal file
69
builtin/credential/app-id/backend_test.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package appId
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/logical"
|
||||
logicaltest "github.com/hashicorp/vault/logical/testing"
|
||||
)
|
||||
|
||||
func TestBackend_basic(t *testing.T) {
|
||||
logicaltest.Test(t, logicaltest.TestCase{
|
||||
Backend: Backend(),
|
||||
Steps: []logicaltest.TestStep{
|
||||
testAccStepMapAppId(t),
|
||||
testAccStepMapUserId(t),
|
||||
testAccLogin(t),
|
||||
testAccLoginInvalid(t),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccStepMapAppId(t *testing.T) logicaltest.TestStep {
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.WriteOperation,
|
||||
Path: "map/app-id/foo",
|
||||
Data: map[string]interface{}{
|
||||
"value": "foo,bar",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func testAccStepMapUserId(t *testing.T) logicaltest.TestStep {
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.WriteOperation,
|
||||
Path: "map/user-id/42",
|
||||
Data: map[string]interface{}{
|
||||
"value": "foo",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func testAccLogin(t *testing.T) logicaltest.TestStep {
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.WriteOperation,
|
||||
Path: "login",
|
||||
Data: map[string]interface{}{
|
||||
"app_id": "foo",
|
||||
"user_id": "42",
|
||||
},
|
||||
Unauthenticated: true,
|
||||
|
||||
Check: logicaltest.TestCheckAuth([]string{"bar", "foo"}),
|
||||
}
|
||||
}
|
||||
|
||||
func testAccLoginInvalid(t *testing.T) logicaltest.TestStep {
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.WriteOperation,
|
||||
Path: "login",
|
||||
Data: map[string]interface{}{
|
||||
"app_id": "foo",
|
||||
"user_id": "48",
|
||||
},
|
||||
ErrorOk: true,
|
||||
Unauthenticated: true,
|
||||
|
||||
Check: logicaltest.TestCheckError(),
|
||||
}
|
||||
}
|
||||
64
builtin/credential/app-id/path_login.go
Normal file
64
builtin/credential/app-id/path_login.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
package appId
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
func pathLogin(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "login",
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"app_id": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "The unique app ID",
|
||||
},
|
||||
|
||||
"user_id": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "The unique user ID",
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.WriteOperation: b.pathLogin,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) pathLogin(
|
||||
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
appId := data.Get("app_id").(string)
|
||||
userId := data.Get("user_id").(string)
|
||||
|
||||
// Look up the apps that this user is allowed to access
|
||||
apps, err := b.MapUserId.Get(req.Storage, userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify that the app is in the list
|
||||
found := false
|
||||
for _, app := range strings.Split(apps, ",") {
|
||||
if strings.TrimSpace(app) == appId {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return logical.ErrorResponse("invalid user ID or app ID"), nil
|
||||
}
|
||||
|
||||
// Get the policies associated with the app
|
||||
policies, err := b.MapAppId.Policies(req.Storage, appId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Auth: &logical.Auth{
|
||||
Policies: policies,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -5,7 +5,8 @@ import (
|
|||
|
||||
auditFile "github.com/hashicorp/vault/builtin/audit/file"
|
||||
|
||||
"github.com/hashicorp/vault/builtin/credential/github"
|
||||
credAppId "github.com/hashicorp/vault/builtin/credential/app-id"
|
||||
credGitHub "github.com/hashicorp/vault/builtin/credential/github"
|
||||
|
||||
"github.com/hashicorp/vault/builtin/logical/aws"
|
||||
"github.com/hashicorp/vault/builtin/logical/consul"
|
||||
|
|
@ -115,7 +116,8 @@ func init() {
|
|||
"file": auditFile.Factory,
|
||||
},
|
||||
CredentialBackends: map[string]logical.Factory{
|
||||
"github": github.Factory,
|
||||
"app-id": credAppId.Factory,
|
||||
"github": credGitHub.Factory,
|
||||
},
|
||||
LogicalBackends: map[string]logical.Factory{
|
||||
"aws": aws.Factory,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,17 @@ import (
|
|||
"github.com/hashicorp/vault/logical"
|
||||
)
|
||||
|
||||
// PathAppend is a helper for appending lists of paths into a single
|
||||
// list.
|
||||
func PathAppend(paths ...[]*Path) []*Path {
|
||||
result := make([]*Path, 0, 10)
|
||||
for _, ps := range paths {
|
||||
result = append(result, ps...)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Path is a single path that the backend responds to.
|
||||
type Path struct {
|
||||
// Pattern is the pattern of the URL that matches this path.
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@ type TestStep struct {
|
|||
// step will be called
|
||||
Check TestCheckFunc
|
||||
|
||||
// ErrorOk, if true, will let erroneous responses through to the check
|
||||
ErrorOk bool
|
||||
|
||||
// Unauthenticated, if true, will make the request unauthenticated.
|
||||
Unauthenticated bool
|
||||
}
|
||||
|
|
@ -172,7 +175,7 @@ func Test(t TestT, c TestCase) {
|
|||
resp.Data,
|
||||
))
|
||||
}
|
||||
if err == nil && resp.IsError() {
|
||||
if err == nil && resp.IsError() && !s.ErrorOk {
|
||||
err = fmt.Errorf("Erroneous response:\n\n%#v", resp)
|
||||
}
|
||||
if err == nil && s.Check != nil {
|
||||
|
|
@ -246,6 +249,17 @@ func TestCheckAuth(policies []string) TestCheckFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// TestCheckError is a helper to check that a response is an error.
|
||||
func TestCheckError() TestCheckFunc {
|
||||
return func(resp *logical.Response) error {
|
||||
if !resp.IsError() {
|
||||
return fmt.Errorf("response should be error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// TestT is the interface used to handle the test lifecycle of a test.
|
||||
//
|
||||
// Users should just use a *testing.T object, which implements this.
|
||||
|
|
|
|||
Loading…
Reference in a new issue