Embedding Grafana using an auth proxy with Auth0

Target publication: https://auth0.com/guest-authors

Embedding Grafana using an auth proxy with Auth0

Our web app at Okra Solar had a requirement that seemed incredibly simple but turned out to be a bit tricky. We wanted to embed a Grafana dashboard for our electricity distributor partners in Cambodia/Philippines so that they can see key metrics about their customer’s usage and generation of power.

Seems like a simple case of dropping in an <iframe> and away you go right?

Sadly it’s not that simple unless you want to disable authentication completely on your grafana instance (and we didn’t want to do that as some of the data is a bit sensitive).

The recommended method is to set up a Grafana Auth Proxy. While researching this task, I found many questions in forums from people asking questions like “How do I set up an auth proxy?” or “Why is my auth proxy not working?” or “Why does Grafana say it failed to load files when I’m using an auth proxy?” but no clear answers. I went through those same questions and thought it might be handy to document the process for solving them.

Requirements:

Allow access to Grafana only to users who logged in via Auth0. We don’t want to expose our Grafana dashboard to just anyone!

Step 1: Set up Auth0 to use RS256 to sign the JWTs

You’ll find this under advanced settings. RS256 is the most secure method and the only one that will work with the auth proxy I found.

Step 2: Configure your app to save the JWT in a cookie

We need to pass the JWT to our auth proxy so that it can check whether the user should be allowed to access Grafana. I chose to store the JWT in a cookie (same-site only and http-only for security) because it was the only viable option supported by caido’s excellent grafana auth proxy docker image. At first I tried using headers but that’s a trap - it will work for the Grafana HTML but not for any of the assets (JS, CSS, etc.). 

We use auth0 with passport js. You may need to adjust depending on how you integrate with auth0.

Note the way id_token is passed to the callback.

       // accessToken is the token to call Auth0 API (not needed in the most cases)

       // extraParams.id_token has the JSON Web Token

       // profile has all the information from the user

       const verifyFunc: VerifyFunction = (accessToken: string, refreshToken: string, extraParams: ExtraVerificationParams, profile: Profile, callback: (error: Error | null, user: Profile, jwtToken: string) => void) => {

           const jsonWebToken = (extraParams as ExtraVerificationParamsWithJWT).id_token

 

           return callback(null, profile, jsonWebToken)

       }

 

       passport.use(new Auth0Strategy(authConfig, verifyFunc))

Then in your callback handler, do something like this. (We’re using Koa)

 

   public static async handleCallback (ctx: Context, next: () => Promise<void>) {

       const authMiddleware = passport.authenticate('auth0', async (err, user, info) => {

           if (err || ['unauthorised', 'unauthorized'].includes(info)) {

               ctx.logout()

               return ctx.redirect(ErrorRoutes.Unauthorised)

           }

           if (!user) return ctx.redirect(PublicRoutes.Login)

 

           // save JWT in cookie to pass to grafana auth proxy

           const jwtExpiresIn = 1000 * 60 * 60 * 24 * 30 // 30 days

           ctx.cookies.set(ServerRuntimeConfig.JWT_COOKIE_NAME, info, {

               maxAge: jwtExpiresIn,

               httpOnly: true,

               sameSite: 'strict',

  // this is essential otherwise the subdomain won’t have access to the cookie

               domain: ServerRuntimeConfig.APP_DOMAIN_NAME

           })

 

           ctx.status = 201

           ctx.body = { success: true }

 

           await ctx.login(user)

           ctx.redirect(Routes.Villages)

       })

       return authMiddleware(ctx, next)

   }

Step 3: Deploy Grafana and Grafana Auth Proxy

We use AWS CDK to deploy Grafana and the Grafana Auth Proxy as docker containers onto Fargate ECS.

Here’s the CDK code. Note the environment variables passed to Grafana to allow use of auth proxy.

Important things to note: 

  1. The auth proxy must be deployed on a subdomain of the main app (e.g. if your app is hosted at app.mycoolstartup.co the auth proxy would be on grafana-auth-proxy.app.mycoolstartup.co) otherwise the auth proxy won’t have access to cookies.

  2. Make sure you pass the same cookie name to both your webapp and the auth proxy.

  3. Use all the environment variables shown

