Chapter 5. Security API

Accumulo controls access to data in its tables in a number of ways: authentication, permissions, and authorizations.

These can be thought of as applying at two levels: authentication and permissions at the higher application and table level, and authorizations—which are used along with column visibilities—at the lower, key-value–pair level. Authentication relates to Accumulo users and how a user confirms its identity to Accumulo. Permissions control what operations Accumulo users are allowed to perform. Authorizations control which key-value pairs Accumulo users are allowed to see.

Accumulo provides the ability to create accounts, grant permissions, and grant authorizations. All of these mechanisms are pluggable, with their defaults being to store and retrieve user information in ZooKeeper. Custom security mechanisms are discussed in “Custom Authentication, Permissions, and Authorization”.

High-level security-related operations such as creating users and granting permissions and authorizations are carried out via the SecurityOperations object, obtained from a Connector object:

SecurityOperations secOps = conn.securityOperations();

Security operations can be logged to an audit log if Accumulo is configured to do so (see “Auditing Security Operations”).

Low-level key-value–pair security occurs naturally whenever ColumnVisibility and Authorizations objects are used when reading and writing data.

For any given set of security mechanisms, there are essentially two ways to manage access control: create an account for every user using Accumulo’s security mechanisms, or create accounts for each application and delegate authentication, permissions, and authorization for each user to the application. In the latter case, it is the application’s job to authenticate individual users, look up their permissions and authorization tokens, and pass their authorizations faithfully onto Accumulo when data is read or written. This is discussed further in “Using an Application Account for Multiple Users”.

Authentication

Accumulo user accounts are used to limit the permissions that an application or an individual user can carry out, and to limit the set of authorization tokens that can be used in lookups. Some basic instance information such as instance ID, locations of master processes, and location of the root tablet can be retrieved from the Instance object itself. This information is available to anyone.

To retrieve any additional information from Accumulo, an application must authenticate as a particular user.

Before authenticating, the user must exist and have an AuthenticationToken associated with it. The default type of AuthenticationToken is the PasswordToken that simply wraps a password for the user. AuthenticationToken can be extended to support other authentication methods such as Lightweight Directory Access Protocol (LDAP).

To create a new user, use the createLocalUser() method:

String principal = "myApplications";
PasswordToken password = new PasswordToken("appSecret");

secOps.createLocalUser(principal, password);

// in version 1.4 and earlier
Authorizations initialAuthorizations = new Authorizations();
secOps.createUser(principal, "password".getBytes(), initialAuthorizations);
Tip

After initialization Accumulo only has one user, the root user, with a password set at initialization time. The root user can be used to create other user accounts and grant privileges. See “Initialization” for more details on setting up the root account.

To authenticate as a user, provide a username, or principal, and an AuthenticationToken when obtaining a Connector object from an Instance:

String principal = "myApp";
AuthenticationToken token = new PasswordToken("appSecret");

Connector connector = instance.getConnector(principal, token);

In addition, the following methods will simply return whether a particular principal and AuthenticationToken are valid:

String principal = "myApp";
AuthenticationToken token = new PasswordToken("appSecret");

boolean authenticated = authenticateUser(principal, token);

// deprecated since 1.5
boolean authenticated = authenticateUser(String user, byte[] password);

To set a user’s password, use changeLocalUserPassword():

String principal = "myUser";
PasswordToken token = new PasswordToken("newPassword");
secOps.changeLocalUserPassword(principal, token);

// in 1.5 and older
secOps.changeUserPassword(principal, "newPassword".getBytes());

To obtain a list of users, use the listLocalUsers() method:

Set<String> users = secOps.listLocalUsers();

// in 1.4 and earlier
Set<String> users = secOps.listUsers();

To remove a user from the system, use the dropLocalUser() method:

secOps.dropLocalUser("user");

// in 1.4 and earlier
secOps.dropUser("user");

Permissions

Once a user is authenticated to Accumulo, the types of operations allowed are governed by the permissions assigned to the Accumulo user.

There are system permissions, which are global; namespace permissions assigned per namespace; and table permissions assigned per table. Some permission names are repeated in more than one scope. For example, there are DROP_TABLE permissions for the system, namespace, and table scopes. These three permissions allow a user to delete any table, delete a table within a namespace, and delete a specific table, respectively. The CREATE_TABLE permissions only appear in system and namespace, because it does not make sense to create a specific table that already exists.

The user that creates a table is assigned all table permissions for that table. Users must be granted table permissions manually for tables they did not create, with the exception that all users can read the root and metadata tables.

If a user tries to perform an operation that is not allowed by the user’s current permissions, an exception will be thrown.

System Permissions

System permissions allow users to perform the following actions:

GRANT

Grant and revoke permissions for users

