[go: nahoru, domu]

Skip to content

Commit

Permalink
NEOS-1169 add tls certificate support for mongodb (#2184)
Browse files Browse the repository at this point in the history
  • Loading branch information
nickzelei committed Jun 21, 2024
1 parent 3b5ddf1 commit 88242b2
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 1 deletion.
73 changes: 73 additions & 0 deletions backend/pkg/clienttls/clienttls.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"

Expand Down Expand Up @@ -78,6 +79,52 @@ func UpsertCLientTlsFiles(config *mgmtv1alpha1.ClientTlsConfig) (*ClientTlsFileC
return &filenames, nil
}

// Joins the client cert and key into a single file
func UpsertClientTlsFileSingleClient(config *mgmtv1alpha1.ClientTlsConfig) (*ClientTlsFileConfig, error) {
if config == nil {
return nil, errors.New("config was nil")
}

errgrp := errgroup.Group{}

filenames := GetClientTlsFileNamesSingleClient(config)

errgrp.Go(func() error {
if filenames.RootCert == nil {
return nil
}
_, err := os.Stat(*filenames.RootCert)
if err != nil && !os.IsNotExist(err) {
return err
} else if err != nil && os.IsNotExist(err) {
if err := os.WriteFile(*filenames.RootCert, []byte(config.GetRootCert()), 0600); err != nil {
return err
}
}
return nil
})
errgrp.Go(func() error {
if filenames.ClientCert != nil && filenames.ClientKey != nil {
_, err := os.Stat(*filenames.ClientKey)
if err != nil && !os.IsNotExist(err) {
return err
} else if err != nil && os.IsNotExist(err) {
if err := os.WriteFile(*filenames.ClientKey, []byte(fmt.Sprintf("%s\n%s", config.GetClientKey(), config.GetClientCert())), 0600); err != nil {
return err
}
}
}
return nil
})

err := errgrp.Wait()
if err != nil {
return nil, err
}

return &filenames, nil
}

func GetClientTlsFileNames(config *mgmtv1alpha1.ClientTlsConfig) ClientTlsFileConfig {
if config == nil {
return ClientTlsFileConfig{}
Expand All @@ -102,6 +149,32 @@ func GetClientTlsFileNames(config *mgmtv1alpha1.ClientTlsConfig) ClientTlsFileCo
return output
}

// Joins the client cert and key into a single file
func GetClientTlsFileNamesSingleClient(config *mgmtv1alpha1.ClientTlsConfig) ClientTlsFileConfig {
if config == nil {
return ClientTlsFileConfig{}
}

basedir := os.TempDir()

output := ClientTlsFileConfig{}
if config.GetRootCert() != "" {
content := hashContent(config.GetRootCert())
fullpath := filepath.Join(basedir, content)
output.RootCert = &fullpath
}
if config.GetClientCert() != "" && config.GetClientKey() != "" {
certContent := hashContent(config.GetClientCert())
keyContent := hashContent(config.GetClientKey())

joinedContent := hashContent(fmt.Sprintf("%s%s", certContent, keyContent))
joinedPath := filepath.Join(basedir, joinedContent)
output.ClientCert = &joinedPath
output.ClientKey = &joinedPath
}
return output
}

func hashContent(content string) string {
hash := sha256.Sum256([]byte(content))
return hex.EncodeToString(hash[:])
Expand Down
24 changes: 23 additions & 1 deletion backend/pkg/mongoconnect/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"log/slog"
"net/url"
"sync"

mgmtv1alpha1 "github.com/nucleuscloud/neosync/backend/gen/go/protos/mgmt/v1alpha1"
Expand Down Expand Up @@ -90,7 +91,7 @@ func (c *Connector) NewFromConnectionConfig(
return nil, errors.New("cc was nil, expected *mgmtv1alpha1.ConnectionConfig")
}

details, err := GetConnectionDetails(cc, clienttls.UpsertCLientTlsFiles, logger)
details, err := GetConnectionDetails(cc, clienttls.UpsertClientTlsFileSingleClient, logger)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -179,5 +180,26 @@ func getGeneralDbConnectConfigFromMongo(config *mgmtv1alpha1.MongoConnectionConf
if dburl == "" {
return nil, fmt.Errorf("must provide valid mongoconfig url")
}

if config.GetClientTls() != nil {
parsedurl, err := url.Parse(dburl)
if err != nil {
return nil, err
}

filenames := clienttls.GetClientTlsFileNamesSingleClient(config.GetClientTls())
query := parsedurl.Query()
if !query.Has("tls") {
query.Set("tls", "true")
}
if filenames.RootCert != nil {
query.Set("tlsCAFile", *filenames.RootCert)
}
if filenames.ClientKey != nil && filenames.ClientCert != nil {
query.Set("tlsCertificateKeyFile", *filenames.ClientKey)
}
parsedurl.RawQuery = query.Encode()
dburl = parsedurl.String()
}
return connstring.ParseAndValidate(dburl)
}
17 changes: 17 additions & 0 deletions docs/docs/connections/mongodb.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ description: MongoDB is a source-available, cross-platform, document-oriented da
id: mongodb
hide_title: false
slug: /connections/mongodb
# cSpell:words textareas
---

## Introduction
Expand All @@ -23,6 +24,22 @@ This guide will help you to configure your MongoDB database connection properly.
**Connection Name**: Enter a unique name for this connection that you'll easily recognize. This is just a label and does not affect the connection itself.
**URL**: Enter your database connection url that will be used to connect to Mongo. Neosync supports both `mongodb` and `mongodb+srv` protocols.

### TLS Authentication

Mongo allows connection via Client TLS in lieu of or in addition to a username and password.

Your setup will vary based on your specific settings, but generally, you can configure Neosync to connect to Mongo by providing a Client Certificate and Client Key in their respective textareas in the Connection(s) form.

By doing so, Neosync will store them on disk and update the connection url to point to those files so that they can be used to connect.

The `tls=true` query parameter will also be added automatically if both of those fields are specified and the `tls` parameter has not been explicitly set in the connection url.
If using Mongo Atlas, the pem file will contain both the certificate and key. They must be split and put into their respective fields in the Connection form.

You may need to add a few query parameters to your URL in order to properly connect.
For example, using Mongo Atlas, you may need to provide the following parameters: `authMechanism=MONGODB-X509&authSource=$external`.

Read more about the Mongo TLS Options [here](https://www.mongodb.com/docs/manual/reference/connection-string/#tls-options).

## Permissions

### Source Connections
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import Spinner from '@/components/Spinner';
import RequiredLabel from '@/components/labels/RequiredLabel';
import PermissionsDialog from '@/components/permissions/PermissionsDialog';
import { useAccount } from '@/components/providers/account-provider';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import {
Expand All @@ -16,6 +22,7 @@ import {
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { MongoDbFormValues } from '@/yup-validations/connections';
import { yupResolver } from '@hookform/resolvers/yup';
import {
Expand Down Expand Up @@ -137,6 +144,70 @@ export default function MongoDbForm(props: Props): ReactElement {
)}
/>

<Accordion type="single" collapsible className="w-full">
<AccordionItem value="bastion">
<AccordionTrigger>Client TLS Certificates</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 p-2">
<div className="text-sm">
Configuring this section allows Neosync to connect to the
database using SSL/TLS.
</div>
<FormField
control={form.control}
name="clientTls.rootCert"
render={({ field }) => (
<FormItem>
<FormLabel>Root Certificate</FormLabel>
<FormDescription>
{`The public key certificate of the CA that issued the
server's certificate. Root certificates are used to
authenticate the server to the client. They ensure that
the server the client is connecting to is trusted.`}
</FormDescription>
<FormControl>
<Textarea {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientTls.clientCert"
render={({ field }) => (
<FormItem>
<FormLabel>Client Certificate</FormLabel>
<FormDescription>
A public key certificate issued to the client by a trusted
Certificate Authority (CA).
</FormDescription>
<FormControl>
<Textarea {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientTls.clientKey"
render={({ field }) => (
<FormItem>
<FormLabel>Client Key</FormLabel>
<FormDescription>
A private key corresponding to the client certificate.
</FormDescription>
<FormControl>
<Textarea {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</AccordionContent>
</AccordionItem>
</Accordion>

<PermissionsDialog
checkResponse={
validationResponse ?? new CheckConnectionConfigResponse({})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,22 @@ export function getConnectionComponentDetails(
connectionName: connection.name,
url: connection.connectionConfig.config.value.connectionConfig
.value,

clientTls: {
rootCert: connection.connectionConfig.config.value.clientTls
?.rootCert
? connection.connectionConfig.config.value.clientTls.rootCert
: '',
clientCert: connection.connectionConfig.config.value.clientTls
?.clientCert
? connection.connectionConfig.config.value.clientTls
.clientCert
: '',
clientKey: connection.connectionConfig.config.value.clientTls
?.clientKey
? connection.connectionConfig.config.value.clientTls.clientKey
: '',
},
}}
onSaved={(resp) => onSaved(resp)}
onSaveFailed={onSaveFailed}
Expand Down
2 changes: 2 additions & 0 deletions frontend/apps/web/app/(mgmt)/[account]/connections/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,8 @@ function buildMongoConnectionConfig(
case: 'url',
value: values.url,
},

clientTls: getClientTlsConfig(values.clientTls),
});

return mongoconfig;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import { setOnboardingConfig } from '@/components/onboarding-checklist/Onboardin
import PermissionsDialog from '@/components/permissions/PermissionsDialog';
import { useAccount } from '@/components/providers/account-provider';
import SkeletonForm from '@/components/skeleton/SkeletonForm';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import {
Expand All @@ -18,6 +24,7 @@ import {
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { useToast } from '@/components/ui/use-toast';
import { useGetAccountOnboardingConfig } from '@/libs/hooks/useGetAccountOnboardingConfig';
import { getConnection } from '@/libs/hooks/useGetConnection';
Expand Down Expand Up @@ -54,6 +61,12 @@ export default function MongoDBForm(): ReactElement {
defaultValues: {
connectionName: '',
url: '',

clientTls: {
rootCert: '',
clientCert: '',
clientKey: '',
},
},
context: { accountId: account?.id ?? '' },
});
Expand Down Expand Up @@ -239,6 +252,70 @@ export default function MongoDBForm(): ReactElement {
)}
/>

<Accordion type="single" collapsible className="w-full">
<AccordionItem value="bastion">
<AccordionTrigger>Client TLS Certificates</AccordionTrigger>
<AccordionContent className="flex flex-col gap-4 p-2">
<div className="text-sm">
Configuring this section allows Neosync to connect to the
database using SSL/TLS.
</div>
<FormField
control={form.control}
name="clientTls.rootCert"
render={({ field }) => (
<FormItem>
<FormLabel>Root Certificate</FormLabel>
<FormDescription>
{`The public key certificate of the CA that issued the
server's certificate. Root certificates are used to
authenticate the server to the client. They ensure that
the server the client is connecting to is trusted.`}
</FormDescription>
<FormControl>
<Textarea {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientTls.clientCert"
render={({ field }) => (
<FormItem>
<FormLabel>Client Certificate</FormLabel>
<FormDescription>
A public key certificate issued to the client by a trusted
Certificate Authority (CA).
</FormDescription>
<FormControl>
<Textarea {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientTls.clientKey"
render={({ field }) => (
<FormItem>
<FormLabel>Client Key</FormLabel>
<FormDescription>
A private key corresponding to the client certificate.
</FormDescription>
<FormControl>
<Textarea {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</AccordionContent>
</AccordionItem>
</Accordion>

<PermissionsDialog
checkResponse={
validationResponse ?? new CheckConnectionConfigResponse({})
Expand Down
Loading

0 comments on commit 88242b2

Please sign in to comment.