From 2736ee27b9115aecc527b4ebdb0a1f25709b66c3 Mon Sep 17 00:00:00 2001 From: Chris Martin Date: Wed, 14 Sep 2022 12:40:24 -0400 Subject: [PATCH] Guides - Best Practices for Production Environments PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5695 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> GitOrigin-RevId: 108ecf326678f717c0e496d5120371a6e0a14845 --- .../best-practices-for-production/index.mdx | 18 ++++ .../observability.mdx.wip | 14 +++ .../performance.mdx.wip | 2 + .../security.mdx | 94 ++++++++++++++++++ docs/docs/guides/index.mdx | 1 + ...t-practices-security-apihasura-diagram.png | Bin 0 -> 14971 bytes 6 files changed, 129 insertions(+) create mode 100644 docs/docs/guides/best-practices-for-production/index.mdx create mode 100644 docs/docs/guides/best-practices-for-production/observability.mdx.wip create mode 100644 docs/docs/guides/best-practices-for-production/performance.mdx.wip create mode 100644 docs/docs/guides/best-practices-for-production/security.mdx create mode 100644 docs/static/img/guides/best-practices-security-apihasura-diagram.png diff --git a/docs/docs/guides/best-practices-for-production/index.mdx b/docs/docs/guides/best-practices-for-production/index.mdx new file mode 100644 index 00000000000..dd8fe7c2852 --- /dev/null +++ b/docs/docs/guides/best-practices-for-production/index.mdx @@ -0,0 +1,18 @@ +--- +title: 'Best Practices:' +description: Best practices for running Hasura in production environments +keywords: + - hasura + - docs + - guide + - best practices + - production +slug: index +sidebar_label: Best Practices for Production Environments +--- + +# Best Practices + +Best practices are the goal of all organizations with many different facets benefiting from those practices. This is particularly true with enterprise software and Hasura is no different. The guides below are broken down by category. + +- [Security](/guides/best-practices-for-production/security.mdx) diff --git a/docs/docs/guides/best-practices-for-production/observability.mdx.wip b/docs/docs/guides/best-practices-for-production/observability.mdx.wip new file mode 100644 index 00000000000..07299da2fbc --- /dev/null +++ b/docs/docs/guides/best-practices-for-production/observability.mdx.wip @@ -0,0 +1,14 @@ +--- +title: 'Best Practices: Observability' +description: Security best practices for a production environment +keywords: + - hasura + - docs + - best practices + - production +sidebar_label: Observability +--- + +# Observability Best Practices + +## Introduction diff --git a/docs/docs/guides/best-practices-for-production/performance.mdx.wip b/docs/docs/guides/best-practices-for-production/performance.mdx.wip new file mode 100644 index 00000000000..5bd6d9d210b --- /dev/null +++ b/docs/docs/guides/best-practices-for-production/performance.mdx.wip @@ -0,0 +1,2 @@ +read replicas +Configure appropriate queries to use the caching directive \ No newline at end of file diff --git a/docs/docs/guides/best-practices-for-production/security.mdx b/docs/docs/guides/best-practices-for-production/security.mdx new file mode 100644 index 00000000000..72b70fdf7ad --- /dev/null +++ b/docs/docs/guides/best-practices-for-production/security.mdx @@ -0,0 +1,94 @@ +--- +description: Security best practices for a production environment +keywords: + - hasura + - docs + - best practices + - production +sidebar_label: Security +--- + +import Thumbnail from '@site/src/components/Thumbnail'; + +# Security Best Practices + +## Introduction +This guide reviews security best practices that should be implemented for a production environment. Applying API security beyond RBAC permissions is mandatory for any API moving towards a production deployment. We recommend that all HTTP layer security work be done at the API gateway level and GraphQL-specific policies be applied at the Hasura level. + + + +Specifics about each security best practice can be found below. + +## Hasura GraphQL Engine + +#### Restrict Access: +Restrict knowledge of admin secrets to the minimally required team members as an admin secret provides unrestricted access to the Hasura GraphQL Engine. SSO collaboration should be used to grant project access without sharing an admin key. Subsequently, implement a plan to rotate admin secrets to limit the exposure of an admin secret being shared too broadly. + +[Multiple admin secrets](https://hasura.io/docs/latest/security/multiple-admin-secrets/) should be used in situations where admin secrets have different rotation timelines or when granting temporary access is needed. + +Leverage [allowed operations lists](https://www.graphql-code-generator.com/plugins/other/hasura-allow-list) whenever possible to restrict unbounded or unexpected operations from being executed against the GraphQL endpoint. Allow lists [must be enabled](https://hasura.io/docs/latest/security/allow-list/#enable-allow-list) via environment variable. These lists can be configured globally or at the role level which allows for each role to have a differently defined set of permissible operations. The allow list should include the complete set of expected operations for a given role to restrict the ability for a user to execute non-permissible operations. Consider using the [Hasura Allow List](https://www.graphql-code-generator.com/plugins/other/hasura-allow-list) codegen plugin to automatically generate allow list metadata from your application code. + +:::info Note + +The admin role will bypass the allowed operations list. + +::: + +#### Limit the API: + +The allowed operations lists workflow is ideal for private/internal APIs or APIs with well-understood and clearly defined operations. Public APIs or APIs with less defined expected operations should additionally configure [depth limits](https://hasura.io/docs/latest/security/api-limits/#depth-limits) and [node limits](https://hasura.io/docs/latest/security/api-limits/#node-limits). + +- Configure both [rate limits](https://hasura.io/docs/latest/security/api-limits/#rate-limits) and [time limits](https://hasura.io/docs/latest/security/api-limits/#time-limits) to restrict frequency and duration of operations. + +- [Limit rows](https://hasura.io/docs/latest/auth/authorization/permission-rules/#limit-rows-permissions) returned by a select operation. + +#### Permissions: + +The row-based access control configuration dictates permissions for the GraphQL API. It is critical that these permissions be configured correctly in order to prevent unauthorized or unintended access to the GraphQL API. + +- Review the [permissions summary](https://hasura.io/docs/latest/deployment/production-checklist/#review-the-summary) for each schema to verify permissions are constructed appropriately for your expected data access. + +- Configure an [anonymous default role](https://hasura.io/docs/latest/auth/authorization/common-roles-auth-examples/#anonymous-users-example) in order to apply global security permissions. This default role should be configured similarly to any other role. This includes [RBAC permissions](https://hasura.io/docs/latest/auth/authorization/basics/), [API limits](https://hasura.io/docs/latest/security/api-limits/), [allowed operations lists](https://www.graphql-code-generator.com/plugins/other/hasura-allow-list) and [disabling schema introspection](https://hasura.io/docs/latest/security/disable-graphql-introspection/). + +#### Disable development components: + +There are several components of Hasura GraphQL Engine that are crucial for development efforts but should be disabled for a production environment. However, it should be expected that some of these components may need to be temporarily re-enabled if a situation arises where a production environment specific issue requires troubleshooting. + +- [Disable APIs](https://hasura.io/docs/latest/deployment/production-checklist/#disable-apis). + +- [Disable the console](https://hasura.io/docs/latest/deployment/production-checklist/#disable-console). + +- [Disable dev mode](https://hasura.io/docs/latest/deployment/production-checklist/#disable-dev-mode). + +- [Disable schema introspection](https://hasura.io/docs/latest/security/disable-graphql-introspection/). + +#### Additional environment variables: + +There are specific environment variables that should be configured to ensure appropriate communication to the Hasura GraphQL Engine server. + +- [Allowed CORS requests](https://hasura.io/docs/latest/deployment/graphql-engine-flags/config-examples/#configure-cors). + +## Database connections + +Hasura GraphQL Engine communicates with your data sources(s) via ODBC connection strings. This means Hasura has the same permissions as the provided credentials in the connection string. + +- Review the database permissions allocated via the provided credentials to ensure the level of access granted to Hasura is appropriate. + +- Use database connections strings with the least privileges required for API operations. + +- Configure [read replicas](https://hasura.io/docs/latest/databases/connect-db/read-replicas/) to route read-only operations (queries) to one (or many) read replicas. + +## Networking/API gateway + +We recommend the following HTTP layer security policies to be configured at the API gateway: +- [Configure HTTPS](https://hasura.io/docs/latest/deployment/enable-https/) on your reverse proxy to ensure encrypted communication between your client and Hasura. +- Implement request and response size restrictions. +- Restricted allowed connection time to prevent incidents such as slowloris attacks. +- Apply both IP filtering and IP rate limiting. + +Consider using a a [web application firewall](https://www.cloudflare.com/learning/ddos/glossary/web-application-firewall-waf/) (WAF) as the first line of defense. A firewall can provide extra protection against common attack types such as cross-site request forgery (CSRF) by filtering and monitoring HTTP traffic between the application and the internet based on a rule set configured by your team. Common WAF solutions include Cloudflare, Akamai and Imperva. diff --git a/docs/docs/guides/index.mdx b/docs/docs/guides/index.mdx index 437f929a40d..e6f80b0e809 100644 --- a/docs/docs/guides/index.mdx +++ b/docs/docs/guides/index.mdx @@ -27,6 +27,7 @@ slug: index - [Code Editor Integrations](/guides/code-editor-integrations/index.mdx) - [MySQL preview](/guides/mysql-preview.mdx) - [Updating to Hasura v2](/guides/upgrade-hasura-v2.mdx) +- [Best Practices for Production Environments](/guides/best-practices-for-production/index.mdx) :::info Note diff --git a/docs/static/img/guides/best-practices-security-apihasura-diagram.png b/docs/static/img/guides/best-practices-security-apihasura-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..c6411b76ee1b8a212659be151fcff334999320e4 GIT binary patch literal 14971 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�w}+wXS+H-2oM4%JpzXKEGxgLyjm4Kngmo+zHt&nkY2*&Q4`? z!r*a+k*UCUg+PYa8GmU^iCq=&3gEGxhg34K*u1Q-W>~NY@72LXD>~XiN2Pgt+A@PQ@R8>vIrl@5ni{f{;zR=W5$xkJKGk2I!iy zYqg72ymC2-1ouKdk5^9ZTUHD@xG0$btb@X8C-?I)(80pxZkE&rMKoYo?2v8lmoOv8 zms=Zkwcwt#)`MP)2PxKMGANPK3v#oG+e@bo0KCoRxT~Q{*ryY!RQjtjJq&}{Xn+BO zj(2qCt7EKI78ojcpG2d(pI%9%26g}J>U#4e>hf=HpikUU{A#n~KRTlV&{BPy);qv6L|^ixtUOY;n{hTZ01jhrp6 zP6zJmAih*7UqYFvumba}y;PsuEaU@?-KEOZr>e{mqj;REDgJ19p9i7Gj-Xkx*X}a+ z;>{`n(u0l}_ckAMI53K%L4VfZ!asmLKu#UZ)HmY~EU)@q~1jV`A zufLB26>*m){1KUp;@b5>Nz3YDwaI!3-aAvx5FTb8t=+3SRDtV3+7z6oD zeq1$Ww8pH@_~f<}w78vOX}c4>`Ly)mkf`U5*Fe}>Qnt}o1#QRGWf0nZ$(e-#3qvJ! zD(}Ml4Ht4)+p&Le9^dD8rE)^t*gW=Z9)Wc6WLde_;P#^w;q5t$X>#(@j9hy4B@q6y zw5;>07;SUZ%t3Lze~IHH6sH97W5{3;C*Hpp{Q1fjmt0hnUm@oW0k6XA_Z?(iy*U7y zIs@NORi*{k-g`w=BlJhTFDt0A#=1e$BTSo9C*Y5?dl|}NsBc{2??vfbA*Yg2Bl@o7 z=D#d+aYBo2<2`Y-;@WXnE1SeF*NrsL6&OBx`2OtCUj}^D3R$B`vI1YtAa>Ep&g0y? zJF^ub=}mh>jkvqo>9kzq>MTchUC=BcUk)`(__d=d9yeToI#8Bab_?_h$b5F5(3;LD zt028)r7Z*(=4hB6(MGV7$Lt^<_a#T{r5J4 za0g|ppq(<5=LGAS1{Q$j64E|!uwdoTx^GtL2{TKzsdOHjk!3H-Cehv#z3n?U63Ygv z+JDwAdz4pv`n6G(hDUDLcn)i{-8{gqlfL&F(A%!d;vNYK?;#TOZY1&Vc)#g=VaQox z75Oi$A`Jzm)v3H4s#U%;v9lPcWfjKV;{ax2Zz7##$IxYTD(pyR|HJ@|k}Nw{BOZni z3?S&)|4%L=wW%#~ VbA56;Zy;oR%+ki9=