HBase normally controls access at the namespace, table, column family or column qualifier level, and for most tenants that is enough. Some data needs a finer grain. A patient record where one column is visible to billing and another only to clinicians, or a row where individual versions carry different classifications, cannot be expressed with table or family grants. HBase handles this with two cell-level mechanisms, cell ACLs and visibility labels, and both are built on a single storage feature: cell tags.
This page explains what a tag is and where it lives, how the VisibilityController evaluates label expressions on every read, how labels and user authorisations are administered, and how labels interact with cell ACLs. It also covers where tags are silently dropped, which is the part that causes incidents. Configuration names and behaviours are quoted from the Apache HBase reference guide, checked on 2026-10-03. Grants at coarser levels are covered in HBase multi-tenancy.
What a cell tag is
A cell in HBase is a key (row, family, qualifier, timestamp, type) and a value. HFile version 3 added a third part, the tags. The reference guide describes a tag as metadata that is part of a cell but separate from the key, value and version. Every cell can have zero or more tags, and each tag has a type and a byte array. In the v3 on-disk layout, a cell with tags carries a 2-byte tags length followed by the tag bytes. The length field is 2 bytes, so a cell's tags are bounded, and heavily labelled cells do grow. Tags exist so that server-side features can attach per-cell metadata. Cell ACLs and visibility labels are the two that most deployments use.
Three properties of tags shape everything else. They are stored in the HFiles, so they survive flushes and compactions. Only server-side code (coprocessors) can read or set them, and the guide states that tags are stripped at the RPC layer before a read response is sent, so clients never see them. And they can be compressed: tag encoding is on by default per column family, it works together with the family's data block encoding, and hbase.regionserver.wal.tags.enablecompression compresses tags in the WAL when WAL compression is enabled. The guide notes that tag compression is not supported with WAL encryption.
Turning visibility labels on
Visibility labels need three things: HFile v3, authorisation turned on, and the VisibilityController coprocessor on both the RegionServers and the Master. Set hfile.format.version to 3, and check what your release defaults to. The guide's server-side configuration is:
<property>
<name>hbase.security.authorization</name>
<value>true</value>
</property>
<property>
<name>hbase.coprocessor.region.classes</name>
<value>org.apache.hadoop.hbase.security.access.AccessController,
org.apache.hadoop.hbase.security.visibility.VisibilityController</value>
</property>
<property>
<name>hbase.coprocessor.master.classes</name>
<value>org.apache.hadoop.hbase.security.access.AccessController,
org.apache.hadoop.hbase.security.visibility.VisibilityController</value>
</property>
<property>
<name>hbase.security.visibility.mutations.checkauths</name>
<value>true</value>
</property>The order is not cosmetic. When both coprocessors are active, the VisibilityController delegates access control on its own system tables to the AccessController, so the guide requires the AccessController to come first. The last property is one most clusters should add. By default a user may label a cell with any defined label, including ones they do not hold. That lets a user write data they cannot read back, and it lets them pick classifications for data they should not be classifying. With checkauths set to true, a mutation that uses a label the user does not hold fails. For how coprocessors load and fail, see HBase coprocessors.
Labels, authorisations and expressions
Labels must be defined before use, and users or groups are then granted authorisations, which are the set of labels they may read. Groups are written with an @ prefix. When checking a user, the server includes the labels of every group the user belongs to. Note that get_auths returns only the labels granted to the user directly, not the ones inherited from groups. The shell commands, as given in the guide, are:
hbase> add_labels [ 'pii', 'hr', 'finance', 'manager', 'contractor' ]
hbase> set_auths 'analyst', [ 'finance' ]
hbase> set_auths 'hr_lead', [ 'hr', 'manager', 'pii' ]
hbase> set_auths 'hr_temp', [ 'hr', 'contractor' ]
hbase> set_auths '@hrgroup', [ 'hr' ]
hbase> get_auths 'hr_lead'
hbase> clear_auths 'hr_temp', [ 'contractor' ]
hbase> scan 'staff', { AUTHORIZATIONS => [ 'hr' ] }A cell's label is an expression built from labels with &, |, ! and parentheses. The guide's examples include fulltime, !public and ( secret | topsecret ) & !probationary. HBase checks only that an expression is well formed, so a misspelled but defined label is accepted as written. Applications attach labels when they write, and the label belongs to that version of the cell:
Put put = new Put(Bytes.toBytes("emp-1042"));
put.addColumn(D, Bytes.toBytes("salary"), Bytes.toBytes("185000"));
put.setCellVisibility(new CellVisibility("(hr|finance)&!contractor"));
table.put(put);
Scan scan = new Scan();
scan.setAuthorizations(new Authorizations("hr")); // narrows, never widensIf a label contains operator characters or non-ASCII text, wrap it with CellVisibility.quote() inside the expression. The shell's set_visibility command relabels existing cells only, and the guide says it is for testing and verification, not production. Labels belong in application write paths, or they will be missing from every cell written after the backfill.
How reads are filtered, with a worked example
On each Get or Scan, the RegionServer builds the request's effective label set in the RPC context and then evaluates every candidate cell's expression against it. Cells that fail are filtered out on the server, so they cost the same disk I/O as cells that are returned but never cross the network. Building the set is pluggable. The hbase.regionserver.scan.visibility.label.generator.class property takes a list of ScanLabelGenerator classes, and each one's output feeds the next. The default chain is FeedUserAuthScanLabelGenerator followed by DefinedSetFilterScanLabelGenerator. If the request carries no authorisations, the user's stored set is used. If it does, labels the user does not hold are dropped. The guide's summary is that request authorisations further filter results rather than granting extra access. EnforcingScanLabelGenerator instead always applies the admin-defined set for the user, whatever the request asks for.
Here is a worked example. Table staff has four cells in row emp-1042: d:name has no label, d:ssn is labelled pii, d:salary is labelled (hr|finance)&!contractor, and d:review is labelled hr&manager. A small Python evaluator that follows the default chain's semantics prints:
analyst ['finance'] -> ['d:name', 'd:salary']
hr_lead ['hr', 'manager', 'pii'] -> ['d:name', 'd:ssn', 'd:salary', 'd:review']
hr_temp ['contractor', 'hr'] -> ['d:name']
hr_lead requesting ['hr','secret'] -> effective ['hr'] -> ['d:name', 'd:salary']
hr_temp requesting ['hr'] -> effective ['hr'] -> ['d:name', 'd:salary']The fourth line shows narrowing working as intended: secret is dropped because hr_lead does not hold it. The last line is the trap. A negated label is evaluated against the request's effective set, and the default chain lets the caller leave a label out of that set. So hr_temp can satisfy !contractor just by not asking for contractor. This matches the source: in DefaultVisibilityLabelServiceImpl, each visibility tag holds one AND-group of label ordinals, a negated label is stored as a negative ordinal and passes when its bit is absent from the set the generator chain produced, and a cell is visible if any tag passes. The same method returns true for every cell when a superuser reads. If negation must act as a deny rule, configure EnforcingScanLabelGenerator so that callers cannot narrow their set, or express the rule positively, for example hr_permanent|finance.
How labels are stored
The default VisibilityLabelService implementation does not store label strings in the cells. Each label gets an ordinal in the hbase:labels system table, which also holds user authorisations. The expression is converted into ordinal form and written as tags. This keeps cells compact, and it has a consequence the guide spells out: another cluster's labels table may map the same label names to different ordinals. Replicating ordinal-based tags to a peer would then silently relabel data. To replicate visibility expressions as strings, the guide configures a RegionServer observer, VisibilityController$VisibilityReplication, in hbase.coprocessor.regionserver.classes.
Cell ACLs and how they combine with labels
Cell ACLs use the same tag storage for a different model. A cell ACL grants actions such as READ or WRITE to named users or groups on that cell, set with Mutation.setACL(user, permission) or a map of grants. The guide is explicit about evaluation order: ACLs are evaluated from least granular to most granular, and evaluation stops at the first grant. So a cell ACL cannot take away access granted at the table or family level. It can only add access where none was granted above it. For that to work, hbase.security.access.early_out must be false. Its default has been true since 0.98.6, and with it true, a user denied at the family level is refused before any cell ACL is consulted.
| Question | Visibility labels | Cell ACLs |
|---|---|---|
| Model | Data carries a classification; users hold clearances | Data carries a list of who may do what |
| Can it restrict beyond a table grant? | Yes: every read is filtered by labels | No: coarser grants win |
| Changing a user's access | One set_auths call, applies to all cells | Rewrite the ACL on every affected cell |
| Best for | Classification levels, PII, regulated columns | Exceptional per-record grants |
The two can be combined. A read must pass the ACL check and the label check, so labels are the usual way to restrict, and cell ACLs are the occasional way to make an exception.
Failure modes
- Superusers see everything. The guide states that visibility labels are not applied for superusers. Service accounts that run as superuser (for example backup, ETL or the REST gateway) bypass every label. Keep them out of the superuser list.
- Tags stripped in transit. Replication carries tags only when
hbase.replication.rpc.codecisorg.apache.hadoop.hbase.codec.KeyValueCodecWithTagson both source and sink. That is the documented default in current releases, but older runbooks set it toKeyValueCodec, so verify it on both sides. Export and Import needhbase.client.rpc.codecset to the tags codec explicitly. Without it, the copy is unlabelled and readable by anyone with a table grant. See HBase replication. - Ordinal mismatch across clusters. Even when tags survive replication, a peer with a different labels table reads different labels. Use the string-replication observer, or keep the labels tables identical.
- Labels added after the fact. The shell's
set_visibilitymisses cells written later. Put labels in the write path, and test for unlabelled sensitive columns. - No way to inspect a cell's labels. The guide notes there is currently no way to see which labels a cell carries (HBASE-12470). Keep the labelling rules in code and audit them there.
- Deletes and labels. Deletes can carry visibility expressions, and the semantics are covered in HBASE-10885. Test that a delete actually removes labelled versions before you rely on it for erasure requests.
- Negation without enforcement. As the worked example shows,
!labelunder the default chain can be satisfied by narrowing the request.
What to do next
- Confirm that
hfile.format.versionis 3 on every RegionServer, and that the AccessController is listed before the VisibilityController in both region and master coprocessor lists. - Set
hbase.security.visibility.mutations.checkauthsto true, then define a small label vocabulary withadd_labels. Grant it to groups withset_auths '@group', not to individuals. - Label cells in the application's write path with
setCellVisibility. Write a test that scans as each role and asserts which columns come back, like the worked example. - Decide whether negation must act as a deny rule. If it must, configure the enforcing label generator or rewrite those expressions positively.
- Audit the superuser list and remove service accounts that do not need to bypass labels.
- On every replication peer and export job, confirm the tags codec is in effect, decide how labels replicate (identical labels tables or string replication), and verify by reading a labelled cell on the peer as a low-clearance user.
- Read the HFile format to see where tags sit in a block, and re-measure cell size and block cache hit rate after labelling.