Skip to content

auto generate additional ssh keys #33974

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
62 changes: 62 additions & 0 deletions cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
package cmd

import (
"bufio"
"encoding/pem"
"fmt"
"os"
"strings"

"code.gitea.io/gitea/modules/generate"

"github.com/mattn/go-isatty"
"github.com/urfave/cli/v2"
"golang.org/x/crypto/ssh"
)

var (
Expand All @@ -21,6 +25,7 @@ var (
Usage: "Generate Gitea's secrets/keys/tokens",
Subcommands: []*cli.Command{
subcmdSecret,
subcmdKeygen,
},
}

Expand All @@ -33,6 +38,17 @@ var (
microcmdGenerateSecretKey,
},
}
keygenFlags = []cli.Flag{
&cli.StringFlag{Name: "bits", Aliases: []string{"b"}, Usage: "Number of bits in the key, ignored when key is ed25519"},
&cli.StringFlag{Name: "type", Aliases: []string{"t"}, Value: "ed25519", Usage: "Keytype to generate"},
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, Usage: "Specifies the filename of the key file", Required: true},
}
subcmdKeygen = &cli.Command{
Name: "ssh-keygen",
Usage: "Generate a ssh keypair",
Flags: keygenFlags,
Action: runGenerateKeyPair,
}

microcmdGenerateInternalToken = &cli.Command{
Name: "INTERNAL_TOKEN",
Expand Down Expand Up @@ -98,3 +114,49 @@ func runGenerateSecretKey(c *cli.Context) error {

return nil
}

func runGenerateKeyPair(c *cli.Context) error {
file := c.String("file")

// Check if file exists to prevent overwrites
if _, err := os.Stat(file); err == nil {
scanner := bufio.NewScanner(os.Stdin)
fmt.Printf("%s already exists.\nOverwrite (y/n)? ", file)
scanner.Scan()
if strings.ToLower(strings.TrimSpace(scanner.Text())) != "y" {
fmt.Println("Aborting")
return nil
}
}
keytype := c.String("type")
bits := c.Int("bits")
// provide defaults for bits, ed25519 ignores bit length so it's omitted
if bits == 0 {
if keytype == "rsa" {
bits = 3072
} else {
bits = 256
}
}

pub, priv, err := generate.NewSSHKey(keytype, bits)
if err != nil {
return err
}
f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it avoid overwriting existing file?

/tmp$ ssh-keygen -t ecdsa -f a
Generating public/private ecdsa key pair.
a already exists.
Overwrite (y/n)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be a good idea, I'll try to figure out how to do it.
Would comments also be a good idea to include?

if err != nil {
return err
}
defer f.Close()
err = pem.Encode(f, priv)
if err != nil {
return err
}
fmt.Printf("Your identification has been saved in %s\n", file)
err = os.WriteFile(file+".pub", ssh.MarshalAuthorizedKey(pub), 0o644)
if err != nil {
return err
}
fmt.Printf("Your public key has been saved in %s", file+".pub")
return nil
}
63 changes: 63 additions & 0 deletions modules/generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@
package generate

import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"encoding/pem"
"fmt"
"io"
"time"

"code.gitea.io/gitea/modules/util"

"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/ssh"
)

// NewInternalToken generate a new value intended to be used by INTERNAL_TOKEN.
Expand Down Expand Up @@ -72,3 +79,59 @@ func NewSecretKey() (string, error) {

return secretKey, nil
}

func NewSSHKey(keytype string, bits int) (ssh.PublicKey, *pem.Block, error) {
pub, priv, err := commonKeyGen(keytype, bits)
if err != nil {
return nil, nil, err
}
pemPriv, err := ssh.MarshalPrivateKey(priv, "")
if err != nil {
return nil, nil, err
}
sshPub, err := ssh.NewPublicKey(pub)
if err != nil {
return nil, nil, err
}

return sshPub, pemPriv, nil
}

// commonKeyGen is an abstraction over rsa, ecdsa and ed25519 generating functions
func commonKeyGen(keytype string, bits int) (publicKey, privateKey crypto.PublicKey, err error) {
switch keytype {
case "rsa":
privateKey, err := rsa.GenerateKey(rand.Reader, bits)
if err != nil {
return nil, nil, err
}
return &privateKey.PublicKey, privateKey, nil
case "ed25519":
return ed25519.GenerateKey(rand.Reader)
case "ecdsa":
curve, err := getElipticCurve(bits)
if err != nil {
return nil, nil, err
}
privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return nil, nil, err
}
return &privateKey.PublicKey, privateKey, nil
default:
return nil, nil, fmt.Errorf("unknown keytype: %s", keytype)
}
}

