Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
26 changes: 15 additions & 11 deletions libs/go/sia/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,19 +556,23 @@ 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)
} 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
}
if opts.SshIncludePrincipals {
sshCsr, err = util.GenerateSSHHostCSRWithXPrincipals(opts.SshPubKeyFile, opts.Domain, primaryServiceName, hostname, opts.PrivateIp, sshPrincipals, opts.ZTSCloudDomains)
} else {
sshCsr, err = util.GenerateSSHHostCSR(opts.SshPubKeyFile, opts.Domain, primaryServiceName, opts.PrivateIp, opts.ZTSCloudDomains)
}
} else {
sshCertRequest, err = util.GenerateSSHHostRequest(opts.SshPubKeyFile, opts.Domain, primaryServiceName, hostname, opts.PrivateIp, opts.InstanceId, sshPrincipals, opts.ZTSCloudDomains)
}
}
Expand Down
44 changes: 44 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,53 @@ func TestGenerateSshRequest(test *testing.T) {
opts.Domain = "athenz"
opts.ZTSCloudDomains = []string{"athenz.io"}
opts.SshHostKeyType = hostkey.Rsa
opts.PrivateIp = "10.11.12.13"
sshReq, sshCsr, err = generateSshRequest(&opts, "api", "hostname.athenz.io")
assert.Nil(test, sshReq)
assert.NotEmpty(test, sshCsr)
assert.Nil(test, err)
// without the ssh include principals option the csr only carries the zts cloud
// domain hostnames and the x-principals field is omitted altogether
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.Nil(test, keyReq.XPrincipals)
assert.NotContains(test, sshCsr, "xprincipals")
// ssh enabled with primary service, key type is rsa and the ssh include principals
// option enabled - the key id principals still only carry the zts cloud domain
// hostnames while the x-principals carry the hostname, the provider specific
// principals and the ip
opts.SshIncludePrincipals = true
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, "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, ssh include principals enabled
// 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.SshIncludePrincipals = false
opts.PrivateIp = ""
// 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
1 change: 1 addition & 0 deletions libs/go/sia/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ type Options struct {
FileDirectUpdate bool //update key/cert files directly instead of using rename
HostnameSuffix string //hostname suffix in case we need to auto-generate hostname
SshPrincipals string //ssh additional principals
SshIncludePrincipals bool //optional flag to include additional principals in host rsa key certs
AccessManagement bool //access management support
ZTSCloudDomains []string //list of domain prefixes for sanDNS entries
AddlSanDNSEntries []string //additional san dns entries to be added to the CSR
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,omitempty" rdl:"optional"`
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, sshPrincipals string, ztsCloudDomains []string) (string, error) {

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
127 changes: 127 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,132 @@ 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
// must not carry an x-principals field at all - an empty x-principals list
// is not the same as an absent one since the server only skips hostname
// validation when the field is not present

assert.Equal(t, []string{"api.athenz-prod.athenz.cloud", "api.athenz-prod.athenz.io"}, req.Principals)
assert.Nil(t, req.XPrincipals)
assert.NotContains(t, csr, "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", "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", "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)
assert.Contains(t, csr, "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", "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", "", "", "", []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)

// finally with no optional arguments and no zts cloud domains there are no
// x-principals to report, and the field must be omitted rather than sent as
// an empty list which the server would validate the hostname against

csr, err = GenerateSSHHostCSRWithXPrincipals("data/ssh-pub-key", "athenz", "api", "", "", "", nil)
assert.Nil(t, err)

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

assert.Empty(t, req.Principals)
assert.Nil(t, req.XPrincipals)
assert.NotContains(t, csr, "xprincipals")
}

func TestGenerateSSHHostRequest(t *testing.T) {

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