CREATE_TABLE

Create and import tables

DROP_TABLE

Remove tables

ALTER_TABLE

Configure table properties, perform actions on tables (compact, merge, online/offline, rename, and split), and grant or revoke permissions on tables

CREATE_USER

Create users and check permissions for users

DROP_USER

Remove users and check permissions for users

ALTER_USER

Change user authentication token or authorizations, and check permissions for users

CREATE_NAMESPACE

Create namespaces

DROP_NAMESPACE

Remove namespaces

ALTER_NAMESPACE

Rename namespaces, configure namespace properties, and grant or revoke permissions on namespaces

SYSTEM

Perform administrative actions including granting and revoking the SYSTEM permission, checking authentication for users, checking permissions and authorizations for users, and performing table actions (merge, online/offline, split, and delete a range of rows)

To grant a system permission to a user, use the grantSystemPermission() method. For example:

String principal = "user";

secOps.grantSystemPermission(principal, SystemPermission.CREATE_TABLE);

To see whether a user has a particular system permission, use the hasSystemPermission() method:

boolean hasPermission = secOps.hasSystemPermission(principal,
    SystemPermission.CREATE_TABLE);

Permissions can be revoked for users via the revokeSystemPermission() method:

secOps.revokeSystemPermission(principal, SystemPermission.CREATE_TABLE);

An example of granting a user system-wide permissions is as follows:

// get a connector as the root user
Connector adminConn = instance.getConnector("root", rootPasswordToken);

// get a security operations object as the root user
SecurityOperations adminSecOps = adminConn.securityOperations();

// admin creates a new user
String principal = "testUser";
PasswordToken token = new PasswordToken("password");
adminSecOps.createLocalUser(principal, token);

// get a connector as our new user
Connector userConn = instance.getConnector(principal, token);

// ...

// user tries to create user table in default namespace
String userTable = "userTable";

try {
  userConn.tableOperations().create(userTable);
} catch (AccumuloSecurityException ex) {
  System.out.println("user unauthorized to create table in default namespace");
}

adminSecOps.grantSystemPermission(principal, SystemPermission.CREATE_TABLE);

userConn.tableOperations().create(userTable);
System.out.println("table creation in default namespace succeeded");

Namespace Permissions

Permissions can apply to namespaces as well. Namespace permissions are granted for a particular namespace. Some of the permissions apply to actions performed on the namespace itself, and some apply to all tables within the namespace:

ALTER_NAMESPACE

Grant and revoke table permissions for tables in the namespace, and alter the namespace

ALTER_TABLE

Alter tables in the namespace

BULK_IMPORT

Import into tables in the namespace

CREATE_TABLE

Create tables in the namespace

DROP_NAMESPACE

Delete the namespace

DROP_TABLE

Delete a table from the namespace

GRANT

Grant and revoke namespace permissions on a namespace, and alter the namespace

READ

Read tables in the namespace

WRITE

Write to tables in the namespace

To check whether a user has a permission for a given namespace, use the hasNamespacePermission() method:

String namespace = "myNamespace";
boolean hasNSWritePermission = secOps.hasNamespacePermission(principal,
    namespace, NamespacePermission.WRITE);

To grant a user a permission for a namespace, use the grantNamespacePermission() method. The permission will apply to all tables within the namespace:

secOps.grantNamespacePermission(principal, namespace, NamespacePermission.WRITE);

To revoke a permission from a user for a namespace, use the revokeNamespacePermission() method:

secOps.revokeNamespacePermission(principal, namespace,
    NamespacePermission.WRITE);

A short example:

String adminNS = "adminNamespace";
adminConn.namespaceOperations().create(adminNS);

try {
  userConn.tableOperations().create(adminNS + ".userTable");
} catch (AccumuloSecurityException ex) {
  System.out.println("user unauthorized to create table in adminNamespace");
}

// allow user to create tables in the root NS
adminSecOps.grantNamespacePermission(principal, adminNS,
    NamespacePermission.CREATE_TABLE);

userConn.tableOperations().create(adminNS + ".userTable");
System.out.println("table creation in adminNamespace succeeded");

Table Permissions

Table permissions are granted per table, allowing users to perform actions on specific tables:

READ

Scan and export the table

WRITE

Write to the table, including deleting data, and perform some administrative actions for the table including flushing and compaction

BULK_IMPORT

Import files to the table

ALTER_TABLE

Configure table properties, perform actions on the table (compact, flush, merge, online/offline, rename, and split), and grant or revoke permissions on the table

GRANT

Grant and revoke permissions for the table

DROP_TABLE

Remove the table

Some actions require a combination of permissions. These include:

Write conditional mutations

TablePermission.READ and TablePermission.WRITE

Clone a table

