|
|
@@ -0,0 +1,470 @@
|
|
|
+# v2 Auth and Security
|
|
|
+
|
|
|
+## etcd Resources
|
|
|
+There are three types of resources in etcd
|
|
|
+
|
|
|
+1. user resources: users and roles in the user store
|
|
|
+2. key-value resources: key-value pairs in the key-value store
|
|
|
+3. settings resources: security settings, auth settings, and dynamic etcd cluster settings (election/heartbeat)
|
|
|
+
|
|
|
+### User Resources
|
|
|
+
|
|
|
+#### Users
|
|
|
+A user is an identity to be authenticated. Each user can have multiple roles. The user has a capability on the resource if one of the roles has that capability.
|
|
|
+
|
|
|
+The special static `root` user has a ROOT role. (Caps for visual aid throughout)
|
|
|
+
|
|
|
+#### Role
|
|
|
+Each role has exact one associated Permission List. An permission list exists for each permission on key-value resources. A role with `manage` permission of a key-value resource can grant/revoke capability of that key-value to other roles.
|
|
|
+
|
|
|
+The special static ROOT role has a full permissions on all key-value resources, the permission to manage user resources and settings resources. Only the ROOT role has the permission to manage user resources and modify settings resources.
|
|
|
+
|
|
|
+#### Permissions
|
|
|
+
|
|
|
+There are two types of permissions, `read` and `write`. All management stems from the ROOT user.
|
|
|
+
|
|
|
+A Permission List is a list of allowed patterns for that particular permission (read or write). Only ALLOW prefixes (incidentally, this is what Amazon S3 does). DENY becomes more complicated and is TBD.
|
|
|
+
|
|
|
+### Key-Value Resources
|
|
|
+A key-value resource is a key-value pairs in the store. Given a list of matching patterns, permission for any given key in a request is granted if any of the patterns in the list match.
|
|
|
+
|
|
|
+The glob match rules are as follows:
|
|
|
+
|
|
|
+* `*` and `\` are special characters, representing "greedy match" and "escape" respectively.
|
|
|
+ * As a corrolary, `\*` and `\\` are the corresponding literal matches.
|
|
|
+* All other bytes match exactly their bytes, starting always from the *first byte*. (For regex fans, `re.match` in Python)
|
|
|
+* Examples:
|
|
|
+ * `/foo` matches only the single key/directory of `/foo`
|
|
|
+ * `/foo*` matches the prefix `/foo`, and all subdirectories/keys
|
|
|
+ * `/foo/*/bar` matches the keys bar in any (recursive) subdirectory of `/foo`.
|
|
|
+
|
|
|
+### Settings Resources
|
|
|
+
|
|
|
+Specific settings for the cluster as a whole. This can include adding and removing cluster members, enabling or disabling security, replacing certificates, and any other dynamic configuration by the administrator.
|
|
|
+
|
|
|
+## v2 Auth
|
|
|
+
|
|
|
+### Basic Auth
|
|
|
+We only support [Basic Auth](http://en.wikipedia.org/wiki/Basic_access_authentication) for the first version. Client needs to attach the basic auth to the HTTP Authorization Header.
|
|
|
+
|
|
|
+### Authorization field for operations
|
|
|
+Added to requests to /v2/keys, /v2/security
|
|
|
+Add code 403 Forbidden to the set of responses from the v2 API
|
|
|
+Authorization: Basic {encoded string}
|
|
|
+
|
|
|
+### Future Work
|
|
|
+Other types of auth can be considered for the future (eg, signed certs, public keys) but the `Authorization:` header allows for other such types
|
|
|
+
|
|
|
+### Things out of Scope for etcd Permissions
|
|
|
+
|
|
|
+* Pluggable AUTH backends like LDAP (other Authorization tokens generated by LDAP et al may be a possiblity)
|
|
|
+* Very fine-grained access controls (eg: users modifying keys outside work hours)
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+## API endpoints
|
|
|
+
|
|
|
+An Error JSON corresponds to:
|
|
|
+{
|
|
|
+ "name": "ErrErrorName",
|
|
|
+ "description" : "The longer helpful description of the error."
|
|
|
+}
|
|
|
+
|
|
|
+#### Users
|
|
|
+
|
|
|
+The User JSON object is formed as follows:
|
|
|
+
|
|
|
+```
|
|
|
+{
|
|
|
+ "user": "userName"
|
|
|
+ "password": "password"
|
|
|
+ "roles": [
|
|
|
+ "role1",
|
|
|
+ "role2"
|
|
|
+ ],
|
|
|
+ "grant": [],
|
|
|
+ "revoke": [],
|
|
|
+ "lastModified": "2006-01-02Z04:05:07"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Password is only passed when necessary. Last Modified is set by the server and ignored in all client posts.
|
|
|
+
|
|
|
+**Get a list of users**
|
|
|
+
|
|
|
+GET/HEAD /v2/security/user
|
|
|
+
|
|
|
+ Sent Headers:
|
|
|
+ Authorization: Basic <BasicAuthString>
|
|
|
+ Possible Status Codes:
|
|
|
+ 200 OK
|
|
|
+ 403 Forbidden
|
|
|
+ 200 Headers:
|
|
|
+ ETag: "<hash of list of users>"
|
|
|
+ Content-type: application/json
|
|
|
+ 200 Body:
|
|
|
+ {
|
|
|
+ "users": ["alice", "bob", "eve"]
|
|
|
+ }
|
|
|
+
|
|
|
+**Get User Details**
|
|
|
+
|
|
|
+GET/HEAD /v2/security/users/alice
|
|
|
+
|
|
|
+ Sent Headers:
|
|
|
+ Authorization: Basic <BasicAuthString>
|
|
|
+ Possible Status Codes:
|
|
|
+ 200 OK
|
|
|
+ 403 Forbidden
|
|
|
+ 404 Not Found
|
|
|
+ 200 Headers:
|
|
|
+ ETag: "users/alice:<lastModified>"
|
|
|
+ Content-type: application/json
|
|
|
+ 200 Body:
|
|
|
+ {
|
|
|
+ "user" : "alice"
|
|
|
+ "roles" : ["fleet", "etcd"]
|
|
|
+ "lastModified": "2015-02-05Z18:00:00"
|
|
|
+ }
|
|
|
+
|
|
|
+**Create A User**
|
|
|
+
|
|
|
+A user can be created with initial roles, if filled in. However, no roles are required; only the username and password fields
|
|
|
+
|
|
|
+PUT /v2/security/users/charlie
|
|
|
+
|
|
|
+ Sent Headers:
|
|
|
+ Authorization: Basic <BasicAuthString>
|
|
|
+ Put Body:
|
|
|
+ JSON struct, above, matching the appropriate name and with starting roles.
|
|
|
+ Possible Status Codes:
|
|
|
+ 200 OK
|
|
|
+ 403 Forbidden
|
|
|
+ 409 Conflict (if exists)
|
|
|
+ 200 Headers:
|
|
|
+ ETag: "users/charlie:<tzNow>"
|
|
|
+ 200 Body: (empty)
|
|
|
+
|
|
|
+**Remove A User**
|
|
|
+
|
|
|
+DELETE /v2/security/users/charlie
|
|
|
+
|
|
|
+ Sent Headers:
|
|
|
+ Authorization: Basic <BasicAuthString>
|
|
|
+ Possible Status Codes:
|
|
|
+ 200 OK
|
|
|
+ 403 Forbidden
|
|
|
+ 404 Not Found
|
|
|
+ 200 Headers:
|
|
|
+ 200 Body: (empty)
|
|
|
+
|
|
|
+**Grant a Role(s) to a User**
|
|
|
+
|
|
|
+PUT /v2/security/users/charlie/grant
|
|
|
+
|
|
|
+ Sent Headers:
|
|
|
+ Authorization: Basic <BasicAuthString>
|
|
|
+ Put Body:
|
|
|
+ { "grantRoles" : ["fleet", "etcd"], (extra JSON data for checking OK) }
|
|
|
+ Possible Status Codes:
|
|
|
+ 200 OK
|
|
|
+ 403 Forbidden
|
|
|
+ 404 Not Found
|
|
|
+ 409 Conflict
|
|
|
+ 200 Headers:
|
|
|
+ ETag: "users/charlie:<tzNow>"
|
|
|
+ 200 Body:
|
|
|
+ JSON user struct, updated. "roles" now contains the grants, and "grantRoles" is empty. If there is an error in the set of roles to be added, for example, a non-existent role, then 409 is returned, with an error JSON stating why.
|
|
|
+
|
|
|
+**Revoke a Role(s) from a User**
|
|
|
+
|
|
|
+PUT /v2/security/users/charlie/revoke
|
|
|
+
|
|
|
+ Sent Headers:
|
|
|
+ Authorization: Basic <BasicAuthString>
|
|
|
+ Put Body:
|
|
|
+ { "revokeRoles" : ["fleet"], (extra JSON data for checking OK) }
|
|
|
+ Possible Status Codes:
|
|
|
+ 200 OK
|
|
|
+ 403 Forbidden
|
|
|
+ 404 Not Found
|
|
|
+ 409 Conflict
|
|
|
+ 200 Headers:
|
|
|
+ ETag: "users/charlie:<tzNow>"
|
|
|
+ 200 Body:
|
|
|
+ JSON user struct, updated. "roles" now doesn't contain the roles, and "revokeRoles" is empty. If there is an error in the set of roles to be removed, for example, a non-existent role, then 409 is returned, with an error JSON stating why.
|
|
|
+
|
|
|
+**Change password**
|
|
|
+
|
|
|
+PUT /v2/security/users/charlie/password
|
|
|
+
|
|
|
+ Sent Headers:
|
|
|
+ Authorization: Basic <BasicAuthString>
|
|
|
+ Put Body:
|
|
|
+ {"user": "charlie", "password": "newCharliePassword"}
|
|
|
+ Possible Status Codes:
|
|
|
+ 200 OK
|
|
|
+ 403 Forbidden
|
|
|
+ 404 Not Found
|
|
|
+ 200 Headers:
|
|
|
+ ETag: "users/charlie:<tzNow>"
|
|
|
+ 200 Body:
|
|
|
+ JSON user struct, updated
|
|
|
+
|
|
|
+#### Roles
|
|
|
+
|
|
|
+A full role structure may look like this. A Permission List structure is used for the "permissions", "grant", and "revoke" keys.
|
|
|
+```
|
|
|
+{
|
|
|
+ "role" : "fleet",
|
|
|
+ "permissions" : {
|
|
|
+ "kv" {
|
|
|
+ "read" : [ "/fleet/" ],
|
|
|
+ "write": [ "/fleet/" ],
|
|
|
+ }
|
|
|
+ }
|
|
|
+ "grant" : {"kv": {...}},
|
|
|
+ "revoke": {"kv": {...}},
|
|
|
+ "members" : ["alice", "bob"],
|
|
|
+ "lastModified": "2015-02-05Z18:00:00"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Get a list of Roles**
|
|
|
+
|
|
|
+GET/HEAD /v2/security/roles
|
|
|
+
|
|
|
+ Sent Headers:
|
|
|
+ Authorization: Basic <BasicAuthString>
|
|
|
+ Possible Status Codes:
|
|
|
+ 200 OK
|
|
|
+ 403 Forbidden
|
|
|
+ 200 Headers:
|
|
|
+ ETag: "<hash of list of roles>"
|
|
|
+ Content-type: application/json
|
|
|
+ 200 Body:
|
|
|
+ {
|
|
|
+ "roles": ["fleet", "etcd", "quay"]
|
|
|
+ }
|
|
|
+
|
|
|
+**Get Role Details**
|
|
|
+
|
|
|
+GET/HEAD /v2/security/roles/fleet
|
|
|
+
|
|
|
+ Sent Headers:
|
|
|
+ Authorization: Basic <BasicAuthString>
|
|
|
+ Possible Status Codes:
|
|
|
+ 200 OK
|
|
|
+ 403 Forbidden
|
|
|
+ 404 Not Found
|
|
|
+ 200 Headers:
|
|
|
+ ETag: "roles/fleet:<lastModified>"
|
|
|
+ Content-type: application/json
|
|
|
+ 200 Body:
|
|
|
+ {
|
|
|
+ "role" : "fleet",
|
|
|
+ "read": {
|
|
|
+ "prefixesAllowed": ["/fleet/"],
|
|
|
+ },
|
|
|
+ "write": {
|
|
|
+ "prefixesAllowed": ["/fleet/"],
|
|
|
+ },
|
|
|
+ "members" : ["alice", "bob"] // Reverse map optional?
|
|
|
+ "lastModified": "2015-02-05Z18:00:00"
|
|
|
+ }
|
|
|
+
|
|
|
+**Create A Role**
|
|
|
+
|
|
|
+PUT /v2/security/roles/rocket
|
|
|
+
|
|
|
+ Sent Headers:
|
|
|
+ Authorization: Basic <BasicAuthString>
|
|
|
+ Put Body:
|
|
|
+ Initial desired JSON state, complete with prefixes and
|
|
|
+ Possible Status Codes:
|
|
|
+ 201 Created
|
|
|
+ 403 Forbidden
|
|
|
+ 404 Not Found
|
|
|
+ 409 Conflict (if exists)
|
|
|
+ 200 Headers:
|
|
|
+ ETag: "roles/rocket:<tzNow>"
|
|
|
+ 200 Body:
|
|
|
+ JSON state of the role
|
|
|
+
|
|
|
+**Remove A Role**
|
|
|
+
|
|
|
+DELETE /v2/security/roles/rocket
|
|
|
+
|
|
|
+ Sent Headers:
|
|
|
+ Authorization: Basic <BasicAuthString>
|
|
|
+ Possible Status Codes:
|
|
|
+ 200 OK
|
|
|
+ 403 Forbidden
|
|
|
+ 404 Not Found
|
|
|
+ 200 Headers:
|
|
|
+ 200 Body: (empty)
|
|
|
+
|
|
|
+**Update a Role’s Permission List for {read,write}ing**
|
|
|
+
|
|
|
+PUT /v2/security/roles/rocket/update
|
|
|
+
|
|
|
+ Sent Headers:
|
|
|
+ Authorization: Basic <BasicAuthString>
|
|
|
+ Put Body:
|
|
|
+ {
|
|
|
+ "role" : "rocket",
|
|
|
+ "grant": {
|
|
|
+ "kv": {
|
|
|
+ "read" : [ "/rocket/"]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "revoke": {
|
|
|
+ "kv": {
|
|
|
+ "read" : [ "/fleet/"]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Possible Status Codes:
|
|
|
+ 200 OK
|
|
|
+ 403 Forbidden
|
|
|
+ 404 Not Found
|
|
|
+ 200 Headers:
|
|
|
+ ETag: "roles/rocket:<tzNow>"
|
|
|
+ 200 Body:
|
|
|
+ JSON state of the role, with change containing empty lists and the deltas applied appropriately.
|
|
|
+
|
|
|
+
|
|
|
+#### TBD Management modification
|
|
|
+
|
|
|
+
|
|
|
+## Example Workflow
|
|
|
+
|
|
|
+Let's walk through an example to show two tenants (applications, in our case) using etcd permissions.
|
|
|
+
|
|
|
+### Enable security
|
|
|
+
|
|
|
+//TODO(barakmich): Maybe this is dynamic? I don't like the idea of rebooting when we don't have to.
|
|
|
+
|
|
|
+#### Default ROOT
|
|
|
+
|
|
|
+etcd always has a ROOT when started with security enabled. The default username is `root`, and the password is `root`.
|
|
|
+
|
|
|
+// TODO(barakmich): if the enabling is dynamic, perhaps that'd be a good time to set a password? Thus obviating the next section.
|
|
|
+
|
|
|
+
|
|
|
+### Change root's password
|
|
|
+
|
|
|
+```
|
|
|
+PUT /v2/security/users/root/password
|
|
|
+ Headers:
|
|
|
+ Authorization: Basic <root:root>
|
|
|
+ Put Body:
|
|
|
+ {"user" : "root", "password": "betterRootPW!"}
|
|
|
+```
|
|
|
+
|
|
|
+//TODO(barakmich): How do you recover the root password? *This* may require a flag and a restart. `--disable-permissions`
|
|
|
+
|
|
|
+### Create Roles for the Applications
|
|
|
+
|
|
|
+Create the rocket role fully specified:
|
|
|
+
|
|
|
+```
|
|
|
+PUT /v2/security/roles/rocket
|
|
|
+ Headers:
|
|
|
+ Authorization: Basic <root:betterRootPW!>
|
|
|
+ Body:
|
|
|
+ {
|
|
|
+ "role" : "rocket",
|
|
|
+ "permissions" : {
|
|
|
+ "kv": {
|
|
|
+ "read": [
|
|
|
+ "/rocket/"
|
|
|
+ ],
|
|
|
+ "write": [
|
|
|
+ "/rocket/"
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+But let's make fleet just a basic role for now:
|
|
|
+
|
|
|
+```
|
|
|
+PUT /v2/security/roles/fleet
|
|
|
+ Headers:
|
|
|
+ Authorization: Basic <root:betterRootPW!>
|
|
|
+ Body:
|
|
|
+ {
|
|
|
+ "role" : "fleet",
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+### Optional: Add some permissions to the roles
|
|
|
+
|
|
|
+Well, we finally figured out where we want fleet to live. Let's fix it.
|
|
|
+(Note that we avoided this in the rocket case. So this step is optional.)
|
|
|
+
|
|
|
+
|
|
|
+```
|
|
|
+PUT /v2/security/roles/fleet/update
|
|
|
+ Headers:
|
|
|
+ Authorization: Basic <root:betterRootPW!>
|
|
|
+ Put Body:
|
|
|
+ {
|
|
|
+ "role" : "fleet",
|
|
|
+ "grant" : {
|
|
|
+ "kv" : {
|
|
|
+ "read": [
|
|
|
+ "/fleet/"
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+### Create Users
|
|
|
+
|
|
|
+Same as before, let's use rocket all at once and fleet separately
|
|
|
+
|
|
|
+```
|
|
|
+PUT /v2/security/users/rocketuser
|
|
|
+ Headers:
|
|
|
+ Authorization: Basic <root:betterRootPW!>
|
|
|
+ Body:
|
|
|
+ {"user" : "rocketuser", "password" : "rocketpw", "roles" : ["rocket"]}
|
|
|
+```
|
|
|
+
|
|
|
+```
|
|
|
+PUT /v2/security/users/fleetuser
|
|
|
+ Headers:
|
|
|
+ Authorization: Basic <root:betterRootPW!>
|
|
|
+ Body:
|
|
|
+ {"user" : "fleetuser", "password" : "fleetpw"}
|
|
|
+```
|
|
|
+
|
|
|
+### Optional: Grant Roles to Users
|
|
|
+
|
|
|
+Likewise, let's explicitly grant fleetuser access.
|
|
|
+
|
|
|
+```
|
|
|
+PUT /v2/security/users/fleetuser/grant
|
|
|
+ Headers:
|
|
|
+ Authorization: Basic <root:betterRootPW!>
|
|
|
+ Body:
|
|
|
+ {"user": "fleetuser", "grant": ["fleet"]}
|
|
|
+```
|
|
|
+
|
|
|
+#### Start to use fleetuser and rocketuser
|
|
|
+
|
|
|
+
|
|
|
+For example:
|
|
|
+
|
|
|
+```
|
|
|
+PUT /v2/keys/rocket/RocketData
|
|
|
+ Headers:
|
|
|
+ Authorization: Basic <rocketuser:rocketpw>
|
|
|
+```
|
|
|
+
|
|
|
+Reads and writes outside the prefixes granted will fail with a 403 Forbidden.
|
|
|
+
|