Bring back ssh host read permission

This commit is contained in:
Tuan Dang
2025-04-10 21:48:25 -07:00
parent 264177638f
commit edf375ca48
11 changed files with 82 additions and 31 deletions
+2 -2
View File
@@ -412,7 +412,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
sshHostId: z.string().trim().describe(SSH_HOSTS.GET_USER_CA_PUBLIC_KEY.sshHostId) sshHostId: z.string().trim().describe(SSH_HOSTS.GET_USER_CA_PUBLIC_KEY.sshHostId)
}), }),
response: { response: {
200: z.string() 200: z.string().describe(SSH_HOSTS.GET_USER_CA_PUBLIC_KEY.publicKey)
} }
}, },
handler: async (req) => { 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) sshHostId: z.string().trim().describe(SSH_HOSTS.GET_HOST_CA_PUBLIC_KEY.sshHostId)
}), }),
response: { response: {
200: z.string() 200: z.string().describe(SSH_HOSTS.GET_HOST_CA_PUBLIC_KEY.publicKey)
} }
}, },
handler: async (req) => { handler: async (req) => {
@@ -68,6 +68,7 @@ export enum ProjectPermissionGroupActions {
} }
export enum ProjectPermissionSshHostActions { export enum ProjectPermissionSshHostActions {
Read = "read",
Create = "create", Create = "create",
Edit = "edit", Edit = "edit",
Delete = "delete", Delete = "delete",
@@ -657,6 +658,7 @@ const buildAdminPermissionRules = () => {
can( can(
[ [
ProjectPermissionSshHostActions.Edit, ProjectPermissionSshHostActions.Edit,
ProjectPermissionSshHostActions.Read,
ProjectPermissionSshHostActions.Create, ProjectPermissionSshHostActions.Create,
ProjectPermissionSshHostActions.Delete, ProjectPermissionSshHostActions.Delete,
ProjectPermissionSshHostActions.IssueHostCert ProjectPermissionSshHostActions.IssueHostCert
@@ -924,6 +926,8 @@ const buildMemberPermissionRules = () => {
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates); can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates); can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts);
can( can(
[ [
ProjectPermissionCmekActions.Create, ProjectPermissionCmekActions.Create,
@@ -415,7 +415,7 @@ export const sshHostServiceFactory = ({
}); });
} }
await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId: host.projectId, projectId: host.projectId,
@@ -424,6 +424,13 @@ export const sshHostServiceFactory = ({
actionProjectType: ActionProjectType.SSH actionProjectType: ActionProjectType.SSH
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSshHostActions.Read,
subject(ProjectPermissionSub.SshHosts, {
hostname: host.hostname
})
);
return host; return host;
}; };
@@ -430,7 +430,8 @@ export const validateExternalSshCaKeyPair = async (publicKey: string, privateKey
if (publicKey.trim() !== derivedPublicKey.trim()) { if (publicKey.trim() !== derivedPublicKey.trim()) {
throw new BadRequestError({ 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."
}); });
} }
+4 -2
View File
@@ -1381,10 +1381,12 @@ export const SSH_HOSTS = {
signedKey: "The SSH certificate or signed SSH public key." signedKey: "The SSH certificate or signed SSH public key."
}, },
GET_USER_CA_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: { 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."
} }
}; };
@@ -1,4 +1,4 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { import {
@@ -15,6 +15,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSecretActions, ProjectPermissionSecretActions,
ProjectPermissionSshHostActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
@@ -1077,7 +1078,7 @@ export const projectServiceFactory = ({
actor, actor,
projectId projectId
}: TListProjectSshHostsDTO) => { }: TListProjectSshHostsDTO) => {
await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId, projectId,
@@ -1086,8 +1087,27 @@ export const projectServiceFactory = ({
actionProjectType: ActionProjectType.SSH actionProjectType: ActionProjectType.SSH
}); });
const allowedHosts = [];
// (dangtony98): room to optimize
const hosts = await sshHostDAL.findSshHostsWithLoginMappings(projectId); 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;
}; };
/** /**
+17 -17
View File
@@ -13,7 +13,7 @@ unauthorized access, and SSH key sprawl.
The following concepts are useful to know when working with Infisical SSH: The following concepts are useful to know when working with Infisical SSH:
- SSH Certificate Authority (CA): A trusted authority that issues SSH certificates. - 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. - SSH Certificate: A short-lived, credential issued by the SSH CA granting time-bound access to infrastructure.
<div align="center"> <div align="center">
@@ -30,10 +30,10 @@ graph TD
</div> </div>
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. 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. to be used to access the host for a time-bound session as part of the SSH operation.
## Client Workflow ## 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: 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). 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. 3. The client uses the issued SSH certificate (and potentially SSH key pair) to temporarily access the host.
<Note> <Note>
@@ -83,12 +83,12 @@ In the following steps, we explore how to configure Infisical SSH to start issui
as part of the SSH operation. as part of the SSH operation.
<Steps> <Steps>
<Step title="Configuring a SSH CA for client key signing"> <Step title="Configuring an SSH CA for client key signing">
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) ![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. project; this CA will be used for client key signing.
![ssh create client ca](/images/platform/ssh/ssh-client-create-ca-1.png) ![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 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. - 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.
</Step> </Step>
<Step title="Configuring the remote host to trust the client"> <Step title="Configuring the remote host to trust the client">
@@ -185,7 +185,7 @@ infisical login
``` ```
</Step> </Step>
<Step title="Obtain a SSH certificate for the client and load it into the SSH agent"> <Step title="Obtain an SSH certificate for the client and load it into the SSH agent">
Run the `infisical ssh issue-credentials` command, specifying the `--addToAgent` flag to automatically load the SSH certificate into the SSH agent. Run the `infisical ssh issue-credentials` command, specifying the `--addToAgent` flag to automatically load the SSH certificate into the SSH agent.
```bash ```bash
infisical ssh issue-credentials --certificateTemplateId=<certificate-template-id> --principals=<username> --addToAgent infisical ssh issue-credentials --certificateTemplateId=<certificate-template-id> --principals=<username> --addToAgent
@@ -229,7 +229,7 @@ If the remote host does not have an existing SSH key pair, you can generate a ne
</Note> </Note>
<Steps> <Steps>
<Step title="Configuring a SSH CA for host key signing"> <Step title="Configuring an SSH CA for host key signing">
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. 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) ![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
</Step> </Step>
<Step title="Configuring the remote host with an SSH certificate"> <Step title="Configuring the remote host with an SSH certificate">
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) ![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) ![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 ```bash
sudo chmod 0640 /etc/ssh/ssh_host_key-cert.pub 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 ```bash
HostKey /etc/ssh/ssh_host_rsa_key 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. You should adjust the `HostKey` directive to match the path to the host's SSH private key as used in step 1.
</Note> </Note>
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 ```bash
sudo systemctl reload sshd 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
</Step> </Step>
<Step title="Configuring the client to trust the remote host"> <Step title="Configuring the client to trust the remote host">
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) ![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). 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).
</Note> </Note>
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 ```bash
@cert-authority *.example.com ssh-rsa ... @cert-authority *.example.com ssh-rsa ...
+13 -2
View File
@@ -6,8 +6,8 @@ description: "Learn how to securely provision user SSH access to your infrastruc
## Concept ## Concept
Infisical SSH can be used to provide users short-lived, secure SSH access to infrastructure; the underlying technology is powered by SSH certificates 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 on the limitations of traditional SSH key-based authentication via mitigation of private key compromise, static key management, and improves upon traditional SSH key-based authentication by mitigating private key compromise, static key management,
unauthorized access, and SSH key sprawl. unauthorized access, and SSH key sprawl.
The following entities and concepts are important to understand when using Infisical SSH: 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 📄 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
```
<Note>
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`.
</Note>
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. 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) ![ssh hosts](/images/platform/ssh/v2/ssh-added-hosts.png)
@@ -76,6 +76,7 @@ export enum ProjectPermissionGroupActions {
} }
export enum ProjectPermissionSshHostActions { export enum ProjectPermissionSshHostActions {
Read = "read",
Create = "create", Create = "create",
Edit = "edit", Edit = "edit",
Delete = "delete", Delete = "delete",
@@ -110,6 +110,7 @@ const GroupPolicyActionSchema = z.object({
}); });
const SshHostPolicyActionSchema = z.object({ const SshHostPolicyActionSchema = z.object({
[ProjectPermissionSshHostActions.Read]: z.boolean().optional(),
[ProjectPermissionSshHostActions.Create]: z.boolean().optional(), [ProjectPermissionSshHostActions.Create]: z.boolean().optional(),
[ProjectPermissionSshHostActions.Edit]: z.boolean().optional(), [ProjectPermissionSshHostActions.Edit]: z.boolean().optional(),
[ProjectPermissionSshHostActions.Delete]: z.boolean().optional(), [ProjectPermissionSshHostActions.Delete]: z.boolean().optional(),
@@ -610,6 +611,9 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
[ProjectPermissionSshHostActions.Create]: action.includes( [ProjectPermissionSshHostActions.Create]: action.includes(
ProjectPermissionSshHostActions.Create ProjectPermissionSshHostActions.Create
), ),
[ProjectPermissionSshHostActions.Read]: action.includes(
ProjectPermissionSshHostActions.Read
),
[ProjectPermissionSshHostActions.IssueHostCert]: action.includes( [ProjectPermissionSshHostActions.IssueHostCert]: action.includes(
ProjectPermissionSshHostActions.IssueHostCert ProjectPermissionSshHostActions.IssueHostCert
), ),
@@ -945,6 +949,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
[ProjectPermissionSub.SshHosts]: { [ProjectPermissionSub.SshHosts]: {
title: "SSH Hosts", title: "SSH Hosts",
actions: [ actions: [
{ label: "Read", value: ProjectPermissionSshHostActions.Read },
{ label: "Create", value: ProjectPermissionSshHostActions.Create }, { label: "Create", value: ProjectPermissionSshHostActions.Create },
{ label: "Modify", value: ProjectPermissionSshHostActions.Edit }, { label: "Modify", value: ProjectPermissionSshHostActions.Edit },
{ label: "Remove", value: ProjectPermissionSshHostActions.Delete }, { label: "Remove", value: ProjectPermissionSshHostActions.Delete },
@@ -42,7 +42,7 @@ const schema = z
(val) => ms(val) > 0, (val) => ms(val) > 0,
"TTL must be a valid time string such as 2 days, 1d, 2h 1y, ..." "TTL must be a valid time string such as 2 days, 1d, 2h 1y, ..."
) )
.default("8h"), .default("8h, 1d, 30m"),
loginMappings: z loginMappings: z
.object({ .object({
loginUser: z.string().trim().min(1), loginUser: z.string().trim().min(1),
@@ -206,7 +206,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
errorText={error?.message} errorText={error?.message}
isRequired isRequired
> >
<Input {...field} placeholder="8h" /> <Input {...field} placeholder="8h, 1d, 30m" />
</FormControl> </FormControl>
)} )}
/> />
@@ -328,7 +328,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
{(value.length === 0 ? [""] : value).map( {(value.length === 0 ? [""] : value).map(
(principal: string, principalIndex: number) => ( (principal: string, principalIndex: number) => (
<div <div
key={`${metadataFieldId}-principal-${principal || principalIndex}`} key={`${metadataFieldId}-principal-${principal}`}
className="flex items-center space-x-2" className="flex items-center space-x-2"
> >
<div className="flex-1"> <div className="flex-1">