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.

A bare D: is an empty DACL: it contains no grants. A NULL DACL is written D:NO_ACCESS_CONTROL and imposes no discretionary access restrictions. Omitting D: has the same binary meaning as D:NO_ACCESS_CONTROL: the SE_DACL_PRESENT bit is clear and the DACL offset is zero. A bare D: sets PRESENT and points to a real ACL containing zero ACEs. There are two states, not three.

NO_ACCESS_CONTROL may accompany ACL flags, such as D:PAINO_ACCESS_CONTROL, but cannot accompany ACEs. The same applies to S:: S:NO_ACCESS_CONTROL is a descriptor with no SACL.

The formatter leaves out a PRESENT-clear ACL, just as you can, unless it carries P, AR or AI flags. Those have nowhere else to go, so it writes them with the marker: D:PNO_ACCESS_CONTROL. A descriptor with nothing at all to show formats as D:NO_ACCESS_CONTROL, because SDDL has no empty string. The C, Rust and Go SDKs format identically, and their output always parses back to the same binary descriptor.

So a missing D: in a tool's output means a NULL DACL. Tools that read only some sections of a descriptor, such as reg sd --owner, also leave out the sections they did not read. There, a missing section means only that it was not requested.

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
ALSystem alarm — continuous auditing
OLSystem alarm, object
XLSystem alarm, callback — carries a condition
ZLSystem alarm, callback object
MLMandatory label — the integrity level
TLProcess trust label
RAResource attribute — carries a claim
SPScoped policy identifier
FLAccess filter — carries a condition

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

XL and ZL are Peios' own. Windows names no callback alarm, so a string holding one does not read on Windows.

The formatter fails on any other ACE type, such as a compound ACE (0x04) or an unallocated value. It does not leave the entry out, because a string without it would describe a descriptor other than the one held.

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

An all-ASCII-digit rights field is an unsigned decimal mask. For example, 1179817 means 0x001200a9, and 0 means no rights. Leading zeroes are accepted as decimal, not octal. The maximum is 4294967295 (0xffffffff); larger values, signs and non-ASCII digits are rejected. Decimal masks must occupy the whole rights field and cannot be appended to mnemonic codes. This grammar applies to libpeios, legacy libp-go, and the independent peios-go SDK.

A rights field may also be a hexadecimal mask, written 0x followed by the value. It consumes the rest of the field, so it can only come last — but it may follow letter codes rather than replacing them. FRFX0x6 grants file read, file execute, and the two bits 0x6 names, which is how you reach a right that has no two-letter code without giving up the ones that do:

(A;;FRFX0x6;;;WD)

FILE_ADD_FILE and FILE_ADD_SUBDIRECTORY are the usual reason to need it. Writing the whole mask as 0x1200af is equally correct and tells a reader nothing.

SID codes #

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

Principals and logon types:

CodeSIDPrincipal
WDS-1-1-0Everyone
ANS-1-5-7Anonymous
IUS-1-5-4Interactive
NUS-1-5-2Network logon
SUS-1-5-6Service
RCS-1-5-12Restricted code
WRS-1-5-33Write-restricted code
PSS-1-5-10Principal self
EDS-1-5-9Enterprise domain controllers
AUS-1-5-11Authenticated Users
SYS-1-5-18Local System
LSS-1-5-19Local Service
NSS-1-5-20Network Service
ACS-1-15-2-1All application packages

Inheritance placeholders:

CodeSIDPrincipal
COS-1-3-0Creator Owner
CGS-1-3-1Creator Group
OWS-1-3-4Owner rights

Integrity levels:

CodeSIDPrincipal
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

The BUILTIN groups, S-1-5-32-*. These are absolute despite the name: the RIDs are fixed, not allocated per machine.

CodeSIDPrincipal
BAS-1-5-32-544Administrators
BUS-1-5-32-545Users
BGS-1-5-32-546Guests
PUS-1-5-32-547Power users
AOS-1-5-32-548Account operators
SOS-1-5-32-549Server operators
POS-1-5-32-550Printer operators
BOS-1-5-32-551Backup operators
RES-1-5-32-552Replicator
RUS-1-5-32-554Pre-Windows 2000 compatible access
RDS-1-5-32-555Remote desktop users
NOS-1-5-32-556Network configuration operators
MUS-1-5-32-558Performance monitor users
LUS-1-5-32-559Performance log users
ISS-1-5-32-568IIS users
CYS-1-5-32-569Cryptographic operators
ERS-1-5-32-573Event log readers
CDS-1-5-32-574Certificate service DCOM access
RAS-1-5-32-575RDS remote access servers
ESS-1-5-32-576RDS endpoint servers
MSS-1-5-32-577RDS management servers
HAS-1-5-32-578Hyper-V administrators
AAS-1-5-32-579Access control assistance operators
RMS-1-5-32-580Remote management users

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.

CO is the one you reach for most often after that. An inheritable ACE naming Creator Owner is rewritten to the new object's owner when a child is created, which is how a container grants each principal rights over what it makes there without granting anything over what anyone else makes.

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
RCRead controlRestricted code
AUAuthenticated UsersACE type: system audit
RARDS remote access serversACE type: resource attribute
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. The ACE type settles it for a mandatory label, so an ML entry's mask comes back as NW, NR and NX. Elsewhere the formatter would have to guess, so it accepts the directory-object codes 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- and machine-relative aliases #

The codes DA, DG, DU, DD, DC, SA, EA, RO, CA, PA, CN, AP, KA, EK and RS name principals relative to a domain — domain admins, domain users, and so on — and LA and LG name the local administrator and guest accounts relative to a machine. 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 or machine SID for them to be relative to. Write the SID you mean instead.

RU reads like one of these and is not: it is BUILTIN\Pre-Windows 2000 Compatible Access, S-1-5-32-554, and resolves like any other BUILTIN group.

Reading a descriptor #

sd show normally renders its selected view for a person to read. Its rights column uses a shorthand of its own — a lone f is all file access (0x001F01FF), not the single bit 0xF. To format the queried owner, group, DACL and integrity-label subset as SDDL:

sd show --sddl /usr/bin/login

The command does not query the full SACL. Formatting cannot recover audit ACEs or other components absent from the query: matching strings are not proof of full descriptor equality, and this output is not a complete backup to replay with sd set. See sd show inspection limits.

Where to go next #

Edit this page