mirror of
https://github.com/ollama/ollama.git
synced 2026-04-23 17:29:54 +02:00
This change adds an "authorized_keys" file similar to sshd which can control access to an Ollama server. The file itself is very simple and consists of various entries for Ollama public keys. The format is: <key format> <public key> <name> [<endpoint>,...] Examples: ssh-ed25519 AAAAC3NzaC1lZDI1NT... bob /api/tags,/api/ps,/api/show,/api/generate,/api/chat Use the "*" wildcard symbol to substitute any value, e.g.: To grant full access to "bob": ssh-ed25519 AAAAC3NzaC1lZDI1NT... bob * To allow all callers to view tags (i.e. "ollama ls"): * * * /api/tags - The key format must be set to "ssh-ed25519" or set to the wildcard character. - The public key must be an ssh based ed25519 (Ollama) public key or set to the wildcard character. - Name can be any string you wish to associate with the public key. Note that if a public key is used in more than one entry in the file, the first instance of the name will be used and subsequent name values will be ignored. - Endpoints is a comma separated list of Ollama Server API endpoints or the wildcard character. The HTTP method is not currently needed, but could be added in the future.
128 lines
2.7 KiB
Go
128 lines
2.7 KiB
Go
package auth
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
const defaultPrivateKey = "id_ed25519"
|
|
|
|
var ErrInvalidToken = errors.New("invalid token")
|
|
|
|
func keyPath() (string, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return filepath.Join(home, ".ollama", defaultPrivateKey), nil
|
|
}
|
|
|
|
func parseToken(token string) (key, sig []byte, _ error) {
|
|
keyData, sigData, ok := strings.Cut(token, ":")
|
|
if !ok {
|
|
return nil, nil, fmt.Errorf("identity: parseToken: %w", ErrInvalidToken)
|
|
}
|
|
sig, err := base64.StdEncoding.DecodeString(sigData)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("identity: parseToken: base64 decoding signature: %w", err)
|
|
}
|
|
return []byte(keyData), sig, nil
|
|
}
|
|
|
|
func Authenticate(token, checkData string) (ssh.PublicKey, error) {
|
|
keyShort, sigBytes, err := parseToken(token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
keyLong := append([]byte("ssh-ed25519 "), keyShort...)
|
|
pub, _, _, _, err := ssh.ParseAuthorizedKey(keyLong)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := pub.Verify([]byte(checkData), &ssh.Signature{
|
|
Format: pub.Type(),
|
|
Blob: sigBytes,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return pub, nil
|
|
}
|
|
|
|
func GetPublicKey() (string, error) {
|
|
keyPath, err := keyPath()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
privateKeyFile, err := os.ReadFile(keyPath)
|
|
if err != nil {
|
|
slog.Info(fmt.Sprintf("Failed to load private key: %v", err))
|
|
return "", err
|
|
}
|
|
|
|
privateKey, err := ssh.ParsePrivateKey(privateKeyFile)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
publicKey := ssh.MarshalAuthorizedKey(privateKey.PublicKey())
|
|
|
|
return strings.TrimSpace(string(publicKey)), nil
|
|
}
|
|
|
|
func NewNonce(r io.Reader, length int) (string, error) {
|
|
nonce := make([]byte, length)
|
|
if _, err := io.ReadFull(r, nonce); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return base64.RawURLEncoding.EncodeToString(nonce), nil
|
|
}
|
|
|
|
func Sign(ctx context.Context, bts []byte) (string, error) {
|
|
keyPath, err := keyPath()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
privateKeyFile, err := os.ReadFile(keyPath)
|
|
if err != nil {
|
|
slog.Info(fmt.Sprintf("Failed to load private key: %v", err))
|
|
return "", err
|
|
}
|
|
|
|
privateKey, err := ssh.ParsePrivateKey(privateKeyFile)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// get the pubkey, but remove the type
|
|
publicKey := ssh.MarshalAuthorizedKey(privateKey.PublicKey())
|
|
parts := bytes.Split(publicKey, []byte(" "))
|
|
if len(parts) < 2 {
|
|
return "", errors.New("malformed public key")
|
|
}
|
|
|
|
signedData, err := privateKey.Sign(rand.Reader, bts)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// signature is <pubkey>:<signature>
|
|
return fmt.Sprintf("%s:%s", bytes.TrimSpace(parts[1]), base64.StdEncoding.EncodeToString(signedData.Blob)), nil
|
|
}
|