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

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)
}),
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) => {

View File

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

View File

@@ -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;
};

View File

@@ -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."
});
}

View File

@@ -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."
}
};

View File

@@ -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;
};
/**

View File

@@ -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.
<div align="center">
@@ -30,10 +30,10 @@ graph TD
</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.
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.
<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.
<Steps>
<Step title="Configuring a SSH CA for client key signing">
1.1. Start by creating a SSH project in the SSH tab of your organization.
<Step title="Configuring an SSH CA for client key signing">
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.
</Step>
<Step title="Configuring the remote host to trust the client">
@@ -185,7 +185,7 @@ infisical login
```
</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.
```bash
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>
<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.
![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 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)
@@ -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.
</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
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 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)
@@ -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).
</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
@cert-authority *.example.com ssh-rsa ...

View File

@@ -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
```
<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.
![ssh hosts](/images/platform/ssh/v2/ssh-added-hosts.png)

View File

@@ -76,6 +76,7 @@ export enum ProjectPermissionGroupActions {
}
export enum ProjectPermissionSshHostActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",

View File

@@ -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 },

View File

@@ -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
>
<Input {...field} placeholder="8h" />
<Input {...field} placeholder="8h, 1d, 30m" />
</FormControl>
)}
/>
@@ -328,7 +328,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
{(value.length === 0 ? [""] : value).map(
(principal: string, principalIndex: number) => (
<div
key={`${metadataFieldId}-principal-${principal || principalIndex}`}
key={`${metadataFieldId}-principal-${principal}`}
className="flex items-center space-x-2"
>
<div className="flex-1">