Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions libs/go/sia/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,19 +556,19 @@ func generateSshRequest(opts *sc.Options, primaryServiceName, hostname string) (
var sshCsr string
var sshCertRequest *zts.SSHCertRequest
if opts.Ssh && opts.Services[0].Name == primaryServiceName {
sshPrincipals := opts.SshPrincipals
// additional ssh host principals are added on best effort basis, hence error below is ignored.
additionalSshHostPrincipals, _ := opts.Provider.GetAdditionalSshHostPrincipals(opts.MetaEndPoint)
if additionalSshHostPrincipals != "" {
if sshPrincipals != "" {
sshPrincipals = sshPrincipals + "," + additionalSshHostPrincipals
} else {
sshPrincipals = additionalSshHostPrincipals
}
}
if opts.SshHostKeyType == hostkey.Rsa {
sshCsr, err = util.GenerateSSHHostCSR(opts.SshPubKeyFile, opts.Domain, primaryServiceName, opts.PrivateIp, opts.ZTSCloudDomains)
sshCsr, err = util.GenerateSSHHostCSRWithXPrincipals(opts.SshPubKeyFile, opts.Domain, primaryServiceName, hostname, opts.PrivateIp, opts.InstanceId, sshPrincipals, opts.ZTSCloudDomains)
} else {
sshPrincipals := opts.SshPrincipals
// additional ssh host principals are added on best effort basis, hence error below is ignored.
additionalSshHostPrincipals, _ := opts.Provider.GetAdditionalSshHostPrincipals(opts.MetaEndPoint)
if additionalSshHostPrincipals != "" {
if sshPrincipals != "" {
sshPrincipals = sshPrincipals + "," + additionalSshHostPrincipals
} else {
sshPrincipals = additionalSshHostPrincipals
}
}
sshCertRequest, err = util.GenerateSSHHostRequest(opts.SshPubKeyFile, opts.Domain, primaryServiceName, hostname, opts.PrivateIp, opts.InstanceId, sshPrincipals, opts.ZTSCloudDomains)
}
}
Expand Down
27 changes: 27 additions & 0 deletions libs/go/sia/agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"fmt"
"log"
"net"
Expand Down Expand Up @@ -627,10 +628,36 @@ func TestGenerateSshRequest(test *testing.T) {
opts.Domain = "athenz"
opts.ZTSCloudDomains = []string{"athenz.io"}
opts.SshHostKeyType = hostkey.Rsa
opts.PrivateIp = "10.11.12.13"
opts.InstanceId = "i-0123"
sshReq, sshCsr, err = generateSshRequest(&opts, "api", "hostname.athenz.io")
assert.Nil(test, sshReq)
assert.NotEmpty(test, sshCsr)
assert.Nil(test, err)
// the csr key id principals only carry the zts cloud domain hostnames while the
// x-principals carry the hostname, the provider specific principals and the ip
var keyReq util.SSHKeyReq
err = json.Unmarshal([]byte(sshCsr), &keyReq)
assert.Nil(test, err)
assert.Equal(test, "host", keyReq.Certtype)
assert.Equal(test, "athenz.api", keyReq.Requser)
assert.Equal(test, "10.11.12.13", keyReq.Reqip)
assert.Equal(test, []string{"api.athenz.athenz.io"}, keyReq.Principals)
assert.Equal(test, []string{"hostname.athenz.io", "my-vm", "my-instance-id", "10.11.12.13", "api.athenz.athenz.io"}, keyReq.XPrincipals)
// ssh enabled with primary service, key type is rsa and opts defines sshPrincipals which
// must be included in the x-principals along with the provider specific principals
opts.SshPrincipals = "cname.athenz.io"
sshReq, sshCsr, err = generateSshRequest(&opts, "api", "hostname.athenz.io")
assert.Nil(test, sshReq)
assert.Nil(test, err)
keyReq = util.SSHKeyReq{}
err = json.Unmarshal([]byte(sshCsr), &keyReq)
assert.Nil(test, err)
assert.Equal(test, []string{"api.athenz.athenz.io"}, keyReq.Principals)
assert.Equal(test, []string{"hostname.athenz.io", "cname.athenz.io", "my-vm", "my-instance-id", "10.11.12.13", "api.athenz.athenz.io"}, keyReq.XPrincipals)
opts.SshPrincipals = ""
opts.PrivateIp = ""
opts.InstanceId = ""
// ssh enabled with primary service and key type is ecdsa - empty csr but not-nil cert request
opts.SshHostKeyType = hostkey.Ecdsa
sshReq, sshCsr, err = generateSshRequest(&opts, "api", "hostname.athenz.io")
Expand Down
61 changes: 53 additions & 8 deletions libs/go/sia/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,15 @@ type RoleCertReqOptions struct {

// SSHKeyReq - congruent with certsign-rdl/certsign.rdl
type SSHKeyReq struct {
Principals []string `json:"principals"`
Ips []string `json:"ips,omitempty" rdl:"optional"`
Pubkey string `json:"pubkey"`
Reqip string `json:"reqip"`
Requser string `json:"requser"`
Certtype string `json:"certtype"`
Transid string `json:"transid"`
Command string `json:"command,omitempty" rdl:"optional"`
Principals []string `json:"principals"`
XPrincipals []string `json:"xprincipals"`
Comment thread
Copilot marked this conversation as resolved.
Outdated
Ips []string `json:"ips,omitempty" rdl:"optional"`
Pubkey string `json:"pubkey"`
Reqip string `json:"reqip"`
Requser string `json:"requser"`
Certtype string `json:"certtype"`
Transid string `json:"transid"`
Command string `json:"command,omitempty" rdl:"optional"`
}

const JwkConfFile = "athenz.conf"
Expand Down Expand Up @@ -420,6 +421,50 @@ func GenerateRoleCertCSR(key *rsa.PrivateKey, options *RoleCertReqOptions) (stri
return GenerateX509CSR(key, csrDetails)
}

func GenerateSSHHostCSRWithXPrincipals(sshPubKeyFile string, domain, service, hostname, ip, instanceId, sshPrincipals string, ztsCloudDomains []string) (string, error) {
Comment thread
havetisyan marked this conversation as resolved.
Outdated

log.Println("Generating SSH Host Certificate CSR...")

pubkey, err := os.ReadFile(sshPubKeyFile)
if err != nil {
log.Printf("Skipping SSH CSR Request - Unable to read SSH Public Key File: %v\n", err)
return "", nil
}
identity := domain + "." + service
transId := fmt.Sprintf("%x", time.Now().Unix())
hyphenDomain := strings.Replace(domain, ".", "-", -1)
principals := []string{}
xprincipals := []string{}
if hostname != "" {
xprincipals = append(xprincipals, hostname)
}
if sshPrincipals != "" {
xprincipals = append(xprincipals, strings.Split(sshPrincipals, ",")...)
}
if ip != "" {
xprincipals = append(xprincipals, ip)
}
for _, ztsDomain := range ztsCloudDomains {
host := fmt.Sprintf("%s.%s.%s", service, hyphenDomain, ztsDomain)
principals = append(principals, host)
xprincipals = append(xprincipals, host)
}
req := &SSHKeyReq{
Principals: principals,
XPrincipals: xprincipals,
Pubkey: string(pubkey),
Reqip: ip,
Requser: identity,
Certtype: "host",
Transid: transId,
}
csr, err := json.Marshal(req)
if err != nil {
return "", err
}
return string(csr), err
}

func GenerateSSHHostCSR(sshPubKeyFile string, domain, service, ip string, ztsCloudDomains []string) (string, error) {

log.Println("Generating SSH Host Certificate CSR...")
Expand Down
108 changes: 108 additions & 0 deletions libs/go/sia/util/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package util

import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"net"
Expand Down Expand Up @@ -1389,6 +1390,113 @@ func TestSetupSIADir(t *testing.T) {
os.RemoveAll("/tmp/sia-test-dir")
}

func TestGenerateSSHHostCSR(t *testing.T) {

// using invalid key file which should return an empty csr with no error

csr, err := GenerateSSHHostCSR("unknown-file", "athenz", "api", "10.11.12.13", []string{"athenz.cloud"})
assert.Nil(t, err)
assert.Empty(t, csr)

// now let's test with real ssh pub key file

csr, err = GenerateSSHHostCSR("data/ssh-pub-key", "athenz.prod", "api", "10.11.12.13", []string{"athenz.cloud", "athenz.io"})
assert.Nil(t, err)

var req SSHKeyReq
err = json.Unmarshal([]byte(csr), &req)
assert.Nil(t, err)

assert.Equal(t, "host", req.Certtype)
assert.Equal(t, "10.11.12.13", req.Reqip)
assert.Equal(t, "athenz.prod.api", req.Requser)
assert.Equal(t, "ssh-pub-key", req.Pubkey)
assert.NotEmpty(t, req.Transid)
assert.Empty(t, req.Command)
assert.Empty(t, req.Ips)

// the legacy csr only includes the zts cloud domain based principals
// and does not include any x-principals

assert.Equal(t, []string{"api.athenz-prod.athenz.cloud", "api.athenz-prod.athenz.io"}, req.Principals)
assert.Empty(t, req.XPrincipals)
}

func TestGenerateSSHHostCSRWithXPrincipals(t *testing.T) {

// using invalid key file which should return an empty csr with no error

csr, err := GenerateSSHHostCSRWithXPrincipals("unknown-file", "athenz", "api", "hostname.athenz.io", "10.11.12.13", "i-0123", "host1.athenz.io,host2.athenz.io", []string{"athenz.cloud"})
assert.Nil(t, err)
assert.Empty(t, csr)

// now let's test with real ssh pub key file

csr, err = GenerateSSHHostCSRWithXPrincipals("data/ssh-pub-key", "athenz", "api", "hostname.athenz.io", "10.11.12.13", "i-0123", "host1.athenz.io,host2.athenz.io", []string{"athenz.cloud"})
assert.Nil(t, err)

var req SSHKeyReq
err = json.Unmarshal([]byte(csr), &req)
assert.Nil(t, err)

assert.Equal(t, "host", req.Certtype)
assert.Equal(t, "10.11.12.13", req.Reqip)
assert.Equal(t, "athenz.api", req.Requser)
assert.Equal(t, "ssh-pub-key", req.Pubkey)
assert.NotEmpty(t, req.Transid)
assert.Empty(t, req.Command)
assert.Empty(t, req.Ips)

// the principals field only carries the zts cloud domain based hostnames
// which are used to generate the key id, while the x-principals field
// carries the full set of hostnames the certificate is requested for

assert.Equal(t, []string{"api.athenz.athenz.cloud"}, req.Principals)
assert.Equal(t, []string{
"hostname.athenz.io",
"host1.athenz.io",
"host2.athenz.io",
"10.11.12.13",
"api.athenz.athenz.cloud",
}, req.XPrincipals)

// now let's test with multiple zts cloud domains and a domain with
// multiple components which must be hyphenated in the hostnames

csr, err = GenerateSSHHostCSRWithXPrincipals("data/ssh-pub-key", "athenz.prod", "api", "hostname.athenz.io", "10.11.12.13", "i-0123", "host1.athenz.io", []string{"athenz.cloud", "athenz.io"})
assert.Nil(t, err)

req = SSHKeyReq{}
err = json.Unmarshal([]byte(csr), &req)
assert.Nil(t, err)

assert.Equal(t, "athenz.prod.api", req.Requser)
assert.Equal(t, []string{"api.athenz-prod.athenz.cloud", "api.athenz-prod.athenz.io"}, req.Principals)
assert.Equal(t, []string{
"hostname.athenz.io",
"host1.athenz.io",
"10.11.12.13",
"api.athenz-prod.athenz.cloud",
"api.athenz-prod.athenz.io",
}, req.XPrincipals)

// now let's test without any of the optional arguments - hostname,
// ip and ssh principals

csr, err = GenerateSSHHostCSRWithXPrincipals("data/ssh-pub-key", "athenz", "api", "", "", "i-0123", "", []string{"athenz.cloud"})
assert.Nil(t, err)

req = SSHKeyReq{}
err = json.Unmarshal([]byte(csr), &req)
assert.Nil(t, err)

assert.Equal(t, "host", req.Certtype)
assert.Empty(t, req.Reqip)
assert.Equal(t, "athenz.api", req.Requser)
assert.Equal(t, []string{"api.athenz.athenz.cloud"}, req.Principals)
assert.Equal(t, []string{"api.athenz.athenz.cloud"}, req.XPrincipals)
}

func TestGenerateSSHHostRequest(t *testing.T) {

// using invalid key file which should return nil
Expand Down
Loading