From edf375ca4805ec444fb8bb9f30ef58a752946c07 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 10 Apr 2025 21:48:25 -0700 Subject: [PATCH] Bring back ssh host read permission --- backend/src/ee/routes/v1/ssh-host-router.ts | 4 +-- .../services/permission/project-permission.ts | 4 +++ .../ee/services/ssh-host/ssh-host-service.ts | 9 ++++- .../ssh/ssh-certificate-authority-fns.ts | 3 +- backend/src/lib/api-docs/constants.ts | 6 ++-- .../src/services/project/project-service.ts | 26 ++++++++++++-- docs/documentation/platform/ssh-old.mdx | 34 +++++++++---------- docs/documentation/platform/ssh.mdx | 15 ++++++-- .../context/ProjectPermissionContext/types.ts | 1 + .../ProjectRoleModifySection.utils.tsx | 5 +++ .../SshHostsPage/components/SshHostModal.tsx | 6 ++-- 11 files changed, 82 insertions(+), 31 deletions(-) diff --git a/backend/src/ee/routes/v1/ssh-host-router.ts b/backend/src/ee/routes/v1/ssh-host-router.ts index 9d4c33d8b..cb1418759 100644 --- a/backend/src/ee/routes/v1/ssh-host-router.ts +++ b/backend/src/ee/routes/v1/ssh-host-router.ts @@ -412,7 +412,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => { sshHostId: z.string().trim().describe(SSH_HOSTS.GET_USER_CA_PUBLIC_KEY.sshHostId) }), response: { - 200: z.string() + 200: z.string().describe(SSH_HOSTS.GET_USER_CA_PUBLIC_KEY.publicKey) } }, handler: async (req) => { @@ -433,7 +433,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => { sshHostId: z.string().trim().describe(SSH_HOSTS.GET_HOST_CA_PUBLIC_KEY.sshHostId) }), response: { - 200: z.string() + 200: z.string().describe(SSH_HOSTS.GET_HOST_CA_PUBLIC_KEY.publicKey) } }, handler: async (req) => { diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 7d2847432..153401900 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -68,6 +68,7 @@ export enum ProjectPermissionGroupActions { } export enum ProjectPermissionSshHostActions { + Read = "read", Create = "create", Edit = "edit", Delete = "delete", @@ -657,6 +658,7 @@ const buildAdminPermissionRules = () => { can( [ ProjectPermissionSshHostActions.Edit, + ProjectPermissionSshHostActions.Read, ProjectPermissionSshHostActions.Create, ProjectPermissionSshHostActions.Delete, ProjectPermissionSshHostActions.IssueHostCert @@ -924,6 +926,8 @@ const buildMemberPermissionRules = () => { can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates); can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates); + can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts); + can( [ ProjectPermissionCmekActions.Create, diff --git a/backend/src/ee/services/ssh-host/ssh-host-service.ts b/backend/src/ee/services/ssh-host/ssh-host-service.ts index b582eb7db..20d363957 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-service.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-service.ts @@ -415,7 +415,7 @@ export const sshHostServiceFactory = ({ }); } - await permissionService.getProjectPermission({ + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId: host.projectId, @@ -424,6 +424,13 @@ export const sshHostServiceFactory = ({ actionProjectType: ActionProjectType.SSH }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSshHostActions.Read, + subject(ProjectPermissionSub.SshHosts, { + hostname: host.hostname + }) + ); + return host; }; diff --git a/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts b/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts index ce5048c48..6df10c6ac 100644 --- a/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts +++ b/backend/src/ee/services/ssh/ssh-certificate-authority-fns.ts @@ -430,7 +430,8 @@ export const validateExternalSshCaKeyPair = async (publicKey: string, privateKey if (publicKey.trim() !== derivedPublicKey.trim()) { throw new BadRequestError({ - message: "Failed to validate matching SSH key pair." + message: + "Failed to validate matching SSH key pair: The provided public key does not match the public key derived from the private key." }); } diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index a39e9b7e7..67a71826b 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1381,10 +1381,12 @@ export const SSH_HOSTS = { signedKey: "The SSH certificate or signed SSH public key." }, GET_USER_CA_PUBLIC_KEY: { - sshHostId: "The ID of the SSH host to get the user SSH CA public key for." + sshHostId: "The ID of the SSH host to get the user SSH CA public key for.", + publicKey: "The public key of the user SSH CA linked to the SSH host." }, GET_HOST_CA_PUBLIC_KEY: { - sshHostId: "The ID of the SSH host to get the host SSH CA public key for." + sshHostId: "The ID of the SSH host to get the host SSH CA public key for.", + publicKey: "The public key of the host SSH CA linked to the SSH host." } }; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 382093b2a..1f45734b3 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -1,4 +1,4 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; import { @@ -15,6 +15,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { ProjectPermissionActions, ProjectPermissionSecretActions, + ProjectPermissionSshHostActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; @@ -1077,7 +1078,7 @@ export const projectServiceFactory = ({ actor, projectId }: TListProjectSshHostsDTO) => { - await permissionService.getProjectPermission({ + const { permission } = await permissionService.getProjectPermission({ actor, actorId, projectId, @@ -1086,8 +1087,27 @@ export const projectServiceFactory = ({ actionProjectType: ActionProjectType.SSH }); + const allowedHosts = []; + + // (dangtony98): room to optimize const hosts = await sshHostDAL.findSshHostsWithLoginMappings(projectId); - return hosts; + + for (const host of hosts) { + try { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSshHostActions.Read, + subject(ProjectPermissionSub.SshHosts, { + hostname: host.hostname + }) + ); + + allowedHosts.push(host); + } catch { + // intentionally ignore projects where user lacks access + } + } + + return allowedHosts; }; /** diff --git a/docs/documentation/platform/ssh-old.mdx b/docs/documentation/platform/ssh-old.mdx index 260f9d7c1..9e9e8aac4 100644 --- a/docs/documentation/platform/ssh-old.mdx +++ b/docs/documentation/platform/ssh-old.mdx @@ -13,7 +13,7 @@ unauthorized access, and SSH key sprawl. The following concepts are useful to know when working with Infisical SSH: - SSH Certificate Authority (CA): A trusted authority that issues SSH certificates. -- Certificate Template: A set of policies bound to a SSH CA for certificates issued under that template; a CA can possess multiple templates, each with different policies for a different purpose (e.g. for admin versus developer access). +- Certificate Template: A set of policies bound to an SSH CA for certificates issued under that template; a CA can possess multiple templates, each with different policies for a different purpose (e.g. for admin versus developer access). - SSH Certificate: A short-lived, credential issued by the SSH CA granting time-bound access to infrastructure.
@@ -30,10 +30,10 @@ graph TD
-When using Infisical SSH to provision client access to a remote host, an operator must create a SSH CA in Infisical; a certificate template under it, +When using Infisical SSH to provision client access to a remote host, an operator must create an SSH CA in Infisical; a certificate template under it, specifying policies such as allowed users that can be requested under that template by a client; and configure the host to trust certificates issued by the Infisical SSH CA. -When a client needs access to a host, they authenticate with Infisical and request a SSH certificate (and optionally key pair) +When a client needs access to a host, they authenticate with Infisical and request an SSH certificate (and optionally key pair) to be used to access the host for a time-bound session as part of the SSH operation. ## Client Workflow @@ -68,7 +68,7 @@ At a high-level, Infisical issues a signed SSH certificate to a client that can To be more specific: 1. The client authenticates with Infisical; this can be done using a user or machine identity [authentication method](/documentation/platform/identities/machine-identities) or a user [authentication method](/documentation/platform/identities/user-identities). -2. The client makes an authenticated request for an SSH certificate via either the `/api/v1/ssh/issue` or `/api/v1/ssh/sign` endpoints. Note that if the client wishes to use an existing SSH key pair, it can use the `/api/v1/ssh/sign` endpoint; otherwise, it can use the `/api/v1/ssh/issue` endpoint to have Infisical issue a new SSH key pair in conjunction with the certificate. +2. The client makes an authenticated request for an SSH certificate via either the `/api/v1/ssh/issue` or `/api/v1/ssh/sign` endpoints. Note that if the client wishes to use an existing SSH key pair, it can use the `/api/v1/ssh/sign` endpoint; otherwise, it can use the `/api/v1/ssh/issue` endpoint to have Infisical issue a new SSH key pair along with the certificate. 3. The client uses the issued SSH certificate (and potentially SSH key pair) to temporarily access the host. @@ -83,12 +83,12 @@ In the following steps, we explore how to configure Infisical SSH to start issui as part of the SSH operation. - - 1.1. Start by creating a SSH project in the SSH tab of your organization. + + 1.1. Start by creating an SSH project in the SSH tab of your organization. ![ssh project create](/images/platform/ssh/ssh-project.png) - 1.2. Next, create a SSH CA in the **Certificate Authorities** tab of the + 1.2. Next, create an SSH CA in the **Certificate Authorities** tab of the project; this CA will be used for client key signing. ![ssh create client ca](/images/platform/ssh/ssh-client-create-ca-1.png) @@ -125,7 +125,7 @@ as part of the SSH operation. - Allow Host Certificates: Whether or not to allow issuance of host certificates; this is not relevant for this step. - Allow Custom Key IDs: Whether or not to allow clients to specify a custom key ID to be included on the certificate as part of the certificate request. - 2.2. Finally, add the user(s) you wish to be able to request a SSH certificate to the SSH project through the **Access Control** tab. + 2.2. Finally, add the user(s) you wish to be able to request an SSH certificate to the SSH project through the **Access Control** tab. @@ -185,7 +185,7 @@ infisical login ``` - + Run the `infisical ssh issue-credentials` command, specifying the `--addToAgent` flag to automatically load the SSH certificate into the SSH agent. ```bash infisical ssh issue-credentials --certificateTemplateId= --principals= --addToAgent @@ -229,7 +229,7 @@ If the remote host does not have an existing SSH key pair, you can generate a ne - + 1.1. In the same SSH project, create another SSH CA in the **Certificate Authorities** tab; this CA will be used for host key signing. ![ssh create host ca](/images/platform/ssh/ssh-host-create-ca-1.png) @@ -266,7 +266,7 @@ If the remote host does not have an existing SSH key pair, you can generate a ne - 2.1. Obtain a SSH certificate for the host by requesting one from the **Certificates** tab. + 3.1. Obtain an SSH certificate for the host by requesting one from the **Certificates** tab. ![ssh host issue certificate 1](/images/platform/ssh/ssh-host-issue-cert-1.png) @@ -280,15 +280,15 @@ If the remote host does not have an existing SSH key pair, you can generate a ne ![ssh host issue certificate 3](/images/platform/ssh/ssh-host-issue-cert-3.png) - 2.2. Create a file containing the certificate in the SSH folder of the remote host; we'll call it `ssh_host_key-cert.pub`. + 3.2. Create a file containing the certificate in the SSH folder of the remote host; we'll call it `ssh_host_key-cert.pub`. - 2.3. Set permissions on the certificate to be `0640`: + 3.3. Set permissions on the certificate to be `0640`: ```bash sudo chmod 0640 /etc/ssh/ssh_host_key-cert.pub ``` - 2.4. Next, add the following lines to the `/etc/ssh/sshd_config` file on the remote host. + 3.4. Next, add the following lines to the `/etc/ssh/sshd_config` file on the remote host. ```bash HostKey /etc/ssh/ssh_host_rsa_key @@ -299,7 +299,7 @@ If the remote host does not have an existing SSH key pair, you can generate a ne You should adjust the `HostKey` directive to match the path to the host's SSH private key as used in step 1. - 2.5. Finally, reload the SSH daemon on the remote host to apply the changes. + 3.5. Finally, reload the SSH daemon on the remote host to apply the changes. ```bash sudo systemctl reload sshd @@ -307,7 +307,7 @@ If the remote host does not have an existing SSH key pair, you can generate a ne - 3.1. Begin by downloading the host CA's public key from the CA's details section. + 4.1. Begin by downloading the host CA's public key from the CA's details section. ![ssh host ca public key](/images/platform/ssh/ssh-host-ca-public-key.png) @@ -315,7 +315,7 @@ If the remote host does not have an existing SSH key pair, you can generate a ne The CA's public key can also be retrieved programmatically via API by making a `GET` request to the endpoint [here](/api-reference/endpoints/ssh/ca/public-key). - 3.2. Next, add the resulting public key to the `known_hosts` file on the client machine (e.g. at the path `~/.ssh/known_hosts`). + 4.2. Next, add the resulting public key to the `known_hosts` file on the client machine (e.g. at the path `~/.ssh/known_hosts`). ```bash @cert-authority *.example.com ssh-rsa ... diff --git a/docs/documentation/platform/ssh.mdx b/docs/documentation/platform/ssh.mdx index d07c1c53b..ae1e43df5 100644 --- a/docs/documentation/platform/ssh.mdx +++ b/docs/documentation/platform/ssh.mdx @@ -6,8 +6,8 @@ description: "Learn how to securely provision user SSH access to your infrastruc ## Concept -Infisical SSH can be used to provide users short-lived, secure SSH access to infrastructure; the underlying technology is powered by SSH certificates -and improves on the limitations of traditional SSH key-based authentication via mitigation of private key compromise, static key management, +Infisical SSH can be configured to provide users on your team short-lived, secure SSH access to infrastructure. Under the hood, it uses SSH certificates +and improves upon traditional SSH key-based authentication by mitigating private key compromise, static key management, unauthorized access, and SSH key sprawl. The following entities and concepts are important to understand when using Infisical SSH: @@ -90,6 +90,17 @@ we will register a remote host with Infisical through a [machine identity](/docu 📄 Updated sshd_config entries ``` + Finally, use the following command to reload the SSH daemon on the remote host to apply the changes: + + ```bash + sudo systemctl reload sshd + ``` + + + The command may differ depending on the host. For older versions of Ubuntu/Debian/CentOS, you may need to use `sudo service ssh reload` instead; + for Alpine or minimal systems, `/etc/init.d/sshd reload`. + + Back in Infisical, you should now see the remote host you just registered in the Infisical SSH project you created in step 1 under the **Hosts** tab. ![ssh hosts](/images/platform/ssh/v2/ssh-added-hosts.png) diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 2a0a3740f..7c92988a1 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -76,6 +76,7 @@ export enum ProjectPermissionGroupActions { } export enum ProjectPermissionSshHostActions { + Read = "read", Create = "create", Edit = "edit", Delete = "delete", diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index 8017ab1cb..7d2210f53 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -110,6 +110,7 @@ const GroupPolicyActionSchema = z.object({ }); const SshHostPolicyActionSchema = z.object({ + [ProjectPermissionSshHostActions.Read]: z.boolean().optional(), [ProjectPermissionSshHostActions.Create]: z.boolean().optional(), [ProjectPermissionSshHostActions.Edit]: z.boolean().optional(), [ProjectPermissionSshHostActions.Delete]: z.boolean().optional(), @@ -610,6 +611,9 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { [ProjectPermissionSshHostActions.Create]: action.includes( ProjectPermissionSshHostActions.Create ), + [ProjectPermissionSshHostActions.Read]: action.includes( + ProjectPermissionSshHostActions.Read + ), [ProjectPermissionSshHostActions.IssueHostCert]: action.includes( ProjectPermissionSshHostActions.IssueHostCert ), @@ -945,6 +949,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { [ProjectPermissionSub.SshHosts]: { title: "SSH Hosts", actions: [ + { label: "Read", value: ProjectPermissionSshHostActions.Read }, { label: "Create", value: ProjectPermissionSshHostActions.Create }, { label: "Modify", value: ProjectPermissionSshHostActions.Edit }, { label: "Remove", value: ProjectPermissionSshHostActions.Delete }, diff --git a/frontend/src/pages/ssh/SshHostsPage/components/SshHostModal.tsx b/frontend/src/pages/ssh/SshHostsPage/components/SshHostModal.tsx index 6bc9fdae4..52003b38f 100644 --- a/frontend/src/pages/ssh/SshHostsPage/components/SshHostModal.tsx +++ b/frontend/src/pages/ssh/SshHostsPage/components/SshHostModal.tsx @@ -42,7 +42,7 @@ const schema = z (val) => ms(val) > 0, "TTL must be a valid time string such as 2 days, 1d, 2h 1y, ..." ) - .default("8h"), + .default("8h, 1d, 30m"), loginMappings: z .object({ loginUser: z.string().trim().min(1), @@ -206,7 +206,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => { errorText={error?.message} isRequired > - + )} /> @@ -328,7 +328,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => { {(value.length === 0 ? [""] : value).map( (principal: string, principalIndex: number) => (