1 Commits

Author SHA1 Message Date
501bd948ca experiment: change tooling 2023-10-28 06:37:31 +00:00
31 changed files with 3320 additions and 6128 deletions

View File

@@ -1,5 +0,0 @@
**/node_modules
.git
.gitignore
.local
.env*

10
.env
View File

@@ -1,10 +0,0 @@
DUMMY_HTML_DIR=./development-test-data-dir/
ASSETS_DOMAIN=assets.suyono.me
MYSQL_HOST=db.host
MYSQL_PORT=3306
MYSQL_USER=dbuser
MYSQL_PASSWORD=dbpasswd
MYSQL_DATABASE=dbname
MYSQL_SSL_CA=/path/to/ca.file
MYSQL_SSL_CERT=/path/to/cert.file
MYSQL_SSL_KEY=/path/to/key.file

5
.gitignore vendored
View File

@@ -5,8 +5,6 @@
/.pnp
.pnp.js
/.local/
# testing
/coverage
@@ -27,8 +25,7 @@ yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env*
# vercel
.vercel

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="dataSourceStorageLocal" created-in="WS-241.17011.90">
<data-source name="devblog@mysql-server" uuid="32913bc6-cafd-416c-b070-eee7c73cf755">
<database-info product="MySQL" version="8.3.0" jdbc-version="4.2" driver-name="MySQL Connector/J" driver-version="mysql-connector-j-8.2.0 (Revision: 06a1f724497fd81c6a659131fda822c9e5085b6c)" dbms="MYSQL" exact-version="8.3.0" exact-driver-version="8.2">
<extra-name-characters>#@</extra-name-characters>
<identifier-quote-string>`</identifier-quote-string>
<jdbc-catalog-is-schema>true</jdbc-catalog-is-schema>
</database-info>
<case-sensitivity plain-identifiers="exact" quoted-identifiers="exact" />
<secret-storage>master_key</secret-storage>
<user-name>devblog</user-name>
<schema-mapping>
<introspection-scope>
<node kind="schema">
<name qname="@" />
<name qname="devblog" />
</node>
</introspection-scope>
</schema-mapping>
<ssl-config use-ide-store="true" use-java-store="true" use-system-store="true">
<ca-cert>$USER_HOME$/Documents/db-ssl/ca.crt</ca-cert>
<client-cert>$USER_HOME$/Documents/db-ssl/mbp.crt</client-cert>
<client-key>$USER_HOME$/Documents/db-ssl/mbp.key</client-key>
<enabled>true</enabled>
<mode>VERIFY_FULL</mode>
</ssl-config>
</data-source>
</component>
</project>

12
.idea/dataSources.xml generated
View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="devblog@mysql-server" uuid="32913bc6-cafd-416c-b070-eee7c73cf755">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://mysql-server.suyono.dev:13306/devblog</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

6
.idea/sqldialects.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="PROJECT" dialect="MySQL" />
</component>
</project>

3
.tool-versions Normal file
View File

@@ -0,0 +1,3 @@
nodejs 20.8.1
yarn 1.22.19
pnpm 8.9.2

View File

@@ -1,37 +0,0 @@
FROM node:lts-alpine as base
FROM base as builder
USER 1000:1000
ADD --chown=1000:1000 . /home/node/nextts
WORKDIR /home/node/nextts
RUN wget -qO- https://get.pnpm.io/install.sh | PNPM_VERSION="8.15.3" ENV="/home/node/.shrc" SHELL="$(which sh)" sh -
ENV PATH=/home/node/.local/share/pnpm:$PATH
RUN pnpm install && pnpm run build
FROM base as runtime
RUN npm install -g pm2
COPY --from=builder /home/node/nextts/public /home/node/nextts/public
COPY --from=builder /home/node/nextts/.next/standalone /home/node/nextts
COPY --from=builder /home/node/nextts/.next/static /home/node/nextts/.next/static
ADD --chown=1000:1000 pm2.config.js /home/node/nextts/
ADD --chown=1000:1000 dummies /home/node/nextts/dummies
RUN chown -R 1000:1000 /home/node/nextts
USER 1000:1000
WORKDIR /home/node/nextts
ENV PORT 3000
ENV NODE_ENV production
ENV HOME /home/node
ENV HOSTNAME "0.0.0.0"
RUN wget -qO- https://get.pnpm.io/install.sh | PNPM_VERSION="8.15.3" ENV="/home/node/.shrc" SHELL="$(which sh)" sh -
ENV PATH=/home/node/.local/share/pnpm:$PATH
RUN pnpm install
CMD ["pm2-runtime", "pm2.config.js"]
#CMD ["node", "server.js"]

View File

@@ -1,9 +1,9 @@
import { getPromisePool } from "@/backend/db";
import { promisePool } from "@/backend/db";
import {RowDataPacket} from "mysql2";
async function query() {
try {
const [rows, fields] = await getPromisePool().query<RowDataPacket[]>('select slug from post limit 1;')
const [rows, fields] = await promisePool.query<RowDataPacket[]>('select slug from post limit 1;')
return(rows[0]['slug'] as string)
} catch (e) {
console.log(e)
@@ -11,18 +11,11 @@ async function query() {
}
}
export default async function DbCheck({ searchParams }: { searchParams: { [key: string]: string | string[] | undefined }}) {
let flag = "empty";
if (typeof searchParams["flag"] === 'string') {
flag = searchParams["flag"]
}
export default async function DbCheck() {
return(
<div className={`flex flex-col`}>
<p>Env: { process.env.MYSQL_HOST }</p>
<p>Result: { await query() }</p>
<p>Flag: { flag }</p>
</div>
)
}

View File

@@ -1,41 +1,23 @@
import {
Raleway,
Syne,
Questrial,
Roboto,
Nunito_Sans,
Open_Sans
} from "next/font/google";
import { Raleway, Syne, Questrial, Nunito_Sans } from "next/font/google";
export const raleway = Raleway({
subsets: ['latin'],
display: "swap",
});
})
export const syne = Syne({
subsets: ['latin'],
display: "swap",
});
})
export const questrial = Questrial({
subsets: ['latin'],
display: "swap",
weight: ['400'],
});
export const roboto = Roboto({
subsets: ['latin'],
weight: ['400'],
display: "swap",
});
export const openSans = Open_Sans({
subsets: ['latin'],
display: "swap",
})
export const nunito = Nunito_Sans({
export const nunito_sans = Nunito_Sans({
subsets: ['latin'],
weight: ['300'],
display: "swap",
})
}
)

View File

@@ -1,5 +1,3 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@import "highlight.js/styles/base16/atelier-forest-light.min.css";
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,9 +1,12 @@
import "./globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import BlogHeader from "@/components/blogHeader";
import BlogFooter from "@/components/blogFooter";
import React from "react";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
@@ -16,7 +19,7 @@ export default function RootLayout({
}) {
return (
<html lang="en">
<body>
<body className={inter.className}>
<div className={`flex flex-col bg-white`}>
<BlogHeader />
{children}

View File

@@ -1,7 +0,0 @@
import { MarkPostString } from '@/components/dummyPost';
import { content } from '@/components/post';
export default async function Mark() {
let postContent = await MarkPostString();
return content(postContent);
}

View File

@@ -1,46 +1,69 @@
import Image from "next/image";
import { getAssetsDomain } from "@/backend/env"
import Link from "next/link";
import { raleway, syne, questrial } from "@/app/fonts";
import { HomePostList } from "@/components/homePostList";
export default function Home() {
return (
<div className={`flex flex-col`}>
<div className={`grid grid-rows-1 grid-cols-1 justify-items-center`}>
<Image
src={`https://${getAssetsDomain()}/placeholder.webp`}
alt={`blog cover`}
className={`object-cover col-start-1 row-start-1 w-screen h-192 z-0`}
width={1581}
height={759}
/>
<div className={`flex flex-col-reverse col-start-1 row-start-1 w-screen`}>
<div className={`bg-neutral-100 bg-opacity-30 flex flex-col py-10 z-10`}>
<p className={`${raleway.className} text-white text-center text-7xl font-thin mb-6`}>
SUYONO
<div className={`flex flex-col`}>
<div className={`bg-cover bg-center flex flex-col-reverse bg-blog-cover`}>
<div className={`bg-neutral-100 bg-opacity-30 flex flex-col py-10`}>
<p className={`${raleway.className} text-white text-center text-7xl font-thin mb-6`}>
SUYONO
</p>
<p className={`${raleway.className} text-white text-center font-thin text-xl mb-10`}>
A Tech Archive
</p>
</div>
<div className={`h-64`}></div> {/* spacer */}
<div className={`h-64`}></div> {/* spacer */}
</div>
<div className={`flex flex-row justify-center my-8`}>
<div className={`border border-slate-100 flex flex-col`}>
<Link
href="/post/nginx-ssl-client-certificate-verification-manage-access-to-a-site"
className={`flex flex-row max-w-4xl items-center`}
>
<Image
src="/assets/pthumb.webp"
alt="post thumbnail"
width={454}
height={341}
/>
<div className={`flex flex-col mx-10`}>
<p className={`${syne.className} text-2xl`}>
Nginx + SSL Client Certificate Verification: Manage Access to a
site
</p>
<p className={`${raleway.className} text-white text-center font-thin text-xl mb-10`}>
A Tech Archive
<p className={`${questrial.className} line-clamp-3 mt-4`}>
Access control is a fundamental part of security. Most entities
rely on the combination of username and password, sometimes with
additional multi-factor authentication to improve security. Some
entities also use the SSL client certificate verification to
manage access to specific resources. One of the use cases where
SSL client certificate verification fits perfectly is managing
access to internet-facing development or staging servers. In
this post, I&apos;ll share how to set up the certificates and
configure nginx to verify users based on their certificates.
</p>
</div>
</div>
</div>
<HomePostList className={`flex flex-col mx-auto my-8`} />
<div className={`flex flex-row bg-teal-50 justify-center`}>
<div className={`max-w-4xl py-28 px-10`}>
<p className={`text-3xl ${raleway.className}`}>Hi There</p>
<p className={`text-base ${raleway.className} my-4`}>
a new take on experience is the best teacher
</p>
<p className={`${raleway.className} text-sm`}>
I started this blog as an archive of my experiences and knowledge.
By writing them out, I hope it will help me unlearn and relearn the
various knowledge and skills I&apos;ve accumulated. I hope the
articles, source code examples, and server config examples I wrote
will help you somehow. Read on and enjoy!
</p>
</div>
</Link>
</div>
</div>
<div className={`flex flex-row bg-teal-50 justify-center`}>
<div className={`max-w-4xl py-28 px-10`}>
<p className={`text-3xl ${raleway.className}`}>Hi There</p>
<p className={`text-base ${raleway.className} my-4`}>
a new take on experience is the best teacher
</p>
<p className={`${raleway.className} text-sm`}>
I started this blog as an archive of my experiences and knowledge.
By writing them out, I hope it will help me unlearn and relearn the
various knowledge and skills I&apos;ve accumulated. I hope the
articles, source code examples, and server config examples I wrote
will help you somehow. Read on and enjoy!
</p>
</div>
</div>
</div>
);
}

