Skip to content

Commit

Permalink
Merge pull request #1370 from platformsh/ssh-cert-only
Browse files Browse the repository at this point in the history
Only use the SSH certificate if available
  • Loading branch information
pjcdawkins authored Dec 23, 2023
2 parents 013832c + 7fd8435 commit 7a851a3
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 58 deletions.
49 changes: 26 additions & 23 deletions config-defaults.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,6 @@ api:
##
# certifier_url: 'https://auth.example.com'

## Wildcard domains for SSH configuration.
## This is also used to detect whether to run diagnostics on an SSH connection error.
## Can be replaced by a single value using the {application.env_prefix}SSH_DOMAIN_WILDCARD env var.
##
# ssh_domain_wildcards: ['*.example.com']

# The maximum lifetime (TTL) of objects in the local cache, in seconds.
projects_ttl: 600
environments_ttl: 120
Expand Down Expand Up @@ -246,23 +240,6 @@ api:
# Overridden by the {application.env_prefix}SESSION_ID env var.
session_id: 'default'

# Whether auto-generated SSH certificate identities should be added to the SSH agent.
# Enabling this may be useful for those who use agent forwarding.
add_to_ssh_agent: false

# Whether to auto-load an SSH certificate on login and SSH commands.
# Overridden by the {application.env_prefix}AUTO_LOAD_SSH_CERT env var
auto_load_ssh_cert: true

# Whether the user's SSH config file (~/.ssh/config) can be written automatically.
#
# The file is validated when SSH certificates are installed or refreshed.
#
# Setting this to true means it will be created or updated automatically,
# null means the user will be asked, false means it will not be touched
# though a recommendation will be displayed.
write_user_ssh_config: null

# Whether the Organizations API is enabled.
organizations: false

Expand All @@ -289,6 +266,12 @@ api:
git_push_timeout: 3600

ssh:
## Wildcard domains for SSH configuration.
## This is also used to detect whether to run diagnostics on an SSH connection error.
## Can be replaced by a single value using the {application.env_prefix}SSH_DOMAIN_WILDCARD env var.
##
# domain_wildcards: ['*.example.com']

# A file containing SSH host keys to install for the user.
# The filename is relative to the CLI root. Note it would need to be built
# into the Phar.
Expand All @@ -300,6 +283,26 @@ ssh:
# files will be written. If both are specified, both will be used.
host_keys: ''

# If an SSH certificate is available, configure SSH to ignore other keys.
cert_only: false

# Whether the user's SSH config file (~/.ssh/config) can be written automatically.
#
# The file is validated when SSH certificates are installed or refreshed.
#
# Setting this to true means it will be created or updated automatically,
# null means the user will be asked, false means it will not be touched
# though a recommendation will be displayed.
write_user_config: null

# Whether auto-generated SSH certificate identities should be added to the SSH agent.
# Enabling this may be useful for those who use agent forwarding.
add_to_agent: false

# Whether to auto-load an SSH certificate on login and SSH commands.
# Overridden by the {application.env_prefix}AUTO_LOAD_SSH_CERT env var
auto_load_cert: true

# How the CLI detects and configures Git repositories as projects.
detection:
## Required keys that must be defined elsewhere:
Expand Down
5 changes: 3 additions & 2 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,16 @@ api:
auth_url: 'https://auth.api.platform.sh'
oauth2_client_id: 'platform-cli'

ssh_domain_wildcards: ['*.platform.sh']

organizations: true
centralized_permissions: true
user_verification: true
metrics: true

vendor_filter: 'platformsh'

ssh:
domain_wildcards: ['*.platform.sh']

