797bc2f412
orca node join --type proxmox bootstraps a remote Proxmox VE 8/9 host
via SSH (REQ-050, REQ-051). The password is used only for initial auth;
subsequent access uses the deployed orca SSH key (D-031).
Changes:
- go.mod: add golang.org/x/crypto v0.54.0 (ssh + ssh/knownhosts + ed25519)
bump x/sys to v0.47.0, add x/term (indirect)
- internal/certpaths: SSHKeyPath, SSHPubPath, KnownHostsPath (D-037)
- internal/security/sshkey.go: GenerateOrLoadSSHKey (Ed25519, PKCS8 PEM,
0600/0644 modes, idempotent load per D-036)
- internal/proxmox/bootstrap.go: BootstrapProxmox SSH dance:
1. Generate/load SSH key
2. SSH dial (password + knownhosts.New TOFU per D-035)
3. Deploy pubkey to ~orca/.ssh/authorized_keys (idempotent)
4. useradd -m orca (idempotent)
5. pveum role add OrcaOperator --privs 'VM.Audit Datastore.AllocateSpace SDN.Use'
6. pveum user add orca@pam (AD-019: PAM realm, not @pve)
7. pveum acl modify / -user orca@pam -role OrcaOperator
8. Write /etc/sudoers.d/orca (AD-020: NOEXEC on pct/qm, no NOEXEC on
apt-get/dpkg, pvesh EXCLUDED — API execute bypasses NOEXEC)
9. visudo -cf validation (abort on failure)
All steps idempotent; audit-logged.
- internal/cli/node.go: --type/--host/--ssh-user/--password/--ssh-port/
--proxmox-user/--proxmox-role flags; joinProxmox() wires to
proxmox.BootstrapProxmox + registers node with kind=proxmox, os=pve.
Password zeroed after use (D-031).
- tests: sshkey generate/load round-trip, idempotency, file modes;
proxmox sudoers content (NOEXEC/NOPASSWD/pvesh-excluded),
privilege set, validation; node join flag wiring
---ci---
project: orca
phase: 2
milestone: v0.6
status: execute
---/ci---
65 lines
2.3 KiB
Go
65 lines
2.3 KiB
Go
// Package certpaths centralizes the on-disk locations of the CA and
|
|
// server cert/key files. The CLI layer, the security layer, and the
|
|
// doctor layer all need to agree on these paths, so they're factored
|
|
// into their own package to avoid import cycles (cli <-> doctor).
|
|
package certpaths
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
const (
|
|
defaultCADir = ".orca"
|
|
caCertFilename = "ca.crt"
|
|
caKeyFilename = "ca.key"
|
|
)
|
|
|
|
// Dir returns the directory the local CA lives in. Honors $ORCA_HOME
|
|
// for testability; otherwise defaults to ~/.orca.
|
|
func Dir() string {
|
|
if p := os.Getenv("ORCA_HOME"); p != "" {
|
|
return p
|
|
}
|
|
home, _ := os.UserHomeDir()
|
|
return filepath.Join(home, defaultCADir)
|
|
}
|
|
|
|
// CACertPath returns the path to ca.crt.
|
|
func CACertPath() string { return filepath.Join(Dir(), caCertFilename) }
|
|
|
|
// CAKeyPath returns the path to ca.key.
|
|
func CAKeyPath() string { return filepath.Join(Dir(), caKeyFilename) }
|
|
|
|
// ServerCertPath returns the path to server.crt.
|
|
func ServerCertPath() string { return filepath.Join(Dir(), "server.crt") }
|
|
|
|
// ServerKeyPath returns the path to server.key.
|
|
func ServerKeyPath() string { return filepath.Join(Dir(), "server.key") }
|
|
|
|
// DBPath returns the path to the orca SQLite database. Honors $ORCA_DB
|
|
// for testability and explicit override; otherwise defaults to
|
|
// ~/.orca/orca.db under the same Dir() as the cert files.
|
|
func DBPath() string {
|
|
if p := os.Getenv("ORCA_DB"); p != "" {
|
|
return p
|
|
}
|
|
return filepath.Join(Dir(), "orca.db")
|
|
}
|
|
|
|
// SSHKeyPath returns the path to the orca SSH private key (Ed25519,
|
|
// D-037). Used by `orca node join --type proxmox` to authenticate
|
|
// to remote Proxmox hosts after the initial password-based bootstrap.
|
|
// File mode 0600 (enforced by security.WriteKey).
|
|
func SSHKeyPath() string { return filepath.Join(Dir(), "orca_ssh_key") }
|
|
|
|
// SSHPubPath returns the path to the orca SSH public key (authorized_keys
|
|
// format). Deployed to remote Proxmox hosts during `orca node join`.
|
|
// File mode 0644 (enforced by security.WriteCert).
|
|
func SSHPubPath() string { return filepath.Join(Dir(), "orca_ssh_key.pub") }
|
|
|
|
// KnownHostsPath returns the path to the SSH known_hosts file used for
|
|
// TOFU host-key pinning (D-035). Captured on first connect, verified
|
|
// on all subsequent connects via golang.org/x/crypto/ssh/knownhosts.
|
|
func KnownHostsPath() string { return filepath.Join(Dir(), "known_hosts") }
|