View File

@@ -1,25 +1,82 @@
import { getPost } from '@/backend/post';
import { content } from '@/components/post'
import { getPost } from "@/backend/post";
import DOMPurify from "dompurify";
import { JSDOM } from "jsdom";
import { nunito_sans, raleway } from "@/app/fonts";
import parse, {
domToReact,
Element,
HTMLReactParserOptions,
} from "html-react-parser";
import { DummyPostSlug, DummyPostString } from "@/components/dummyPost";
const options: HTMLReactParserOptions = {
replace: (domNode) => {
if (domNode instanceof Element && domNode.attribs) {
// console.log(domNode.attribs)
if (domNode.name === "h1") {
if (domNode.attribs.class === "title") {
return (
<h1 className={`${raleway.className} mx-auto w-224 text-4xl`}>
{domToReact(domNode.children)}
</h1>
);
} else {
return (
<h1 className={`${raleway.className} mx-auto w-224 text-3xl`}>
{domToReact(domNode.children)}
</h1>
);
}
} else if (domNode.name === "h2") {
return (
<h1 className={`${raleway.className} mx-auto w-224 text-2xl`}>
{domToReact(domNode.children)}
</h1>
);
} else if (domNode.name === "h3") {
return (
<h1 className={`${raleway.className} mx-auto w-224 text-xl`}>
{domToReact(domNode.children)}
</h1>
);
} else if (domNode.name === "p") {
if (domNode.attribs.class === "paragraph") {
return (
<h1 className={`${nunito_sans.className} mx-auto w-224`}>
{domToReact(domNode.children)}
</h1>
);
} else {
const classes = domNode.attribs.class.split(" ");
if (classes.includes("code")) {
if (classes.includes("shell")) {
} else if (classes.includes("go") || classes.includes("golang")) {
} else {
}
}
}
}
}
},
};
export default async function Post({ params }: { params: { slug: string } }) {
// let content;
//
// const dummySlug = DummyPostSlug();
// if (dummySlug === params.slug) {
// content = await DummyPostString();
// } else {
// content = await getPost(params.slug);
// }
//
// content = DOMPurify(new JSDOM("<!DOCTYPE html>").window).sanitize(content);
// const elem = parse(content, options);
//
// return (
// <div className={`flex flex-col`}>
// {elem}
// </div>
// );
let content = "";
let postContent :string = await getPost(params.slug);
return(content(postContent));
const dummySlug = await DummyPostSlug();
if (dummySlug === params.slug) {
content = await DummyPostString();
console.log(content);
} else {
content = await getPost(params.slug);
}
content = DOMPurify(new JSDOM("<!DOCTYPE html>").window).sanitize(content);
// console.log(content)
const elem = parse(content, options);
return <div className={`flex flex-col`}>{elem}</div>;
}

