1 package bannerctl
2
3 import (
4 "context"
5 "database/sql"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "time"
10
11 "github.com/google/uuid"
12 "google.golang.org/grpc/codes"
13 "google.golang.org/grpc/status"
14
15 kms "cloud.google.com/go/kms/apiv1"
16 "cloud.google.com/go/kms/apiv1/kmspb"
17 "github.com/go-logr/logr"
18 "github.com/golang-jwt/jwt"
19 ctrl "sigs.k8s.io/controller-runtime"
20 "sigs.k8s.io/controller-runtime/pkg/client"
21
22 "edge-infra.dev/pkg/edge/api/services/channels"
23 bannerAPI "edge-infra.dev/pkg/edge/apis/banner/v1alpha1"
24 "edge-infra.dev/pkg/edge/edgeencrypt"
25 "edge-infra.dev/pkg/k8s/runtime/controller/reconcile"
26 "edge-infra.dev/pkg/k8s/runtime/controller/reconcile/recerr"
27 )
28
29 type EncryptionInfraReconciler struct {
30 client.Client
31 Log logr.Logger
32
33 Conditions reconcile.Conditions
34 ForemanProjectID string
35 GCPRegion string
36 SecretManager secretManager
37 KmsClient *kms.KeyManagementClient
38 kmsKey edgeencrypt.KmsKey
39 SigningMethod jwt.SigningMethod
40 ChannelService channels.Service
41
42 IntervalTime time.Duration
43 RequeueTime time.Duration
44 ResourceTimeout time.Duration
45 }
46
47 func (r *EncryptionInfraReconciler) SetupWithManager(mgr ctrl.Manager) error {
48 r.kmsKey = edgeencrypt.KmsKey{ProjectID: r.ForemanProjectID, Location: r.GCPRegion}
49 return ctrl.NewControllerManagedBy(mgr).
50 For(&bannerAPI.Banner{}).
51 Complete(r)
52 }
53
54 func (r *EncryptionInfraReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, recErr error) {
55 banner := &bannerAPI.Banner{}
56 if err := r.Get(ctx, req.NamespacedName, banner); err != nil {
57 return ctrl.Result{}, client.IgnoreNotFound(err)
58 }
59 log := ctrl.Log.WithValues("banner", banner.Name)
60
61
62 bcs, err := getBannerChannels(ctx, r.ChannelService, banner.Name)
63 if err != nil {
64 return ctrl.Result{RequeueAfter: r.IntervalTime}, nil
65 }
66 if len(bcs) == 0 {
67 return ctrl.Result{RequeueAfter: r.RequeueTime}, nil
68 }
69
70 recErr = r.reconcile(ctx, banner, bcs)
71 if recErr != nil {
72 log.Error(recErr, "failed to reconcile")
73 return ctrl.Result{RequeueAfter: r.IntervalTime}, nil
74 }
75
76 return ctrl.Result{RequeueAfter: r.RequeueTime}, nil
77 }
78
79 func (r *EncryptionInfraReconciler) reconcile(ctx context.Context, banner *bannerAPI.Banner, channels []channels.BannerChannel) recerr.Error {
80 secretClient, err := r.SecretManager.NewWithOptions(ctx, banner.Spec.GCP.ProjectID)
81 if err != nil {
82 return recerr.New(err, bannerAPI.PlatformSecretsCreationFailedReason)
83 }
84
85
86
87 err = r.createKeyRing(ctx, banner.Name)
88 if err != nil {
89 return recerr.New(err, bannerAPI.PlatformSecretsCreationFailedReason)
90 }
91
92
93 err = r.createKMSSigningKey(ctx, secretClient, banner, edgeencrypt.EncryptionJWTSecret, edgeencrypt.EncryptionJWTSecretManager,
94 map[string]string{"banner": banner.Name})
95 if err != nil {
96 return recerr.New(err, bannerAPI.PlatformSecretsCreationFailedReason)
97 }
98
99 bannerEdgeID, err := uuid.Parse(banner.Name)
100 if err != nil {
101 return recerr.New(err, bannerAPI.PlatformSecretsCreationFailedReason)
102 }
103
104 for _, channel := range channels {
105
106 err = r.createKMSEncryptionKey(ctx, secretClient, banner,
107 fmt.Sprintf(edgeencrypt.EncryptionSecret, channel.ID),
108 fmt.Sprintf(edgeencrypt.EncryptionSecretManager, channel.ID),
109 map[string]string{"channel": channel.Name},
110 )
111 if err != nil {
112 return recerr.New(err, bannerAPI.PlatformSecretsCreationFailedReason)
113 }
114
115
116 err = r.CreateChannelKeyVersion(ctx, bannerEdgeID, channel.ID, banner.Spec.GCP.ProjectID,
117 fmt.Sprintf(edgeencrypt.EncryptionSecretManager, channel.ID))
118 if err != nil {
119 return recerr.New(err, bannerAPI.PlatformSecretsCreationFailedReason)
120 }
121
122
123 err = r.createBearerToken(ctx, secretClient, banner, edgeencrypt.EncryptionJWTSecret, channel.ID.String(),
124 channel.Name, fmt.Sprintf(edgeencrypt.EncryptionTokenSecretManager, channel.ID))
125 if err != nil {
126 return recerr.New(err, bannerAPI.PlatformSecretsCreationFailedReason)
127 }
128 }
129
130 return nil
131 }
132
133
134 func (r *EncryptionInfraReconciler) createKeyRing(ctx context.Context, bannerEdgeID string) error {
135 return createKeyRing(ctx, r.KmsClient, r.kmsKey, bannerEdgeID)
136 }
137
138
139 func createKeyRing(ctx context.Context, kmsClient *kms.KeyManagementClient, kmsKey edgeencrypt.KmsKey, ring string) error {
140
141 _, err := kmsClient.GetKeyRing(ctx, &kmspb.GetKeyRingRequest{Name: kmsKey.Ring(ring)})
142 if err != nil && status.Code(err) == codes.NotFound {
143 _, err = kmsClient.CreateKeyRing(ctx, &kmspb.CreateKeyRingRequest{
144 Parent: kmsKey.RingParent(),
145 KeyRingId: ring,
146 })
147 }
148 if err != nil {
149 return fmt.Errorf("failed to create key ring: %w", err)
150 }
151 return nil
152 }
153
154
155 func (r *EncryptionInfraReconciler) createKMSSigningKey(ctx context.Context, secretClient secretManagerClient, banner *bannerAPI.Banner, channel, secretID string, labels map[string]string) error {
156 return createKmsKey(ctx, r.KmsClient, secretClient, r.kmsKey, banner.Name, channel, labels, edgeencrypt.DefaultSigningCryptoKeyPurpose, edgeencrypt.DefaultSigningCryptoKeyAlgorithm, secretID, r.ResourceTimeout)
157 }
158
159
160 func (r *EncryptionInfraReconciler) createKMSEncryptionKey(ctx context.Context, secretClient secretManagerClient, banner *bannerAPI.Banner, channel, secretID string, labels map[string]string) error {
161 return createKmsKey(ctx, r.KmsClient, secretClient, r.kmsKey, banner.Name, channel, labels, edgeencrypt.DefaultCryptoKeyPurpose, edgeencrypt.DefaultCryptoKeyAlgorithm, secretID, r.ResourceTimeout)
162 }
163
164 func createKmsKey(ctx context.Context, kmsClient *kms.KeyManagementClient, secretClient secretManagerClient, kmsKey edgeencrypt.KmsKey, ring, key string, labels map[string]string,
165 purpose kmspb.CryptoKey_CryptoKeyPurpose, algorithm kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm, secretID string, timeout time.Duration) error {
166 _, err := secretClient.GetSecret(ctx, secretID)
167 if err == nil {
168
169 return nil
170 } else if status.Code(err) != codes.NotFound {
171 return fmt.Errorf("failed to get secret from secret manager: %w", err)
172 }
173
174 keyPath := kmsKey.KeyPath(ring, key, "1")
175
176 pubKey, err := kmsClient.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{Name: keyPath})
177 if err != nil && status.Code(err) == codes.NotFound {
178 _, err = kmsClient.CreateCryptoKey(ctx, &kmspb.CreateCryptoKeyRequest{
179 Parent: kmsKey.Ring(ring),
180 CryptoKeyId: key,
181 CryptoKey: &kmspb.CryptoKey{
182 Purpose: purpose,
183 VersionTemplate: &kmspb.CryptoKeyVersionTemplate{Algorithm: algorithm},
184 Labels: labels,
185 },
186 })
187 if err != nil {
188 return fmt.Errorf("failed to create key ring: %w", err)
189 }
190
191 pubKey, err = getPublicKeyWithRetry(ctx, kmsClient, keyPath, timeout)
192 }
193 if err != nil {
194 return fmt.Errorf("failed to get public key from kms: %w", err)
195 }
196 pk, err := edgeencrypt.NewPublicKey(pubKey.Pem, "1")
197 if err != nil {
198 return fmt.Errorf("failed to create public key: %w", err)
199 }
200 data, err := json.Marshal(pk)
201 if err != nil {
202 return fmt.Errorf("failed to marshal public key: %w", err)
203 }
204
205 return saveToSecretManager(ctx, secretClient, secretID, data, 1, labels)
206 }
207
208
209 func (r *EncryptionInfraReconciler) createBearerToken(ctx context.Context, secretClient secretManagerClient, banner *bannerAPI.Banner, keyName, channelID, channelName, secretID string) error {
210 key := r.kmsKey.KeyPath(banner.Name, keyName, "1")
211
212 return createBearerToken(ctx, secretClient, r.SigningMethod, key, channelID, channelName, secretID, edgeencrypt.Encryption, 1,
213 map[string]string{"channel": channelName}, banner.Name)
214 }
215
216 func createBearerToken(ctx context.Context, secretClient secretManagerClient, sm jwt.SigningMethod, key string, channelID, channelName, secretID string,
217 role edgeencrypt.Role, version int, labels map[string]string, banner ...string) error {
218 _, err := secretClient.GetSecret(ctx, secretID)
219 if err == nil {
220
221 return nil
222 } else if status.Code(err) != codes.NotFound {
223 return fmt.Errorf("failed to get secret from secret manager: %w", err)
224 }
225
226 token, err := edgeencrypt.CreateToken(sm, key, edgeencrypt.DefaultDuration, channelID, channelName, role, banner...)
227 if err != nil {
228 return fmt.Errorf("failed to create bearer token: %w", err)
229 }
230 t := &edgeencrypt.Token{BearerToken: token, Version: "1"}
231 data, err := json.Marshal(t)
232 if err != nil {
233 return fmt.Errorf("failed to marshal bearer token to json: %w", err)
234 }
235 return saveToSecretManager(ctx, secretClient, secretID, data, version, labels)
236 }
237
238 func saveToSecretManager(ctx context.Context, secretClient secretManagerClient, secretID string, data []byte, _ int, labels map[string]string) error {
239
240 err := secretClient.AddSecret(ctx, secretID, data, labels, false, nil, "")
241 if err != nil {
242 return fmt.Errorf("failed to add secret to secret manager: %w", err)
243 }
244 return nil
245 }
246
247 func (r *EncryptionInfraReconciler) CreateChannelKeyVersion(ctx context.Context, bannerEdgeID, channelID uuid.UUID, project, secretID string) error {
248 ck := channels.ChannelKeyVersion{
249 ChannelID: channelID,
250 BannerEdgeID: bannerEdgeID,
251 Version: 1,
252 SecretManagerLink: secretManagerLink(project, secretID),
253 }
254 return saveChannelKeyVersion(ctx, r.ChannelService, bannerEdgeID, channelID, ck)
255 }
256
257 func getPublicKeyWithRetry(ctx context.Context, kmsClient *kms.KeyManagementClient, key string, timeout time.Duration) (*kmspb.PublicKey, error) {
258 var (
259 result *kmspb.PublicKey
260 err error
261 )
262 for {
263 select {
264 case <-time.After(timeout):
265 if err != nil {
266 return nil, err
267 }
268 return nil, fmt.Errorf("timeout waiting for key %s to be ready", key)
269 default:
270 result, err = kmsClient.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{Name: key})
271 if err == nil {
272 return result, nil
273 }
274 time.Sleep(2 * time.Second)
275 }
276 }
277 }
278
279 func saveChannelKeyVersion(ctx context.Context, cs channels.Service, bannerEdgeID, channelID uuid.UUID, ckv channels.ChannelKeyVersion) error {
280 _, err := cs.GetLatestChannelKeyVersion(ctx, bannerEdgeID, channelID)
281 if err != nil {
282 if !errors.Is(err, sql.ErrNoRows) {
283 return fmt.Errorf("failed to get latest channel key version: %w", err)
284 }
285
286 _, err = cs.CreateChannelKeyVersion(ctx, ckv)
287 if err != nil {
288 return fmt.Errorf("failed to create channel key version: %w", err)
289 }
290 }
291 return nil
292 }
293
294 func secretManagerLink(project, secretID string) string {
295 return fmt.Sprintf("projects/%s/secrets/%s", project, secretID)
296 }
297
298 func getBannerChannels(ctx context.Context, cs channels.Service, bannerEdgeID string) ([]channels.BannerChannel, error) {
299 bannerID, err := uuid.Parse(bannerEdgeID)
300 if err != nil {
301 return nil, fmt.Errorf("failed to parse banner edge id: %w", err)
302 }
303
304 bcs, err := cs.GetBannerChannels(ctx, bannerID)
305 if err != nil {
306 return nil, fmt.Errorf("failed to get banner channels: %w", err)
307 }
308 return bcs, nil
309 }
310
View as plain text