setupDBForGrafana(dbPassword: string) {

   const privatesubnets = this.vpc.selectSubnets({ subnetType: SubnetType.PRIVATE });

   const dbSubnetGroupName = "grafana-db-subnet-group";

   new rds.CfnDBSubnetGroup(this, 'rdsdbsubnets', {

     dbSubnetGroupDescription: "grafana rds db subnet group",

     dbSubnetGroupName,

     subnetIds: privatesubnets.subnetIds

   });

   const dbsecuritygroup = new ec2.CfnSecurityGroup(this, 'dbsecuritygroup', {

     groupDescription: "grafana db security group",

     groupName: "grafana-db-sg",

     vpcId: this.vpc.vpcId,

     securityGroupIngress: [

       {

         cidrIp: this.vpc.vpcCidrBlock,

         fromPort: 3306,

         ipProtocol: "tcp",

         toPort: 3306

       }

     ],

     securityGroupEgress: [

       {

         cidrIp: "0.0.0.0/0",

         ipProtocol: "-1"

       }

     ]

   });

   const rdsdb = new rds.CfnDBCluster(this, 'rdsdb', {

     availabilityZones: this.vpc.availabilityZones.slice(0, 3), // rds is limited to 3 azs

     backupRetentionPeriod: 7,

     dbSubnetGroupName: dbSubnetGroupName,

     dbClusterIdentifier: "grafana-db",

     engine: "aurora",

     port: 3306,

     masterUsername: "admin",

     masterUserPassword: dbPassword,

     databaseName: 'grafana',

     vpcSecurityGroupIds: [

       dbsecuritygroup.ref

     ],

     storageEncrypted: true,

     engineMode: "serverless",

     scalingConfiguration: {

       autoPause: true,

       minCapacity: 1

     },

     deletionProtection: false

   });

 

   return rdsdb;

 }

 

 setupReverseProxyForGrafana() {

   this.grafanaReverseProxy = new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'Grafana Reverse Proxy', {

     assignPublicIp: false,

     cluster: this.ecsCluster,

     cpu: this.props.taskCPU,

     desiredCount: 1,

     domainZone: this.hostedZone,

     domainName: this.props.grafanaReverseProxyDomainName,

     certificate: this.grafanaReverseProxyCertificate,

     taskImageOptions: {

       image: ecs.ContainerImage.fromRegistry('caido/grafana-auth-proxy'),

       containerPort: 5000,

       environment: {

         PROXY_SERVED_URL: `https://${this.props.grafanaDomainName}`,

         PROXY_PORT: '5000',

         PROXY_COOKIE_AUTH: 'true',

         PROXY_COOKIE: this.props.harvestJWTTokenName,

         PROXY_JWK_FETCH_URL: `https://${this.props.auth0Domain}/.well-known/jwks.json`,

         PROXY_JWT_ISSUER: `https://${this.props.auth0Domain}/`,

         PROXY_JWT_AUDIENCE: this.props.auth0ClientId,

         PROXY_JWT_GRAFANA_CLAIM: 'email'

       }

     },

     memoryLimitMiB: this.props.taskMemoryLimit,

     publicLoadBalancer: true,

     healthCheckGracePeriod: cdk.Duration.seconds(300)

   });

 

   this.grafanaReverseProxy.targetGroup.configureHealthCheck({

     path: "/api/health",

     interval: cdk.Duration.seconds(120),

     unhealthyThresholdCount: 5

   });

 

   const responseTimeAlarm = new Alarm(this, 'slow-grafana-proxy-response-time-alarm', {

     metric: this.grafanaReverseProxy.loadBalancer.metricTargetResponseTime(),

     threshold: 15,

     evaluationPeriods: 2

   });

   responseTimeAlarm.addAlarmAction(new SnsAction(this.alarmSNSTopic));

 }

 

 setupGrafana() {

   const bootstrapPassword = "23423fdfsdfsdsf";

 

   const rdsdb = this.setupDBForGrafana(bootstrapPassword);

   const taskdef = new ecs.FargateTaskDefinition(this, 'taskdef', { cpu: 256, memoryLimitMiB: 512, });

   const logging: ecs.LogDriver = new ecs.AwsLogDriver({ streamPrefix: this.node.id });

 

   const container = taskdef.addContainer("grafana", {

     image: ecs.ContainerImage.fromRegistry("grafana/grafana"),

     logging,

     environment: {

       GF_INSTALL_PLUGINS: "grafana-clock-panel,grafana-simple-json-datasource",

       GF_DATABASE_TYPE: "mysql",

       GF_DATABASE_PASSWORD: String(rdsdb.masterUserPassword),

       GF_DATABASE_USER: String(rdsdb.masterUsername),

       GF_DATABASE_HOST: rdsdb.attrEndpointAddress,

       GF_AUTH_ANONYMOUS_ENABLED: "false",

       GF_SECURITY_ADMIN_PASSWORD: bootstrapPassword,

       GF_SECURITY_ALLOW_EMBEDDING: "true",

       GF_AWS_default_REGION: this.region,

       GF_USERS_ALLOW_SIGN_UP: "false",

       GF_USERS_AUTO_ASSIGN_ORG: "true",

       GF_USERS_AUTO_ASSIGN_ORG_ROLE: "Viewer",

       GF_AUTH_PROXY_ENABLED: "true",

       GF_AUTH_PROXY_HEADER_NAME: "X-WEBAUTH-USER",

       GF_AUTH_PROXY_HEADER_PROPERTY: "email",

       GF_AUTH_PROXY_AUTO_SIGN_UP: "true",

       GF_SERVER_DOMAIN: this.props.grafanaReverseProxyDomainName,

       GF_SERVER_ROOT_URL: `https://${this.props.grafanaReverseProxyDomainName}/`,

     }

   });

   container.taskDefinition.taskRole.addManagedPolicy(

     ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess')

   );

   container.addPortMappings({ containerPort: 3000 });

 

   this.grafanaService = new ecs_patterns.ApplicationLoadBalancedFargateService(this, "grafanaservice", {

     cluster: this.ecsCluster,

     taskDefinition: taskdef,

     serviceName: 'grafana',

     publicLoadBalancer: true,

     domainZone: this.hostedZone,

     domainName: this.props.grafanaDomainName,

     certificate: this.grafanaCertificate,

   });

   this.grafanaService.targetGroup.configureHealthCheck({ path: "/login" });

 

   this.setupReverseProxyForGrafana();

 }

Leave a Comment

Your email address will not be published.