SDDL

A security descriptor is a binary structure, but almost nobody writes one by hand. SDDL — the Security Descriptor Definition Language — is its text form: a single line that names an owner, a group, and the two access control lists.

You meet it wherever a descriptor has to be written down rather than computed:

  • sd set and sd show --sddl
  • mount's --synth-sddl, which supplies the template descriptor for a filesystem that has no native descriptor storage
  • registry seeds, which carry descriptors for the keys they create
  • peinit service definitions, whose ControlSecurity and ServiceSecurity values are descriptors

This article is the canonical list of the codes Peios accepts. The concepts behind them — what a DACL is, how inheritance propagates, what the SACL carries — have their own articles, linked at the end.

The shape of a descriptor string #

Four optional sections, each introduced by a letter and a colon:

O:<sid>G:<sid>D:<acl>S:<acl>
SectionContents
O:The owner SID.
G:The primary group SID.
D:The discretionary ACL — who may do what.
S:The system ACL — auditing, the integrity label, policy references.

A section you omit is absent from the descriptor rather than empty, and the formatter emits the sections in the order above regardless of the order you wrote them. A section may appear only once; a repeat is an error rather than an override.

One case is easy to write by accident. A bare D: with no ACEs after it is a NULL DACL, which grants everyone everything — not an empty DACL, which grants nobody anything. D: and D:(A;;GA;;;WD) are close together on the keyboard and very far apart in effect.

ACL flags #

An ACL body may open with one or more flags, in any order, before the first (:

FlagMeaning
PProtected. The ACL does not inherit from its parent.
AIAuto-inherited. The ACL was produced by inheritance propagation.
ARAuto-inherit required. Requests that propagation be applied.

So D:PAI(A;;GA;;;SY) is a protected, auto-inherited DACL with one ACE.

An ACE #

Each ACE is a parenthesised list of six semicolon-separated fields, or seven when the ACE carries a payload:

(type;flags;rights;object_guid;inherit_object_guid;account_sid)
(type;flags;rights;object_guid;inherit_object_guid;account_sid;payload)

The two GUID fields apply only to object ACEs and are left empty otherwise — supplying one on a non-object ACE is an error rather than a value that is quietly discarded. The seventh field carries the condition expression on a callback ACE, or the claim on a resource-attribute ACE.

Reading a real one:

O:SYG:SYD:(A;OICI;GA;;;SY)

Owner SYSTEM, group SYSTEM, and a DACL with one ACE: allow (A), inherited by both containers and objects (OI CI), all access (GA), no object GUIDs, to SYSTEM (SY). That is the descriptor Peios seeds /run with at boot.

ACE types #

CodeACE type
AAccess allowed
DAccess denied
AUSystem audit
OAAccess allowed, object
ODAccess denied, object
OUSystem audit, object
XAAccess allowed, callback — carries a condition
XDAccess denied, callback — carries a condition
XUSystem audit, callback — carries a condition
ZAAccess allowed, callback object
ZDAccess denied, callback object
ZUSystem audit, callback object
MLMandatory label — the integrity level
RAResource attribute — carries a claim
SPScoped policy identifier

The callback types are how a conditional ACE is spelled: XA is an allow ACE with an expression in its seventh field.

ACE flags #

CodeMeaning
CIContainer inherit — child containers receive it.
OIObject inherit — child objects receive it.
NPNo propagate — children receive it, grandchildren do not.
IOInherit only — it does not apply to the object carrying it.
IDInherited — this ACE arrived by propagation.
SAAudit successful access.
FAAudit failed access.

SA and FA are meaningful only on an audit ACE in the SACL.

Access rights #

Generic rights, mapped to concrete rights by the object class:

CodeRight
GAGeneric all
GRGeneric read
GWGeneric write
GXGeneric execute

Standard rights, which mean the same thing on every object class:

CodeRight
SDDelete
RCRead control — read the descriptor
WDWrite DACL
WOWrite owner

File composites:

CodeMaskRight
FA0x001F01FFAll file access
FR0x00120089File read
FW0x00120116File write
FX0x001200A0File execute

Registry-key composites:

CodeMaskRight
KA0x000F003FAll key access
KR0x00020019Key read
KW0x00020006Key write
KX0x00020019Key execute

Directory-object rights, for object ACEs:

CodeRight
CCCreate child
DCDelete child
LCList children
SWSelf write
RPRead property
WPWrite property
DTDelete tree
LOList object
CRControl access

Mandatory-label policy bits, valid on an ML ACE:

CodePolicy
NWNo write up
NRNo read up
NXNo execute up

A rights field may also be a hexadecimal mask, written 0x followed by the value. It consumes the rest of the field, so it cannot be combined with letter codes — write the whole mask in hex or none of it.

SID codes #

An account field takes either a full S-1-… SID or one of these aliases:

CodeSIDPrincipal
WDS-1-1-0Everyone
ANS-1-5-7Anonymous
SUS-1-5-6Service
AUS-1-5-11Authenticated Users
SYS-1-5-18Local System
LSS-1-5-19Local Service
NSS-1-5-20Network Service
BAS-1-5-32-544Administrators
BUS-1-5-32-545Users
LWS-1-16-4096Low integrity
MES-1-16-8192Medium integrity
MPS-1-16-8448Medium-plus integrity
HIS-1-16-12288High integrity
SIS-1-16-16384System integrity

SU is worth knowing about: it is the group every token minted for a service logon carries, so it is how you grant something to services as a class rather than naming each one. Membership follows from how a process was started rather than from which account it runs as, so an ordinary user process cannot acquire it.

Two SIDs you might expect have no alias and must be written in full: the null SID S-1-0-0, and the protected-process integrity level S-1-16-20480.

The same two letters mean different things in different fields #

The code tables overlap, and a code is interpreted by the field it appears in rather than by its spelling. The collisions that catch people out:

CodeAs a rightAs an accountElsewhere
WDWrite DACLEveryone
AUAuthenticated UsersACE type: system audit
FAAll file accessACE flag: audit failed access
SDDelete

So in (A;FA;FA;;;WD) the first FA is an ACE flag, the second is a rights mask, and WD is Everyone rather than write-DACL. Each is unambiguous in place, but none of them reads that way at a glance.

Not everything that parses is emitted #

Reading a descriptor and formatting it again does not always return the string you started with, and this is deliberate rather than a defect.

The directory-object rights (CC through CR) and the mandatory-label bits (NW, NR, NX) occupy the same low bit positions as one another. Which of them a bit means depends on the object class and the ACE type, so the formatter would have to guess. Instead it accepts all of them when parsing and emits none of them, falling back to a hexadecimal mask. KX is dropped for a related reason: it shares a mask with KR, so only one of the two can ever come back.

The formatter also prefers composites to their components, emitting FA rather than the eight codes that add up to it.

Domain-relative aliases #

The codes DA, DG, DU, DD, DC, LA, LG, SA, EA, RO, CA, PA, CN, RS and RU name principals relative to a domain — domain admins, domain users, and so on. Peios recognises them so that it can reject them with a message that says what is wrong, and it cannot resolve them: there is no domain SID for them to be relative to. Write the SID you mean instead.

Reading a descriptor #

sd show renders a descriptor for a person to read, not as SDDL, and its rights column uses a shorthand of its own — a lone f is all file access (0x001F01FF), not the single bit 0xF. When you want the descriptor as a string you can compare, store or feed back in, ask for it:

sd show --sddl /usr/bin/login

Where to go next #

Edit this page