SystemPermission.CREATE_TABLE or NamespacePermission.CREATE_TABLE and TablePermission.READ on table being cloned

To grant a table permission to a user for a table, use the grantTablePermission() method:

String table = "myTable";

secOps.grantTablePermission(principal, table, TablePermission.WRITE);

To check whether a user has a specific permission on a table, use the hasTablePermission() method:

boolean canWrite = secOps.hasTablePermission(principal, table,
    TablePermission.WRITE);

To revoke a permission from a user for a given table, use the revokeTablePermission() method:

secOps.revokeTablePermission(principal, table, TablePermission.WRITE);

These actions can be carried out in the shell as well, as detailed in “Application Permissions”.

An example of using table permissions is as follows:

String adminTable = "adminTable";
adminConn.tableOperations().create(adminTable);

// user tries to write data
BatchWriterConfig config = new BatchWriterConfig();
BatchWriter writer = userConn.createBatchWriter(adminTable, config);
Mutation m = new Mutation("testRow");
m.put("", "testColumn", "testValue");

try {
  writer.addMutation(m);
  writer.close();
} catch (Exception ex) {
  System.out.println("user unable to write to admin table");
}

// admin grants permission for user to write data
adminSecOps.grantTablePermission(principal, adminTable, TablePermission.WRITE);

writer = userConn.createBatchWriter(adminTable, config);

writer.addMutation(m);
writer.close();
System.out.println("user can write to admin table");

See the full listing of PermissionsExample.java for more detail.

Authorizations

Once a user is authenticated to Accumulo and is given permission to read a table, the user’s authorizations govern which key-value pairs can be retrieved.

Authorizations are applied to Scanners and BatchScanners. The set of authorizations that can be used for a particular scan is limited by the set of authorizations associated with the user account. If a user attempts to scan with an authorization that is not already associated with the user account specified in the Connector, an exception will be thrown.

To see the list of authorizations associated with a user, use the getUserAuthorizations() method:

Authorizations auths = secOps.getUserAuthorizations(principal);

To associate authorizations with a user, use the changeUserAuthorizations() method. This will replace any existing authorizations associated with the user:

Authorizations auths = new Authorizations("a","b","c");
secOps.changeUserAuthorizations(principal, auths);
Caution

Be sure to include existing authorizations when using the changeUserAuthorizations() method to add new authorization tokens, or else previous tokens will be lost. Existing tokens can be retrieved with the getUserAuthorizations() method.

A user’s authorizations encapsulate a set of strings, sometimes referred to as authorization tokens. These strings have no intrinsic meaning for Accumulo, but an application can assign its own meaning to them, such as groups or roles for its users.

For a user to be able to read data from Accumulo, a table name and a set of authorization tokens must be provided to either the createScanner() or the createBatchScanner() method of the Connector:

Scanner scan = conn.createScanner("myTable", new Authorizations("a", "b", "c"));

Because the Connector is associated with a specific user, the authorizations provided when a Scanner or BatchScanner is obtained must be a subset of the authorizations assigned to that user. If they are not, an exception will be thrown.

Tip

Passing in a set of authorizations at scan time allows a user to act in different roles at different times. It also allows applications to manage their own users apart from Accumulo. You can choose to have one Accumulo user account for an entire application and to let the application set the authorizations for each scan based on the current user of the application.

Although this requires the application to take on the responsibility for managing accurate authorizations for their users, it also prevents users from having to interact with Accumulo or the underlying Hadoop system directly, allowing more strict control over access to your data.

See “An Example of Using Authorizations” for an example of using less than all of the possible authorizations associated with a user.

Column Visibilities

To determine which key-value pairs can be seen given a particular set of authorizations, each key has a column visibility portion. A column visibility consists of a Boolean expression containing tokens, & (and), | (or), and parentheses—such as (a&bc)|def. Evaluation of the Boolean expression requires each string to be interpreted as true or false. For a given set of authorizations, a string is interpreted as true if it is contained in the set of authorizations.

The visibility (a&bc)|def would evaluate to true for authorization sets containing the string def or containing both of the strings a and bc. When the visibility evaluates to true for a given key and a set of authorizations, that key-value pair is returned to the user. If not, the key-value pair is not included in the set of key-value pairs returned to the client. Thus it isn’t possible to find out that a particular key-value pair exists, or to see the full key or value, without satisfying the column visibility.

Tokens used in column visibilities can consist of letters, numbers, underscore, dash, colon, and as of Accumulo version 1.5, can contain periods and forward slashes. As of version 1.5, tokens can also contain arbitrary characters if the token is surrounded by quotes, as in "a?b"&c. The corresponding authorizations do not need to be quoted, so the minimum set of authorizations needed to view this example visibility would contain a?b and c.

