{"id":97081,"date":"2023-06-07T15:56:47","date_gmt":"2023-06-07T15:56:47","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=97081"},"modified":"2026-03-18T12:10:49","modified_gmt":"2026-03-18T12:10:49","slug":"unmasking-sql-server-dynamic-data-masking-part-2-setting-up-masking","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/blogs\/unmasking-sql-server-dynamic-data-masking-part-2-setting-up-masking\/","title":{"rendered":"SQL Server Dynamic Data Masking Setup Guide"},"content":{"rendered":"<p>Dynamic Data Masking (DDM) in SQL Server hides sensitive column data from users who don&#8217;t have UNMASK permission &#8211; without changing the stored data. You add masks using ALTER TABLE &#8230; ALTER COLUMN &#8230; ADD MASKED WITH (FUNCTION = &#8216;&#8230;&#8217;), choosing from four mask functions: default() (full mask), email() (shows first letter and domain), random(start, end) (random number in range), and partial(prefix, padding, suffix) (custom pattern). Users with SELECT permission see masked values; users with UNMASK permission see the real data. DDM supplements &#8211; but does not replace &#8211; your existing security model.<\/p>\n<p><strong>This is part of a series on Dynamic Data Masking by <a href=\"https:\/\/www.red-gate.com\/simple-talk\/author\/ben-johnston\/\">Ben Johnston<\/a>. For the rest of the series, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/tag\/BenJohnston_DynamicDataMasking\/\">click here<\/a><\/strong><\/p>\n\n<p>This is the second part of a series on SQL Server Dynamic Data masking. The <a href=\"https:\/\/www.red-gate.com\/simple-talk\/blogs\/unmasking-sql-server-dynamic-data-masking\/\">first part in the series<\/a> was a brief introduction to dynamic data masking, including use cases.<\/p>\n<p>The focus of this blog will is setting up masking and some base examples.<\/p>\n<h2>Configuring Masking<\/h2>\n<p>Masking data is a straightforward process. Choose the table and columns to be masked, decide on the appearance of the mask, execute the masking script, and verify the results. Masking data also assumes some foundational setup tasks have been done and minimal best practices are being followed. Users that need to retrieve data that has been masked only need minimal viable security (generally <code>SELECT<\/code> privileges), no table ownership or <code>dbo<\/code> rights. If the user owns the table or has <code>dbo<\/code> rights, the data isn&#8217;t masked for them.<\/p>\n<p>Simplicity is important with security design and implementation. Simple design doesn&#8217;t imply only using the default database roles or assigning the same security to each user. Applied simplicity is like Occam&#8217;s Razor for security. It is the most straight forward security design that will support your known current and future security needs.<\/p>\n<p>A simple security design is easy to maintain, and the security of each user is clear. This relates directly to dynamic data masking. Table selection for masking is an important task and involves working closely with the business. Data shouldn&#8217;t be restricted via masking unless necessary.<\/p>\n<p>Remember, masking isn&#8217;t a method to implement general security, it is a supplement to existing security and a way to expose additional data in a relatively safe manner that users couldn&#8217;t ordinarily access. The most sensitive data shouldn&#8217;t be exposed even with masking, so items like social security numbers, tax IDs, etc. should still only be exposed via masking with extreme caution and to the most trusted users, if at all.<\/p>\n<h3>Mask Appearance<\/h3>\n<p>Choosing the appearance of the mask is also a business decision that can be guided by the architecture. Selecting a standard for patterns on your project is recommended. Consistency makes it easier for users to interpret the data they are seeing, knowing that data is masked, and it also speeds the development process since the mask doesn&#8217;t need to be determined for each column, just each column type.<\/p>\n<p>From a technical perspective the appearance doesn&#8217;t impact functionality. General guidelines for masks would be to choose something that is not a default for your unmasked columns or a mask that has meaning in your organization.<\/p>\n<h3>Preparation<\/h3>\n<p>The examples in this series are based on the WideWorldImporters database, that is the current standard demo database from Microsoft. More information about how to acquire that database as well as to initialize<\/p>\n<h3>Script Creation<\/h3>\n<p>Adding masking to an existing table follows the standard alter syntax.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">USE WideWorldImporters\nGO\n\/*\n* Add masking to sales.Customers.CreditLimit column.\n* This is the syntax for a manual ALTER statement\n*\/\nALTER TABLE Sales.Customers\nALTER COLUMN CreditLimit\nADD MASKED WITH (FUNCTION = 'default()')\nGO<\/pre>\n<p>If you add masking to the column in your create script the syntax is very similar to that of a constraint. If you are in a devops environment with continuous deployment, near continuous deployment, or just automated deployments, this is likely the syntax needed.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">USE WideWorldImporters\nGO\n\/*\n* Add masking to sales.Customers.CreditLimit column\n* during table creation.\n* This is the syntax for a devops environment using\n* SSDT\/sqlpackage.exe or other automated deployment.\n*\n* Note that previous examples add masking to \n* additional columns.\n* Each column needs to have masking functionality added\n* in a devops environment using declarative coding.\n* \n*\/\nCREATE TABLE Sales.Customers(\n\tCustomerID int NOT NULL \n       CONSTRAINT PK_Sales_Customers PRIMARY KEY CLUSTERED \n\t,CustomerName nvarchar(100) \n          MASKED WITH (FUNCTION = 'default()') NOT NULL\n\t,BillToCustomerID int NOT NULL\n\t,CustomerCategoryID int NOT NULL\n-- &lt;columns skipped for size&gt;\n\t,LastEditedBy int NOT NULL\n\t,ValidFrom datetime2(7) NOT NULL\n\t,ValidTo datetime2(7) NOT NULL\n);<\/pre>\n<h3>Limitations<\/h3>\n<p>Some column types and columns used in temporal tables can&#8217;t be masked. Microsoft documentation includes columns encrypted with Always Encrypted, <code>filestream<\/code> columns, <code>COLUMN_SET<\/code> or columns part of a column set, columns used in Polybase, and computed columns (but the referenced column can be masked).<\/p>\n<p>Although not listed by the documentation, period columns for system-versioned temporal tables can&#8217;t be masked and there are exceptions to the computed columns. For example:<\/p>\n<pre class=\"\">--Show default mask for date \nALTER TABLE Application.People \n   ALTER COLUMN ValidFrom \n    ADD MASKED WITH (FUNCTION = 'default()');<\/pre>\n<p>Trying this will cause the following error to be raised:<\/p>\n<p><code>Msg 13599, Level 16, State 1, Line 5<\/code><br \/><code>Period column 'ValidFrom' in a system-versioned temporal table cannot be altered.<\/code><\/p>\n<p>Computed columns can&#8217;t be masked directly, but the referenced columns can be masked and that will flow through to the computed column. The order that computed columns and masked columns are added to the table are important and will determine if the column can actually be masked.<\/p>\n<p>The following shows that the referenced columns can&#8217;t be masked after the computed column is created.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">--Example showing computed columns can't be masked\n--and columns used by computed columns cant be masked\n--after the table is created\nUSE WideWorldImporters;\nGO\n--Computed column\nALTER TABLE Application.People\nALTER COLUMN SearchName\nADD MASKED WITH (FUNCTION = 'default()');\nGO\n--Column referenced by computed column\nALTER TABLE Application.People\nALTER COLUMN PreferredName\nADD MASKED WITH (FUNCTION = 'default()');\nGO<\/pre>\n<p>This returns:<\/p>\n<p><code>Msg 4928, Level 16, State 1, Line 8<\/code><br \/><code>Cannot alter column 'SearchName' because it is 'COMPUTED'.<\/code><\/p>\n<p><code>Msg 5074, Level 16, State 1, Line 14<\/code><br \/><code>The column 'SearchName' is dependent on column 'PreferredName'.<\/code><\/p>\n<p><code>Msg 4922, Level 16, State 9, Line 14<\/code><br \/><code>ALTER TABLE ALTER COLUMN PreferredName failed because one or more objects access this column.<\/code><\/p>\n<p>The easiest solution for this is to create the table with masking enabled in the <code>CREATE TABLE<\/code> statement. Then add the computed column later via an <code>ALTER<\/code> statement, but this solution won&#8217;t work for temporal tables.<\/p>\n<p><code>Msg 13724, Level 16, State 1, Line 62<\/code><br \/><code>System-versioned table schema modification failed because adding computed columns while system-versioning is ON is not supported.<\/code><\/p>\n<p>Adding the masked columns to the computed column referenced columns at the time of creation is successful.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">CREATE TABLE [Application].[PeopleV2](\n  [PersonID] [int] \n    MASKED WITH (FUNCTION = 'default()') NOT NULL,\n  [FullName] [nvarchar](50) \n    MASKED WITH (FUNCTION = 'default()') NOT NULL,\n  [PreferredName] [nvarchar](50) \n    MASKED WITH (FUNCTION = 'default()') NOT NULL ,\n   SearchName AS (concat([PreferredName],N' ',[FullName])) \n     PERSISTED NOT NULL,\n   [IsPermittedToLogon] [bit] NOT NULL,\n   [LogonName] [nvarchar](50) \n      MASKED WITH (FUNCTION = 'default()') NULL,\n   [IsExternalLogonProvider] [bit] NOT NULL,\n   [HashedPassword] [varbinary](max) \n      MASKED WITH (FUNCTION = 'default()') NULL,\n   [IsSystemUser] [bit] NOT NULL,\n   [IsEmployee] [bit] NOT NULL,\n   [IsSalesperson] [bit] NOT NULL,\n   [UserPreferences] [nvarchar](max) NULL,\n   [PhoneNumber] [nvarchar](20) NULL,\n   [FaxNumber] [nvarchar](20) NULL,\n   [EmailAddress] [nvarchar](256) NULL,\n   [Photo] [varbinary](max) NULL,\n   [CustomFields] [nvarchar](max) NULL,\n   [OtherLanguages]  AS \n        (json_query([CustomFields],N'$.OtherLanguages')),\n   [LastEditedBy] [int] NOT NULL,\n   [ValidFrom] [datetime2](7) \n        GENERATED ALWAYS AS ROW START NOT NULL,\n   [ValidTo] [datetime2](7) \n        GENERATED ALWAYS AS ROW END NOT NULL,\n    CONSTRAINT [PK_Application_PeopleV2] \n        PRIMARY KEY CLUSTERED \n(\n\t[PersonID] ASC\n),\nPERIOD FOR SYSTEM_TIME ([ValidFrom], [ValidTo])\n);\nWITH (\nSYSTEM_VERSIONING = ON (HISTORY_TABLE = [Application].[People_ArchiveV2])\n);\nGO<\/pre>\n<p>This will execute without error. Note that since altering the table to add masking isn&#8217;t possible, so if you want to add it you will need to create a new temporal table and move the history. More about this can be found in this blog about <a href=\"https:\/\/www.red-gate.com\/simple-talk\/blogs\/temporal-tables-part-2-changing-history\/\">changing temporal history<\/a>.<\/p>\n<h3>Verification<\/h3>\n<p>After masking has been implemented verification and testing follows. Verification, or a smoke test, will require a user with limited access. A user without a login is a good choice for this verification and can be safely use in production environments. If your test team isn&#8217;t familiar with this technique or their testing tools don&#8217;t support this scenario, an actual login and user account can be created. If there is a use case for unmasked data, testing this scenario is warranted. Special attention should be paid to intersections of additional security. Row Level Security (RLS), user roles, group assigned security should all have separate test cases.<\/p>\n<p>In comparison to RLS, which requires an access predicate and a security policy, a single statement is required for data masking. Users with <code>UNMASK<\/code>, <code>ALTER<\/code> permission or <code>dbo<\/code> permission will view unmasked data, other users will see the mask which is the default of 0. The basic select statement returns the following.<\/p>\n<p>Note that the sample code contains the <code>EXECUTE<\/code> <code>AS<\/code> statement, which runs the query as the <code>MaskedReader<\/code> database user. This is the user setup previously in the sample without unmasking privileges. To use the <code>EXECUTE AS<\/code> statement, you must run the query with a user that has IMPERSONATE rights. If your current login doesn&#8217;t have proper permission, you will get the message \u201cCannot execute as the database principal because the principal &#8220;MaskedUser&#8221; does not exist, this type of principal cannot be impersonated, or you do not have permission.\u201d. Full documentation on the statement is available here: <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/statements\/execute-as-transact-sql?view=sql-server-ver16\">https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/statements\/execute-as-transact-sql?view=sql-server-ver16<\/a><\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">USE WideWorldImporters\nGO\nEXECUTE AS USER = 'MaskedReader'\nGO\nSELECT\n\tCustomerID\n\t,CustomerName\n\t,CreditLimit\nFROM sales.Customers\nWHERE CreditLimit IS NOT NULL;\nGO\nREVERT;\nGO<\/pre>\n<p>Looking at the returned data, all of the <code>CreditLimit<\/code> values will appear to be 0.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"377\" height=\"112\" class=\"wp-image-97110\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-1-1.png\" \/><\/p>\n<p>It is interesting to note that <code>NULL<\/code> columns return without a mask as <code>NULL<\/code>. This piece of information may be valuable in some scenarios, so understand that it works in this way.<br \/><strong>Read also:\u00a0<br \/><\/strong><a href=\"https:\/\/www.red-gate.com\/simple-talk\/blogs\/sql-server-security-primer\/\">SQL Server security primer<\/a><br \/><a href=\"https:\/\/www.red-gate.com\/simple-talk\/blogs\/auditing-sql-server-part-1-discovery-and-documentation\/\">Auditing SQL Server<\/a><\/p>\n<h3>Setup in Azure<\/h3>\n<p>Microsoft Azure SQL Database includes the option to mask columns via the GUI. This can be useful for test cases or smaller companies wanting to try out masking. I am a strong advocate of automated deployments, multiple environments for testing and development, and repeatability of deployments, so this wouldn&#8217;t be my first choice. It can be a quick way to test masking and is useful for administrators not familiar with SQL Server.<\/p>\n<p>In Azure, the overview for the database shows that Dynamic Data Masking is configured. If you click into the masking box, the columns that are masked are shown. You also get the option to configure additional columns with masking.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-97111\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-2-1.png\" width=\"484\" height=\"160\" \/><\/p>\n<p>This shows the columns configured with the setup scripts for this article, plus the SSN column created and masked during one of the examples in this article.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-97112\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-3-1.png\" width=\"773\" height=\"540\" \/><\/p>\n<p>Scrolling down further shows columns not masked, but recommended for consideration for masking. The default mask can be added by simply clicking the button to the left of the column. The column to be masked then moves to the previous list, Masking rules, and will be added once the Save button is pressed. If more control is wanted, click the &#8220;Add mask&#8221; button at the top of the screen.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"135\" height=\"39\" class=\"wp-image-97113\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-4-1.png\" \/><\/p>\n<p>This opens a new window allowing you to select the schema, table, column, and specific mask to apply.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-97114\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-5-1.png\" width=\"250\" height=\"468\" \/><\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-97115\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-6-1.png\" width=\"267\" height=\"212\" \/><\/p>\n<p>The column then shows up in the Masking rules table. It also needs to be saved before it is applied to the table.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-97116\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-7-1.png\" width=\"410\" height=\"126\" \/><\/p>\n<p>Remember to script out the table definition and add the masking definition to your database project if you use automated deployments with SSDT. The column definition will look similar to the following when scripted.<\/p>\n<pre class=\"\">[DealDescription] [nvarchar](30) \n     MASKED WITH (FUNCTION = 'default()') NOT NULL,<\/pre>\n<p>Masking can be removed in a similar fashion in Azure. Select the masked column, select the Delete button, and save the changes. The same rule applies here for DevOps environments, the column should also be removed from the table script.<\/p>\n<h3>Testing<\/h3>\n<p>Testing will involve your regular process and full verification with your test team. Testing stories should include all tables with masking, all masked columns, masked users and users with elevated authority. If you have other layers of security, such as RLS, it is recommended to test the intersection of masking with those layers.<\/p>\n<p>Consistency and the ability to recreate tests is an important aspect of testing. If your team uses an automated test suite, masking should be part of those tests. Test scripts can include simple <code>SELECT<\/code> statements and execute as a specific user account. The setup section shows creating a user without a login. Users like this can be created for testing. They are relatively safe even in production since the user changing context needs the impersonation privilege.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">USE WideWorldImporters;\nGO\nEXECUTE AS USER = 'MaskedReader';\nGO\nSELECT\n\tCountryID\n\t,CountryName\n\t,IsoAlpha3Code\nFROM Application.Countries\nWHERE CountryName IN ('Armenia','Bahrain','Belize');\nGO\nREVERT;\nGO\nEXECUTE AS USER = 'UnmaskedReader';\nGO\nSELECT\n\tCountryID\n\t,CountryName\n\t,IsoAlpha3Code\nFROM Application.Countries\nWHERE CountryName IN ('Armenia','Bahrain','Belize');\nGO\nREVERT;\nGO<\/pre>\n<p>Resultset with a non-privileged user.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"385\" height=\"115\" class=\"wp-image-97117\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-8-1.png\" \/><\/p>\n<p>Resultset using a privileged user.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"387\" height=\"118\" class=\"wp-image-97118\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-9-1.png\" \/><\/p>\n<p>The test team can also use the Azure dashboard to verify masking options if they have the proper authorization to view the database there. If they can&#8217;t access the database view the dashboard, they can use SSMS and SQL scripts to verify the table definition and the metadata. This will be dependent on the skills of the test team, but exploratory scripts presented later in this article can be used by the team to validate masked columns and the mask used. Scripts can also be used to validate unmasking privileges for users.<\/p>\n<pre class=\"\">\/*\n* The following can be used to validate masking security for users\n* It requires a basic understanding of the security structure\n* used by the application and database.\n* It can be expanded for SQL Server 2022 to include column level masking.\n*\/\nSELECT\n\t@@SERVERNAME\tCOLLATE Latin1_General_100_CI_AS  ServerName\n\t,DB_NAME()\tCOLLATE Latin1_General_100_CI_AS  DatabaseName\n\t,SU.name\tCOLLATE Latin1_General_100_CI_AS  UserName\n\t,SR.name\tCOLLATE Latin1_General_100_CI_AS  RoleName\n\t,'Database role' COLLATE Latin1_General_100_CI_AS SecurityType\nFROM sys.database_role_members DRM\n\tINNER JOIN sys.sysusers SU\n\t\tON DRM.member_principal_id = SU.uid\n\tINNER JOIN sys.sysusers SR\n\t\tON DRM.role_principal_id = SR.uid\nwHERE SR.name IN ('db_owner')\nUNION\nSELECT\n\t@@SERVERNAME    COLLATE Latin1_General_100_CI_AS  ServerName\n\t,'master'\tCOLLATE Latin1_General_100_CI_AS  DatabaseName\n\t,SP.name\tCOLLATE Latin1_General_100_CI_AS  LoginName\n\t,ROL.name\tCOLLATE Latin1_General_100_CI_AS  RoleName\n\t,'Server role'  COLLATE Latin1_General_100_CI_AS  SecurityType\nFROM master.sys.server_role_members SRM\n\tINNER JOIN master.sys.server_principals SP\n\t\tON SRM.member_principal_id = SP.principal_id\n\tINNER JOIN master.sys.server_principals ROL\n\t\tON SRM.role_principal_id = ROL.principal_id\nWHERE ROL.name\t\tIN ('sysadmin')\nUNION\nSELECT\n\t@@SERVERNAME    COLLATE Latin1_General_100_CI_AS ServerName\n\t,db_name()\tCOLLATE Latin1_General_100_CI_AS DatabaseName\n\t,SUGR.name\tCOLLATE Latin1_General_100_CI_AS UserName\n\t,DP.permission_name\tCOLLATE Latin1_General_100_CI_AS RoleName\n\t,'Database permission'\tCOLLATE Latin1_General_100_CI_AS SecurityType\nFROM sys.database_permissions DP\n\tINNER JOIN sys.sysusers SUGR\n\t\tON DP.grantee_principal_id = SUGR.uid\nWHERE DP.permission_name\tIN \n   ('ALTER ANY MASK','CONTROL','UNMASK')<\/pre>\n<p>Testers, and administrators, can use the output from a script like the above to validate masking. The script can be modified to include column level unmasking for SQL Server 2022. It is a general pattern to assign security at a group level, so be sure to include that in your test plans.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"497\" height=\"124\" class=\"wp-image-97119\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-10-1.png\" \/><br \/><br \/><strong>Read also: <\/strong><a href=\"https:\/\/www.red-gate.com\/simple-talk\/blogs\/sql-server-row-level-security-deep-dive-part-5-rls-attacks\/\">SQL Server Row-Level Security deep dive<\/a><\/p>\n<h2>Masking Data<\/h2>\n<p>In this section I will demonstrate how masks look by default, and then what you can alter them to look like using a custom mask.<\/p>\n<h3>Default Masks<\/h3>\n<p>Default masks by type follow. Other preconfigured masks include email, random, custom, and datetime. The mask displayed is a purely aesthetic preference. It has no impact on performance, security, or storage.<\/p>\n<p>In all of the examples so far, I have set the mask to DEFAULT. These are the default values for the basic datatypes:<\/p>\n<table>\n<tbody>\n<tr>\n<td>\n<p><strong>Data type<\/strong><\/p>\n<\/td>\n<td>\n<p><strong>Default mask<\/strong><\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p>Date<\/p>\n<\/td>\n<td>\n<p>01.01.1900<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p>String<\/p>\n<\/td>\n<td>\n<p>XXXX<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p>Numeric<\/p>\n<\/td>\n<td>\n<p>0<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p>Binary<\/p>\n<\/td>\n<td>\n<p>0<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p>Datetime (SQL 2022)<\/p>\n<\/td>\n<td>\n<p>01.23.2023 (depends on datepart masked)<\/p>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>In the next code, I will add a few more masks to the data:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">--Show default mask for string\nALTER TABLE Application.People\nALTER COLUMN LogonName\nADD MASKED WITH (FUNCTION = 'default()');\nGO\n--Show default mask for numeric\nALTER TABLE Application.People\nALTER COLUMN PersonID\nADD MASKED WITH (FUNCTION = 'default()');\nGO\n--Show default mask for date\nALTER TABLE Purchasing.PurchaseOrders\nALTER COLUMN OrderDate\nADD MASKED WITH (FUNCTION = 'default()');\nGO\n--Show default mask for binary\nALTER TABLE Application.People\nALTER COLUMN HashedPassword\nADD MASKED WITH (FUNCTION = 'default()');\nGO<\/pre>\n<h3>Specialized Masks<\/h3>\n<p>Specialized masks can be used for any column type. This is very similar to application front end development. A hard-coded character can be used, a different date format, random values or a custom string. Most common formats are represented in the specialized masks available in SQL.<\/p>\n<p>Basic default masks, random mask, custom string and the new datetime mask are shown below. Note that the datetime mask is new to SQL 2022 and will only work in that engine or Azure SQL.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">--Show default EMAIL mask\nALTER TABLE Application.People\nALTER COLUMN EmailAddress\nADD MASKED WITH (FUNCTION = 'email()');\nGO\n--Random mask\nALTER TABLE Purchasing.PurchaseOrders\nALTER COLUMN SupplierID\nADD MASKED WITH (FUNCTION = 'random(1,100)');\nGO\n--Custom string\nALTER TABLE Application.People\nALTER COLUMN PhoneNumber\nADD MASKED WITH (FUNCTION = 'partial(6,\"555-555\",1)');\nGO\n--Datetime (SQL Server 2022 functionality)\nALTER TABLE Purchasing.PurchaseOrders\nALTER COLUMN ExpectedDeliveryDate\nADD MASKED WITH (FUNCTION = 'datetime(\"M\")');\nGO<\/pre>\n<p>Sample output from default masks created above.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">EXECUTE AS USER = 'MaskedReader';\nGO\nSELECT TOP 100\n\tAP.PersonID\n\t,AP.LogonName\n\t,AP.HashedPassword\n\t,AP.EmailAddress\n\t,AP.PhoneNumber\n\t,PO.OrderDate\n\t,PO.SupplierID\n\t,PO.ExpectedDeliveryDate\nFROM Application.People AP\n\tINNER JOIN Purchasing.PurchaseOrders PO\n\t\tON AP.PersonID\t\t= PO.LastEditedBy;\nGO\nREVERT;\nGO<\/pre>\n<p>The output from this code shows you the various masks in use:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"696\" height=\"286\" class=\"wp-image-97120\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-11-1.png\" \/><\/p>\n<p>Note that the above example shows the <code>PersonID<\/code> getting masked in the base table. This type of masking gap will be explored in later sections, but you wouldn&#8217;t perform masking like this without masking all references to the column also. The <code>PersonID<\/code> can be determined by joining to another table via a foreign key and looking at the referencing column.<\/p>\n<p><strong>Joining to Data<\/strong><\/p>\n<p>When a primary key \/ unique constraint is masked, it can still be used for the join column. The key for this is to remember to mask every child table that references the masked column. Without masking on both tables, there is no need to unmask data and no concerns with malicious unmasking, the unmasking is already done by the database developer. If the child table isn&#8217;t masked, this is a glaring gap in the masking strategy. There is a long-standing debate on the use of natural keys for a primary key versus artificial keys. This is a clear reason to use artificial keys. The use of masking implies that there is some implicit knowledge in the column value, so an artificial key (identity or GUID), removes that implicit knowledge.<\/p>\n<p>The following example shows an example of masking a primary key column but not the corresponding child table. The child data can be used to unmask the parent data, so design your masking carefully.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk \">USE WideWorldImporters;\nGO\nCREATE TABLE dbo.KeyExample (\n\tCountryCode\tnvarchar(3) \n       MASKED WITH (FUNCTION = 'default()') NOT NULL \n       CONSTRAINT PK_dboKeyExample PRIMARY KEY CLUSTERED\n)\nCREATE TABLE dbo.LookupExample (\n\tLookupExampleID\tint\t NOT NULL\t\n     \tCONSTRAINT PK_dboLookupExample PRIMARY KEY \n               CLUSTERED\t identity\n\t,SomeValue int\tNOT NULL\n\t,CountryCode\tnvarchar(3) NOT NULL\t\n\tCONSTRAINT FK_dboLookupExample_dboKeyExample\t\n        FOREIGN KEY REFERENCES dbo.KeyExample(CountryCode)\n);\nGO\n--Execute as a user that can view unmasked data\n--Current user has dbo rights\nINSERT INTO dbo.KeyExample (\n\tCountryCode\n)\nSELECT\n\tIsoAlpha3Code\nFROM Application.Countries\nORDER BY IsoAlpha3Code;\n--Uses the TABLESAMPLE keyword to return semi-random rows\n--Very efficient but will not return an exact number of rows\nINSERT INTO dbo.LookupExample (\n\tSomeValue\n\t,CountryCode\n)\nSELECT\n\t--Psuedo-random numbers. Each value will differ by row. \n        --RAND() function will return same value for each row.\n\tCONVERT(INT,CONVERT(VARBINARY,NEWID())) SomeValue\n\t,IsoAlpha3Code\nFROM Application.Countries\nTABLESAMPLE(10 ROWS);\nEXECUTE AS USER = 'MaskedReader';\nGO\n--Select from the masked column and the unmasked column\n--You wouldn't normally repeat the columns, but this shows\n--the gap in masking\nSELECT\n\tKE.CountryCode\n\t,LE.LookupExampleID\n\t,LE.SomeValue\n\t,LE.CountryCode\nFROM dbo.KeyExample KE\n\tINNER JOIN dbo.LookupExample LE\n\t\tON KE.CountryCode = LE.CountryCode;\nGO\nREVERT;\nGO\n--Cleanup\n--Drop this first or drop the foreign key before the \n--parent table\nDROP TABLE IF EXISTS dbo.LookupExample\nGO\nDROP TABLE IF EXISTS dbo.KeyExample\nGO<\/pre>\n<p>Output from the above script shows the masked and unmasked values. If the masked data was treated as if it were the value of the mask, you would end up with many many rows returned because every row would match the other.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"560\" height=\"227\" class=\"wp-image-97121\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-12-1.png\" \/><\/p>\n<h3>Moving masked data to a new table<\/h3>\n<p>SQL includes many methods to move data, modify data, merge data, and extract data. Basic CRUD (CReate, Update, and Delete) functionality. Masking data does not limit that functionality, but it does introduce some caveats.<\/p>\n<p>If a user without authorization to view unmasked data tries to move that data, in any way, the data moved will still be masked, in fact, it will be permanently obfuscated in the destination. If you can only view masked data and you perform a <code>SELECT INTO<\/code>, the new table that you create will only contain masked data. If you perform an <code>INSERT<\/code> or an <code>UPDATE<\/code> to a different or new table, those impacted columns will contain masked data. Anything else would completely compromise the intention of masking the data.<\/p>\n<p>Users with authorization to view unmasked data can perform all of these operations and retain the original, unmasked data. If a transactional, user application contains masked data, the <code>INSERT<\/code> and <code>UPDATE<\/code> operations will need to be performed by a user with the correct authorization.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">USE WideWorldImporters;\nGO\nEXECUTE AS USER = 'MaskedReader';\nGO\n--MaskedReader only able to see masked data\nSELECT\n\tCustomeriD\n\t,CustomerName\nFROM Sales.Customers;\n--Insert data from the base table into #Customer\nSELECT \n\tCustomerID\n\t,CustomerName\nINTO #Customer\nFROM Sales.Customers;\n--Validate that the data is still masked\nSELECT\n\tCustomerID\n\t,CustomerName\nFROM #Customer;\n--Revert back to dbo (or current user)\nREVERT;\nGO\n--Run as a user that is able to see masked data\n--Same dataset as MaskedReader (due to RLS for MaskedReader)\n--Notice that CustomerName is unmasked\nSELECT\n\tCustomerID\n\t,CustomerName\nFROM Sales.Customers\nWHERE CustomerID IN (74,84,116,431,454,488,560,821,1050,1051);\n--Temp table created by MaskedReader\n--Data in the table is masked since the user that \n--created the table could only view a masked version of CustomerName\nSELECT\n\tCustomerID\n\t,CustomerName\nFROM #Customer;\nDROP TABLE #Customer;\nGO<\/pre>\n<p>The masked user only sees the masked version of the CustomerName column.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"270\" height=\"113\" class=\"wp-image-97122\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-13-1.png\" \/><\/p>\n<p>The data is still masked when it is inserted into a new table, in this instance, the temp table #Customer.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"270\" height=\"113\" class=\"wp-image-97123\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-14-1.png\" \/><\/p>\n<p>A privileged account is able to see CustomerName, but notice that the temp table created by the MaskedReader account only contains masked data.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"404\" height=\"115\" class=\"wp-image-97124\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-15-1.png\" \/><\/p>\n<p>In the new table, #Customer, the data is permanently obfuscated, even with a privileged account.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"270\" height=\"113\" class=\"wp-image-97125\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-16-1.png\" \/><\/p>\n<p>If a user without authorization to see masked data also has rights to update that masked data, the base data is not obfuscated. It is correct at the physical level, but masked users, even the user that inserts or updates the data, is not able to see the unmasked version of the data.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">USE WideWorldImporters;\nGO\n--Show the unmodified version of Sales.Customers\n--For an ID that is available to MaskedReader via RLS\n--CustomerName = 'Tailspin Toys (Indios, PR)'\n--Run as dbo \/ privileged account\nSELECT\n\tCustomerID\n\t,CustomerName\nFROM Sales.Customers\nWHERE CustomerID = 74;\nGO\n--Allow MaskedReader to update the Sales.Customers table\n--Note that due to RLS, even with UPDATE rights \n--on the table, the account can only update rows \n--available via RLS\nGRANT UPDATE ON Sales.Customers TO MaskedReader;\nGO\n--Run as MaskedReader\nEXECUTE AS USER = 'MaskedReader';\nGO\n--Update CustomerName\n--The column is masked for this user\nUPDATE Sales.Customers\nSET CustomerName = 'Update for masked user'\nWHERE CustomerID =74;\nGO\n--Even though the user just updated the column, \n--it is still masked\nSELECT\n\tCustomerID\n\t,CustomerName\nFROM Sales.Customers\nWHERE CustomerID = 74;\nGO\n--Revert back to dbo\nREVERT;\nGO\n--Remove UPDATE rights\nREVOKE UPDATE ON Sales.Customers TO MaskedReader;\nGO\n--Show the column when queried by a privileged user\n--Note that the column is the original text - not a masked text\nSELECT\n\tCustomerID\n\t,CustomerName\nFROM Sales.Customers\nWHERE CustomerID = 74;\nGO\n--Reset the CustomerName to the original value\nUPDATE Sales.Customers\nSET CustomerName = 'Tailspin Toys (Indios, PR)'\nWHERE CustomerID = 74;\nGO<\/pre>\n<p>The first dataset returned by that batch of statements shows the unmodified version of the <code>CustomerName<\/code> for <code>CustomerID<\/code> 74, when run with a privileged account.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"351\" height=\"57\" class=\"wp-image-97126\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-17-1.png\" \/><\/p>\n<p>The second dataset shows the <code>CustomerName<\/code> after the <code>MaskedReader<\/code> has updated the value. Even though the user updated the column, that user is not able to see the unmasked value.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"270\" height=\"60\" class=\"wp-image-97127\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-18-1.png\" \/><\/p>\n<p>The final dataset shows output when a privileged user accesses the data.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"337\" height=\"60\" class=\"wp-image-97128\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-19-1.png\" \/><\/p>\n<h3>Allowing access to data<\/h3>\n<p>There are several ways to allow access to masked data. The most straightforward path is granting a user <code>UNMASK<\/code> rights to a table. This can also be done by granting the user <code>ALTER<\/code> to the table, or to the schema. The <code>ALTER ANY MASK<\/code> permission also grants this right but wouldn&#8217;t be used for most users. Some scenarios require the ability for users to temporarily unmask data. Even if a user doesn&#8217;t have the ability to unmask the data, it can be displayed in a query by using the <code>EXECUTE AS<\/code> command utilizing a user with the correct access. This must be done in the context of a stored procedure or by a user with the proper rights.<br \/><strong>Read also:\u00a0<\/strong><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/database-administration-sql-server\/sql-server-triggers-good-scary\/\"><span data-sheets-root=\"1\">SQL Server triggers for access logging<\/span><\/a><\/p>\n<h2>Performance<\/h2>\n<p>Dynamic data masking has no discernable notice on performance. Masking is performed in the data engine and only has to modify data as it is output. While queries are running in the engine, there is no impact to the data. It doesn&#8217;t impact the query plan, indexes, statistics, storage on the page, standard security, or RLS.<\/p>\n<p>The only potential impact happens during output when masking security is verified and the column output is obfuscated.<\/p>\n<h2>Coded Alternatives to Dynamic Data Masking<\/h2>\n<p>There are several alternatives to dynamic data masking that can be implemented on the database tier. As mentioned previously, masking is available in almost all front-end application languages. If users aren&#8217;t allowed to hit data directly, masking can still be used to control what is returned to the user. This can be helpful for reporting scenarios.<\/p>\n<h3>Views<\/h3>\n<p>Dynamic data masking works without modification in views. Reporting scenarios or other use cases where default masking behavior is desired. Additional custom scenarios are also supported by using views.<\/p>\n<p>A common scenario in reporting or even transactional systems is excluding sensitive data by only allowing access in a view, and not including certain columns. In the following example, the masked columns can remain masked or masking can be removed. If the data is not available to users, and they don&#8217;t have direct access to the database, the data will not be presented.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">USE WideWorldImporters;\nGO\nCREATE OR ALTER VIEW dbo.vw_CustomerCity\nAS\nSELECT\n\tCustomerID\n\t,CityName\nFROM sales.Customers C\n\tINNER JOIN Application.Cities CTY\n\t\tON C.DeliveryCityID\t\t= CTY.CityID;\nGO\nEXECUTE AS USER = 'MaskedReader';\nGO\nSELECT *\nFROM dbo.vw_CustomerCity;\nGO\nREVERT;\nGO<\/pre>\n<p>The output for this view is the same for every user since the masked column, <code>CustomerName<\/code>, is not presented in the view.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"321\" height=\"113\" class=\"wp-image-97129\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-20-1.png\" \/><\/p>\n<p>Data can also be masked directly in the view without using dynamic data masking by partially masking the data in the view. This is similar to dynamic data masking but it applies to every user hitting the view. In the following example, every user will receive a masked version of the CustomerName column. The rows returned will differ based on their RLS security.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">USE WideWorldImporters;\nGO\nALTER TABLE sales.Customers\nALTER COLUMN CustomerName \nDROP MASKED;\nGO\nCREATE OR ALTER VIEW dbo.vw_CustomerName\nAS\nSELECT\n\tCustomerID\n\t,LEFT(CustomerName,1) + N'xxx' CustomerName\n\t,CityName\nFROM sales.Customers C\n\tINNER JOIN Application.Cities CTY\n\t\tON C.DeliveryCityID\t\t= CTY.CityID;\nGO<\/pre>\n<p>Selecting from the view with the restricted user account shows fewer rows, with masking applied.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">USE WideWorldImporters;\nGO\nEXECUTE AS USER = 'MaskedReader';\nGO\nSELECT\n\tCustomerID\n\t,CustomerName\n\t,CityName\nFROM dbo.vw_CustomerName\nORDER BY \n\tCustomerName\n\t,CityName;\nGO\nREVERT;\nGO<\/pre>\n<p>This returns the following (abbreviated for space) dataset:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"483\" height=\"114\" class=\"wp-image-97130\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-21-1.png\" \/><\/p>\n<p>Selecting from the view using the less restricted user account returns more rows, but the CustomerName is still restricted.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">USE WideWorldImporters;\nGO\nEXECUTE AS USER = 'UnmaskedReader';\nGO\nSELECT\n\tCustomerID\n\t,CustomerName\n\t,CityName\nFROM dbo.vw_CustomerName\nORDER BY \n\tCustomerName\n\t,CityName;\nGO\nREVERT;\nGO<\/pre>\n<p>As shown by the output, all users are returned the obfuscated <code>CustomerName<\/code> when the view is used.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"483\" height=\"114\" class=\"wp-image-97131\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-22-1.png\" \/><\/p>\n<p>Reenable masking on the <code>Sales.Customers.CustomerName<\/code> column.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">USE WideWorldImporters;\nGO\nALTER TABLE sales.Customers\nALTER COLUMN CustomerName\nADD MASKED WITH (FUNCTION = 'default()');\nGO<\/pre>\n<p>One of the disadvantages of custom masking in views is the loss of much of the functionality afforded by dynamic data masking. The <code>ORDER BY<\/code> statement in the <code>SELECT<\/code> applies to the masked data, not the base, unmasked data. Other functionality such as using the column for joins or functions is also lost or severely restricted. This still may fit the need of projects depending on the design patterns. It also simplifies implementation. It also assumes that the users don&#8217;t have access to the base table, <code>Sales.Customers<\/code>, if they shouldn&#8217;t have access to the masked column. If the purpose of masking is to prevent data from appearing in reports, it would also suit that need.<\/p>\n<h3>Stored Procedures<\/h3>\n<p>Stored procedures have added functionality available to work with dynamic data masking. Adding some additional checks to a stored procedure allows the user security to be changed dynamically, by using <code>EXECUTE AS USER<\/code>. There are multiple ways security for a user can be created and verified. In the following example, a new user role is created in the database, <code>AllowUnmasking<\/code>, and the <code>MaskedReader<\/code> user is added to this role.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">USE WideWorldImporters;\nGO\n--Create a role for unmasking\nCREATE ROLE AllowUnmasking AUTHORIZATION dbo;\nGO\n--Add the MaskedReader to the new role\nALTER ROLE AllowUnmasking ADD MEMBER MaskedReader;\nGO\nCREATE OR ALTER PROCEDURE dbo.prcSalesCustomers_SELECT\nWITH EXECUTE AS OWNER \n--Execute as owner, dbo, so EXECUTE AS USER can \n--be called in the procedure\nAS\nSET NOCOUNT ON;\n--Find the original user executing the stored procedure.\n--Store the value in the @User variable\nDECLARE @User nvarchar(255);\nEXEC AS CALLER;\nSELECT @User = USER_NAME();\nREVERT;\n--Check if the calling user is a member of the \n--AllowUnmasking role defined earlier\n--If it is a member, execute the SELECT as UnmaskedReader\n--In a production scenario, you may want to create a \n--different account\n--if RLS is present or additional security is needed.\nIF (SELECT IS_ROLEMEMBER('AllowUnmasking',@User)) = 1 \nBEGIN\n\tEXECUTE AS USER = 'UnmaskedReader';\nEND\nELSE\nBEGIN\n\t--Execute as the original calling user if \n     --they are not a member of the AllowUnmasking role, \n     --reverting back to default behavior.\n\t--This could also use  EXECUTE AS USER = 'MaskedReader'\n\tEXECUTE AS USER = @User\nEND\nSELECT\n\tC.CustomerID\n\t,C.CustomerName\n\t,CTY.CityName\nFROM sales.Customers C\n\tINNER JOIN Application.Cities CTY\n\t\tON C.DeliveryCityID\t\t= CTY.CityID\nORDER BY\n\tCustomerName\n\t,CityName;\n--Revert back to the calling user\nREVERT;\nGO\nGRANT EXEC ON dbo.prcSalesCustomers_SELECT TO MaskedReader;\nGO<\/pre>\n<p>Executing as a user that would normally have masked data, but is setup with the AllowUnmasking role returns unmasked data from the stored procedure, but not when accessing the table directly.<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">USE WideWorldImporters;\nGO\nEXECUTE AS USER = 'MaskedReader';\nEXEC dbo.prcSalesCustomers_SELECT;\nGO\nSELECT\n\tC.CustomerID\n\t,C.CustomerName\n\t,CTY.CityName\nFROM sales.Customers C\n\tINNER JOIN Application.Cities CTY\n\t\tON C.DeliveryCityID\t\t= CTY.CityID;\nREVERT;\nGO<\/pre>\n<p>Output from the stored procedure shows the full <code>CustomerName<\/code>. Access to a stored procedure can be limited in many ways. The access can be controlled by an API layer or a report layer, schema level access can be granted or denied, the individual stored procedure can be limited, and additional lookups can be done in the stored procedure itself to determine if the user can be unmasked or not. In this example, any user calling the procedure is checked against the role <code>AllowUnmasking<\/code> to determine how the data is presented.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"562\" height=\"112\" class=\"wp-image-97132\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-23-1.png\" \/><\/p>\n<p>Even though the user is part of the <code>AllowUnmasking<\/code> role in this example, direct access to the data is not allowed, as shown int the output for the direct query.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"393\" height=\"114\" class=\"wp-image-97133\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/06\/word-image-97081-24-1.png\" \/><\/p>\n<h2>Summary<\/h2>\n<p>SQL Server Dynamic Data Masking can be implemented using repeatable scripts and automated via standard dev-ops deployments or it can be configured manually via the GUI. Some alternatives to dynamic data masking were presenting, including using a standard UI lay, a view layer, or stored procedures. Future sections included side channel attacks, mitigations to those attacks and other considerations for implementation.<\/p>\n\n\n<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs: How to set up Dynamic Data Masking in SQL Server<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. How do you set up Dynamic Data Masking in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p>Use ALTER TABLE [table] ALTER COLUMN [column] ADD MASKED WITH (FUNCTION = &#8216;default()&#8217;) to apply a mask. Choose the appropriate mask function for the data type: default() for full masking, email() for email addresses, random() for numeric ranges, or partial() for custom patterns. Users need only SELECT permission to see masked data &#8211; no additional roles required.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. What mask functions are available in Dynamic Data Masking?<\/h3>\n            <div class=\"faq-answer\">\n                <p>SQL Server provides four built-in functions: default() which replaces the entire value (xxxx for strings, 0 for numbers), email() which shows the first letter and domain (aXXX@XXXX.com), random(start, end) which returns a random number in a range, and partial(prefix, padding, suffix) which lets you define a custom pattern showing partial real data.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. Is Dynamic Data Masking secure enough for production?<\/h3>\n            <div class=\"faq-answer\">\n                <p>DDM is a data exposure control, not a security boundary. Users with db_owner, UNMASK permission, or direct table access can see unmasked data. DDM is best used as a supplementary layer &#8211; exposing additional columns to users who wouldn&#8217;t otherwise have access &#8211; combined with proper role-based security, RLS, and encryption for truly sensitive data like SSNs or tax IDs.<\/p>\n            <\/div>\n            <\/section>\n\n\n\n<section id=\"my-first-block-block_309451720c10a81fa60080637ee3488e\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Protect your data. Demonstrate compliance.<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            With Redgate, stay ahead of threats with real-time monitoring and alerts, protect sensitive data with automated discovery &#038; masking, and demonstrate compliance with traceability across every environment.                                    <\/div>\n            <\/div>\n                            <a href=\"https:\/\/www.red-gate.com\/solutions\/use-cases\/security-and-compliance\/\" class=\"btn btn--secondary btn--lg\">Learn more<\/a>\n                    <\/div>\n    <\/div>\n<\/section>","protected":false},"excerpt":{"rendered":"<p>Set up SQL Server Dynamic Data Masking step by step. Covers ALTER TABLE masking syntax, mask functions (default, email, random, custom), permissions, and best practices.&hellip;<\/p>\n","protected":false},"author":19670,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[2],"tags":[159004],"coauthors":[98702],"class_list":["post-97081","post","type-post","status-publish","format-standard","hentry","category-blogs","tag-benjohnston_dynamicdatamasking"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/97081","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/19670"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=97081"}],"version-history":[{"count":12,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/97081\/revisions"}],"predecessor-version":[{"id":109322,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/97081\/revisions\/109322"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=97081"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=97081"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=97081"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=97081"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}