Security
Security is where SQLCLR reputations are made and lost. The feature is not dangerous; undisciplined trust decisions are. This page is the discipline.
CLR strict security, in depth
Before SQL Server 2017, SAFE meant something: Code Access Security
constrained what the IL could do, and the engine treated the permission set
as a boundary. Then the .NET team stopped supporting CAS as a security
boundary (determined attackers could break out of it), and SQL Server did
the honest thing. clr strict security (on by default since 2017) makes
the engine authorize every assembly, whatever its declared set, as if
it were UNSAFE.
Loading UNSAFE code has always had strict requirements, and now they
apply to everything. One of these must be true:
- An identity vouches for the code: a login derived from the assembly's
signing certificate or asymmetric key, holding the
UNSAFE ASSEMBLYpermission. - The instance explicitly allowlists the binary by its SHA-512 hash.
There is a third, historical path (database TRUSTWORTHY ON combined with
the owner's permissions), and it is the subject of the next section.
The declared permission set still gates runtime behavior: SAFE code
attempting file I/O still throws. Treat that as a guardrail, not a
boundary. The boundary is your review of what you sign.
Why TRUSTWORTHY ON is the wrong shortcut
ALTER DATABASE ... SET TRUSTWORTHY ON makes CLR trust errors go away,
which is why every old forum thread suggests it. Do not.
- It is database-wide. You wanted to trust one reviewed assembly; you elevated everything the database's principals can produce, now and in the future.
- It composes with ownership into escalation. A
TRUSTWORTHYdatabase owned bysa(the default when a sysadmin restores one) lets anydb_ownermanufacture a path to sysadmin. - It does not even persist honestly. The flag resets to
OFFon every restore and attach, so teams script it back on automatically, and a one-time shortcut quietly becomes standing policy that nobody remembers approving. - It audits terribly. A reviewer sees a database-wide waiver where a signed artifact should be.
Signing costs perhaps an hour the first time. TRUSTWORTHY costs an
architecture review the day someone competent finds it.
Signing with a certificate
Sign the DLL itself (Authenticode), then teach the instance to recognize the signature. One-time setup per signing key, per instance:
# 1. Sign the built DLL with your code-signing certificate
signtool sign /f ClrSigning.pfx /p <password> /fd SHA256 SqlClrExamples.dll
-- 2. Import the certificate's public half into master, straight from the DLL
USE master;
CREATE CERTIFICATE ClrSigningCert
FROM EXECUTABLE FILE = N'C:\clr\SqlClrExamples.dll';
-- 3. Create a login to hold the grant (it cannot be used to connect)
CREATE LOGIN ClrSigningLogin FROM CERTIFICATE ClrSigningCert;
GRANT UNSAFE ASSEMBLY TO ClrSigningLogin;
From here, CREATE ASSEMBLY succeeds in any database on the instance for
anything signed with that certificate, rebuilds included. Yes, the grant
says UNSAFE ASSEMBLY even for SAFE assemblies: under strict security,
that is the level at which all trust is expressed.
Signing with an asymmetric key
The strong-name variant needs no Authenticode tooling, just the .NET SDK:
sn -k SqlClrKey.snk
csc /target:library /keyfile:SqlClrKey.snk /out:SqlClrExamples.dll *.cs
USE master;
CREATE ASYMMETRIC KEY SqlClrKey
FROM EXECUTABLE FILE = N'C:\clr\SqlClrExamples.dll';
CREATE LOGIN SqlClrKeyLogin FROM ASYMMETRIC KEY SqlClrKey;
GRANT UNSAFE ASSEMBLY TO SqlClrKeyLogin;
Same shape, same result. I default to certificates in shops that run a PKI and asymmetric keys everywhere else.
Allowlisting with trusted assemblies
sys.sp_add_trusted_assembly registers the SHA-512 of an exact binary in
master. No keys, no signatures; the instance trusts those bits and only
those bits:
EXEC sys.sp_add_trusted_assembly
@hash = 0x8CA0A6F521EF4D3A9BD0C22E7F0A11BC0F334D5E,
@description = N'SqlClrExamples 2.1.0, reviewed KK 2026-07-15';
Every rebuild changes the hash, so registration belongs in the deploy pipeline. In regulated environments I treat that as a feature: the allowlist is the record of exactly which bits were approved and when. Remember it is server-scoped; every availability-group replica needs the same entries, or your first failover is a CLR outage.
Audit what is deployed
Trust decisions decay unless inspected. Three queries I run on every engagement:
-- What CLR code exists, and at what permission level?
SELECT name, permission_set_desc, clr_name, create_date, modify_date
FROM sys.assemblies
WHERE is_user_defined = 1;
-- What T-SQL objects bind into it?
SELECT OBJECT_SCHEMA_NAME(am.object_id) AS schema_name,
OBJECT_NAME(am.object_id) AS module_name,
a.name AS assembly_name,
am.assembly_class, am.assembly_method
FROM sys.assembly_modules AS am
JOIN sys.assemblies AS a ON a.assembly_id = am.assembly_id
WHERE a.is_user_defined = 1;
-- What has the instance been told to trust?
SELECT hash, description, create_date
FROM sys.trusted_assemblies;
Cross-check deployed bits against build artifacts with
HASHBYTES('SHA2_512', content) over sys.assembly_files; the query
lives in Deployment, along with keeping all of this
repeatable instead of archaeological.