Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use mtls between frontend and backend #37

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a2e8ab9
refactor(frontend): define and use createProxyConfig convenience func…
RobertRosca Nov 1, 2024
549b1c4
feat(frontend/mtls): try to load and enable mtls for proxied backend …
RobertRosca Nov 1, 2024
4782ab8
feat(api/mtls): add mtls support
RobertRosca Nov 1, 2024
a6061e3
deploy(api): userns_mode should be host not keep-id
RobertRosca Nov 1, 2024
ac9122b
deploy(api/mtls): mount certs directory into container
RobertRosca Nov 1, 2024
c4d9040
feat(frontend/mtls): configure mtls by settings proxy options.agent
RobertRosca Nov 1, 2024
72d1b7f
deploy(api): explicitly map host/container port
RobertRosca Nov 1, 2024
36c02e5
chore: add mtls files to gitignore
RobertRosca Nov 1, 2024
f9d9e97
build(api): start via python module call not uvicorn
RobertRosca Nov 5, 2024
6cc972d
build(api): fix certs mount target
RobertRosca Nov 5, 2024
58ee737
fix(frontend): apply override proxy configs
RobertRosca Nov 5, 2024
7ae418f
deploy(frontend): set node extra ca certs env var for mtls, mount certs
RobertRosca Nov 5, 2024
4018916
build: update poetry.lock file
CammilleCC Nov 14, 2024
c6dcf33
style(frontend): run prettier auto-format
RobertRosca Dec 6, 2024
36c1b87
refactor(frontend/config): rename variables
RobertRosca Dec 12, 2024
8f1b402
refactor(frontend/config): remove need for ssl config function, throw…
RobertRosca Dec 12, 2024
4e19f3d
refactor(frontend/config): simplify mTLS config check
RobertRosca Dec 12, 2024
bcb42eb
refactor(frontend/config): set httpsAgent to undefined instead of null
RobertRosca Dec 12, 2024
7714480
refactor(frontend/config): simplify defaultProxyConfig assignment
RobertRosca Dec 12, 2024
4c32dcb
refactor(frontend/config): explode all proxy configs for consistency
RobertRosca Dec 12, 2024
515d3b0
feat(frontend/config): exit early if url/api url missing
RobertRosca Dec 12, 2024
a7881fd
feat(frontend/config): simplify some checks, better errors/comments
RobertRosca Dec 12, 2024
ea4c8c8
refactor(frontend/config): use URL object for API/URL
RobertRosca Dec 12, 2024
be4620f
feat(refactor/config): reuse mtls config for https
RobertRosca Dec 12, 2024
5448d09
fix(api/settings): update env var names
RobertRosca Dec 13, 2024
52b04fc
chore(api/settings): use UTC timezone
RobertRosca Dec 13, 2024
d2ab499
chore(api/settings): use FilePath for cert config
RobertRosca Dec 13, 2024
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
.vscode
damnit_proposals.json
certs/
*.crt
*.key
*.pem
2 changes: 1 addition & 1 deletion api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ COPY ./README.md ./README.md

EXPOSE 8000

