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

View File

@@ -631,7 +631,6 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSshHostActions.Edit,
ProjectPermissionSshHostActions.Create,
ProjectPermissionSshHostActions.Delete,
ProjectPermissionSshHostActions.IssueUserCert,
ProjectPermissionSshHostActions.IssueHostCert
],
ProjectPermissionSub.SshHosts
@@ -885,8 +884,6 @@ const buildMemberPermissionRules = () => {
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
can([ProjectPermissionSshHostActions.IssueUserCert], ProjectPermissionSub.SshHosts);
can(
[
ProjectPermissionCmekActions.Create,

View File

@@ -92,11 +92,11 @@ export const sshHostServiceFactory = ({
userDAL
});
const allAllowedHosts = [];
const allowedHosts = [];
for await (const project of sshProjects) {
try {
const { permission } = await permissionService.getProjectPermission({
await permissionService.getProjectPermission({
actor,
actorId,
projectId: project.id,
@@ -107,20 +107,13 @@ export const sshHostServiceFactory = ({
const projectHosts = await sshHostDAL.findSshHostsWithPrincipalsAcrossProjects([project.id], principals);
const allowedHosts = projectHosts.filter((host) =>
permission.can(
ProjectPermissionSshHostActions.IssueUserCert,
subject(ProjectPermissionSub.SshHosts, { hostname: host.hostname })
)
);
allAllowedHosts.push(...allowedHosts);
allowedHosts.push(...projectHosts);
} catch {
// intentionally ignore projects where user lacks access
}
}
return allAllowedHosts;
return allowedHosts;
};
const createSshHost = async ({
@@ -370,7 +363,7 @@ export const sshHostServiceFactory = ({
});
}
const { permission } = await permissionService.getProjectPermission({
await permissionService.getProjectPermission({
actor,
actorId,
projectId: host.projectId,
@@ -379,13 +372,22 @@ export const sshHostServiceFactory = ({
actionProjectType: ActionProjectType.SSH
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSshHostActions.IssueUserCert,
subject(ProjectPermissionSub.SshHosts, {
hostname: host.hostname
})
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`
});
}
const keyId = `${actor}-${actorId}`;
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.userSshCaId });
@@ -403,22 +405,6 @@ export const sshHostServiceFactory = ({
const keyAlgorithm = SshCertKeyAlgorithm.ED25519;
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
const principals = [...internalPrincipals, loginUser];
@@ -519,7 +505,7 @@ export const sshHostServiceFactory = ({
});
const principals = [host.hostname];
const keyId = `host:${host.hostname}`;
const keyId = `host-${host.id}`;
const { serialNumber, signedPublicKey, ttl } = await createSshCert({
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),

View File

@@ -254,7 +254,7 @@ func issueCredentials(cmd *cobra.Command, args []string) {
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")
}
@@ -774,6 +774,85 @@ func sshAddHost(cmd *cobra.Command, args []string) {
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()
if err != nil {
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)
publicKey, err := client.Ssh().GetSshHostUserCaPublicKey(host.ID)
if err != nil {
util.HandleError(err, "Failed to fetch associated User CA public key")
}
if writeUserCaToFile {
// Expand ~ if used in file path
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)
publicKey, err := client.Ssh().GetSshHostUserCaPublicKey(host.ID)
if err != nil {
util.HandleError(err, "Failed to fetch associated User CA public key")
}
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)
}
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() {
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")
@@ -850,6 +981,9 @@ func init() {
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().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)

View File

@@ -79,7 +79,6 @@ export enum ProjectPermissionSshHostActions {
Create = "create",
Edit = "edit",
Delete = "delete",
IssueUserCert = "issue-user-cert",
IssueHostCert = "issue-host-cert"
}

View File

@@ -103,7 +103,6 @@ const SshHostPolicyActionSchema = z.object({
[ProjectPermissionSshHostActions.Create]: z.boolean().optional(),
[ProjectPermissionSshHostActions.Edit]: z.boolean().optional(),
[ProjectPermissionSshHostActions.Delete]: z.boolean().optional(),
[ProjectPermissionSshHostActions.IssueUserCert]: z.boolean().optional(),
[ProjectPermissionSshHostActions.IssueHostCert]: z.boolean().optional()
});
@@ -570,9 +569,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
[ProjectPermissionSshHostActions.Create]: action.includes(
ProjectPermissionSshHostActions.Create
),
[ProjectPermissionSshHostActions.IssueUserCert]: action.includes(
ProjectPermissionSshHostActions.IssueUserCert
),
[ProjectPermissionSshHostActions.IssueHostCert]: action.includes(
ProjectPermissionSshHostActions.IssueHostCert
),
@@ -911,7 +907,6 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
{ label: "Create", value: ProjectPermissionSshHostActions.Create },
{ label: "Modify", value: ProjectPermissionSshHostActions.Edit },
{ label: "Remove", value: ProjectPermissionSshHostActions.Delete },
{ label: "Issue User Certificate", value: ProjectPermissionSshHostActions.IssueUserCert },
{ label: "Issue Host Certificate", value: ProjectPermissionSshHostActions.IssueHostCert }
]
},