View File

@@ -1,42 +1,20 @@
import mysql, { PoolOptions, Pool } from "mysql2";
import { Pool as pPool } from "mysql2/promise"
import * as fs from 'fs';
import * as appEnv from "@/backend/env";
import mysql, { PoolOptions } from "mysql2";
let pool: Pool | undefined;
let promisePool: pPool | undefined;
export function getPool(): Pool {
const access: PoolOptions = {
host: appEnv.getMysqlHost(),
port: appEnv.getMysqlPort(),
user: appEnv.getMysqlUser(),
password: appEnv.getMysqlPassword(),
database: appEnv.getMysqlDatabase(),
waitForConnections: true,
connectionLimit: 10,
maxIdle: 10,
idleTimeout: 60000,
queueLimit: 0,
enableKeepAlive: true,
keepAliveInitialDelay: 0,
ssl: {
ca: fs.readFileSync(appEnv.getMysqlSslCaFile()),
key: fs.readFileSync(appEnv.getMysqlSslKeyFile()),
cert: fs.readFileSync(appEnv.getMysqlSslCertFile())
}
}
if (typeof pool === 'undefined') {
pool = mysql.createPool(access)
}
return pool
const access: PoolOptions = {
host: process.env.MYSQL_HOST,
port: 'MYSQL_PORT' in process.env && typeof process.env.MYSQL_PORT === 'string' ? parseInt(process.env.MYSQL_PORT) : 3306,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: 'MYSQL_DATABASE' in process.env ? process.env.MYSQL_DATABASE : 'blog',
waitForConnections: true,
connectionLimit: 10,
maxIdle: 10,
idleTimeout: 60000,
queueLimit: 0,
enableKeepAlive: true,
keepAliveInitialDelay: 0,
}
export function getPromisePool(): pPool {
if (typeof promisePool === 'undefined') {
promisePool = getPool().promise()
}
return promisePool
}
export const pool = mysql.createPool(access)
export const promisePool = pool.promise()

View File

@@ -1,71 +0,0 @@
export function getAssetsDomain(): string {
if (typeof process.env.ASSETS_DOMAIN === 'undefined') {
throw new Error("missing env ASSETS_DOMAIN");
}
return process.env.ASSETS_DOMAIN;
}
export function getMysqlHost(): string {
if (typeof process.env.MYSQL_HOST === 'undefined') {
throw new Error("missing env MYSQL_HOST")
}
return process.env.MYSQL_HOST
}
export function getMysqlPort(): number {
if (typeof process.env.MYSQL_PORT === 'undefined') {
throw new Error("missing env MYSQL_PORT")
}
return parseInt(process.env.MYSQL_PORT)
}
export function getMysqlUser(): string {
if (typeof process.env.MYSQL_USER === 'undefined') {
throw new Error("missing env MYSQL_USER")
}
return process.env.MYSQL_USER
}
export function getMysqlPassword(): string {
if (typeof process.env.MYSQL_PASSWORD === 'undefined') {
throw new Error("missing env MYSQL_PASSWORD")
}
return process.env.MYSQL_PASSWORD
}
export function getMysqlDatabase(): string {
if (typeof process.env.MYSQL_DATABASE === 'undefined') {
throw new Error("missing env MYSQL_DATABASE")
}
return process.env.MYSQL_DATABASE
}
export function getMysqlSslCaFile(): string {
if (typeof process.env.MYSQL_SSL_CA === 'undefined') {
throw new Error("missing env MYSQL_SSL_CA")
}
return process.env.MYSQL_SSL_CA
}
export function getMysqlSslCertFile(): string {
if (typeof process.env.MYSQL_SSL_CERT === 'undefined') {
throw new Error("missing env MYSQL_SSL_CERT")
}
return process.env.MYSQL_SSL_CERT
}
export function getMysqlSslKeyFile(): string {
if (typeof process.env.MYSQL_SSL_KEY === 'undefined') {
throw new Error("missing env MYSQL_SSL_KEY")
}
return process.env.MYSQL_SSL_KEY
}

View File

@@ -1,9 +1,9 @@
import { RowDataPacket } from "mysql2";
import { getPromisePool } from "@/backend/db";
import { promisePool } from "@/backend/db";
export async function getPost(slug: string): Promise<string> {
try {
const [rows, fields] = await getPromisePool().query<RowDataPacket[]>(
const [rows, fields] = await promisePool.query<RowDataPacket[]>(
'select content from post where slug = ?', [slug])
return rows[0]['content']
} catch (e) {

View File

@@ -1,13 +1,78 @@
import { readFile } from 'node:fs/promises';
export async function MarkPostString() {
let path = ""
if ('DUMMY_HTML_DIR' in process.env && typeof process.env.DUMMY_HTML_DIR === "string") {
path = process.env.DUMMY_HTML_DIR + "test1.md";
}
return await readFile(path, "utf-8")
export async function DummyPostString() {
const ReactDOMServer = (await import('react-dom/server')).default
const component = await DummyPost()
return ReactDOMServer.renderToStaticMarkup(component)
}
export function DummyPostSlug() {
export async function DummyPostSlug() {
return "dummy-post"
}
export async function DummyPost() {
return(
<div>
<h1 className="title">Nginx + SSL Client Certificate Verification: Manage access to a site</h1>
<p className="paragraph">Access control is a fundamental part of security. Most entities rely on
the combination of username and password, sometimes with additional multi-factor authentication
to improve security. Some entities also use the SSL client certificate verification to manage access
to specific resources. One of the use cases where SSL client certificate verification fits perfectly is
managing access to internet-facing development or staging servers. In this post, I&apos;ll share how
to set up the certificates and configure nginx to verify users based on their certificates.</p>
<h1>Preparing the certificates</h1>
<p className="paragraph">There are two certificates we are going to create. The first one is the root
certificate. It will be placed in the Nginx server. The second one is the client certificate. It will
be installed in the client machine/browsers.</p>
<h2>Root CA</h2>
<p className="paragraph">For generating a root CA, execute these two steps:</p>
<h3>Generate RSA Key</h3>
<p className="code">openssl genrsa -aes256 -out ca.key 4096</p>
<h3>Create Root CA crt file.</h3>
<p className="code">openssl req -new -x509 -days 3650 -key ca.key -out ca.crt</p>
<h2>Setup CA configuration</h2>
<p className="paragraph">This is an optional step, but if you want to be able to revoke access you
previously granted, you need to do this step.</p>
<p className="paragraph">Create a file named ca.cnf in the same directory as the ca.key and ca.crt.</p>
<p className="code">[ ca ]
default_ca = gca
[ crl_ext ]
authorityKeyIdentifier=keyid:always
[ gca ]
dir = ./
new_certs_dir = $dir
unique_subject = no
certificate = $dir/ca.crt
database = $dir/certindex
private_key = $dir/ca.key
serial = $dir/certserial
default_days = 365
default_md = sha256
policy = gca_policy
x509_extensions = gca_extensions
crlnumber = $dir/crlnumber
default_crl_days = 365
[ gca_policy ]
commonName = supplied
stateOrProvinceName = supplied
countryName = optional
emailAddress = optional
organizationName = supplied
organizationUnitName = optional
[ gca_extensions ]
basicConstraints = CA:false
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
keyUsage = digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth
crlDistributionPoints = URI:http://example.com/root.crl
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = example.com
DNS.2 = *.example.com</p>
</div>
)
}

View File

@@ -1,47 +0,0 @@
import Link from "next/link";
import Image from "next/image";
import {getAssetsDomain} from "@/backend/env";
import {questrial, syne} from "@/app/fonts";
import { RowDataPacket } from "mysql2";
import { getPromisePool } from "@/backend/db";
interface post extends RowDataPacket {
id: number,
slug: string,
title: string,
preview: string,
cover_url: string,
cover_alt: string,
preview_cover_height: number,
preview_cover_width: number,
}
export type HomePostListProps = {
className?: string;
}
export async function HomePostList(props :HomePostListProps) {
const [posts] = await getPromisePool().query<post[]>(
'SELECT * FROM post LIMIT 10;'
);
return (
<div className={props.className}>
{posts.map(post => (
<div key={post.id} className={`border border-slate-100 flex flex-col`}>
<Link
href={`/post/${post.slug}`}
className={`flex flex-row max-w-4xl items-center`} >
<Image src={post.cover_url}
alt={post.cover_alt}
height={post.preview_cover_height}
width={post.preview_cover_width} />
<div className={`flex flex-col mx-10`}>
<p className={`${syne.className} text-2xl`}>{post.title}</p>
<p className={`${questrial.className} line-clamp-3 mt-4`}>{post.preview}</p>
</div>
</Link>
</div>
))}
</div>
)
}

View File

@@ -1,120 +0,0 @@
import { unified } from 'unified';
import remarkGfm from 'remark-gfm';
import remarkParse from 'remark-parse';
import remarkRehype, { Options as RemarkRehypeOptions } from 'remark-rehype';
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
import rehypeHighlight, { Options as RehypeHighlightOptions } from 'rehype-highlight';
import rehypeReact, { Options as RehypeReactOptions } from 'rehype-react';
import { all } from 'lowlight';
import { Fragment, jsx, jsxs } from 'react/jsx-runtime';
import { raleway, roboto, nunito } from "@/app/fonts";
import rehypeRaw from "rehype-raw";
import Image from "next/image";
import { getAssetsDomain } from "@/backend/env";
export async function content(content: string) {
let result = await unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype, { allowDangerousHtml: true } as RemarkRehypeOptions)
.use(rehypeRaw)
.use(rehypeSanitize, {
...defaultSchema,
tagNames: [
...(defaultSchema.tagNames || []),
'figure',
'figcaption',
]
})
.use(rehypeHighlight, {
languages: all,
} as RehypeHighlightOptions)
.use(rehypeReact, {
Fragment: Fragment,
jsx: jsx,
jsxs: jsxs,
passNode: true,
components: {
h1: (props) => (
<h1 className={`${raleway.className} mx-auto w-224 text-4xl`}>
{props.children}
</h1>
),
h2: (props) => (
<h2 className={`${raleway.className} mx-auto mt-6 w-224 text-3xl`}>
{props.children}
</h2>
),
h3: (props) => (
<h3 className={`${raleway.className} mx-auto mt-4 w-224 text-2xl`}>
{props.children}
</h3>
),
h4: (props) => (
<h4 className={`${raleway.className} mx-auto mt-3 w-224 text-xl`}>
{props.children}
</h4>
),
p: (props) => (
<p className={`${nunito.className} mx-auto mt-2 w-224`}>
{props.children}
</p>
),
pre: (props) => (
<div className={`w-224 mx-auto mt-2`}>
<pre>{props.children}</pre>
</div>
),
hr: (props) => <hr className={`mx-auto w-224 mt-6`} />,
img: (props) => {
let src: string = "";
let alt: string = "";
if (typeof props.src === "undefined") {
src = `https://${getAssetsDomain()}/broken-image.svg`;
} else {
src = props.src;
}
if (typeof props.alt === "string") {
alt = props.alt;
}
if (
typeof props.width === "undefined" ||
typeof props.height === "undefined"
) {
return (
<Image src={src} alt={alt} className={`mx-auto`} />
);
} else {
return (
<Image
src={src}
alt={alt}
className={`mx-auto`}
width={
typeof props.width === "string"
? parseInt(props.width, 10)
: props.width
}
height={
typeof props.height === "string"
? parseInt(props.height, 10)
: props.height
}
/>
);
}
},
figcaption: (props) => (
<figcaption className={`${nunito.className} text-center mb-6`}>
{props.children}
</figcaption>
),
},
} as RehypeReactOptions)
.process(content);
return result.result;
}

View File

@@ -1,130 +0,0 @@
<h1 class="title">Nginx + SSL Client Certificate Verification: Manage access to a site</h1>
<p class="paragraph">Access control is a fundamental part of security. Most entities rely on
the combination of username and password, sometimes with additional multi-factor authentication
to improve security. Some entities also use the SSL client certificate verification to manage access
to specific resources. One of the use cases where SSL client certificate verification fits perfectly is
managing access to internet-facing development or staging servers. In this post, I&apos;ll share how
to set up the certificates and configure nginx to verify users based on their certificates.</p>
<h1>Preparing the certificates</h1>
<p class="paragraph">There are two certificates we are going to create. The first one is the root
certificate. It will be placed in the Nginx server. The second one is the client certificate. It will
be installed in the client machine/browsers.</p>
<h2>Root CA</h2>
<p class="paragraph">For generating a root CA, execute these two steps:</p>
<h3>Generate RSA Key</h3>
<code lang="shell">openssl genrsa -aes256 -out ca.key 4096</code>
<h3>Create Root CA crt file.</h3>
<code lang="shell">openssl req -new -x509 -days 3650 -key ca.key -out ca.crt</code>
<h2>Setup CA configuration</h2>
<p class="paragraph">This is an optional step, but if you want to be able to revoke access you
previously granted, you need to do this step.</p>
<p class="paragraph">Create a file named ca.cnf in the same directory as the ca.key and ca.crt.</p>
<code lang="ini" class="linenumber">[ ca ]
default_ca = gca
[ crl_ext ]
authorityKeyIdentifier=keyid:always
[ gca ]
dir = ./
new_certs_dir = $dir
unique_subject = no
certificate = $dir/ca.crt
database = $dir/certindex
; mark(1[9:11]) dimgrey
private_key = $dir/ca.key
serial = $dir/certserial
default_days = 365
default_md = sha256
policy = gca_policy
x509_extensions = gca_extensions
crlnumber = $dir/crlnumber
default_crl_days = 365
[ gca_policy ]
commonName = supplied
stateOrProvinceName = supplied
countryName = optional
emailAddress = optional
organizationName = supplied
organizationUnitName = optional
[ gca_extensions ]
basicConstraints = CA:false
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
keyUsage = digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth
crlDistributionPoints = URI:http://example.com/root.crl
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = example.com
DNS.2 = *.example.com</code>
<p class="paragraph">Initialize an empty file for the CA database.</p>
<code lang="shell">touch certindex</code>
<p class="paragraph">Initialize value for certserial and crlnumber</p>
<code lang="shell">echo 01 > certserial
echo 01 > crlnumber</code>
<h2>User Certificates</h2>
<h3>Generate the user RSA key.</h3>
<code lang="shell">openssl genrsa -aes256 -out client01/user.key 4096</code>
<h3>Create Certificate-Signing Request (CSR)</h3>
<code lang="shell">openssl req -new -key client01/user.key -out client01/user.csr</code>
<h3>Sign the CSR.</h3>
<p class="paragraph">If you did the setup CA configuration step, sign the CSR file by running this command.</p>
<code lang="shell">openssl ca -config ca.cnf -in client01/user.csr -out client01/user.crt</code>
<p class="paragraph">If you skipped the setup CA configuration step, sign the CSR file by running this command.</p>
<code lang="shell">openssl x509 -req -days 365 -in client01/user.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client01/user.crt</code>
<h3>Convert the crt file to pfx/p12 file.</h3>
<p class="paragraph">Most of the time, browsers/client machines only accept a certificate in the pfx format. Run this
command to convert the crt file to the pfx/p12 format.</p>
<code lang="shell">openssl pkcs12 -export -out client01/user.pfx -inkey client01/user.key -in client01/user.crt -certfile ca.crt</code>
<p class="paragraph">You'll be prompted to enter an export password. You must input the exact password when adding
the certificate to a browser.</p>
<br/>
<h1>Setting up nginx with client certificates verification</h1>
<p class="paragraph">Add these lines to a server block in your nginx configuration</p>
<code lang="shell" class="linenumber">ssl_client_certificate /path/to/client/verfication/ca.crt;
ssl_verify_client optional;
ssl_verify_depth 2;</code>
<p class="paragraph">You can do location-based access control. Location-based here refers to a location block in your
nginx configuration, for example:</p>
<code lang="shell"> location /private {
# mark(1[13:41]) dimgrey
if ($ssl_client_verify != SUCCESS) {
return 403;
}
....
}
</code>
<p class="paragraph">Here is a complete example of a server block in the nginx configuration</p>
<code lang="shell">server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name www.example.com;
ssl_certificate /path/to/your/https/certificate.pem;
ssl_certificate_key /path/to/your/https/private-key.pem;
include snippets/ssl-params.conf;
# mark(1:3) dimgrey
ssl_client_certificate /path/to/client/verification/ca.crt;
ssl_verify_client optional;
ssl_verify_depth 2;
root /usr/share/nginx/html;
index index.html index.htm;
location / {
# mark(1[13:41]) dimgrey
if ($ssl_client_verify != SUCCESS) {
return 403;
}
}
}</code>
<br/>
<h1>Adding the User Certificates to the client machine/browsers</h1>

View File

@@ -1,337 +0,0 @@
# Nginx + SSL Client Certificate Verification: Manage access to a site
Access control is a fundamental part of security. Most entities rely on
the combination of username and password, sometimes with additional multi-factor authentication
to improve security. Some entities also use the SSL client certificate verification to manage access
to specific resources. One of the use cases where SSL client certificate verification fits perfectly is
managing access to internet-facing development or staging servers. In this post, I&apos;ll share how
to set up the certificates and configure nginx to verify users based on their certificates.
## Preparing the certificates
There are two certificates we are going to create. The first one is the root
certificate. It will be placed in the Nginx server. The second one is the client certificate. It will
be installed in the client machine/browsers.
### Root CA
For generating a root CA, execute these two steps:
#### Generate RSA Key
```sh
openssl genrsa -aes256 -out ca.key 4096
```
#### Create Root CA file
```sh
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt
```
### Setup CA configuration
This is an optional step, but if you want to be able to revoke access you previously granted, you need to do this step.
Create a file named ca.cnf in the same directory as the ca.key and ca.crt.
```ini
[ ca ]
default_ca = gca
[ crl_ext ]
authorityKeyIdentifier=keyid:always
[ gca ]
dir = ./
new_certs_dir = $dir
unique_subject = no
certificate = $dir/ca.crt
database = $dir/certindex
private_key = $dir/ca.key
serial = $dir/certserial
default_days = 365
default_md = sha256
policy = gca_policy
x509_extensions = gca_extensions
crlnumber = $dir/crlnumber
default_crl_days = 365
[ gca_policy ]
commonName = supplied
stateOrProvinceName = supplied
countryName = optional
emailAddress = optional
organizationName = supplied
organizationUnitName = optional
[ gca_extensions ]
basicConstraints = CA:false
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
keyUsage = digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth
crlDistributionPoints = URI:http://example.com/root.crl
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = example.com
DNS.2 = *.example.com
```
Initialize an empty file for the CA database.
```sh
touch certindex
```
Initialize value for certserial and crlnumber
```sh
echo 01 > certserial
echo 01 > crlnumber
```
### User Certificates
#### Generate the user RSA key
```sh
openssl genrsa -aes256 -out client01/user.key 4096
```
#### Create Certificate-Signing Request (CSR)
```sh
openssl req -new -key client01/user.key -out client01/user.csr
```
#### Sign the CSR
If you did the setup CA configuration step, sign the CSR file by running this command.
```sh
openssl ca -config ca.cnf -in client01/user.csr -out client01/user.crt
```
If you skipped the setup CA configuration step, sign the CSR file by running this command.
```sh
openssl x509 -req -days 365 -in client01/user.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client01/user.crt
```
#### Convert the crt file to pfx/p12 file
Most of the time, browsers/client machines only accept a certificate in the pfx format. Run this
command to convert the crt file to the pfx/p12 format
```sh
openssl pkcs12 -export -out client01/user.pfx -inkey client01/user.key -in client01/user.crt -certfile ca.crt
```
You'll be prompted to enter an export password. You must input the exact password when adding
the certificate to a browser.
---
## Setting up nginx with client certificates verification
Add these lines to a server block in your nginx configuration
```nginx
ssl_client_certificate /path/to/client/verfication/ca.crt;
ssl_verify_client optional;
ssl_verify_depth 2;
```
You can do location-based access control. Location-based here refers to a location block in your
nginx configuration, for example:
```nginx
location /private {
if ($ssl_client_verify != SUCCESS) { # add this condition
return 403; # to make nginx return 403
} # when the client has no valid certificate
....
}
```
Here is a complete example of a server block in the nginx configuration
```nginx
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name www.example.com;
ssl_certificate /path/to/your/https/certificate.pem;
ssl_certificate_key /path/to/your/https/private-key.pem;
include snippets/ssl-params.conf;
# the folowing three lines make nginx verify client certificate
ssl_client_certificate /path/to/client/verification/ca.crt;
ssl_verify_client optional;
ssl_verify_depth 2;
root /usr/share/nginx/html;
index index.html index.htm;
location / {
if ($ssl_client_verify != SUCCESS) { # add this condition
return 403; # to make nginx return 403
} # when the client has no valid certificate
}
}
```
---
## Adding the User Certificates to the client machine/browsers
Most browsers have a search feature on their setting page. Just search for certificates. It will show you the setting
for certificate management.
### Google Chrome (and Chromium-based browsers)
Google Chrome and most Chromium-based (e.g., Vivaldi, Microsoft Edge) browsers installed on Windows or Mac rely on
the operating system key management. So when you open the setting for certificate management, an external window
will open.
<figure>
<img alt="MacOS Keychain Access" src="https://assets.suyono.me/post/nginx-ssl-client-certificate-verification-manage-access-to-a-site/mac_keychain_1.png" width="740" height="483">
<figcaption>MacOS Keychain Access Window, accessible from settings page</figcaption>
</figure>
On Mac, it is called Keychain Access. To add a certificate, drag the pfx file onto Keychain Access. You'll need to
input the exact export password when you convert the crt file to pfx/p12 format.
When you click Manage device certificate from the browser setting page, this window will open on Windows. You can import
the pfx file using this window.
<figure>
<img alt="Windows certificate dialog" src="https://assets.suyono.me/post/nginx-ssl-client-certificate-verification-manage-access-to-a-site/certificates.png" width="503" height="467">
<figcaption>Certificates dialog on Widows, open from chrome settings page</figcaption>
</figure>
Alternatively, you can use the certmgr to import the certificate. You can open it from the Windows setting or Control Panel.
<figure>
<img alt="Windows certificates manager" src="https://assets.suyono.me/post/nginx-ssl-client-certificate-verification-manage-access-to-a-site/certmgr.png" width="626" height="444">
<figcaption>Windows certificates manager, accessible from Control Panel</figcaption>
</figure>
### Mozilla Firefox
Firefox has its own Certificate Manager dialog. You can import and manage the certificate from it. It also connects to
the operating system certificate management.
<figure>
<img alt="Firefox certificates manager" src="https://assets.suyono.me/post/nginx-ssl-client-certificate-verification-manage-access-to-a-site/firefox_certificate_manager_1.png" width="740" height="441">
<figcaption>Mozilla Firefox Certificate Manager</figcaption>
</figure>
---
## Testing
You can use any browser or tool, like cURL, to test the client certificate verification setup. If your client
certificate verification succeeds, you can open the page using a browser. If your browser shows something like
403 Forbidden, it means either your browser does not have the certificate or something wrong in your setup.
### cURL
Without a valid client certificate
```sh
curl -v https://www.example.com/
```
response
```
> GET / HTTP/2
> Host: www.example.com
> user-agent: curl/7.74.0
> accept: */*
>
< HTTP/2 403
< server: nginx
< date: Wed, 12 Jul 2023 04:54:02 GMT
< content-type: text/html
< content-length: 146
<
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>
```
With a valid client certificate
```sh
curl --cert user.crt --key user.key -v https://www.example.com/
```
response
```
> GET / HTTP/2
> Host: www.example.com
> user-agent: curl/7.74.0
> accept: */*
>
< HTTP/2 200
< server: nginx
.
.
.
snipped
```
---
## Revoking Access
This setup recognizes users by the certificate they are using. Revoking access here means revoking the users'
certificates. We can achieve this by leveraging OpenSSL's CRL feature. To use it, we need to have the CA database.
I explained how to set it up in the section above.
### Revoke client certificate
```sh
openssl ca -config ca.cnf -revoke user.crt
```
### Generate CRL file
```sh
openssl ca -config ca.cnf -gencrl -out crl.pem
```
### Verifying CRL file
```sh
openssl crl -in crl.pem -noout -text
```
### Nginx configuration for CRL
You need to add the `ssl_crl` directive in the Nginx configuration file, as shown in the example below.
```nginx
....
ssl_client_certificate /path/to/client/verification/ca.crt;
ssl_verify_client optional;
ssl_verify_depth 2;
ssl_crl /path/to/crl.pem; # configure nginx to read the crl file
root /usr/share/nginx/html;
....
```

View File

@@ -1,18 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
webpack: (config) => {
config.externals = [...config.externals, "jsdom"];
return config;
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "assets.suyono.me",
pathname: "/**"
},
]
}
}

View File

@@ -9,36 +9,27 @@
"lint": "next lint"
},
"dependencies": {
"@types/dompurify": "^3.0.3",
"@types/jsdom": "^21.1.3",
"@types/node": "20.6.5",
"@types/react": "18.3.1",
"@types/react-dom": "18.3.0",
"autoprefixer": "10.4.19",
"eslint": "8.57.0",
"eslint-config-next": "14.2.3",
"highlight.js": "^11.9.0",
"lowlight": "^3.1.0",
"mysql2": "^3.9.7",
"next": "14.2.3",
"postcss": "8.4.38",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-remark": "^2.1.0",
"redis": "^4.6.13",
"rehype-highlight": "^7.0.0",
"rehype-raw": "^7.0.0",
"rehype-react": "^8.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.0",
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.0",
"sharp": "^0.33.3",
"tailwindcss": "3.4.3",
"typescript": "5.2.2",
"unified": "^11.0.4"
"@types/react": "18.2.22",
"@types/react-dom": "18.2.7",
"autoprefixer": "10.4.16",
"dompurify": "^3.0.6",
"eslint": "8.50.0",
"eslint-config-next": "13.5.2",
"html-react-parser": "^4.2.2",
"jsdom": "^22.1.0",
"mysql2": "^3.6.1",
"next": "13.5.2",
"postcss": "8.4.30",
"react": "18.2.0",
"react-dom": "18.2.0",
"redis": "^4.6.10",
"tailwindcss": "3.3.3",
"typescript": "5.2.2"
},
"devDependencies": {
"prettier": "3.0.3"
},
"packageManager": "pnpm@9.14.4+sha512.c8180b3fbe4e4bca02c94234717896b5529740a6cbadf19fa78254270403ea2f27d4e1d46a08a0f56c89b63dc8ebfd3ee53326da720273794e6200fcf0d184ab"
}
}

View File

@@ -1,7 +0,0 @@
module.exports = {
apps: [{
script: "server.js",
instances: 4,
exec_mode: "cluster"
}]
}

5119
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
public/assets/pthumb.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -12,12 +12,10 @@ const config: Config = {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic':
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
'blog-cover': "url('/assets/placeholder.webp')"
},
width: {
'224': '56rem',
},
height: {
'192': '48rem',
}
},
},

3052
yarn.lock Normal file

File diff suppressed because it is too large Load Diff