CMD ["poetry", "run", "uvicorn", "damnit_api.main:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["poetry", "run", "python3", "-m", "damnit_api.main"]
3 changes: 2 additions & 1 deletion api/compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ name: damnit-web-dev

services:
api:
userns_mode: keep-id
userns_mode: host
ports:
- 8123:8000
volumes:
- ./certs:/certs
- /gpfs/exfel/data/scratch/xdana/tmp/damnit-web:/var/tmp
- /gpfs:/gpfs
- /pnfs:/pnfs
7 changes: 6 additions & 1 deletion api/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ services:
env_file:
- .env
ports:
- 8000
- 8000:8000
environment:
DW_API_MTLS__CLIENT_CERT: /certs/server.crt
DW_API_MTLS__CLIENT_KEY: /certs/server.key
DW_API_MTLS__ROOT_CERT: /certs/root_ca.crt
RobertRosca marked this conversation as resolved.
Show resolved Hide resolved
volumes:
- ./tmp-damnit-web/:/tmp/damnit-web/
- ./certs:/certs
85 changes: 3 additions & 82 deletions api/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions api/src/damnit_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ async def http_exception_handler(request: Request, exc: HTTPException):

logger.info("Starting uvicorn with settings", **settings.uvicorn.model_dump())

if settings.uvicorn.ssl_cert_reqs != 2:
logger.warning(
"Not configured to require mTLS. This is not recommended for production."
)

uvicorn.run(
"damnit_api.main:create_app",
**settings.uvicorn.model_dump(),
Expand Down
20 changes: 20 additions & 0 deletions api/src/damnit_api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
SecretStr,
UrlConstraints,
field_validator,
model_validator,
)
from pydantic_settings import BaseSettings, SettingsConfigDict

Expand All @@ -24,6 +25,25 @@ class UvicornSettings(BaseModel):
reload: bool = True
factory: bool = True

ssl_keyfile: Path | None = None
ssl_certfile: Path | None = None
ssl_ca_certs: Path | None = None
ssl_cert_reqs: int | None = None

@model_validator(mode="after")
def ssl_all_if_one(self):
"""Ensure all SSL settings are set if one is set."""
files = [self.ssl_keyfile, self.ssl_certfile, self.ssl_ca_certs]
if any(files) and not all(files):
RobertRosca marked this conversation as resolved.
Show resolved Hide resolved
msg = "ssl_keyfile, ssl_certfile, and ssl_ca_certs must all be set"
raise ValueError(msg)

if all(files):
# Default to 2 (require mTLS) if any SSL settings are set
self.ssl_cert_reqs = self.ssl_cert_reqs or 2

return self

@field_validator("factory", mode="after")
@classmethod
def factory_must_be_true(cls, v, values):
Expand Down
4 changes: 4 additions & 0 deletions frontend/compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ networks:

services:
frontend:
environment:
- NODE_EXTRA_CA_CERTS=/certs/root_ca.crt
volumes:
- ./certs:/certs
networks:
- proxy
labels:
Expand Down
60 changes: 37 additions & 23 deletions frontend/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,45 @@
import { defineConfig, loadEnv } from "vite"
import path from "path"
import react from "@vitejs/plugin-react"
import fs from "fs"
import https from 'https';

export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
const baseUrl = (env.VITE_BASE_URL || "/").replace(/\/?$/, "/")

let sslConfig = null
if (env.VITE_MTLS_KEY && env.VITE_MTLS_CLIENT && env.VITE_MTLS_CA) {
const keyPath = path.resolve(__dirname, env.VITE_MTLS_KEY)
const certPath = path.resolve(__dirname, env.VITE_MTLS_CLIENT)
const caPath = path.resolve(__dirname, env.VITE_MTLS_CA)

if (fs.existsSync(keyPath) && fs.existsSync(certPath) && fs.existsSync(caPath)) {
sslConfig = {
key: fs.readFileSync(keyPath),
cert: fs.readFileSync(certPath),
ca: fs.readFileSync(caPath),
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tested locally, I found that adding these two helps making things compatible:

Suggested change
}
secureProtocol: "TLSv1_2_method",
ciphers: [
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
].join(":"),
}

}

function createProxyConfig(overrides){
CammilleCC marked this conversation as resolved.
Show resolved Hide resolved
const defaults = {
target: `https://${env.VITE_BACKEND_API}`,
CammilleCC marked this conversation as resolved.
Show resolved Hide resolved
changeOrigin: false,
rewrite: (path) => path.replace(new RegExp(`^${baseUrl}`), "/"),
secure: sslConfig ? true : false,
configure: (proxy, _options) => {
if (sslConfig) {
const httpsAgent = new https.Agent(sslConfig);
_options.agent = httpsAgent;
}
},
}

return {...defaults, ...overrides}
}

return {
base: baseUrl,
plugins: [react()],
Expand All @@ -17,29 +51,9 @@ export default defineConfig(({ mode }) => {
host: true,
port: Number(env.VITE_PORT) || 5173,
proxy: {
[`${baseUrl}graphql`]: {
target: `ws://${env.VITE_BACKEND_API}`,
changeOrigin: false,
secure: false,
ws: true,
rewriteWsOrigin: false,
// REMOVEME: The API will have a base path similar to the frontend at some point
rewrite: (path) => path.replace(new RegExp(`^${baseUrl}`), "/"),
},
[`${baseUrl}oauth`]: {
target: `http://${env.VITE_BACKEND_API}`,
changeOrigin: false,
secure: false,
// REMOVEME: The API will have a base path similar to the frontend at some point
rewrite: (path) => path.replace(new RegExp(`^${baseUrl}`), "/"),
},
[`${baseUrl}metadata`]: {
target: `http://${env.VITE_BACKEND_API}`,
changeOrigin: false,
secure: false,
// REMOVEME: The API will have a base path similar to the frontend at some point
rewrite: (path) => path.replace(new RegExp(`^${baseUrl}`), "/"),
},
[`${baseUrl}graphql`]: createProxyConfig({ws: true}),
[`${baseUrl}oauth`]: createProxyConfig({}),
[`${baseUrl}metadata`]: createProxyConfig({}),
},
},
test: {
Expand Down