detection:
git_remote_name: 'platform'
git_domain: 'platform.sh' # matches git.eu-5.platform.sh, etc.
Expand Down
2 changes: 1 addition & 1 deletion src/Command/SshCert/SshCertLoadCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ protected function configure()
->addOption('new-key', null, InputOption::VALUE_NONE, '[Deprecated] Use --new instead')
->setDescription('Generate an SSH certificate');
$help = 'This command checks if a valid SSH certificate is present, and generates a new one if necessary.';
if ($this->config()->getWithDefault('api.auto_load_ssh_cert', false)) {
if ($this->config()->getWithDefault('ssh.auto_load_cert', false)) {
$envPrefix = $this->config()->get('application.env_prefix');
$help .= "\n\nCertificates allow you to make SSH connections without having previously uploaded a public key. They are more secure than keys and they allow for other features."
. "\n\nNormally the certificate is loaded automatically during login, or when making an SSH connection. So this command is seldom needed."
Expand Down
2 changes: 1 addition & 1 deletion src/Command/WelcomeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ protected function execute(InputInterface $input, OutputInterface $output)

$this->showSessionInfo();

if ($this->api()->isLoggedIn() && !$this->config()->getWithDefault('api.auto_load_ssh_cert', false)) {
if ($this->api()->isLoggedIn() && !$this->config()->getWithDefault('ssh.auto_load_cert', false)) {
/** @var \Platformsh\Cli\Service\SshKey $sshKey */
$sshKey = $this->getService('ssh_key');
if (!$sshKey->hasLocalKey()) {
Expand Down
20 changes: 17 additions & 3 deletions src/Service/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -370,9 +370,12 @@ private function applyEnvironmentOverrides()
'OAUTH2_TOKEN_URL' => 'api.oauth2_token_url',
'OAUTH2_REVOKE_URL' => 'api.oauth2_revoke_url',
'CERTIFIER_URL' => 'api.certifier_url',
'AUTO_LOAD_SSH_CERT' => 'api.auto_load_ssh_cert',
'AUTO_LOAD_SSH_CERT' => 'ssh.auto_load_cert',
'API_AUTO_LOAD_SSH_CERT' => 'ssh.auto_load_cert',
'USER_AGENT' => 'api.user_agent',
'API_DOMAIN_SUFFIX' => 'detection.api_domain_suffix',
'API_WRITE_USER_SSH_CONFIG' => 'ssh.write_user_config',
'API_ADD_TO_SSH_AGENT' => 'ssh.add_to_agent',
]);

foreach ($overrideMap as $var => $key) {
Expand All @@ -382,9 +385,9 @@ private function applyEnvironmentOverrides()
}
}

// Special case: replace the list api.ssh_domain_wildcards with the value of {PREFIX}SSH_DOMAIN_WILDCARD.
// Special case: replace the list ssh.domain_wildcards with the value of {PREFIX}SSH_DOMAIN_WILDCARD.
if (($value = $this->getEnv('SSH_DOMAIN_WILDCARD')) !== false) {
$this->config['api']['ssh_domain_wildcards'] = [$value];
$this->config['ssh']['domain_wildcards'] = [$value];
}

// Special case: {PREFIX}NO_LEGACY_WARNING disables the migration prompt.
Expand Down Expand Up @@ -637,6 +640,17 @@ private function applyDynamicDefaults()
if (!isset($this->config['service']['applications_config_file'])) {
$this->config['service']['applications_config_file'] = $this->get('service.project_config_dir') . '/applications.yaml';
}

// Migrate renamed config keys.
if (isset($this->config['api']['add_to_ssh_agent'])) {
$this->config['ssh']['add_to_agent'] = $this->config['api']['add_to_ssh_agent'];
}
if (isset($this->config['api']['auto_load_ssh_cert'])) {
$this->config['ssh']['auto_load_cert'] = $this->config['api']['auto_load_ssh_cert'];
}
if (isset($this->config['api']['ssh_domain_wildcards'])) {
$this->config['ssh']['domain_wildcards'] = $this->config['api']['ssh_domain_wildcards'];
}
}

private function applyUrlDefaults()
Expand Down
4 changes: 2 additions & 2 deletions src/Service/Ssh.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ protected function getSshOptions()
$options['IdentityFile'] = [
$this->sshConfig->formatFilePath($sshCert->privateKeyFilename()),
];
foreach ($this->sshConfig->getUserDefaultSshIdentityFiles() as $identityFile) {
$options['IdentityFile'][] = $this->sshConfig->formatFilePath($identityFile);
if ($this->certifier->useCertificateOnly()) {
$options['IdentitiesOnly'] = 'yes';
}
}
}
Expand Down
55 changes: 33 additions & 22 deletions src/Service/SshConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,38 +87,50 @@ public function configureSessionSsh()
$this->fs->remove($legacy);
}

