Add configure sshd flag to infisical ssh add-host command, update issue user cert permissioning

This commit is contained in:
Tuan Dang
2025-04-09 14:41:10 -07:00
parent 5a114586dc
commit 2382937385
5 changed files with 167 additions and 56 deletions
@@ -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
View File
@@ -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"
} }
@@ -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 }
] ]
}, },