func getElipticCurve(bits int) (elliptic.Curve, error) {
switch bits {
case 256:
return elliptic.P256(), nil
case 384:
return elliptic.P384(), nil
case 521:
return elliptic.P521(), nil
default:
return nil, fmt.Errorf("unsupported ECDSA curve bit length: %d", bits)
}
}
2 changes: 1 addition & 1 deletion modules/setting/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ var SSH = struct {
ServerMACs: []string{"[email protected]", "hmac-sha2-256", "hmac-sha1"},
MinimumKeySizeCheck: true,
MinimumKeySizes: map[string]int{"ed25519": 256, "ed25519-sk": 256, "ecdsa": 256, "ecdsa-sk": 256, "rsa": 3071},
ServerHostKeys: []string{"ssh/gitea.rsa", "ssh/gogs.rsa"},
ServerHostKeys: []string{"ssh/gitea.rsa", "ssh/gitea.ed25519", "ssh/gitea.ecdsa", "ssh/gogs.rsa"},
AuthorizedKeysCommandTemplate: "{{.AppPath}} --config={{.CustomConf}} serv key-{{.Key.ID}}",
PerWriteTimeout: PerWriteTimeout,
PerWritePerKbTimeout: PerWritePerKbTimeout,
Expand Down
57 changes: 38 additions & 19 deletions modules/ssh/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ package ssh
import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"io"
Expand All @@ -23,6 +20,7 @@ import (
"syscall"

asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
Expand Down Expand Up @@ -54,6 +52,14 @@ import (

const giteaPermissionExtensionKeyID = "gitea-perm-ext-key-id"

type KeyType string

const (
RSA KeyType = "rsa"
ECDSA KeyType = "ecdsa"
ED25519 KeyType = "ed25519"
)

func getExitStatusFromError(err error) int {
if err == nil {
return 0
Expand Down Expand Up @@ -367,17 +373,17 @@ func Listen(host string, port int, ciphers, keyExchanges, macs []string) {

if len(keys) == 0 {
filePath := filepath.Dir(setting.SSH.ServerHostKeys[0])

if err := os.MkdirAll(filePath, os.ModePerm); err != nil {
log.Error("Failed to create dir %s: %v", filePath, err)
}

err := GenKeyPair(setting.SSH.ServerHostKeys[0])
err := initDefaultKeys(filePath)
if err != nil {
log.Fatal("Failed to generate private key: %v", err)
}
log.Trace("New private key is generated: %s", setting.SSH.ServerHostKeys[0])
keys = append(keys, setting.SSH.ServerHostKeys[0])
for _, keytype := range []string{"rsa", "ecdsa", "ed25519"} {
filename := filePath + "/gitea." + keytype
keys = append(keys, filename)
}
}

for _, key := range keys {
Expand All @@ -387,7 +393,6 @@ func Listen(host string, port int, ciphers, keyExchanges, macs []string) {
log.Error("Failed to set Host Key. %s", err)
}
}

go func() {
_, _, finished := process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), "Service: Built-in SSH server", process.SystemProcessType, true)
defer finished()
Expand All @@ -398,13 +403,17 @@ func Listen(host string, port int, ciphers, keyExchanges, macs []string) {
// GenKeyPair make a pair of public and private keys for SSH access.
// Public key is encoded in the format for inclusion in an OpenSSH authorized_keys file.
// Private Key generated is PEM encoded
func GenKeyPair(keyPath string) error {
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
func GenKeyPair(keyPath, keytype string) error {
bits := 4096
if keytype == "ecdsa" {
bits = 256
}

publicKey, privateKeyPEM, err := generate.NewSSHKey(keytype, bits)
if err != nil {
return err
}

privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}
f, err := os.OpenFile(keyPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return err
Expand All @@ -419,13 +428,7 @@ func GenKeyPair(keyPath string) error {
return err
}

// generate public key
pub, err := gossh.NewPublicKey(&privateKey.PublicKey)
if err != nil {
return err
}

public := gossh.MarshalAuthorizedKey(pub)
public := gossh.MarshalAuthorizedKey(publicKey)
p, err := os.OpenFile(keyPath+".pub", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return err
Expand All @@ -438,3 +441,19 @@ func GenKeyPair(keyPath string) error {
_, err = p.Write(public)
return err
}

// initDefaultKeys mirrors how ssh-keygen -A operates
// it runs checks if public and private keys are already defined and creates new ones if not present
// key naming does not follow the openssh convention due to existing settings being gitea.{keytype} so generation follows gitea convention
func initDefaultKeys(path string) error {
var errs []error
keytypes := []string{"rsa", "ecdsa", "ed25519"}
for _, keytype := range keytypes {
privExists, _ := util.IsExist(path + "/gitea." + keytype)
pubExists, _ := util.IsExist(path + "/gitea." + keytype + ".pub")
if !privExists || !pubExists {
errs = append(errs, GenKeyPair(path+"/gitea."+keytype, keytype))
}
}
return errors.Join(errs...)
}
Loading