Limiting Authorizations Written

By default, if users have write permission for a table, they can write keys that they do not have authorization to retrieve. You can change this behavior by configuring a constraint on the table. With the VisibilityConstraint, users cannot write data they are not allowed to read:

connector.tableOperations().addConstraint(tableName,
    VisibilityConstraint.class.getName());

This can also be accomplished through the Accumulo shell. When the table is created, add an -evc flag to the createtable command:

user@accumulo> createtable -evc tableName

To add the constraint to an existing table, use the constraint command instead:

user@accumulo> constraint -t tableName 
               -a org.apache.accumulo.core.security.VisibilityConstraint

An Example of Using Authorizations

We’ll illustrate bringing the concepts of users, permissions, authorizations, and column visibilities together in a quick example. Let’s say we are writing an application to keep track of the information associated with a safe in a bank. The safe contains a set of safety deposit boxes that are used by bank employees and customers to store objects securely.

There is an outer door to the safe that is protected by a combination known only to a few bank employees. Other bank employees can see privileged information about the safe, but not information about the contents of customers’ boxes.

Customers can write down and read information about safety deposit boxes they rent but cannot see any information privileged to bank employees or other customers.

First we’ll create a table as an administrator and write initial information about a particular safe:

// get a connector as the root user
Connector adminConn = instance.getConnector("root", rootPasswordToken);

// get a security operations object as the root user
SecurityOperations secOps = adminConn.securityOperations();

