Skip to content

Commit 80dd6b5

Browse files
committed
Azure: port forward SSH to bootstrap host
https://issues.redhat.com/browse/CORS-3302
1 parent 4771f7b commit 80dd6b5

File tree

6 files changed

+262
-87
lines changed

6 files changed

+262
-87
lines changed

pkg/asset/installconfig/azure/session.go

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -83,24 +83,9 @@ func GetSessionWithCredentials(cloudName azure.CloudEnvironment, armEndpoint str
8383
return nil, fmt.Errorf("failed to get Azure environment for the %q cloud: %w", cloudName, err)
8484
}
8585

86-
var cloudConfig cloud.Configuration
87-
switch cloudName {
88-
case azure.StackCloud:
89-
cloudConfig = cloud.Configuration{
90-
ActiveDirectoryAuthorityHost: cloudEnv.ActiveDirectoryEndpoint,
91-
Services: map[cloud.ServiceName]cloud.ServiceConfiguration{
92-
cloud.ResourceManager: {
93-
Audience: cloudEnv.TokenAudience,
94-
Endpoint: cloudEnv.ResourceManagerEndpoint,
95-
},
96-
},
97-
}
98-
case azure.USGovernmentCloud:
99-
cloudConfig = cloud.AzureGovernment
100-
case azure.ChinaCloud:
101-
cloudConfig = cloud.AzureChina
102-
default:
103-
cloudConfig = cloud.AzurePublic
86+
cloudConfig, err := GetCloudConfiguration(cloudName, armEndpoint)
87+
if err != nil {
88+
return nil, fmt.Errorf("failed to get cloud configuration for the %q cloud: %w", cloudName, err)
10489
}
10590

10691
if credentials == nil {
@@ -114,13 +99,13 @@ func GetSessionWithCredentials(cloudName azure.CloudEnvironment, armEndpoint str
11499
switch {
115100
case credentials.ClientCertificatePath != "":
116101
logrus.Warnf("Using client certs to authenticate. Please be warned cluster does not support certs and only the installer does.")
117-
cred, err = newTokenCredentialFromCertificates(credentials, cloudConfig)
102+
cred, err = newTokenCredentialFromCertificates(credentials, *cloudConfig)
118103
authType = ClientCertificateAuth
119104
case credentials.ClientSecret != "":
120-
cred, err = newTokenCredentialFromCredentials(credentials, cloudConfig)
105+
cred, err = newTokenCredentialFromCredentials(credentials, *cloudConfig)
121106
authType = ClientSecretAuth
122107
default:
123-
cred, err = newTokenCredentialFromMSI(credentials, cloudConfig)
108+
cred, err = newTokenCredentialFromMSI(credentials, *cloudConfig)
124109
authType = ManagedIdentityAuth
125110
}
126111
if err != nil {
@@ -130,11 +115,48 @@ func GetSessionWithCredentials(cloudName azure.CloudEnvironment, armEndpoint str
130115
if err != nil {
131116
return nil, err
132117
}
133-
session.CloudConfig = cloudConfig
118+
session.CloudConfig = *cloudConfig
134119
session.AuthType = authType
135120
return session, nil
136121
}
137122

123+
// GetCloudConfiguration gets a cloud configuration from the cloud name and endpoint.
124+
func GetCloudConfiguration(cloudName azure.CloudEnvironment, armEndpoint string) (*cloud.Configuration, error) {
125+
var cloudEnv azureenv.Environment
126+
var err error
127+
switch cloudName {
128+
case azure.StackCloud:
129+
cloudEnv, err = azureenv.EnvironmentFromURL(armEndpoint)
130+
default:
131+
cloudEnv, err = azureenv.EnvironmentFromName(string(cloudName))
132+
}
133+
if err != nil {
134+
return nil, err
135+
}
136+
137+
var cloudConfig cloud.Configuration
138+
switch cloudName {
139+
case azure.StackCloud:
140+
cloudConfig = cloud.Configuration{
141+
ActiveDirectoryAuthorityHost: cloudEnv.ActiveDirectoryEndpoint,
142+
Services: map[cloud.ServiceName]cloud.ServiceConfiguration{
143+
cloud.ResourceManager: {
144+
Audience: cloudEnv.TokenAudience,
145+
Endpoint: cloudEnv.ResourceManagerEndpoint,
146+
},
147+
},
148+
}
149+
case azure.USGovernmentCloud:
150+
cloudConfig = cloud.AzureGovernment
151+
case azure.ChinaCloud:
152+
cloudConfig = cloud.AzureChina
153+
default:
154+
cloudConfig = cloud.AzurePublic
155+
}
156+
157+
return &cloudConfig, nil
158+
}
159+
138160
// credentialsFromFileOrUser returns credentials found
139161
// in ~/.azure/osServicePrincipal.json and, if no creds are found,
140162
// asks for them and stores them on disk in a config file

pkg/asset/machines/azure/azuremachines.go

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -201,16 +201,10 @@ func GenerateMachines(platform *azure.Platform, pool *types.MachinePool, userDat
201201
OSDisk: osDisk,
202202
AdditionalTags: tags,
203203
DisableExtensionOperations: ptr.To(true),
204-
// Do not allocate a public IP since it isn't
205-
// accessible as we are using an outbound LB for the
206-
// control plane. This is temporary until we have a
207-
// workaround for accessing SSH (Most likely port
208-
// forwarding SSH off the LB until the bootstrap node
209-
// is destroyed).
210-
AllocatePublicIP: false,
211-
AdditionalCapabilities: additionalCapabilities,
212-
SecurityProfile: securityProfile,
213-
Identity: capz.VMIdentityUserAssigned,
204+
AllocatePublicIP: true,
205+
AdditionalCapabilities: additionalCapabilities,
206+
SecurityProfile: securityProfile,
207+
Identity: capz.VMIdentityUserAssigned,
214208
UserAssignedIdentities: []capz.UserAssignedIdentity{
215209
{
216210
ProviderID: userAssignedIdentityID,

pkg/asset/machines/azure/machines.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ func provider(platform *azure.Platform, mpool *azure.MachinePool, osImage string
184184
image.Version = mpool.OSImage.Version
185185
} else if useImageGallery {
186186
// image gallery names cannot have dashes
187-
galleryName := strings.Replace(clusterID, "-", "_", -1)
187+
galleryName := strings.ReplaceAll(clusterID, "-", "_")
188188
id := clusterID
189189
if hyperVGen == "V2" {
190190
id += "-gen2"

pkg/infrastructure/azure/azure.go

Lines changed: 88 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import (
99
"strings"
1010
"time"
1111

12+
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
1213
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
14+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
1315
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
1416
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3"
1517
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4"
@@ -25,6 +27,7 @@ import (
2527
"sigs.k8s.io/controller-runtime/pkg/client"
2628

2729
"github.com/openshift/installer/pkg/asset/ignition/bootstrap"
30+
azconfig "github.com/openshift/installer/pkg/asset/installconfig/azure"
2831
"github.com/openshift/installer/pkg/asset/manifests/capiutils"
2932
"github.com/openshift/installer/pkg/infrastructure/clusterapi"
3033
"github.com/openshift/installer/pkg/rhcos"
@@ -46,8 +49,10 @@ type Provider struct {
4649
StorageClientFactory *armstorage.ClientFactory
4750
StorageAccountKeys []armstorage.AccountKey
4851
NetworkClientFactory *armnetwork.ClientFactory
49-
Tags map[string]*string
5052
lbBackendAddressPool *armnetwork.BackendAddressPool
53+
CloudConfiguration cloud.Configuration
54+
TokenCredential azcore.TokenCredential
55+
Tags map[string]*string
5156
}
5257

5358
var _ clusterapi.PreProvider = (*Provider)(nil)
@@ -241,7 +246,7 @@ func (p *Provider) InfraReady(ctx context.Context, in clusterapi.InfraReadyInput
241246
cloudConfiguration := session.CloudConfig
242247

243248
resourceGroupName := p.ResourceGroupName
244-
storageAccountName := fmt.Sprintf("cluster%s", randomString(5))
249+
storageAccountName := fmt.Sprintf("%ssa", strings.ReplaceAll(in.InfraID, "-", ""))
245250
containerName := "vhd"
246251
blobName := fmt.Sprintf("rhcos%s.vhd", randomString(5))
247252

@@ -471,7 +476,7 @@ func (p *Provider) InfraReady(ctx context.Context, in clusterapi.InfraReadyInput
471476
}
472477
logrus.Debugf("created public ip: %s", *publicIP.ID)
473478

474-
loadBalancer, err := updateExternalLoadBalancer(ctx, publicIP, lbInput)
479+
loadBalancer, err := updateOutboundLoadBalancerToAPILoadBalancer(ctx, publicIP, lbInput)
475480
if err != nil {
476481
return fmt.Errorf("failed to update external load balancer: %w", err)
477482
}
@@ -486,8 +491,8 @@ func (p *Provider) InfraReady(ctx context.Context, in clusterapi.InfraReadyInput
486491
p.StorageAccountName = storageAccountName
487492
p.StorageURL = storageURL
488493
p.StorageAccount = storageAccount
489-
p.StorageClientFactory = storageClientFactory
490494
p.StorageAccountKeys = storageAccountKeys
495+
p.StorageClientFactory = storageClientFactory
491496
p.NetworkClientFactory = networkClientFactory
492497
p.lbBackendAddressPool = lbBap
493498

@@ -519,16 +524,6 @@ func (p *Provider) PostProvision(ctx context.Context, in clusterapi.PostProvisio
519524
if err != nil {
520525
return fmt.Errorf("error creating vm client: %w", err)
521526
}
522-
nicClient, err := armnetwork.NewInterfacesClient(ssn.Credentials.SubscriptionID, ssn.TokenCreds,
523-
&arm.ClientOptions{
524-
ClientOptions: policy.ClientOptions{
525-
Cloud: cloudConfiguration,
526-
},
527-
},
528-
)
529-
if err != nil {
530-
return fmt.Errorf("error creating nic client: %w", err)
531-
}
532527

533528
vmIDs, err := getControlPlaneIDs(in.Client, in.InstallConfig.Config.ControlPlane.Replicas, in.InfraID)
534529
if err != nil {
@@ -539,7 +534,7 @@ func (p *Provider) PostProvision(ctx context.Context, in clusterapi.PostProvisio
539534
infraID: in.InfraID,
540535
resourceGroup: p.ResourceGroupName,
541536
vmClient: vmClient,
542-
nicClient: nicClient,
537+
nicClient: p.NetworkClientFactory.NewInterfacesClient(),
543538
ids: vmIDs,
544539
bap: p.lbBackendAddressPool,
545540
}
@@ -553,29 +548,101 @@ func (p *Provider) PostProvision(ctx context.Context, in clusterapi.PostProvisio
553548
securityGroupName: fmt.Sprintf("%s-nsg", in.InfraID),
554549
securityRuleName: "ssh_in",
555550
securityRulePort: "22",
556-
securityGroupsClient: p.NetworkClientFactory.NewSecurityGroupsClient(),
551+
securityRulePriority: 220,
552+
networkClientFactory: p.NetworkClientFactory,
557553
}); err != nil {
558554
return fmt.Errorf("failed to add security rule: %w", err)
559555
}
556+
557+
loadBalancerName := in.InfraID
558+
frontendIPConfigName := "public-lb-ip-v4"
559+
frontendIPConfigID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/frontendIPConfigurations/%s",
560+
subscriptionID,
561+
p.ResourceGroupName,
562+
loadBalancerName,
563+
frontendIPConfigName,
564+
)
565+
566+
// Create an inbound nat rule that forwards port 22 on the
567+
// public load balancer to the bootstrap host. This takes 2
568+
// stages to accomplish. First, the nat rule needs to be added
569+
// to the frontend IP configuration on the public load
570+
// balancer. Second, the nat rule needs to be addded to the
571+
// bootstrap interface with the association to the rule on the
572+
// public load balancer.
573+
inboundNatRule, err := addInboundNatRuleToLoadBalancer(ctx, &inboundNatRuleInput{
574+
resourceGroupName: p.ResourceGroupName,
575+
loadBalancerName: loadBalancerName,
576+
frontendIPConfigID: frontendIPConfigID,
577+
inboundNatRuleName: "ssh_in",
578+
inboundNatRulePort: 22,
579+
networkClientFactory: p.NetworkClientFactory,
580+
})
581+
if err != nil {
582+
return fmt.Errorf("failed to create inbound nat rule: %w", err)
583+
}
584+
_, err = associateInboundNatRuleToInterface(ctx, &inboundNatRuleInput{
585+
resourceGroupName: p.ResourceGroupName,
586+
loadBalancerName: loadBalancerName,
587+
bootstrapNicName: fmt.Sprintf("%s-bootstrap-nic", in.InfraID),
588+
frontendIPConfigID: frontendIPConfigID,
589+
inboundNatRuleID: *inboundNatRule.ID,
590+
inboundNatRuleName: "ssh_in",
591+
inboundNatRulePort: 22,
592+
networkClientFactory: p.NetworkClientFactory,
593+
})
594+
if err != nil {
595+
return fmt.Errorf("failed to associate inbound nat rule to interface: %w", err)
596+
}
560597
}
561598

562599
return nil
563600
}
564601

565-
// PostDestroy removes SSH access from the network security rules after the
566-
// bootstrap machine is destroyed.
602+
// PostDestroy removes SSH access from the network security rules and removes
603+
// SSH port forwarding off the public load balancer when the bootstrap machine
604+
// is destroyed.
567605
func (p *Provider) PostDestroy(ctx context.Context, in clusterapi.PostDestroyerInput) error {
568-
err := deleteSecurityGroupRule(ctx, &securityGroupInput{
569-
resourceGroupName: p.ResourceGroupName,
606+
session, err := azconfig.GetSession(in.Metadata.Azure.CloudName, in.Metadata.Azure.ARMEndpoint)
607+
if err != nil {
608+
return fmt.Errorf("failed to get session: %w", err)
609+
}
610+
611+
networkClientFactory, err := armnetwork.NewClientFactory(
612+
session.Credentials.SubscriptionID,
613+
session.TokenCreds,
614+
&arm.ClientOptions{
615+
ClientOptions: policy.ClientOptions{
616+
Cloud: session.CloudConfig,
617+
},
618+
},
619+
)
620+
if err != nil {
621+
return fmt.Errorf("error creating network client factory: %w", err)
622+
}
623+
624+
// XXX: why is in.Metadata.Azure.ResourceGroupName empty?
625+
err = deleteSecurityGroupRule(ctx, &securityGroupInput{
626+
resourceGroupName: fmt.Sprintf("%s-rg", in.Metadata.InfraID),
570627
securityGroupName: fmt.Sprintf("%s-nsg", in.Metadata.InfraID),
571628
securityRuleName: "ssh_in",
572629
securityRulePort: "22",
573-
securityGroupsClient: p.NetworkClientFactory.NewSecurityGroupsClient(),
630+
networkClientFactory: networkClientFactory,
574631
})
575632
if err != nil {
576633
return fmt.Errorf("failed to delete security rule: %w", err)
577634
}
578635

636+
err = deleteInboundNatRule(ctx, &inboundNatRuleInput{
637+
resourceGroupName: fmt.Sprintf("%s-rg", in.Metadata.InfraID),
638+
loadBalancerName: in.Metadata.InfraID,
639+
inboundNatRuleName: "ssh_in",
640+
networkClientFactory: networkClientFactory,
641+
})
642+
if err != nil {
643+
return fmt.Errorf("failed to delete inbound nat rule: %w", err)
644+
}
645+
579646
return nil
580647
}
581648

0 commit comments

Comments
 (0)