Conversation
fiftin
commented
Feb 21, 2026
- feat(secrets): generate private ssh key on server
- feat(secrets): gen SSH key on server
- fix(secrets): show public key for updated secrets too
- feat(secrets): don't allow user override plain field
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Pull request overview
This pull request implements a server-side SSH key generation feature for Semaphore UI. Users can now optionally generate SSH key pairs on the server instead of providing their own private keys. The generated public key is displayed to users for copying to authorized_keys files on target systems.
Changes:
- Added server-side SSH key generation using RSA 2048-bit keys with public/private key pair creation
- Implemented UI components to display generated public keys in a dialog with copy-to-clipboard functionality
- Extended database schema to store public key metadata in the Plain field for SSH keys
- Added generate_ssh_key flag to control key generation behavior in the frontend and backend
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| services/server/access_key_svc.go | Implements maybeGenerateSSHPrivateKey function to generate SSH keys and store public key in Plain field |
| db/AccessKey.go | Adds GenerateSSHKey transient field for API communication |
| db/sql/access_key.go | Updates SQL queries to persist Plain field for both create and update operations |
| db/bolt/access_key.go | Contains commented implementation note for BoltDB Plain field handling |
| api/projects/keys.go | Returns Plain field in create response to enable frontend public key display |
| web/src/views/project/Keys.vue | Adds public key display dialog with copy functionality after key creation/update |
| web/src/components/KeyForm.vue | Adds generate_ssh_key checkbox and displays public key for existing generated keys |
|
|
||
| <v-checkbox | ||
| v-model="item.generate_ssh_key" | ||
| label="Generate SSH Key" |
There was a problem hiding this comment.
The checkbox label "Generate SSH Key" is hardcoded and not internationalized, while other labels in the form use the i18n translation function. This creates inconsistency with the rest of the codebase where all user-facing strings are translated.
Consider changing label="Generate SSH Key" to :label="$t('generateSshKey')" and adding the corresponding translation keys.
| label="Generate SSH Key" | |
| :label="$t('generateSshKey')" |
| <div style="position: relative"> | ||
| <pre | ||
| style=" | ||
| overflow: auto; | ||
| background: gray; | ||
| color: white; | ||
| border-radius: 10px; | ||
| margin-top: 5px; | ||
| " | ||
| class="pa-2" | ||
| >{{ publicKey }}</pre | ||
| > | ||
|
|
||
| <CopyClipboardButton | ||
| style="position: absolute; right: 0; top: 0; transform: scale(0.9);" |
There was a problem hiding this comment.
Inline styles are used extensively for the public key display section. Consider extracting these styles to a scoped <style> section or using CSS classes for better maintainability and consistency. This makes it easier to maintain and update styling across the application.
| <div style="position: relative"> | |
| <pre | |
| style=" | |
| overflow: auto; | |
| background: gray; | |
| color: white; | |
| border-radius: 10px; | |
| margin-top: 5px; | |
| " | |
| class="pa-2" | |
| >{{ publicKey }}</pre | |
| > | |
| <CopyClipboardButton | |
| style="position: absolute; right: 0; top: 0; transform: scale(0.9);" | |
| <div class="public-key-container"> | |
| <pre | |
| class="pa-2 public-key-display" | |
| >{{ publicKey }}</pre | |
| > | |
| <CopyClipboardButton | |
| class="public-key-copy-button" |
| style=" | ||
| overflow: auto; | ||
| background: gray; | ||
| color: white; | ||
| border-radius: 10px; | ||
| margin-top: 5px; | ||
| " | ||
| class="pa-2" |
There was a problem hiding this comment.
Inline styles are duplicated between KeyForm.vue and Keys.vue for displaying the public key. The same styling is applied to the <pre> element in both files (gray background, white color, border-radius, etc.). Consider creating a reusable component or shared styles to avoid this duplication and ensure consistency.
| style=" | |
| overflow: auto; | |
| background: gray; | |
| color: white; | |
| border-radius: 10px; | |
| margin-top: 5px; | |
| " | |
| class="pa-2" | |
| class="pa-2 mt-1 rounded overflow-auto grey darken-3 white--text" |
| const isGeneratedOnCreate = e && e.action === 'new'; | ||
| const isGeneratedOnUpdate = e && e.action === 'edit' && e.item && e.item.generate_ssh_key; |
There was a problem hiding this comment.
The logic for determining if a public key should be shown on update (line 156) assumes that if generate_ssh_key is true, a key was generated. However, this flag comes from the request body, not the response. If the backend fails to generate the key (but doesn't return an error), or if the key generation is skipped for some reason, the dialog may still appear with an empty public key. Consider adding an additional check to ensure the public key was actually generated before showing the dialog.
| func maybeGenerateSSHPrivateKey(key *db.AccessKey) error { | ||
| if !key.GenerateSSHKey || key.Type != db.AccessKeySSH { | ||
| key.Plain = nil | ||
| return nil | ||
| } | ||
|
|
||
| var b bytes.Buffer | ||
| privateKeyFile := bufio.NewWriter(&b) | ||
|
|
||
| publicKey, err := util.GeneratePrivateKey(privateKeyFile) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| err = privateKeyFile.Flush() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| key.SshKey.PrivateKey = b.String() | ||
|
|
||
| type sshPublicKey struct { | ||
| PublicKey string `json:"public_key"` | ||
| } | ||
|
|
||
| plainBytes, err := json.Marshal(sshPublicKey{ | ||
| PublicKey: publicKey, | ||
| }) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| plain := string(plainBytes) | ||
| key.Plain = &plain | ||
| return nil | ||
| } |
There was a problem hiding this comment.
The SSH key generation functionality introduced by this PR lacks automated test coverage. Consider adding tests to verify that maybeGenerateSSHPrivateKey correctly generates keys when GenerateSSHKey is true, sets the Plain field with the public key, and properly handles the case when GenerateSSHKey is false. Tests should also verify that the generated private key is valid and properly encrypted.
Example test cases to add:
- Test that GenerateSSHKey=true generates both private and public keys
- Test that GenerateSSHKey=false preserves existing private key
- Test that Plain field is set correctly with public key JSON
- Test that generated keys are valid RSA keys
| v-model="item.generate_ssh_key" | ||
| label="Generate SSH Key" | ||
| v-if="!isReadOnly && item.type === 'ssh'" | ||
| :disabled="formSaving || !canEditSecrets" |
There was a problem hiding this comment.
The checkbox for generating SSH keys is only disabled when canEditSecrets is false, but it should also be disabled for existing keys where override_secret is required. When editing an existing key without override_secret checked, users can toggle generate_ssh_key, but this won't have any effect because the secret won't be updated. This creates a confusing user experience.
Consider adding || (!isNew && !item.override_secret) to the disabled condition to make it clear that SSH key generation only works when secrets can be edited.
| :disabled="formSaving || !canEditSecrets" | |
| :disabled="formSaving || !canEditSecrets || (!isNew && !item.override_secret)" |
| :max-width="700" | ||
| v-model="createdPublicKeyDialog" | ||
| :save-button-text="null" | ||
| title="Generated SSH Public Key" |
There was a problem hiding this comment.
The dialog title "Generated SSH Public Key" is hardcoded and not internationalized. All other strings in the file use the i18n translation function (e.g., $t('create'), $t('save')). This string should be extracted to the translation files for consistency and to support multiple languages.
Consider changing title="Generated SSH Public Key" to title="$t('generatedSshPublicKey')" and adding the corresponding translation keys.
| title="Generated SSH Public Key" | |
| :title="$t('generatedSshPublicKey')" |