diff --git a/libexec/flua/Makefile b/libexec/flua/Makefile
--- a/libexec/flua/Makefile
+++ b/libexec/flua/Makefile
@@ -18,7 +18,8 @@
 # FreeBSD Extensions
 .PATH: ${.CURDIR}/modules
 SRCS+=	linit_flua.c
-SRCS+=	lfs.c lposix.c lfbsd.c
+SRCS+=	lfs.c lposix.c lfbsd.c lhash.c
+LIBADD+=	md
 
 CFLAGS+=	-I${SRCTOP}/lib/liblua -I${.CURDIR}/modules -I${LUASRC}
 CFLAGS+=	-DLUA_PROGNAME="\"${PROG}\""
diff --git a/libexec/flua/linit_flua.c b/libexec/flua/linit_flua.c
--- a/libexec/flua/linit_flua.c
+++ b/libexec/flua/linit_flua.c
@@ -36,6 +36,7 @@
 #include "lfs.h"
 #include "lposix.h"
 #include "lfbsd.h"
+#include "lhash.h"
 #include "lua_ucl.h"
 
 /*
@@ -62,6 +63,7 @@
   {"posix.unistd", luaopen_posix_unistd},
   {"ucl", luaopen_ucl},
   {"fbsd", luaopen_fbsd},
+  {"hash", luaopen_hash},
   {NULL, NULL}
 };
 
diff --git a/libexec/flua/modules/lhash.h b/libexec/flua/modules/lhash.h
new file mode 100644
--- /dev/null
+++ b/libexec/flua/modules/lhash.h
@@ -0,0 +1,11 @@
+/*-
+ * Copyright (c) 2024 Netflix, Inc
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <lua.h>
+
+int luaopen_hash(lua_State *L);
diff --git a/libexec/flua/modules/lhash.c b/libexec/flua/modules/lhash.c
new file mode 100644
--- /dev/null
+++ b/libexec/flua/modules/lhash.c
@@ -0,0 +1,169 @@
+/*-
+ * Copyright (c) 2024 Netflix, Inc
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <lua.h>
+#include "lauxlib.h"
+#include "lhash.h"
+
+#include <sha256.h>
+#include <string.h>
+
+#define SHA256_META "SHA256 meta table"
+#define SHA256_DIGEST_LEN 32
+
+/*
+ * Note C++ comments indicate the before -- after state of the stack, in with a
+ * similar convention to forth's ( ) comments. Lua indexes are from 1 and can be
+ * read left to right (leftmost is 1). Negative are relative to the end (-1 is
+ * rightmost). A '.' indicates a return value left on the stack (all values to
+ * its right). Trivial functions don't do this.
+ */
+
+/*
+ * Updates the digest with the new data passed in. Takes 1 argument, which
+ * is converted to a string.
+ */
+static int
+lua_sha256_update(lua_State *L)
+{
+	size_t len;
+	const unsigned char *data;
+	SHA256_CTX *ctx;
+
+	ctx = luaL_checkudata(L, 1, SHA256_META);
+	data = luaL_checklstring(L, 2, &len);
+	SHA256_Update(ctx, data, len);
+
+	lua_pushvalue(L, 1);
+
+	return (1);
+}
+
+/*
+ * Finalizes the digest value and returns it as a 32-byte binary string. The ctx
+ * is zeroed.
+ */
+static int
+lua_sha256_digest(lua_State *L)
+{
+	SHA256_CTX *ctx;
+	unsigned char digest[SHA256_DIGEST_LEN];
+
+	ctx = luaL_checkudata(L, 1, SHA256_META);
+	SHA256_Final(digest, ctx);
+	lua_pushlstring(L, digest, sizeof(digest));
+
+	return (1);
+}
+
+/*
+ * Finalizes the digest value and returns it as a 64-byte ascii string of hex
+ * numbers. The ctx is zeroed.
+ */
+static int
+lua_sha256_hexdigest(lua_State *L)
+{
+	SHA256_CTX *ctx;
+	char hexdigest[SHA256_DIGEST_LEN * 2 + 1];
+
+	ctx = luaL_checkudata(L, 1, SHA256_META);
+	SHA256_End(ctx, hexdigest);
+	lua_pushstring(L, hexdigest);
+
+	return (1);
+}
+
+/*
+ * Zeros out the ctx before garbage collection. Normally this is done in
+ * obj:digest or obj:hexdigest, but if not, it will be wiped here. Lua
+ * manages freeing the ctx memory.
+ */
+static int
+lua_sha256_done(lua_State *L)
+{
+	SHA256_CTX *ctx;
+
+	ctx = luaL_checkudata(L, 1, SHA256_META);
+	memset(ctx, 0, sizeof(*ctx));
+
+	return (0);
+}
+
+/*
+ * Create object obj which accumulates the state of the sha256 digest
+ * for its contents and any subsequent obj:update call. It takes zero
+ * or 1 arguments.
+ */
+static int
+lua_sha256(lua_State *L)
+{
+	SHA256_CTX *ctx;
+	int top;
+
+	/* We take 0 or 1 args */
+	top = lua_gettop(L);				// data -- data
+	if (top > 1) {
+		lua_pushnil(L);
+		return (1);
+	}
+	
+	ctx = lua_newuserdata(L, sizeof(*ctx));		// data -- data ctx
+	SHA256_Init(ctx);
+	if (top == 1) {
+		size_t len;
+		const unsigned char *data;
+
+		data = luaL_checklstring(L, 1, &len);
+		SHA256_Update(ctx, data, len);
+	}
+	luaL_getmetatable(L, SHA256_META);		// data ctx -- data ctx meta
+	lua_setmetatable(L, -2);			// data ctx -- data ctx
+
+	return (1);					// data . ctx
+}
+
+/*
+ * Setup the metatable to manage our userdata that we create in lua_sha256. We
+ * request a finalization call with __gc so we can zero out the ctx buffer so
+ * that we don't leak secrets if obj:digest or obj:hexdigest aren't called.
+ */
+static void
+register_metatable_sha256(lua_State *L)
+{
+	luaL_newmetatable(L, SHA256_META);		// -- meta
+
+	lua_newtable(L);				// meta -- meta tbl
+	lua_pushcfunction(L, lua_sha256_update);	// meta tbl -- meta tbl fn
+	lua_setfield(L, -2, "update");			// meta tbl fn -- meta tbl
+	lua_pushcfunction(L, lua_sha256_digest);	// meta tbl -- meta tbl fn
+	lua_setfield(L, -2, "digest");			// meta tbl fn -- meta tbl
+	lua_pushcfunction(L, lua_sha256_hexdigest);	// meta tbl -- meta tbl fn
+	lua_setfield(L, -2, "hexdigest");		// meta tbl fn -- meta tbl
+
+	/* Associate tbl with metatable */
+	lua_setfield(L, -2, "__index");			// meta tbl -- meta
+	lua_pushcfunction(L, lua_sha256_done);		// meta -- meta fn
+	lua_setfield(L, -2, "__gc");			// meta fn -- meta
+
+	lua_pop(L, 1);					// meta --
+}
+
+#define REG_SIMPLE(n)	{ #n, lua_ ## n }
+static const struct luaL_Reg hashlib[] = {
+	REG_SIMPLE(sha256),
+	{ NULL, NULL },
+};
+#undef REG_SIMPLE
+
+int
+luaopen_hash(lua_State *L)
+{
+	register_metatable_sha256(L);
+
+	luaL_newlib(L, hashlib);
+
+	return 1;
+}
diff --git a/libexec/flua/modules/lposix.c b/libexec/flua/modules/lposix.c
--- a/libexec/flua/modules/lposix.c
+++ b/libexec/flua/modules/lposix.c
@@ -24,7 +24,6 @@
  *
  */
 
-#include <sys/cdefs.h>
 #include <sys/stat.h>
 
 #include <errno.h>