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.

- 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.

@@ -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.

@@ -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.

@@ -280,15 +280,15 @@ If the remote host does not have an existing SSH key pair, you can generate a ne

- 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.

@@ -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.

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) => (