mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 02:26:40 +00:00
Add configure sshd flag to infisical ssh add-host command, update issue user cert permissioning
This commit is contained in:
@@ -631,7 +631,6 @@ const buildAdminPermissionRules = () => {
|
|||||||
ProjectPermissionSshHostActions.Edit,
|
ProjectPermissionSshHostActions.Edit,
|
||||||
ProjectPermissionSshHostActions.Create,
|
ProjectPermissionSshHostActions.Create,
|
||||||
ProjectPermissionSshHostActions.Delete,
|
ProjectPermissionSshHostActions.Delete,
|
||||||
ProjectPermissionSshHostActions.IssueUserCert,
|
|
||||||
ProjectPermissionSshHostActions.IssueHostCert
|
ProjectPermissionSshHostActions.IssueHostCert
|
||||||
],
|
],
|
||||||
ProjectPermissionSub.SshHosts
|
ProjectPermissionSub.SshHosts
|
||||||
@@ -885,8 +884,6 @@ const buildMemberPermissionRules = () => {
|
|||||||
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
|
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
||||||
|
|
||||||
can([ProjectPermissionSshHostActions.IssueUserCert], ProjectPermissionSub.SshHosts);
|
|
||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
ProjectPermissionCmekActions.Create,
|
ProjectPermissionCmekActions.Create,
|
||||||
|
|||||||
@@ -92,11 +92,11 @@ export const sshHostServiceFactory = ({
|
|||||||
userDAL
|
userDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const allAllowedHosts = [];
|
const allowedHosts = [];
|
||||||
|
|
||||||
for await (const project of sshProjects) {
|
for await (const project of sshProjects) {
|
||||||
try {
|
try {
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
@@ -107,20 +107,13 @@ export const sshHostServiceFactory = ({
|
|||||||
|
|
||||||
const projectHosts = await sshHostDAL.findSshHostsWithPrincipalsAcrossProjects([project.id], principals);
|
const projectHosts = await sshHostDAL.findSshHostsWithPrincipalsAcrossProjects([project.id], principals);
|
||||||
|
|
||||||
const allowedHosts = projectHosts.filter((host) =>
|
allowedHosts.push(...projectHosts);
|
||||||
permission.can(
|
|
||||||
ProjectPermissionSshHostActions.IssueUserCert,
|
|
||||||
subject(ProjectPermissionSub.SshHosts, { hostname: host.hostname })
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
allAllowedHosts.push(...allowedHosts);
|
|
||||||
} catch {
|
} catch {
|
||||||
// intentionally ignore projects where user lacks access
|
// intentionally ignore projects where user lacks access
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return allAllowedHosts;
|
return allowedHosts;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createSshHost = async ({
|
const createSshHost = async ({
|
||||||
@@ -370,7 +363,7 @@ export const sshHostServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId: host.projectId,
|
projectId: host.projectId,
|
||||||
@@ -379,13 +372,22 @@ export const sshHostServiceFactory = ({
|
|||||||
actionProjectType: ActionProjectType.SSH
|
actionProjectType: ActionProjectType.SSH
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
const internalPrincipals = await convertActorToPrincipals({
|
||||||
ProjectPermissionSshHostActions.IssueUserCert,
|
actor,
|
||||||
subject(ProjectPermissionSub.SshHosts, {
|
actorId,
|
||||||
hostname: host.hostname
|
userDAL
|
||||||
})
|
});
|
||||||
|
|
||||||
|
const mapping = host.loginMappings.find(
|
||||||
|
(m) => m.loginUser === loginUser && m.allowedPrincipals.some((allowed) => internalPrincipals.includes(allowed))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!mapping) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: `You are not allowed to login as ${loginUser} on this host`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const keyId = `${actor}-${actorId}`;
|
const keyId = `${actor}-${actorId}`;
|
||||||
|
|
||||||
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.userSshCaId });
|
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.userSshCaId });
|
||||||
@@ -403,22 +405,6 @@ export const sshHostServiceFactory = ({
|
|||||||
const keyAlgorithm = SshCertKeyAlgorithm.ED25519;
|
const keyAlgorithm = SshCertKeyAlgorithm.ED25519;
|
||||||
const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm);
|
const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm);
|
||||||
|
|
||||||
const internalPrincipals = await convertActorToPrincipals({
|
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
userDAL
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapping = host.loginMappings.find(
|
|
||||||
(m) => m.loginUser === loginUser && m.allowedPrincipals.some((allowed) => internalPrincipals.includes(allowed))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!mapping) {
|
|
||||||
throw new UnauthorizedError({
|
|
||||||
message: `You are not allowed to login as ${loginUser} on this host`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// (dangtony98): include the loginUser as a principal on the issued certificate
|
// (dangtony98): include the loginUser as a principal on the issued certificate
|
||||||
const principals = [...internalPrincipals, loginUser];
|
const principals = [...internalPrincipals, loginUser];
|
||||||
|
|
||||||
@@ -519,7 +505,7 @@ export const sshHostServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const principals = [host.hostname];
|
const principals = [host.hostname];
|
||||||
const keyId = `host:${host.hostname}`;
|
const keyId = `host-${host.id}`;
|
||||||
|
|
||||||
const { serialNumber, signedPublicKey, ttl } = await createSshCert({
|
const { serialNumber, signedPublicKey, ttl } = await createSshCert({
|
||||||
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
|
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
|
||||||
|
|||||||
+147
-13
@@ -254,7 +254,7 @@ func issueCredentials(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to parse addToAgent flag")
|
util.HandleError(err, "Unable to parse addToAgent flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
if outFilePath == "" && addToAgent == false {
|
if outFilePath == "" && !addToAgent {
|
||||||
util.PrintErrorMessageAndExit("You must provide either --outFilePath or --addToAgent flag to use this command")
|
util.PrintErrorMessageAndExit("You must provide either --outFilePath or --addToAgent flag to use this command")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -774,6 +774,85 @@ func sshAddHost(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to parse --userCaOutFilePath flag")
|
util.HandleError(err, "Unable to parse --userCaOutFilePath flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writeHostCertToFile, err := cmd.Flags().GetBool("writeHostCertToFile")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse --writeHostCertToFile flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
configureSshd, err := cmd.Flags().GetBool("configureSshd")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse --configureSshd flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
forceOverwrite, err := cmd.Flags().GetBool("force")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse --force flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
if configureSshd && (!writeUserCaToFile || !writeHostCertToFile) {
|
||||||
|
util.PrintErrorMessageAndExit("--configureSshd requires both --writeUserCaToFile and --writeHostCertToFile to also be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-check for file overwrites before proceeding
|
||||||
|
if writeUserCaToFile {
|
||||||
|
if strings.HasPrefix(userCaOutFilePath, "~") {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to resolve ~ in userCaOutFilePath")
|
||||||
|
}
|
||||||
|
userCaOutFilePath = strings.Replace(userCaOutFilePath, "~", homeDir, 1)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(userCaOutFilePath); err == nil && !forceOverwrite {
|
||||||
|
util.PrintErrorMessageAndExit("File already exists at " + userCaOutFilePath + ". Use --force to overwrite.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keyTypes := []string{"ed25519", "ecdsa", "rsa"}
|
||||||
|
var hostKeyPath, certOutPath, hostPrivateKeyPath string
|
||||||
|
if writeHostCertToFile {
|
||||||
|
for _, keyType := range keyTypes {
|
||||||
|
pub := fmt.Sprintf("/etc/ssh/ssh_host_%s_key.pub", keyType)
|
||||||
|
cert := fmt.Sprintf("/etc/ssh/ssh_host_%s_key-cert.pub", keyType)
|
||||||
|
priv := fmt.Sprintf("/etc/ssh/ssh_host_%s_key", keyType)
|
||||||
|
|
||||||
|
if _, err := os.Stat(pub); err == nil {
|
||||||
|
hostKeyPath = pub
|
||||||
|
certOutPath = cert
|
||||||
|
hostPrivateKeyPath = priv
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hostKeyPath == "" {
|
||||||
|
util.PrintErrorMessageAndExit("No supported SSH host public key found at /etc/ssh")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(certOutPath); err == nil && !forceOverwrite && writeHostCertToFile {
|
||||||
|
util.PrintErrorMessageAndExit("File already exists at " + certOutPath + ". Use --force to overwrite.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if configureSshd {
|
||||||
|
sshdConfig := "/etc/ssh/sshd_config"
|
||||||
|
existing, err := os.ReadFile(sshdConfig)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to read sshd_config")
|
||||||
|
}
|
||||||
|
configLines := []string{
|
||||||
|
"TrustedUserCAKeys " + userCaOutFilePath,
|
||||||
|
"HostKey " + hostPrivateKeyPath,
|
||||||
|
"HostCertificate " + certOutPath,
|
||||||
|
}
|
||||||
|
for _, line := range configLines {
|
||||||
|
for _, existingLine := range strings.Split(string(existing), "\n") {
|
||||||
|
trimmed := strings.TrimSpace(existingLine)
|
||||||
|
if trimmed == line && !strings.HasPrefix(trimmed, "#") && !forceOverwrite {
|
||||||
|
util.PrintErrorMessageAndExit("sshd_config already contains: " + line + ". Use --force to overwrite.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
customHeaders, err := util.GetInfisicalCustomHeadersMap()
|
customHeaders, err := util.GetInfisicalCustomHeadersMap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to get custom headers")
|
util.HandleError(err, "Unable to get custom headers")
|
||||||
@@ -797,19 +876,10 @@ func sshAddHost(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
fmt.Println("✅ Successfully registered host:", host.Hostname)
|
fmt.Println("✅ Successfully registered host:", host.Hostname)
|
||||||
|
|
||||||
publicKey, err := client.Ssh().GetSshHostUserCaPublicKey(host.ID)
|
|
||||||
if err != nil {
|
|
||||||
util.HandleError(err, "Failed to fetch associated User CA public key")
|
|
||||||
}
|
|
||||||
|
|
||||||
if writeUserCaToFile {
|
if writeUserCaToFile {
|
||||||
// Expand ~ if used in file path
|
publicKey, err := client.Ssh().GetSshHostUserCaPublicKey(host.ID)
|
||||||
if strings.HasPrefix(userCaOutFilePath, "~") {
|
if err != nil {
|
||||||
homeDir, err := os.UserHomeDir()
|
util.HandleError(err, "Failed to fetch associated User CA public key")
|
||||||
if err != nil {
|
|
||||||
util.HandleError(err, "Unable to resolve ~ in userCaOutFilePath")
|
|
||||||
}
|
|
||||||
userCaOutFilePath = strings.Replace(userCaOutFilePath, "~", homeDir, 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := writeToFile(userCaOutFilePath, publicKey, 0644); err != nil {
|
if err := writeToFile(userCaOutFilePath, publicKey, 0644); err != nil {
|
||||||
@@ -818,7 +888,68 @@ func sshAddHost(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
fmt.Println("📁 Wrote User CA public key to:", userCaOutFilePath)
|
fmt.Println("📁 Wrote User CA public key to:", userCaOutFilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if writeHostCertToFile {
|
||||||
|
pubKeyBytes, err := os.ReadFile(hostKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to read SSH host public key")
|
||||||
|
}
|
||||||
|
res, err := client.Ssh().IssueSshHostHostCert(host.ID, infisicalSdk.IssueSshHostHostCertOptions{
|
||||||
|
PublicKey: string(pubKeyBytes),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to issue SSH host certificate")
|
||||||
|
}
|
||||||
|
if err := writeToFile(certOutPath, res.SignedKey, 0644); err != nil {
|
||||||
|
util.HandleError(err, "Failed to write SSH host certificate to file")
|
||||||
|
}
|
||||||
|
fmt.Println("📁 Wrote host certificate to:", certOutPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if configureSshd {
|
||||||
|
sshdConfig := "/etc/ssh/sshd_config"
|
||||||
|
contentBytes, err := os.ReadFile(sshdConfig)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to read sshd_config")
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(contentBytes), "\n")
|
||||||
|
|
||||||
|
configMap := map[string]string{
|
||||||
|
"TrustedUserCAKeys": userCaOutFilePath,
|
||||||
|
"HostKey": hostPrivateKeyPath,
|
||||||
|
"HostCertificate": certOutPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
seenKeys := map[string]bool{}
|
||||||
|
for i, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
for key, value := range configMap {
|
||||||
|
if strings.HasPrefix(trimmed, key+" ") {
|
||||||
|
seenKeys[key] = true
|
||||||
|
if strings.HasPrefix(trimmed, "#") || forceOverwrite {
|
||||||
|
lines[i] = fmt.Sprintf("%s %s", key, value)
|
||||||
|
} else {
|
||||||
|
util.PrintErrorMessageAndExit("sshd_config already contains: " + trimmed + ". Use --force to overwrite.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append missing lines
|
||||||
|
for key, value := range configMap {
|
||||||
|
if !seenKeys[key] {
|
||||||
|
lines = append(lines, fmt.Sprintf("%s %s", key, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write back to file
|
||||||
|
if err := os.WriteFile(sshdConfig, []byte(strings.Join(lines, "\n")), 0644); err != nil {
|
||||||
|
util.HandleError(err, "Failed to update sshd_config")
|
||||||
|
}
|
||||||
|
fmt.Println("📄 Updated sshd_config entries")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
sshSignKeyCmd.Flags().String("token", "", "Issue SSH certificate using machine identity access token")
|
sshSignKeyCmd.Flags().String("token", "", "Issue SSH certificate using machine identity access token")
|
||||||
sshSignKeyCmd.Flags().String("certificateTemplateId", "", "The ID of the SSH certificate template to issue the SSH certificate for")
|
sshSignKeyCmd.Flags().String("certificateTemplateId", "", "The ID of the SSH certificate template to issue the SSH certificate for")
|
||||||
@@ -850,6 +981,9 @@ func init() {
|
|||||||
sshAddHostCmd.Flags().String("hostname", "", "Hostname of the SSH host (required)")
|
sshAddHostCmd.Flags().String("hostname", "", "Hostname of the SSH host (required)")
|
||||||
sshAddHostCmd.Flags().Bool("writeUserCaToFile", false, "Write User CA public key to /etc/ssh/infisical_user_ca.pub")
|
sshAddHostCmd.Flags().Bool("writeUserCaToFile", false, "Write User CA public key to /etc/ssh/infisical_user_ca.pub")
|
||||||
sshAddHostCmd.Flags().String("userCaOutFilePath", "/etc/ssh/infisical_user_ca.pub", "Custom file path to write the User CA public key")
|
sshAddHostCmd.Flags().String("userCaOutFilePath", "/etc/ssh/infisical_user_ca.pub", "Custom file path to write the User CA public key")
|
||||||
|
sshAddHostCmd.Flags().Bool("writeHostCertToFile", false, "Write SSH host certificate to /etc/ssh/ssh_host_<type>_key-cert.pub")
|
||||||
|
sshAddHostCmd.Flags().Bool("configureSshd", false, "Update TrustedUserCAKeys, HostKey, and HostCertificate in the sshd_config file")
|
||||||
|
sshAddHostCmd.Flags().Bool("force", false, "Force overwrite of existing certificate files as part of writeUserCaToFile and writeHostCertToFile")
|
||||||
|
|
||||||
sshCmd.AddCommand(sshAddHostCmd)
|
sshCmd.AddCommand(sshAddHostCmd)
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ export enum ProjectPermissionSshHostActions {
|
|||||||
Create = "create",
|
Create = "create",
|
||||||
Edit = "edit",
|
Edit = "edit",
|
||||||
Delete = "delete",
|
Delete = "delete",
|
||||||
IssueUserCert = "issue-user-cert",
|
|
||||||
IssueHostCert = "issue-host-cert"
|
IssueHostCert = "issue-host-cert"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-5
@@ -103,7 +103,6 @@ const SshHostPolicyActionSchema = z.object({
|
|||||||
[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(),
|
||||||
[ProjectPermissionSshHostActions.IssueUserCert]: z.boolean().optional(),
|
|
||||||
[ProjectPermissionSshHostActions.IssueHostCert]: z.boolean().optional()
|
[ProjectPermissionSshHostActions.IssueHostCert]: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -570,9 +569,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
[ProjectPermissionSshHostActions.Create]: action.includes(
|
[ProjectPermissionSshHostActions.Create]: action.includes(
|
||||||
ProjectPermissionSshHostActions.Create
|
ProjectPermissionSshHostActions.Create
|
||||||
),
|
),
|
||||||
[ProjectPermissionSshHostActions.IssueUserCert]: action.includes(
|
|
||||||
ProjectPermissionSshHostActions.IssueUserCert
|
|
||||||
),
|
|
||||||
[ProjectPermissionSshHostActions.IssueHostCert]: action.includes(
|
[ProjectPermissionSshHostActions.IssueHostCert]: action.includes(
|
||||||
ProjectPermissionSshHostActions.IssueHostCert
|
ProjectPermissionSshHostActions.IssueHostCert
|
||||||
),
|
),
|
||||||
@@ -911,7 +907,6 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
|||||||
{ 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 },
|
||||||
{ label: "Issue User Certificate", value: ProjectPermissionSshHostActions.IssueUserCert },
|
|
||||||
{ label: "Issue Host Certificate", value: ProjectPermissionSshHostActions.IssueHostCert }
|
{ label: "Issue Host Certificate", value: ProjectPermissionSshHostActions.IssueHostCert }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user