$domainWildcards = $this->config->getWithDefault('api.ssh_domain_wildcards', []);
$domainWildcards = $this->config->getWithDefault('ssh.domain_wildcards', []);
if (!$domainWildcards) {
return false;
}

$lines = [];

if ($certificate = $this->certifier->getExistingCertificate()) {
$certificate = $this->certifier->getExistingCertificate();
$onlyCertificate = $certificate !== null && $this->certifier->useCertificateOnly();
if ($certificate) {
$executable = $this->config->get('application.executable');
$refreshCommand = sprintf('%s ssh-cert:load --refresh-only --yes --quiet', $executable);
if (!OsUtil::isWindows()) {
$refreshCommand .= ' 2>/dev/null';
}
// Use Match solely to run the refresh command.
$lines[] = '# Auto-refresh the SSH certificate:';
$lines[] = '# Auto-refresh the SSH certificate.';
$lines[] = sprintf('Match host "%s" exec "%s"', \implode(',', $domainWildcards), $refreshCommand);

$lines[] = '';
$lines[] = '# Cancel the Match, so that the following configuration will apply regardless of';
$lines[] = "# the command's execution.";
$lines[] = 'Host ' . implode(' ', $domainWildcards);

// Indentation in the SSH config is for readability (it has no other effect).
$lines[] = '';
$lines[] = '# Include the certificate and its key:';
$lines[] = '# Include the certificate and its key.';
$lines[] = sprintf('CertificateFile %s', $this->formatFilePath($certificate->certificateFilename()));
$lines[] = sprintf('IdentityFile %s', $this->formatFilePath($certificate->privateKeyFilename()));
if ($onlyCertificate) {
$lines[] = 'IdentitiesOnly yes';
}
} else {
$lines[] = 'Host ' . implode(' ', $domainWildcards);
}

$sessionIdentityFile = $this->sshKey->selectIdentity();
if ($sessionIdentityFile !== null) {
$lines[] = '';
$lines[] = '# This SSH key was detected as corresponding to the session:';
$lines[] = sprintf('IdentityFile %s', $this->formatFilePath($sessionIdentityFile));
$sessionIdentityFile = null;
if (!$onlyCertificate) {
$sessionIdentityFile = $this->sshKey->selectIdentity();
if ($sessionIdentityFile !== null) {
$lines[] = '';
$lines[] = '# This SSH key was detected as corresponding to the session.';
$lines[] = sprintf('IdentityFile %s', $this->formatFilePath($sessionIdentityFile));
$lines[] = '';
}
}

$sessionSpecificFilename = $this->getSessionSshDir() . DIRECTORY_SEPARATOR . 'config';
Expand All @@ -131,7 +143,7 @@ public function configureSessionSsh()
}

// Add default files if there is no preferred session identity file.
if ($sessionIdentityFile === null && ($defaultFiles = $this->getUserDefaultSshIdentityFiles())) {
if ($sessionIdentityFile === null && !$onlyCertificate && ($defaultFiles = $this->getUserDefaultSshIdentityFiles())) {
$lines[] = '';
$lines[] = '# Include SSH "default" identity files:';
foreach ($defaultFiles as $identityFile) {
Expand All @@ -147,15 +159,12 @@ public function configureSessionSsh()
'# It is updated automatically when certain CLI commands are run.',
];

$wildcards = $this->config->getWithDefault('api.ssh_domain_wildcards', []);
if (count($wildcards)) {
$includerLines[] = 'Host ' . implode(' ', $wildcards);
$includerLines[] = ' Include ' . $sessionSpecificFilename;
$this->writeSshIncludeFile(
$includerFilename,
$includerLines
);
}
$includerLines[] = 'Host ' . implode(' ', $domainWildcards);
$includerLines[] = ' Include ' . $sessionSpecificFilename;
$this->writeSshIncludeFile(
$includerFilename,
$includerLines
);

return true;
}
Expand Down Expand Up @@ -238,7 +247,7 @@ public function addUserSshConfig(QuestionHelper $questionHelper)

$filename = $this->getUserSshConfigFilename();

$wildcards = $this->config->getWithDefault('api.ssh_domain_wildcards', []);
$wildcards = $this->config->getWithDefault('ssh.domain_wildcards', []);
if (!$wildcards) {
return false;
}
Expand All @@ -252,7 +261,9 @@ public function addUserSshConfig(QuestionHelper $questionHelper)
$manualMessage = 'To configure SSH, add the following lines to: <comment>' . $filename . '</comment>'
. "\n" . $suggestedConfig;

$writeUserSshConfig = $this->config->getWithDefault('api.write_user_ssh_config', null);
$writeUserSshConfig = $this->config->has('api.write_user_ssh_config')
? $this->config->get('api.write_user_ssh_config')
: $this->config->getWithDefault('ssh.write_user_config', null);
if ($writeUserSshConfig === false) {
$this->stdErr->writeln($manualMessage);
return true;
Expand Down
2 changes: 1 addition & 1 deletion src/Service/SshDiagnostics.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public function sshHostIsInternal($uri)
return false;
}
// Check against the wildcard list.
foreach ($this->config->getWithDefault('api.ssh_domain_wildcards', []) as $wildcard) {
foreach ($this->config->getWithDefault('ssh.domain_wildcards', []) as $wildcard) {
if (\strpos($host, \str_replace('*.', '', $wildcard)) !== false) {
return true;
}
Expand Down
16 changes: 13 additions & 3 deletions src/SshCert/Certifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,17 @@ public function __construct(Api $api, Config $config, Shell $shell, Filesystem $
*/
public function isAutoLoadEnabled()
{
return !self::$disableAutoLoad && $this->config->getWithDefault('api.auto_load_ssh_cert', false);
return !self::$disableAutoLoad && $this->config->getWithDefault('ssh.auto_load_cert', false);
}

/**
* Whether to use the certificate only, if one is available.
*
* @return bool
*/
public function useCertificateOnly()
{
return $this->config->get('ssh.cert_only');
}

/**
Expand Down Expand Up @@ -99,7 +109,7 @@ private function doGenerateCertificate()
$this->fs->mkdir($dir, 0700);

// Remove the old certificate and key from the SSH agent.
if ($this->config->getWithDefault('api.add_to_ssh_agent', false)) {
if ($this->config->getWithDefault('ssh.add_to_agent', false)) {
$this->shell->execute(['ssh-add', '-d', $dir . DIRECTORY_SEPARATOR . self::PRIVATE_KEY_FILENAME], null, false, !$this->stdErr->isVeryVerbose());
}

Expand Down Expand Up @@ -129,7 +139,7 @@ private function doGenerateCertificate()
// Add the key to the SSH agent, if possible, silently.
// In verbose mode the full command will be printed, so the user can
// re-run it to check error details.
if ($this->config->getWithDefault('api.add_to_ssh_agent', false)) {
if ($this->config->getWithDefault('ssh.add_to_agent', false)) {
$lifetime = ($certificate->metadata()->getValidBefore() - time()) ?: 3600;
$this->shell->execute(['ssh-add', '-t', $lifetime, $sshPair['private']], null, false, !$this->stdErr->isVerbose());
}
Expand Down

0 comments on commit 7a851a3

Please sign in to comment.