// admin creates a new table and writes some data
// protected with Column Visibilities
System.out.println("
--- creating table ---");
String safeTable = "safeTable";
adminConn.tableOperations().create(safeTable);

The admin writes the initial information about a safe, including the name, location, and combination:

// admin writes initial data
System.out.println("
--- writing initial data ---");
BatchWriterConfig config = new BatchWriterConfig();
BatchWriter writer = adminConn.createBatchWriter(safeTable, config);
Mutation m = new Mutation("safe001");

// write information about this particular safe
m.put("info", "safeName", new ColumnVisibility("public"),
    "Super Safe Number 17");
m.put("info", "safeLocation", new ColumnVisibility("bankEmployee"),
    "3rd floor of bank 2");
m.put("info", "safeOuterDoorCombo",
    new ColumnVisibility("bankEmployee&safeWorker"), "123-456-789");

// store some information about bank owned contents stored in the safe
m.put("contents", "box001",
    new ColumnVisibility("bankEmployee"), "bank charter");

// commit mutations
writer.addMutation(m);
writer.close();

Next the administrator will need to create user accounts for customers. In this example we’re using one account per individual user.

Each customer gets a unique user ID and authorization token, in addition to the public token:

// admin creates a new customer user
String customer = "customer003";
PasswordToken customerToken = new PasswordToken("customerPassword");
secOps.createLocalUser(customer, customerToken);

// set authorizations for user and grant permission to read and write
// to the safe table
Authorizations customerAuths = new Authorizations("public", "customer003");
secOps.changeUserAuthorizations(customer, customerAuths);
secOps.grantTablePermission(customer, safeTable, TablePermission.READ);

Now the newly created customer can log in and is prevented from seeing any information privileged to bank employees:

// get a connector as our customer user
Connector customerConn = instance.getConnector(customer, customerToken);

// user attempts to get a scanner with
// authorizations not associated with the user
System.out.println("
--- customer scanning table for bank employee " +
    "privileged information ---");
Scanner scanner;
try {
  scanner = customerConn.createScanner(safeTable,
      new Authorizations("public", "bankEmployee"));

  for(Map.Entry<Key, Value> e : scanner) {
    System.out.println(e);
  }
} catch (Exception ex) {
  System.out.println("problem scanning table: " + ex.getMessage());
}

This results in the output:

--- customer scanning table for bank employee privileged information ---
problem scanning table:
    org.apache.accumulo.core.client.AccumuloSecurityException:
Error BAD_AUTHORIZATIONS for user customer003 on table safeTable(ID:1) -
The user does not have the specified authorizations assigned

If the customer scans the table with all the authorizations associated with his account, she will see the information marked as public:

// user reads data with authorizations associated with the user
System.out.println("
--- customer scanning table for allowed information ---");
scanner = customerConn.createScanner(safeTable, customerAuths);
for(Map.Entry<Key, Value> e : scanner) {
  System.out.println(e);
}

The output is:

--- customer scanning table for allowed information ---
safe001 info:safeName [public] 1409424734681 false Super Safe Number 17

The customer must be granted write access to the table before writing any information. The customer can then write information protected with a column visibility consisting of just his own unique authorization token. Subsequent scans will return this information along with the public safe information:

// admin grants write permission to user
secOps.grantTablePermission(customer, safeTable, TablePermission.WRITE);

// user writes information only she can see to the table
// describing the contents of a rented safety deposit box
System.out.println("
--- customer writing own information ---");
BatchWriter userWriter = customerConn.createBatchWriter(safeTable, config);
Mutation userM = new Mutation("safe001");
userM.put("contents", "box004", new ColumnVisibility("customer003"),
    "jewelry, extra cash");
userWriter.addMutation(userM);
userWriter.flush();

// scan to see the bank info and our own info
System.out.println("
--- customer scanning table for allowed information ---");
scanner = customerConn.createScanner(safeTable, customerAuths);
for(Map.Entry<Key, Value> e : scanner) {
  System.out.println(e);
}

The output is:

--- customer writing own information ---
--- customer scanning table for allowed information ---
safe001 contents:box004 [customer003] 1409424734828 false jewelry, extra cash
safe001 info:safeName [public] 1409424734681 false Super Safe Number 17

Now the administrator will create an account for a bank employee. The bank employee will have access to bank privileged information, public information, but not any information associated with any customer:

// admin creates a new bank employee user
String bankEmployee = "bankEmployee005";
PasswordToken bankEmployeeToken = new PasswordToken("bankEmployeePassword");
secOps.createLocalUser(bankEmployee, bankEmployeeToken);

// admin sets authorizations for bank employee
// and grants read permission for the table
Authorizations bankEmployeeAuths = new Authorizations("bankEmployee", "public");
secOps.changeUserAuthorizations(bankEmployee, bankEmployeeAuths);
secOps.grantTablePermission(bankEmployee, safeTable, TablePermission.READ);

// connect as bank employee
Connector bankConn = instance.getConnector(bankEmployee, bankEmployeeToken);

If the bank employee attempts to scan for customer information, an exception will be thrown:

// attempt to scan customer information
System.out.println("
--- bank employee scanning table for customer " +
    "information ---");
Scanner bankScanner;
try {
  bankScanner = bankConn.createScanner(safeTable,
      new Authorizations("customer003"));

  for(Map.Entry<Key, Value> e : bankScanner) {
    System.out.println(e);
  }
} catch (Exception ex) {
  System.out.println("problem scanning table: " + ex.getMessage());
}

Resulting in the output:

--- bank employee scanning table for customer information ---
problem scanning table:
    org.apache.accumulo.core.client.AccumuloSecurityException:
Error BAD_AUTHORIZATIONS for user bankEmployee005 on table safeTable(ID:1) -
The user does not have the specified authorizations assigned

Now we’ll have the bank employee scan for all information she is allowed to see. Because this employee has a set of authorizations different from the customer’s, this view of the table will be different than the view the customer gets when doing the same scan:

// bank employee scans all information they are allowed to see
System.out.println("
--- bank employee scanning table for allowed " +
    "information ---");
bankScanner = bankConn.createScanner(safeTable, bankEmployeeAuths);

for(Map.Entry<Key, Value> e : bankScanner) {
  System.out.println(e);
}

Here is the output:

--- bank employee scanning table for allowed information ---
safe001 contents:box001 [bankEmployee] 1409424734681 false bank charter
safe001 info:safeLocation [bankEmployee] 1409424734681 false 3rd floor of bank 2
safe001 info:safeName [public] 1409424734681 false Super Safe Number 17

It is also possible to perform a scan using less than all the authorizations we possess. In this case, the bank employee will generate a view of the table that is viewable by users with only the public token:

// bank employee scans using a subset of authorizations
// to check which information is viewable to the public
System.out.println("
--- bank employee scanning table for only public " +
    "information ---");
bankScanner = bankConn.createScanner(safeTable, new Authorizations("public"));

for(Map.Entry<Key, Value> e : bankScanner) {
  System.out.println(e);
}

Here is the view generated:

--- bank employee scanning table for only public information ---
safe001 info:safeName [public] 1409424734681 false Super Safe Number 17

Finally, we may want to protect the table against attempts to write information to a key-value pair that is protected with a visibility that the writing user cannot satisfy. This prevents confusing situations in which a user writes data but then cannot read it out:

// admin protects table against users writing new data they cannot read
adminConn.tableOperations().addConstraint(safeTable,
    "org.apache.accumulo.core.security.VisibilityConstraint");

// customer attempts to write information protected with a bank authorization
// which would erase the combination for the outer door of the safe
System.out.println("
--- customer attempting to overwrite bank " +
    "information ---");
try {
  userM = new Mutation("safe001");
  userM.put("info", "safeOuterDoorCombo",
      new ColumnVisibility("bankEmployee&safeWorker"), "------");
  userWriter.addMutation(userM);
  userWriter.flush();
} catch (Exception e) {
  System.out.println("problem attempting to write data: " + e.getMessage());
}

This results in the error:

--- customer attempting to overwrite bank information ---
problem attempting to write data: # constraint violations :
1  security codes: {}  # server errors 0 # exceptions 0
Note

Even if users are able to write a new key-value pair using the same row ID and column as an existing key, they can only cause the newly written key-value pair to obscure the old key-value pair, via Accumulo’s VersioningIterator, which by default returns only the newest version of a key-value pair. It would be possible in this case to configure a scan to read more than one version for a key, which would allow authorized users to see the old key-value pair. But it would not be possible for the new key-value pair to cause the value of the old key-value pair to become visible. According to the column visibility of the new key-value pair, it would simply be obscured.

This inability to expose information this way, by writing new key-value pairs, makes it possible to build highly secure applications more easily, because applications do not have to explicitly prevent this issue.

Using a Default Visibility

You may have noticed in our example application that all key-value pairs were protected with at least one token in a column visibility. We used the public token to denote information that everyone was able to read, and distributed the public authorization token to all users.

It is possible to have a table in which some key-value pairs have column visibilities and others do not. The default behavior for unlabeled data is to allow any user to read it. This can be changed by applying a default visibility to a table.

When the default visibility is specified, unlabeled key-value pairs will be treated as if they are labeled with the default column visibility.

To specify the default visibility for a table, set the table.security.scan.visibility.default property to the desired column visibility expression.

For example:

ops.setProperty("table.security.scan.visibility.default", "public");

When key-value pairs with empty labels are scanned, if they are returned as part of the scan they are displayed as having a blank column visibility, even when a default visibility is set.

Here is an example of the way a view of a table will change after the default visibility is set. First we’ll create a table that has a key-value pair with a blank column visibility and see it show up in all scans:

// get a connector as the root user
Connector conn = instance.getConnector("root", rootPasswordToken);

// create an example table
String exampleTable = "example";
conn.tableOperations().create(exampleTable);

// write some data with col vis and others without
BatchWriterConfig config = new BatchWriterConfig();
BatchWriter writer = conn.createBatchWriter(exampleTable, config);
Mutation m = new Mutation("one");

m.put("", "col1",  "value in unlabeled entry");
m.put("", "col2",  new ColumnVisibility("public"), "value in public entry");
m.put("", "col3",  new ColumnVisibility("private"), "value in private entry");

writer.addMutation(m);
writer.close();

// add auths to root account
conn.securityOperations().changeUserAuthorizations("root",
    new Authorizations("public", "private"));
// scan with no auths
System.out.println("
no auths:");
Scanner scan = conn.createScanner(exampleTable, Authorizations.EMPTY);
for(Map.Entry<Key, Value> e : scan) {
  System.out.println(e);
}

// scan with public auth
System.out.println("
public auth:");
scan = conn.createScanner(exampleTable, new Authorizations("public"));
for(Map.Entry<Key, Value> e : scan) {
  System.out.println(e);
}

// scan with public and private auth
System.out.println("
public and private auths:");
scan = conn.createScanner(exampleTable,
    new Authorizations("public", "private"));
for(Map.Entry<Key, Value> e : scan) {
  System.out.println(e);
}

The output of this is as follows:

no auths:
one :col1 [] 1409429068159 false value in unlabeled entry
public auth:
one :col1 [] 1409429068159 false value in unlabeled entry
one :col2 [public] 1409429068159 false value in public entry
public and private auths:
one :col1 [] 1409429068159 false value in unlabeled entry
one :col2 [public] 1409429068159 false value in public entry
one :col3 [private] 1409429068159 false value in private entry

Now we’ll add a default visibility:

// turn on default visibility
System.out.println("
turning on default visibility");
conn.tableOperations().setProperty(exampleTable,
    "table.security.scan.visibility.default", "x");

// scan with no auths
System.out.println("
no auths:");
scan = conn.createScanner(exampleTable, Authorizations.EMPTY);
for(Map.Entry<Key, Value> e : scan) {
  System.out.println(e);
}

// scan with public auth
System.out.println("
public auth:");
scan = conn.createScanner(exampleTable, new Authorizations("public"));
for(Map.Entry<Key, Value> e : scan) {
  System.out.println(e);
}

// scan with public and private auth
System.out.println("
public and private auths:");
scan = conn.createScanner(exampleTable,
    new Authorizations("public", "private"));
for(Map.Entry<Key, Value> e : scan) {
  System.out.println(e);
}

The output for this now appears as:

turning on default visibility
no auths:
public auth:
one :col2 [public] 1409429068159 false value in public entry
public & private auths:
one :col2 [public] 1409429068159 false value in public entry
one :col3 [private] 1409429068159 false value in private entry

Making Authorizations Work

For authorizations to be effective in protecting access to data in Accumulo, applications and users must:

  1. Properly apply column visibilities to data at ingest time.

  2. Apply the right authorizations at scan time.

Often Accumulo applications will rely on using specially vetted libraries for creating the proper column visibilities. If not, then ingest clients can be individually reviewed and trusted.

For retrieving authorizations, a separate service can be employed to manage the association of individual users to their sets of authorizations. This service is trusted by the application, and the application itself is trusted to faithfully pass along the authorizations retrieved from such a service to Accumulo.

A typical deployment can be like that shown in Figure 5-1.

A Typical Accumulo Deployment
Figure 5-1. A typical Accumulo deployment

Auditing Security Operations

Accumulo can be configured to log security operations. Auditing is configured in the auditLog.xml file in the Accumulo conf/ directory. The logging is done via the Java log4j package and by default is configured to log via a DailyRollingFileAppender to a local file named <hostname>.audit in the Accumulo log directory. The following section of the auditLog.xml file configures the logging level:

<logger name="Audit"  additivity="false">
  <appender-ref ref="Audit" />
  <level value="OFF"/>
</logger>

By default, logging is turned off. To enable logging security operations that fail due to lack of permissions, set the level to WARN:

<level value="WARN"/>

To log all security operations, set the level to INFO. This will include successful security operations logged as operation: permitted as well as unsuccessful operations logged as operation: denied. Scanning with an authorization the user does not possess is an example of an operation that would be logged as denied at the INFO level:

<level value="INFO"/>

Custom Authentication, Permissions, and Authorization

The authentication, permissions, and authorization tasks for Accumulo accounts are handled in ZooKeeper by default. These tasks are handled by three classes: ZKAuthenticator for authenticating users, ZKAuthorizor for associating users with authorizations, and ZKPermHandler for determining what actions a user can carry out on the system and tables.

As of Accumulo version 1.5 developers can provide custom classes that override these default security mechanisms. This allows organizations that manage users and their authorizations in a centralized system to integrate those existing systems with Accumulo. In these cases, the custom classes must be available to server processes and specified in the accumulo-site.xml configuration file.

Not all of the three mechanisms must be overridden at the same time. For example, you can choose to rely on ZooKeeper for permissions handling and authentication, while using a custom authorization mechanism.

The default configuration of these mechanisms’ properties is shown in Table 5-1.

Table 5-1. Accumulo authentication and authorization properties
Setting name Default Purpose

instance.security.authorizor

org.apache.accumulo.server.security.handler.ZKAuthorizor

Associate users with authorization tokens

instance.security.authenticator

org.apache.accumulo.server.security.handler.ZKAuthenticator

Authenticate users

instance.security.permissionHandler

org.apache.accumulo.server.security.handler.ZKPermHandler

Manage users’ system and table-level permissions

These settings cannot be changed in ZooKeeper on a running cluster. They must be changed in the accumulo-site.xml file and require a restart of Accumulo for the changes to take effect.

Creating a custom mechanism is done by implementing the Authenticator, Authorizor, or PermissionHandler interface. These interfaces define the methods required by Accumulo to determine the access restrictions for user requests.

Custom Authentication Example

Here we’ll implement a trivial authenticator that uses only one hardcoded username and password. This would be impractical for any real-world deployment because no changes to the initial settings are possible, but it will help us illustrate the process of configuring and deploying a custom authentication scheme.

For this incredibly simple example we’ll implement only a few methods, shown in the following code. The rest of the methods of the interface that must be implemented we will leave empty:

public class HardCodedAuthenticator implements Authenticator {

  @Override
  public boolean authenticateUser(String principal, AuthenticationToken token)
      throws AccumuloSecurityException {
    return principal.equals("onlyUser") &&
      new String(((PasswordToken)token).getPassword()).equals("onlyPassword");
  }

  @Override
  public Set<String> listUsers() throws AccumuloSecurityException {
    HashSet<String> users = new HashSet<String>();
    users.add("onlyUser");
    return users;
  }

  @Override
  public boolean userExists(String user) throws AccumuloSecurityException {
    return user.equals("onlyUser");
  }

  @Override
  public Set<Class<? extends AuthenticationToken>> getSupportedTokenTypes() {
    return (Set)Sets.newHashSet(PasswordToken.class);
  }

  @Override
  public boolean validTokenClass(String tokenClass) {
    return tokenClass.equals(PasswordToken.class.toString());
  }
  ...
}

We can build and deploy our example code JAR as described in “Deploying JARs”.

Next we need to stop Accumulo if it’s running and configure it to use our Authenticator. In practice, custom security mechanisms like this should most likely be configured before Accumulo is initialized, so that the proper authorizations and permissions can be coordinated with the creation of the initial root user.

We’ll only change the authenticator in accumulo-site.xml in this example:

<property>
  <name>instance.security.authenticator</name>
  <value>com.accumulobook.tableapi.HardCodedAuthenticator</value>
</property>

Once configuration is done, we can start up Accumulo and attempt to authenticate using the username onlyUser and password onlyPassword:

[centos@centos]$ bin/accumulo shell -u onlyUser
Password: ************
Shell - Apache Accumulo Interactive Shell
-
- version: 1.6.0
- instance name: test
-
- type 'help' for a list of available commands
-
onlyUser@test> tables
accumulo.metadata
accumulo.root
trace

Any attempt to use our previous root account will fail:

[centos@centos]$ bin/accumulo shell -u root
[shell.Shell] ERROR: org.apache.accumulo.core.client.AccumuloSecurityException:
    Error BAD_CREDENTIALS for user root - Username or Password is Invalid
[centos@centos]$

Our hardcoded user account will not have permissions to manipulate anything, or any authorizations, so it is not very practical. In practice, these custom mechanisms will need to store information in a centralized location accessible to all processes, as the default ZooKeeper implementation does. For example, you could use a simple relational database or an LDAP service.

Custom authorizers and permissions handlers can be created and deployed similarly.

Other Security Considerations

In addition to column visibilities being properly applied at ingest time and the proper authorizations retrieved and used in scans, there are some other things to consider when building a secure application on the Accumulo API:

  • Direct access to tablet servers must be limited to trusted applications—because the application is trusted to present the proper authorizations at scan time. A rogue client may be configured to pass in authorizations the user does not have.

  • Access to the underlying HDFS instance must not be allowed. Otherwise an HDFS client could open and read all the key-value pairs stored in Accumulo’s files without presenting the proper authorizations.

  • Similarly, access should be disallowed to the underlying Linux filesystem on machines on which tablet server and HDFS DataNode processes run.

  • Access to ZooKeeper should be restricted because Accumulo uses it to store configuration information about the cluster, including the list of Accumulo accounts and passwords.

Using an Application Account for Multiple Users

Many Accumulo applications do not create accounts through Accumulo for each individual user. This is because some clients choose to do their own authentication and authorization of individual users via a centralized service within an organization. Clients are therefore trusted to present user credentials properly.

When applications are deployed this way, client applications must still authenticate themselves to Accumulo before performing any reads or writes. Administrators and application designers can restrict the privileges that a client has to particular tables, as well as the maximal set of authorizations the client is allowed to pass for any of the users it is serving. This way, even though users can have more authorizations granted to them than an application requires, a client application’s account can be restricted to those authorizations deemed necessary to carry out the actions of that particular application.

Network

The network that Accumulo uses to communicate between nodes and to HDFS and ZooKeeper should be protected against unauthorized access. Most Accumulo deployments do not use Secure Socket Layer (SSL) between nodes, but rather use SSL between user browsers and trusted web applications.

See “Network Security” for more information on securing the network for an Accumulo deployment.

Disk Encryption

Disks can be encrypted to prevent unauthorized reading of the data should a physical hard drive be stolen. But if those with physical access to the cluster are not trusted, then the operating system and memory of the machines participating in the Accumulo cluster would have to be similarly protected. When running Accumulo in multitenant environments, such as a cloud infrastructure-as-a-service provider like Amazon’s EC2 or Rackspace, consideration should be given to the security precautions implemented by the service provider.

For both situations—running in a cluster without trusting those with physical access or running in the cloud—it may be feasible to employ application-level encryption of values and to devise keys that are not sensitive. This is problematic when it comes to building a secondary index, which can rely on the ordering of values to perform scans.

If scans across ranges of terms in an index can be foregone, then using a strategy involving hashes of values as keys can still provide fast simple lookups. Ranges of terms could no longer be scanned because secure hashes of index terms would, by virtue of the design of hash functions, no longer have any meaningful sort order. In this case, adjacent keys would have no relationship to each other.

Accumulo also supports encryption of data at rest via modules that implement the org.apache.accumulo.core.security.crypto.CryptoModule interface, which consists of the following methods:

CryptoModuleParameters getEncryptingOutputStream(CryptoModuleParameters params)

CryptoModuleParameters getDecryptingInputStream(CryptoModuleParameters params)

CryptoModuleParameters generateNewRandomSessionKey(CryptoModuleParameters params)

CryptoModuleParameters initializeCipher(CryptoModuleParameters params)

The DefaultCryptoModule class is an example that can be used to encrypt data stored in HDFS. This implementation stores the master key along with files in HDFS, which may not meet security requirements. For details on configuring Accumulo to use this or other modules, see “Encryption of Data at Rest”.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.202.30