From be8562824dc6da84d057409dd4ae2d8e49cf45c2 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 5 Nov 2024 18:38:09 +0800 Subject: [PATCH] feat: add mssql secret rotation template --- .../secret-rotation-queue-fn.ts | 14 +- .../secret-rotation-queue-types.ts | 1 + .../secret-rotation-queue.ts | 21 ++- .../secret-rotation/templates/index.ts | 8 + .../secret-rotation/templates/mssql.ts | 33 +++++ .../secret-rotation/templates/types.ts | 4 +- .../platform/secret-rotation/mssql.mdx | 139 ++++++++++++++++++ docs/mint.json | 1 + .../images/secretRotation/mssqlserver.png | Bin 0 -> 13199 bytes 9 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 backend/src/ee/services/secret-rotation/templates/mssql.ts create mode 100644 docs/documentation/platform/secret-rotation/mssql.mdx create mode 100644 frontend/public/images/secretRotation/mssqlserver.png diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts index 93f63a685..46c519d58 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-fn.ts @@ -85,7 +85,8 @@ export const secretRotationDbFn = async ({ password, username, client, - variables + variables, + options }: TSecretRotationDbFn) => { const appCfg = getConfig(); @@ -117,7 +118,8 @@ export const secretRotationDbFn = async ({ password, connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT, ssl, - pool: { min: 0, max: 1 } + pool: { min: 0, max: 1 }, + options } }); const data = await db.raw(query, variables); @@ -153,6 +155,14 @@ export const getDbSetQuery = (db: TDbProviderClients, variables: { username: str variables: [variables.username] }; } + + if (db === TDbProviderClients.MsSqlServer) { + return { + query: `ALTER LOGIN ?? WITH PASSWORD = '${variables.password}'`, + variables: [variables.username] + }; + } + // add more based on client return { query: `ALTER USER ?? IDENTIFIED BY '${variables.password}'`, diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-types.ts b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-types.ts index c40a8da0f..1cbcbb1ab 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-types.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue-types.ts @@ -24,4 +24,5 @@ export type TSecretRotationDbFn = { query: string; variables: unknown[]; ca?: string; + options: Record; }; diff --git a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts index 19976f148..e9c4ecbd9 100644 --- a/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts +++ b/backend/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts @@ -172,6 +172,16 @@ export const secretRotationQueueFactory = ({ // set a random value for new password newCredential.internal.rotated_password = alphaNumericNanoId(32); const { admin_username: username, admin_password: password, host, database, port, ca } = newCredential.inputs; + + const options = ( + provider.template.client === TDbProviderClients.MsSqlServer + ? { + encrypt: true, + cryptoCredentialsDetails: ca ? { ca } : {} + } + : {} + ) as Record; + const dbFunctionArg = { username, password, @@ -179,8 +189,10 @@ export const secretRotationQueueFactory = ({ database, port, ca: ca as string, - client: provider.template.client === TDbProviderClients.MySql ? "mysql2" : provider.template.client + client: provider.template.client === TDbProviderClients.MySql ? "mysql2" : provider.template.client, + options } as TSecretRotationDbFn; + // set function await secretRotationDbFn({ ...dbFunctionArg, @@ -189,12 +201,17 @@ export const secretRotationQueueFactory = ({ username: newCredential.internal.username as string }) }); + // test function + const testQuery = + provider.template.client === TDbProviderClients.MsSqlServer ? "SELECT GETDATE()" : "SELECT NOW()"; + await secretRotationDbFn({ ...dbFunctionArg, - query: "SELECT NOW()", + query: testQuery, variables: [] }); + newCredential.outputs.db_username = newCredential.internal.username; newCredential.outputs.db_password = newCredential.internal.rotated_password; // clean up diff --git a/backend/src/ee/services/secret-rotation/templates/index.ts b/backend/src/ee/services/secret-rotation/templates/index.ts index 05811d5bd..39774ae28 100644 --- a/backend/src/ee/services/secret-rotation/templates/index.ts +++ b/backend/src/ee/services/secret-rotation/templates/index.ts @@ -1,4 +1,5 @@ import { AWS_IAM_TEMPLATE } from "./aws-iam"; +import { MSSQL_TEMPLATE } from "./mssql"; import { MYSQL_TEMPLATE } from "./mysql"; import { POSTGRES_TEMPLATE } from "./postgres"; import { SENDGRID_TEMPLATE } from "./sendgrid"; @@ -26,6 +27,13 @@ export const rotationTemplates: TSecretRotationProviderTemplate[] = [ description: "Rotate MySQL@7/MariaDB user credentials", template: MYSQL_TEMPLATE }, + { + name: "mssql", + title: "Microsoft SQL Server", + image: "mssqlserver.png", + description: "Rotate Microsoft SQL server user credentials", + template: MSSQL_TEMPLATE + }, { name: "aws-iam", title: "AWS IAM", diff --git a/backend/src/ee/services/secret-rotation/templates/mssql.ts b/backend/src/ee/services/secret-rotation/templates/mssql.ts new file mode 100644 index 000000000..30096590d --- /dev/null +++ b/backend/src/ee/services/secret-rotation/templates/mssql.ts @@ -0,0 +1,33 @@ +import { TDbProviderClients, TProviderFunctionTypes } from "./types"; + +export const MSSQL_TEMPLATE = { + type: TProviderFunctionTypes.DB as const, + client: TDbProviderClients.MsSqlServer, + inputs: { + type: "object" as const, + properties: { + admin_username: { type: "string" as const }, + admin_password: { type: "string" as const }, + host: { type: "string" as const }, + database: { type: "string" as const, default: "master" }, + port: { type: "integer" as const, default: "1433" }, + username1: { + type: "string", + default: "infisical-sql-user1", + desc: "SQL Server login name that must be created at server level with a matching database user" + }, + username2: { + type: "string", + default: "infisical-sql-user2", + desc: "SQL Server login name that must be created at server level with a matching database user" + }, + ca: { type: "string", desc: "SSL certificate for db auth(string)" } + }, + required: ["admin_username", "admin_password", "host", "database", "username1", "username2", "port"], + additionalProperties: false + }, + outputs: { + db_username: { type: "string" }, + db_password: { type: "string" } + } +}; diff --git a/backend/src/ee/services/secret-rotation/templates/types.ts b/backend/src/ee/services/secret-rotation/templates/types.ts index 690b6ccf0..2adc40ba3 100644 --- a/backend/src/ee/services/secret-rotation/templates/types.ts +++ b/backend/src/ee/services/secret-rotation/templates/types.ts @@ -8,7 +8,9 @@ export enum TDbProviderClients { // postgres, cockroack db, amazon red shift Pg = "pg", // mysql and maria db - MySql = "mysql" + MySql = "mysql", + + MsSqlServer = "mssql" } export enum TAwsProviderSystems { diff --git a/docs/documentation/platform/secret-rotation/mssql.mdx b/docs/documentation/platform/secret-rotation/mssql.mdx new file mode 100644 index 000000000..c34bb9034 --- /dev/null +++ b/docs/documentation/platform/secret-rotation/mssql.mdx @@ -0,0 +1,139 @@ +--- +title: "Microsoft SQL Server" +description: "Learn how to automatically rotate Microsoft SQL Server user passwords." +--- + +The Infisical SQL Server secret rotation allows you to automatically rotate your database users' passwords at a predefined interval. + +## Prerequisites + +1. Create two SQL Server logins and database users with the required permissions. We'll refer to them as `user-a` and `user-b`. +2. Create another SQL Server login with permissions to alter logins for `user-a` and `user-b`. We'll refer to this as the `admin` login. + +Here's how to set up the prerequisites: + +```sql +-- Create the logins (at server level) +CREATE LOGIN [user-a] WITH PASSWORD = 'ComplexPassword1'; +CREATE LOGIN [user-b] WITH PASSWORD = 'ComplexPassword2'; + +-- Create database users for the logins (in your specific database) +USE [YourDatabase]; +CREATE USER [user-a] FOR LOGIN [user-a]; +CREATE USER [user-b] FOR LOGIN [user-b]; + +-- Grant necessary permissions to the users +GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [user-a]; +GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [user-b]; + +-- Create admin login with permission to alter other logins +CREATE LOGIN [admin] WITH PASSWORD = 'AdminComplexPassword'; +CREATE USER [admin] FOR LOGIN [admin]; + +-- Grant permission to alter any login +GRANT ALTER ANY LOGIN TO [admin]; +``` + +To learn more about SQL Server's permission system, please visit this [documentation](https://learn.microsoft.com/en-us/sql/relational-databases/security/authentication-access/getting-started-with-database-engine-permissions). + +## How it works + +1. Infisical connects to your database using the provided `admin` login credentials. +2. A random value is generated and the password for `user-a` is updated with the new value. +3. The new password is then tested by logging into the database. +4. If test is successful, it's saved to the output secret mappings so that rest of the system gets the newly rotated value(s). +5. The process is then repeated for `user-b` on the next rotation. +6. The cycle repeats until secret rotation is deleted/stopped. + +## Rotation Configuration + + + + Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar + + + + + SQL Server admin username + + + + SQL Server admin password + + + + SQL Server host url (e.g., your-server.database.windows.net) + + + + Database port number (default: 1433) + + + + Database name (default: master) + + + + The first login name to rotate - `user-a` + + + + The second login name to rotate - `user-b` + + + + Optional database certificate to connect with database + + + + + When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project. + + + The environment where the rotated credentials should be mapped to. + + + + The secret path where the rotated credentials should be mapped to. + + + + What interval should the credentials be rotated in days. + + + + Select an existing secret key where the rotated database username value should be saved to. + + + + Select an existing select key where the rotated database password value should be saved to. + + + + + +## FAQ + + + + When a system has multiple nodes by horizontal scaling, redeployment doesn't happen instantly. + + This means that when the secrets are rotated, and the redeployment is triggered, the existing system will still be using the old credentials until the change rolls out. + + To avoid causing failure for them, the old credentials are not removed. Instead, in the next rotation, the previous user's credentials are updated. + + + + The admin account is used by Infisical to update the credentials for `user-a` and `user-b`. + + You don't need to grant all permissions for your admin account but rather just the permission to alter logins (ALTER ANY LOGIN). + + + + When using Azure SQL Database, you'll need to: + + 1. Use the full server name as your host (e.g., your-server.database.windows.net) + 2. Ensure your admin account is either the Azure SQL Server admin or an Azure AD account with appropriate permissions + 3. Configure your Azure SQL Server firewall rules to allow connections from Infisical's IP addresses + + diff --git a/docs/mint.json b/docs/mint.json index 4baf26a56..b8bf6c4fa 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -165,6 +165,7 @@ "documentation/platform/secret-rotation/sendgrid", "documentation/platform/secret-rotation/postgres", "documentation/platform/secret-rotation/mysql", + "documentation/platform/secret-rotation/mssql", "documentation/platform/secret-rotation/aws-iam" ] }, diff --git a/frontend/public/images/secretRotation/mssqlserver.png b/frontend/public/images/secretRotation/mssqlserver.png new file mode 100644 index 0000000000000000000000000000000000000000..108ed60f9171ac4c49217149deef3c6dadd2e252 GIT binary patch literal 13199 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�>wW3 z0a?vf;Mpm7IdNz^b&kAmL9SGdFK5w;T9UU(xoMMDC(ewMDS7Pe6GW|Ds}Y6dmWM{V zU<8Nz=L9t#cA~tD`11DjyQjzIJ6==$y7MI|wFA7JGj1x%18^TnDsun|`#ue#ldb$#dc;O@~!&>Me2 zuGnd2`xv%E$|*yIEu18fjb3bH>rYyaI+3VQ5zFFn`TkfRa zDOy+MYes1D1AL8_wz2X9eYr+Hm^y3cCyP-~f_`lhq$T~XAuDSv?wya!71K}H7sva| zX@EKoW?$oCc)8Yhl{vLdV?z9iovIpX1p$9U&fk2PN&CtQ>;*zTt(6xqW0e|FC z&+0b0_WMF=r!8oSW$gN$*Nbn_#g1+Hv9D9BZ**#-^2&R8Nr4v4iWD2nOkf0OjYih9 z`e^}?tanEXGTMkC0q?OOla!v8xGn*Uhz!B8ru#Vj-9C?eRd$v+az>5gEG?t_Nwq?j z4(_k`E>inLV^zRwSiX*$ZYREDzdnNI*Kj@?z&rOSei)3U`?+X7Rh!`KH+<#Bp4ny{ z+dI*>h2&HDvzsq@NZuB0$tM;pZi`oB^|x*~5MT;U&?Yz+5LHBG58kgg7@9q<(>jv{ zXJAo3RB$2#{%#;oKI8JGPi@gjVhwYCX|KRAclK&Y`2Y@Li9aZ%{W{#24*sLh1J%{E>@Xmf_oFKT{Zj6FDC=0-B=ZwS&$=yij-fNb;` zYkOM8yN`Kz@Re3%g8>6MeWGdH*(&;Q0bw*0+Ucuvzx4u?+qQ%nbfox`-KqN*fa@eE zVpS}Tk5(nI@Z1YfQ8dt`X#WLlb@RJ>2#)%lrZMwW*1QTGEn&eHxxrRuo(m5=<4g+I zCp?*Z`9+!P8A_K%{Sy4l% zjf+1Dc;lSkN|y%ah`ih4Vp9_H_l4Jgz4sHL=H6r`xG>Is1s6`UKVonuPn0E!7u?YT#vg953wN;1UTYdQ=^^WGWkW2eX zV}m4wB62>7Wpo0;4PQfx3IPp%H?~zOLD_o|Q2+T@4v#c<53B@B{)v*!F|d7MubJNP&8&IW zbpknv!AHC5uUp=!AH4hT9y08%oQ|u=XA{Eq=%DH>T$|t?qhP$x|E19aLBJI3QqinRXV}gQ)ZJ4rB&*V- zxjzYj`ja%x7gnh87a>O5!BWagpAB=;IF%H7@u@J&|7IB-=EHk*N#4l5&-t zm~kVkF|ZcDs2z*E$TVx&IpN*9uS;GjIi?5=Gi3&ed531PX6u#TJNziSoTIBr1)s>= zYDtPMfL*H7dQ^UZr9P9Eq*Tnc@T<9b-lNFNf}~bRzsEEMmNS1=2D7x=3qAkm%9RjX z{1GFAe1BwvhslRA*eJ*cIUeJDm&$Jt+t-1r@G}SDg*1Qa z<4Jx~e913=cmUkgaWz^a17UY?-iWGfV}Mr1Zki)Gv81$a52TVp%uHaO1$6>ARtd|}2G6A;)3p`o`mMCrQ8=w2ac&#{GSt^LB#P>MwN+w zS9|-d<(KREsVEXF%aBHR?DV|OfbC%d^qA`)2+ul5 zdEm*p3Ek1urJFC~NtIW@(6joS-7ZInX@W`hGk~8DJ znx8LGN#eyT%~3wuLYw55SFq6y8<_`ul#HHu2+KM(7|z+m%jW z$KT~sCi3%)zWbtGUAx1wt*#(w zC8%{Z9K^DOTl^L#C^4%DYdAg8#{-f9U-9_*=7rwcI_Q#chR+iSUU z$RuXj$zkC!-C3F-JTjNY>_ccN1t3p>dtG)71-~Bt;e>oyn-{zHD9I)iLGx5L4wU9O za%+q$tolJjA3}1yr6(CzC#D|;-^EKCB*aI>XP0^Cjbp9P}`&33ZQUX&PwxP%w zA~=QRK)$N1aBu#7A6npdQ#X6sRL<}8R9NoeO#EHHfFe6sJ_;&Y$f_+-MGH;+|7D;2 h9~mkZ%V`fj%QYP(XjUtq-!p7rWF!>Dt3-|b{|8G12ZI0r literal 0 HcmV?d00001